diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index d6b8aa2879..e158b955e0 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -5,7 +5,7 @@ on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
- timeout-minutes: 15
+ timeout-minutes: 30
strategy:
fail-fast: false
matrix:
@@ -13,8 +13,27 @@ jobs:
steps:
- name: Install OS dependencies
if: matrix.os == 'ubuntu-24.04'
- run: sudo apt-get install -y fonts-liberation2 fonts-noto-core fonts-noto-cjk
+ run: sudo apt-get update && sudo apt-get install -y fonts-liberation2 fonts-noto-core fonts-noto-cjk patchelf
- uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - name: Install Python dependencies for signature fields
+ run: pip install -r scripts/requirements-signature-fields.txt
+ - name: Install Python build dependencies for signature helper
+ run: pip install nuitka ordered-set
+ - name: Build signature helper (macOS/Linux)
+ if: matrix.os != 'windows-2022'
+ run: python scripts/build_embedder_helper.py --platform auto
+ - name: Build signature helper (Windows)
+ if: matrix.os == 'windows-2022'
+ shell: cmd
+ run: |
+ for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set VSINSTALL=%%i
+ call "%VSINSTALL%\Common7\Tools\VsDevCmd.bat" && python scripts\build_embedder_helper.py --platform windows
- name: Setup .NET 9
uses: actions/setup-dotnet@v4
with:
@@ -24,6 +43,36 @@ jobs:
run: dotnet workload install macos
- name: Build
run: dotnet run --project NAPS2.Tools -- build debug -v
+ - name: Copy signature helper into debug output (Windows)
+ if: matrix.os == 'windows-2022'
+ shell: pwsh
+ run: |
+ Get-ChildItem -Path "NAPS2.App.WinForms/bin" -Recurse -Directory -Filter "net9-windows" |
+ ForEach-Object {
+ $toolsDir = Join-Path $_.FullName "tools"
+ New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
+ Copy-Item -Force "build/windows/naps2-signature-helper.exe" (Join-Path $toolsDir "naps2-signature-helper.exe")
+ }
+ - name: Copy signature helper into debug output (Linux)
+ if: matrix.os == 'ubuntu-24.04'
+ shell: bash
+ run: |
+ set -euo pipefail
+ while IFS= read -r -d '' dir; do
+ mkdir -p "$dir/tools"
+ cp build/linux/naps2-signature-helper "$dir/tools/naps2-signature-helper"
+ chmod +x "$dir/tools/naps2-signature-helper"
+ done < <(find NAPS2.App.Gtk/bin -type d -name net9 -path '*/Debug*/*' -print0)
+ - name: Copy signature helper into debug output (macOS)
+ if: matrix.os == 'macos-15'
+ shell: bash
+ run: |
+ set -euo pipefail
+ while IFS= read -r -d '' app; do
+ mkdir -p "$app/Contents/tools"
+ cp build/macos/naps2-signature-helper "$app/Contents/tools/naps2-signature-helper"
+ chmod +x "$app/Contents/tools/naps2-signature-helper"
+ done < <(find NAPS2.App.Mac/bin -type d -name "NAPS2.app" -path '*/Debug*/*' -print0)
- name: Test
if: matrix.os != 'macos-15'
run: dotnet run --project NAPS2.Tools -- test -v --nogui
@@ -36,3 +85,21 @@ jobs:
- name: Test (ImageSharp images)
if: matrix.os == 'ubuntu-24.04'
run: dotnet run --project NAPS2.Tools -- test -v --nogui --images is --scope sdk
+ - name: Upload Windows binary
+ if: matrix.os == 'windows-2022'
+ uses: actions/upload-artifact@v4
+ with:
+ name: naps2-windows-debug
+ path: NAPS2.App.WinForms/bin/Debug*/net9-windows/
+ - name: Upload Linux binary
+ if: matrix.os == 'ubuntu-24.04'
+ uses: actions/upload-artifact@v4
+ with:
+ name: naps2-linux-debug
+ path: NAPS2.App.Gtk/bin/Debug*/net9/
+ - name: Upload macOS binary
+ if: matrix.os == 'macos-15'
+ uses: actions/upload-artifact@v4
+ with:
+ name: naps2-macos-debug
+ path: NAPS2.App.Mac/bin/Debug*/net9-macos/
diff --git a/.gitignore b/.gitignore
index 4cb7c8c5bc..88db4d7da6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,7 @@ packages/
*.suo
.vs/
.idea/
-.nuget/
\ No newline at end of file
+.nuget/
+build_output.log
+build/*
+naps2.egg-info/*
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000..af6b36dd2c
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "third_party/pyHanko"]
+ path = third_party/pyHanko
+ url = https://github.com/MatthiasValvekens/pyHanko.git
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000000..2c0733315e
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.11
diff --git a/NAPS2.App.Mac/NAPS2.App.Mac.csproj b/NAPS2.App.Mac/NAPS2.App.Mac.csproj
index a692a0d6c6..785893a252 100644
--- a/NAPS2.App.Mac/NAPS2.App.Mac.csproj
+++ b/NAPS2.App.Mac/NAPS2.App.Mac.csproj
@@ -8,6 +8,7 @@
../NAPS2.Lib/Icons/favicon.ico
12.0
+ 15.0
osx-x64;osx-arm64
partial
diff --git a/NAPS2.Images.Mac/NAPS2.Images.Mac.csproj b/NAPS2.Images.Mac/NAPS2.Images.Mac.csproj
index 2cb119960d..adf166d1d6 100644
--- a/NAPS2.Images.Mac/NAPS2.Images.Mac.csproj
+++ b/NAPS2.Images.Mac/NAPS2.Images.Mac.csproj
@@ -1,7 +1,7 @@
- net6;net8;net8-macos
+ net6;net8;net9-macos
enable
true
false
@@ -17,14 +17,14 @@
-
+
-
+
MONOMAC
-
+
@@ -32,7 +32,7 @@
-
+
diff --git a/NAPS2.Lib.Mac/EtoForms/Ui/MacDesktopForm.cs b/NAPS2.Lib.Mac/EtoForms/Ui/MacDesktopForm.cs
index 1357d5d71c..eeb402d1a5 100644
--- a/NAPS2.Lib.Mac/EtoForms/Ui/MacDesktopForm.cs
+++ b/NAPS2.Lib.Mac/EtoForms/Ui/MacDesktopForm.cs
@@ -110,6 +110,7 @@ protected override void CreateToolbarsAndMenus()
.Append(Commands.ZoomOut)
.Separator()
.Append(Commands.Crop)
+ .Append(Commands.SignatureField)
.Append(Commands.BrightCont)
.Append(Commands.HueSat)
.Append(Commands.BlackWhite)
diff --git a/NAPS2.Lib/EtoForms/Desktop/DesktopSubFormController.cs b/NAPS2.Lib/EtoForms/Desktop/DesktopSubFormController.cs
index 7391a3c187..1515276c8d 100644
--- a/NAPS2.Lib/EtoForms/Desktop/DesktopSubFormController.cs
+++ b/NAPS2.Lib/EtoForms/Desktop/DesktopSubFormController.cs
@@ -39,6 +39,7 @@ public IDesktopSubFormController WithSelection(Func> sele
public void ShowSharpenForm() => ShowImageForm();
public void ShowSplitForm() => ShowImageForm();
public void ShowRotateForm() => ShowImageForm();
+ public void ShowSignatureFieldForm() => ShowImageForm();
public void ShowCombineForm()
{
diff --git a/NAPS2.Lib/EtoForms/Desktop/IDesktopSubFormController.cs b/NAPS2.Lib/EtoForms/Desktop/IDesktopSubFormController.cs
index e50d3e2488..b09733be20 100644
--- a/NAPS2.Lib/EtoForms/Desktop/IDesktopSubFormController.cs
+++ b/NAPS2.Lib/EtoForms/Desktop/IDesktopSubFormController.cs
@@ -11,6 +11,7 @@ public interface IDesktopSubFormController
void ShowSplitForm();
void ShowCombineForm();
void ShowRotateForm();
+ void ShowSignatureFieldForm();
void ShowProfilesForm();
void ShowOcrForm();
void ShowBatchScanForm();
diff --git a/NAPS2.Lib/EtoForms/Ui/AboutForm.cs b/NAPS2.Lib/EtoForms/Ui/AboutForm.cs
index 55ca15aa4e..c72845fa2b 100644
--- a/NAPS2.Lib/EtoForms/Ui/AboutForm.cs
+++ b/NAPS2.Lib/EtoForms/Ui/AboutForm.cs
@@ -53,7 +53,7 @@ protected override void BuildLayout()
C.NoWrap(AssemblyHelper.Product),
L.Row(
L.Column(
- C.NoWrap(string.Format(MiscResources.Version, AssemblyHelper.Version)),
+ C.NoWrap(string.Format(MiscResources.Version, AssemblyHelper.DisplayVersion)),
C.UrlLink(NAPS2_HOMEPAGE)
),
Config.Get(c => c.HiddenButtons).HasFlag(ToolbarButtons.Donate)
@@ -95,4 +95,4 @@ private LayoutElement GetUpdateWidget()
return new UpdateCheckWidget(_updateChecker, Config);
#endif
}
-}
\ No newline at end of file
+}
diff --git a/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs b/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs
index ef1185058e..9d5e022538 100644
--- a/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs
+++ b/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs
@@ -147,6 +147,11 @@ public DesktopCommands(DesktopController desktopController, DesktopScanControlle
Text = UiStrings.Crop,
IconName = "transform_crop_small"
};
+ SignatureField = new ActionCommand(desktopSubFormController.ShowSignatureFieldForm)
+ {
+ Text = "Place Signature Field",
+ IconName = "document_sign_small"
+ };
BrightCont = new ActionCommand(desktopSubFormController.ShowBrightnessContrastForm)
{
Text = UiStrings.BrightnessContrast,
@@ -381,6 +386,7 @@ public DesktopCommands WithSelection(Func> selectionFunc)
public ActionCommand ImageMenu { get; set; }
public ActionCommand ViewImage { get; set; }
public ActionCommand Crop { get; set; }
+ public ActionCommand SignatureField { get; set; }
public ActionCommand BrightCont { get; set; }
public ActionCommand HueSat { get; set; }
public ActionCommand BlackWhite { get; set; }
diff --git a/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs b/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs
index 1ad1a5a578..7f334828ec 100644
--- a/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs
+++ b/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs
@@ -321,6 +321,7 @@ protected virtual void CreateToolbarsAndMenus()
.Append(Commands.ViewImage)
.Separator()
.Append(Commands.Crop)
+ .Append(Commands.SignatureField)
.Append(Commands.BrightCont)
.Append(Commands.HueSat)
.Append(Commands.BlackWhite)
diff --git a/NAPS2.Lib/EtoForms/Ui/SignatureFieldForm.cs b/NAPS2.Lib/EtoForms/Ui/SignatureFieldForm.cs
new file mode 100644
index 0000000000..24a973723b
--- /dev/null
+++ b/NAPS2.Lib/EtoForms/Ui/SignatureFieldForm.cs
@@ -0,0 +1,264 @@
+using Eto.Drawing;
+using Eto.Forms;
+using NAPS2.Pdf;
+
+namespace NAPS2.EtoForms.Ui;
+
+public class SignatureFieldForm : UnaryImageFormBase
+{
+ private const int BORDER_WIDTH = 2;
+ private const int MIN_FIELD_SIZE = 20;
+
+ private readonly ColorScheme _colorScheme;
+
+ // Mouse down location
+ private PointF _mouseOrigin;
+
+ // Field placement as fractions of the total image size (updated as the user drags)
+ private float _fieldX, _fieldY, _fieldW, _fieldH;
+
+ // Field placement as pixels (updated on mouse up)
+ private float _realX, _realY, _realW, _realH;
+
+ private bool _isDragging;
+ private bool _hasPlacement;
+
+ public SignatureFieldForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController,
+ ColorScheme colorScheme, IIconProvider iconProvider) :
+ base(config, imageList, thumbnailController)
+ {
+ Title = "Place Signature Field";
+ IconName = "document_sign_small";
+
+ _colorScheme = colorScheme;
+
+ OverlayBorderSize = BORDER_WIDTH;
+ Overlay.MouseDown += Overlay_MouseDown;
+ Overlay.MouseMove += Overlay_MouseMove;
+ Overlay.MouseUp += Overlay_MouseUp;
+ }
+
+ protected override void OnShown(EventArgs e)
+ {
+ base.OnShown(e);
+ DefaultButton.Focus();
+ }
+
+ protected override void Apply()
+ {
+ // Fields are now saved immediately on mouse up, so nothing to do here
+ // Just close the form
+ }
+
+ protected override void Revert()
+ {
+ // Undo last field (like Ctrl+Z)
+ using var processedImage = Image.GetClonedImage();
+ var currentFields = processedImage.PostProcessingData.SignatureFields;
+
+ if (currentFields != null && currentFields.Count > 0)
+ {
+ // Remove the last field
+ var updatedFields = currentFields.Take(currentFields.Count - 1).ToList();
+
+ var updatedPostProcessingData = processedImage.PostProcessingData with
+ {
+ SignatureFields = updatedFields.Count > 0 ? updatedFields : null
+ };
+
+ // Create updated processed image
+ var updatedProcessedImage = processedImage.WithPostProcessingData(updatedPostProcessingData, false);
+
+ // Replace the internal image in the UiImage
+ Image.ReplaceInternalImage(updatedProcessedImage);
+
+ Console.WriteLine($"Reverted last signature field. Remaining fields: {updatedFields.Count}");
+ }
+
+ // Also clear any in-progress field
+ _fieldX = _fieldY = _fieldW = _fieldH = 0;
+ _realX = _realY = _realW = _realH = 0;
+ _hasPlacement = false;
+ Overlay.Invalidate();
+ }
+
+ protected override IMemoryImage RenderPreview()
+ {
+ return WorkingImage!.Clone();
+ }
+
+ protected override List Transforms => new List();
+
+ private void Overlay_MouseDown(object? sender, MouseEventArgs e)
+ {
+ _isDragging = true;
+ _mouseOrigin = e.Location;
+ Overlay.Invalidate();
+ }
+
+ private void Overlay_MouseUp(object? sender, MouseEventArgs e)
+ {
+ if (_isDragging && _fieldW > 0 && _fieldH > 0)
+ {
+ _realX = _fieldX * RealImageWidth;
+ _realY = _fieldY * RealImageHeight;
+ _realW = _fieldW * RealImageWidth;
+ _realH = _fieldH * RealImageHeight;
+ _hasPlacement = true;
+
+ // Immediately save the field to PostProcessingData
+ SaveCurrentField();
+
+ // Reset for next field placement
+ _fieldX = _fieldY = _fieldW = _fieldH = 0;
+ _realX = _realY = _realW = _realH = 0;
+ _hasPlacement = false;
+ }
+ _isDragging = false;
+ // Invalidate overlay to redraw with the saved field in green
+ Overlay.Invalidate();
+ }
+
+ private void SaveCurrentField()
+ {
+ if (!_hasPlacement)
+ {
+ return;
+ }
+
+ Console.WriteLine($"SaveCurrentField DEBUG:");
+ Console.WriteLine($" _realX={_realX}, _realY={_realY}, _realW={_realW}, _realH={_realH}");
+ Console.WriteLine($" RealImageWidth={RealImageWidth}, RealImageHeight={RealImageHeight}");
+ Console.WriteLine($" _overlayW={_overlayW}, _overlayH={_overlayH}");
+ Console.WriteLine($" _fieldX={_fieldX}, _fieldY={_fieldY}, _fieldW={_fieldW}, _fieldH={_fieldH}");
+
+ // Create signature field placement
+ var fieldPlacement = SignatureFieldPlacement.FromPixels(
+ $"Signature_{Guid.NewGuid():N}",
+ _realX,
+ _realY,
+ _realW,
+ _realH,
+ RealImageWidth,
+ RealImageHeight);
+
+ // Get the current processed image
+ using var processedImage = Image.GetClonedImage();
+
+ // Update the post-processing data with the signature field
+ var currentFields = processedImage.PostProcessingData.SignatureFields ?? new List();
+ var updatedFields = new List(currentFields) { fieldPlacement };
+
+ var updatedPostProcessingData = processedImage.PostProcessingData with
+ {
+ SignatureFields = updatedFields
+ };
+
+ // Create updated processed image
+ var updatedProcessedImage = processedImage.WithPostProcessingData(updatedPostProcessingData, false);
+
+ // Replace the internal image in the UiImage
+ Image.ReplaceInternalImage(updatedProcessedImage);
+
+ Console.WriteLine($"Saved signature field: {fieldPlacement.FieldName} at ({fieldPlacement.NormalizedX}, {fieldPlacement.NormalizedY})");
+ }
+
+ private void UpdateFieldPlacement(PointF mousePos)
+ {
+ if (!_isDragging) return;
+
+ var delta = mousePos - _mouseOrigin;
+
+ // Convert to overlay-relative coordinates
+ var origin = _mouseOrigin - new PointF(_overlayL, _overlayT);
+ var current = mousePos - new PointF(_overlayL, _overlayT);
+
+ // Calculate normalized coordinates (handle negative deltas)
+ if (delta.Y > 0)
+ {
+ _fieldY = (origin.Y / _overlayH).Clamp(0, 1);
+ _fieldH = ((current.Y - origin.Y) / _overlayH).Clamp(0, 1 - _fieldY);
+ }
+ else
+ {
+ _fieldY = (current.Y / _overlayH).Clamp(0, 1);
+ _fieldH = ((origin.Y - current.Y) / _overlayH).Clamp(0, 1 - _fieldY);
+ }
+
+ if (delta.X > 0)
+ {
+ _fieldX = (origin.X / _overlayW).Clamp(0, 1);
+ _fieldW = ((current.X - origin.X) / _overlayW).Clamp(0, 1 - _fieldX);
+ }
+ else
+ {
+ _fieldX = (current.X / _overlayW).Clamp(0, 1);
+ _fieldW = ((origin.X - current.X) / _overlayW).Clamp(0, 1 - _fieldX);
+ }
+ }
+
+ private void Overlay_MouseMove(object? sender, MouseEventArgs e)
+ {
+ Overlay.Cursor = Cursors.Crosshair;
+ UpdateFieldPlacement(e.Location);
+ Overlay.Invalidate();
+ }
+
+ protected override void PaintOverlay(object? sender, PaintEventArgs e)
+ {
+ base.PaintOverlay(sender, e);
+
+ if (_overlayW == 0 || _overlayH == 0)
+ {
+ return;
+ }
+
+ // Draw existing signature fields from post-processing data
+ using var processedImage = Image.GetClonedImage();
+ var fieldCount = processedImage.PostProcessingData.SignatureFields?.Count ?? 0;
+ Console.WriteLine($"PaintOverlay: Drawing {fieldCount} existing fields");
+
+ if (processedImage.PostProcessingData.SignatureFields != null)
+ {
+ // Use the SAME color as dragging so it's visible on Mac
+ var existingFieldPen = new Pen(_colorScheme.CropColor, BORDER_WIDTH * 2);
+ var fillColor = new Color(_colorScheme.CropColor, 0.3f);
+
+ foreach (var field in processedImage.PostProcessingData.SignatureFields)
+ {
+ var (x, y, w, h) = field.ToPixels(RealImageWidth, RealImageHeight);
+ var overlayX = _overlayL + (x / RealImageWidth) * _overlayW;
+ var overlayY = _overlayT + (y / RealImageHeight) * _overlayH;
+ var overlayW = (w / RealImageWidth) * _overlayW;
+ var overlayH = (h / RealImageHeight) * _overlayH;
+
+ Console.WriteLine($" Drawing field at overlay ({overlayX}, {overlayY}) size ({overlayW}, {overlayH})");
+ Console.WriteLine($" Overlay bounds: L={_overlayL}, T={_overlayT}, W={_overlayW}, H={_overlayH}");
+
+ // Draw fill first, then border (same as dragging)
+ e.Graphics.FillRectangle(fillColor, overlayX, overlayY, overlayW, overlayH);
+ e.Graphics.DrawRectangle(existingFieldPen, overlayX, overlayY, overlayW, overlayH);
+ }
+ }
+
+ // Draw the current field being placed
+ if (_isDragging && (_fieldW > 0 || _fieldH > 0))
+ {
+ var offsetX = _fieldX * _overlayW;
+ var offsetY = _fieldY * _overlayH;
+ var offsetW = _fieldW * _overlayW;
+ var offsetH = _fieldH * _overlayH;
+
+ var x = _overlayL + offsetX;
+ var y = _overlayT + offsetY;
+
+ // Draw border
+ var fieldPen = new Pen(_colorScheme.CropColor, BORDER_WIDTH);
+ e.Graphics.DrawRectangle(fieldPen, x, y, offsetW, offsetH);
+
+ // Draw semi-transparent fill
+ var fillColor = new Color(_colorScheme.CropColor, 0.2f);
+ e.Graphics.FillRectangle(fillColor, x, y, offsetW, offsetH);
+ }
+ }
+}
diff --git a/NAPS2.Sdk/Images/PostProcessingData.cs b/NAPS2.Sdk/Images/PostProcessingData.cs
index be30a077d9..2a2f65ed78 100644
--- a/NAPS2.Sdk/Images/PostProcessingData.cs
+++ b/NAPS2.Sdk/Images/PostProcessingData.cs
@@ -1,4 +1,5 @@
using System.Threading;
+using NAPS2.Pdf;
namespace NAPS2.Images;
@@ -12,9 +13,10 @@ public record PostProcessingData(
PageSide PageSide,
Barcode Barcode,
CancellationTokenSource? OcrCts,
- string? OriginalFilePath)
+ string? OriginalFilePath,
+ List? SignatureFields)
{
- public PostProcessingData() : this(null, null, 0, PageSide.Unknown, Barcode.NoDetection, null, null)
+ public PostProcessingData() : this(null, null, 0, PageSide.Unknown, Barcode.NoDetection, null, null, null)
{
}
}
\ No newline at end of file
diff --git a/NAPS2.Sdk/NAPS2.Sdk.csproj b/NAPS2.Sdk/NAPS2.Sdk.csproj
index 02c6cefbb2..2897382cf7 100644
--- a/NAPS2.Sdk/NAPS2.Sdk.csproj
+++ b/NAPS2.Sdk/NAPS2.Sdk.csproj
@@ -3,7 +3,7 @@
net6;net8;net462
- $(TargetFrameworks);net8-macos
+ $(TargetFrameworks);net9-macos
enable
true
@@ -22,7 +22,7 @@
-
+
MAC
@@ -33,9 +33,10 @@
-
+
-
+
+
@@ -78,7 +79,7 @@
-
+
diff --git a/NAPS2.Sdk/Pdf/PdfExporter.cs b/NAPS2.Sdk/Pdf/PdfExporter.cs
index be6c01fdfc..f8123fe51f 100644
--- a/NAPS2.Sdk/Pdf/PdfExporter.cs
+++ b/NAPS2.Sdk/Pdf/PdfExporter.cs
@@ -139,7 +139,12 @@ void IncrementProgress()
var stream = FinalizeAndSaveDocument(document, exportParams, producer);
if (progress.IsCancellationRequested) return false;
- return MergePassthroughPages(stream, output, pdfPages, exportParams, progress);
+ var result = MergePassthroughPages(stream, output, pdfPages, exportParams, progress);
+ if (!result) return false;
+
+ // Embed signature fields if any exist
+ result = EmbedSignatureFields(output, imagePages.Concat(pdfPages).ToList(), exportParams, progress);
+ return result;
}
finally
{
@@ -193,6 +198,139 @@ private bool MergePassthroughPages(MemoryStream stream, OutputPathOrStream outpu
}
}
+ private bool EmbedSignatureFields(OutputPathOrStream output, List allPages,
+ PdfExportParams exportParams, ProgressHandler progress)
+ {
+ Console.WriteLine("=== EmbedSignatureFields called ===");
+ if (progress.IsCancellationRequested) return false;
+
+ // Collect all signature fields from all pages (without assuming PdfSharp page dimensions).
+ // For passthrough pages, PdfSharp's Page.Width/Height can remain at defaults unless OCR runs.
+ // We'll read the *actual* page dimensions from the generated PDF file instead.
+ var rawFields = new List<(int pageIndex, SignatureFieldPlacement field, double imageWidth, double imageHeight)>();
+ foreach (var state in allPages)
+ {
+ var signatureFields = state.Image.PostProcessingData.SignatureFields;
+ Console.WriteLine($"Page {state.PageIndex}: SignatureFields = {signatureFields?.Count ?? 0}");
+
+ if (signatureFields == null || signatureFields.Count == 0)
+ {
+ continue;
+ }
+
+ foreach (var field in signatureFields)
+ {
+ rawFields.Add((state.PageIndex, field, field.OriginalImageWidth, field.OriginalImageHeight));
+ }
+ }
+
+ // If no fields to embed, we're done
+ if (rawFields.Count == 0)
+ {
+ Console.WriteLine("No signature fields to embed");
+ return true;
+ }
+
+ Console.WriteLine($"Total fields to embed: {rawFields.Count}");
+
+ // We need to work with a file, so if output is a stream, save to temp file first
+ string? tempInputPath = null;
+ string? tempOutputPath = null;
+ bool needsStreamHandling = output.Stream != null;
+
+ try
+ {
+ string inputPath;
+ string outputPath;
+
+ if (needsStreamHandling)
+ {
+ // Save stream to temp file
+ tempInputPath = Path.Combine(_scanningContext.TempFolderPath, Path.GetRandomFileName() + ".pdf");
+ Console.WriteLine($"Saving stream to temp file: {tempInputPath}");
+ using (var fileStream = new FileStream(tempInputPath, FileMode.Create, FileAccess.Write))
+ {
+ output.Stream!.Position = 0;
+ output.Stream.CopyTo(fileStream);
+ }
+ Console.WriteLine($"Stream saved, file size: {new FileInfo(tempInputPath).Length} bytes");
+ inputPath = tempInputPath;
+
+ tempOutputPath = Path.Combine(_scanningContext.TempFolderPath, Path.GetRandomFileName() + ".pdf");
+ outputPath = tempOutputPath;
+ }
+ else
+ {
+ // Working with file paths
+ inputPath = output.Path!;
+ tempOutputPath = Path.Combine(_scanningContext.TempFolderPath, Path.GetRandomFileName() + ".pdf");
+ outputPath = tempOutputPath;
+ }
+
+ // Read actual page dimensions from the PDF file that we're about to post-process.
+ // This avoids mismatches on passthrough pages where PdfSharp page dimensions may not be updated.
+ var pageDimensions = new Dictionary();
+ var password = exportParams.Encryption.EncryptPdf ? exportParams.Encryption.OwnerPassword : null;
+ lock (PdfiumNativeLibrary.Instance)
+ {
+ using var doc = Pdfium.PdfDocument.Load(inputPath, password);
+ for (int pageIndex = 0; pageIndex < doc.PageCount; pageIndex++)
+ {
+ using var page = doc.GetPage(pageIndex);
+ pageDimensions[pageIndex] = (page.Width, page.Height);
+ }
+ }
+
+ var fieldsToEmbed = rawFields.Select(f =>
+ {
+ var (pageWidth, pageHeight) = pageDimensions[f.pageIndex];
+ Console.WriteLine($" Field: {f.field.FieldName} at ({f.field.NormalizedX}, {f.field.NormalizedY}) size ({f.field.NormalizedWidth}, {f.field.NormalizedHeight})");
+ Console.WriteLine($" Page dimensions (actual PDF): {pageWidth}x{pageHeight} points");
+ Console.WriteLine($" Image dimensions: {f.imageWidth}x{f.imageHeight} pixels (from field)");
+ return (f.pageIndex, f.field, pageWidth, pageHeight, f.imageWidth, f.imageHeight);
+ }).ToList();
+
+ // Embed signature fields using Python/pyHanko
+ var embedder = new SignatureFieldEmbedder(_logger);
+ var success = embedder.EmbedFields(inputPath, outputPath, fieldsToEmbed, pageDimensions);
+
+ if (success && File.Exists(outputPath))
+ {
+ // Copy result back
+ if (needsStreamHandling)
+ {
+ output.Stream!.SetLength(0);
+ output.Stream.Position = 0;
+ using var resultStream = new FileStream(outputPath, FileMode.Open, FileAccess.Read);
+ resultStream.CopyTo(output.Stream);
+ }
+ else
+ {
+ File.Copy(outputPath, output.Path!, true);
+ }
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error embedding signature fields");
+ return true; // Don't fail the export, just log the error
+ }
+ finally
+ {
+ // Clean up temp files
+ if (tempInputPath != null && File.Exists(tempInputPath))
+ {
+ try { File.Delete(tempInputPath); } catch { }
+ }
+ if (tempOutputPath != null && File.Exists(tempOutputPath))
+ {
+ try { File.Delete(tempOutputPath); } catch { }
+ }
+ }
+ }
+
private void CopyPage(Pdfium.PdfDocument destDoc, Pdfium.PdfDocument sourceDoc, PageExportState state)
{
destDoc.ImportPages(sourceDoc, "1", state.PageIndex);
@@ -784,4 +922,4 @@ public void Dispose()
{
}
}
-}
\ No newline at end of file
+}
diff --git a/NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs b/NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs
new file mode 100644
index 0000000000..2fa2eb1499
--- /dev/null
+++ b/NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs
@@ -0,0 +1,454 @@
+using System.Diagnostics;
+using System.Text;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+
+namespace NAPS2.Pdf;
+
+///
+/// Helper class to embed signature fields into PDFs using pyHanko (Python).
+///
+public class SignatureFieldEmbedder
+{
+ private readonly ILogger _logger;
+
+ public SignatureFieldEmbedder(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ /// Embeds signature fields into a PDF file using pyHanko.
+ ///
+ /// Path to the input PDF file
+ /// Path to the output PDF file
+ /// List of signature field placements with page dimensions
+ /// Dictionary mapping page index to page dimensions (width, height) in PDF points
+ /// True if successful, false otherwise
+ public bool EmbedFields(string inputPdfPath, string outputPdfPath,
+ List<(int pageIndex, SignatureFieldPlacement field, double pageWidth, double pageHeight, double imageWidth, double imageHeight)> fields,
+ Dictionary pageDimensions)
+ {
+ if (fields.Count == 0)
+ {
+ // No fields to embed, just copy the file
+ File.Copy(inputPdfPath, outputPdfPath, true);
+ return true;
+ }
+
+ // Find Python executable
+ Console.WriteLine("=== Starting signature field embedding ===");
+ Console.WriteLine($"Input PDF: {inputPdfPath}");
+ Console.WriteLine($"Output PDF: {outputPdfPath}");
+ Console.WriteLine($"Number of fields to embed: {fields.Count}");
+
+ _logger.LogInformation("=== Starting signature field embedding ===");
+ _logger.LogInformation("Input PDF: {InputPath}", inputPdfPath);
+ _logger.LogInformation("Output PDF: {OutputPath}", outputPdfPath);
+ _logger.LogInformation("Number of fields to embed: {FieldCount}", fields.Count);
+
+ // Prefer bundled helper executable (no Python interpreter needed)
+ var helperExe = FindBundledHelper();
+ if (helperExe != null)
+ {
+ Console.WriteLine($"Using bundled helper: {helperExe}");
+ _logger.LogInformation("Using bundled signature helper: {HelperExe}", helperExe);
+
+ // Convert fields to JSON format expected by the helper
+ var helperFieldsJson = ConvertFieldsToJson(fields, pageDimensions);
+ Console.WriteLine($"Fields JSON: {helperFieldsJson}");
+ _logger.LogInformation("Fields JSON: {FieldsJson}", helperFieldsJson);
+
+ try
+ {
+ var arguments = $"\"{inputPdfPath}\" \"{outputPdfPath}\" \"{helperFieldsJson.Replace("\"", "\\\"")}\"";
+ Console.WriteLine($"Executing: {helperExe} {arguments}");
+ _logger.LogInformation("Executing: {HelperExe} {Arguments}", helperExe, arguments);
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = helperExe,
+ Arguments = arguments,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(startInfo);
+ if (process == null)
+ {
+ _logger.LogError("Failed to start signature helper process");
+ File.Copy(inputPdfPath, outputPdfPath, true);
+ return false;
+ }
+
+ var output = process.StandardOutput.ReadToEnd();
+ var error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ Console.WriteLine($"Signature helper process exited with code: {process.ExitCode}");
+ _logger.LogInformation("Signature helper process exited with code: {ExitCode}", process.ExitCode);
+ if (!string.IsNullOrEmpty(output))
+ {
+ Console.WriteLine($"Helper stdout: {output}");
+ _logger.LogInformation("Helper stdout: {Output}", output);
+ }
+ if (!string.IsNullOrEmpty(error))
+ {
+ Console.WriteLine($"Helper stderr: {error}");
+ _logger.LogWarning("Helper stderr: {Error}", error);
+ }
+
+ if (process.ExitCode == 0)
+ {
+ Console.WriteLine("Signature fields embedded successfully!");
+ _logger.LogInformation("Signature fields embedded successfully");
+ return true;
+ }
+ else if (process.ExitCode == 2)
+ {
+ Console.WriteLine("ERROR: pyHanko not installed. Install with: pip install pyHanko");
+ _logger.LogWarning("pyHanko not installed. Signature fields will not be embedded. Install with: pip install pyHanko");
+ File.Copy(inputPdfPath, outputPdfPath, true);
+ return false;
+ }
+ else
+ {
+ Console.WriteLine($"ERROR: Failed to embed signature fields. Exit code: {process.ExitCode}");
+ _logger.LogError("Failed to embed signature fields. Exit code: {ExitCode}", process.ExitCode);
+ File.Copy(inputPdfPath, outputPdfPath, true);
+ return false;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Exception while embedding signature fields");
+ File.Copy(inputPdfPath, outputPdfPath, true);
+ return false;
+ }
+ }
+
+ Console.WriteLine("Bundled helper not found, falling back to Python script");
+ _logger.LogInformation("Bundled signature helper not found, falling back to Python script");
+
+ var pythonExe = FindPythonExecutable();
+ if (pythonExe == null)
+ {
+ Console.WriteLine("ERROR: Python executable not found");
+ _logger.LogWarning("Python executable not found. Signature fields will not be embedded.");
+ File.Copy(inputPdfPath, outputPdfPath, true);
+ return false;
+ }
+ Console.WriteLine($"Using Python: {pythonExe}");
+ _logger.LogInformation("Using Python: {PythonExe}", pythonExe);
+
+ // Find the script path
+ var scriptPath = FindScriptPath();
+ if (scriptPath == null || !File.Exists(scriptPath))
+ {
+ Console.WriteLine($"ERROR: Script not found at: {scriptPath}");
+ _logger.LogWarning("Signature field embedding script not found at: {ScriptPath}", scriptPath);
+ File.Copy(inputPdfPath, outputPdfPath, true);
+ return false;
+ }
+ Console.WriteLine($"Using script: {scriptPath}");
+ _logger.LogInformation("Using script: {ScriptPath}", scriptPath);
+
+ // Convert fields to JSON format expected by Python script
+ var fieldsJson = ConvertFieldsToJson(fields, pageDimensions);
+ Console.WriteLine($"Fields JSON: {fieldsJson}");
+ _logger.LogInformation("Fields JSON: {FieldsJson}", fieldsJson);
+
+ try
+ {
+ // Invoke Python script
+ var arguments = $"\"{scriptPath}\" \"{inputPdfPath}\" \"{outputPdfPath}\" \"{fieldsJson.Replace("\"", "\\\"")}\"";
+ Console.WriteLine($"Executing: {pythonExe} {arguments}");
+ _logger.LogInformation("Executing: {PythonExe} {Arguments}", pythonExe, arguments);
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = pythonExe,
+ Arguments = arguments,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(startInfo);
+ if (process == null)
+ {
+ _logger.LogError("Failed to start Python process");
+ File.Copy(inputPdfPath, outputPdfPath, true);
+ return false;
+ }
+
+ var output = process.StandardOutput.ReadToEnd();
+ var error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ Console.WriteLine($"Python process exited with code: {process.ExitCode}");
+ _logger.LogInformation("Python process exited with code: {ExitCode}", process.ExitCode);
+ if (!string.IsNullOrEmpty(output))
+ {
+ Console.WriteLine($"Python stdout: {output}");
+ _logger.LogInformation("Python stdout: {Output}", output);
+ }
+ if (!string.IsNullOrEmpty(error))
+ {
+ Console.WriteLine($"Python stderr: {error}");
+ _logger.LogWarning("Python stderr: {Error}", error);
+ }
+
+ if (process.ExitCode == 0)
+ {
+ Console.WriteLine("Signature fields embedded successfully!");
+ _logger.LogInformation("Signature fields embedded successfully");
+ return true;
+ }
+ else if (process.ExitCode == 2)
+ {
+ Console.WriteLine("ERROR: pyHanko not installed. Install with: pip install pyHanko");
+ _logger.LogWarning("pyHanko not installed. Signature fields will not be embedded. Install with: pip install pyHanko");
+ File.Copy(inputPdfPath, outputPdfPath, true);
+ return false;
+ }
+ else
+ {
+ Console.WriteLine($"ERROR: Failed to embed signature fields. Exit code: {process.ExitCode}");
+ _logger.LogError("Failed to embed signature fields. Exit code: {ExitCode}", process.ExitCode);
+ File.Copy(inputPdfPath, outputPdfPath, true);
+ return false;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Exception while embedding signature fields");
+ File.Copy(inputPdfPath, outputPdfPath, true);
+ return false;
+ }
+ }
+
+ ///
+ /// Attempts to find the bundled signature helper executable shipped alongside the application.
+ ///
+ ///
+ /// This is preferred over the Python script path because it does not require a Python interpreter.
+ /// Search is performed relative to ..
+ ///
+ /// The full path to the helper executable if found; otherwise, null.
+ private string? FindBundledHelper()
+ {
+ var appDir = AppDomain.CurrentDomain.BaseDirectory;
+ Console.WriteLine($"App directory: {appDir}");
+
+ var helperBaseName = "naps2-signature-helper";
+ var helperNames = new[] { helperBaseName + ".exe", helperBaseName };
+
+ var possibleDirs = new List
+ {
+ appDir,
+ Path.Combine(appDir, "tools"),
+ Path.Combine(appDir, "..", "tools"),
+ Path.Combine(appDir, "..", "..", "tools"),
+ };
+
+ // Additional macOS bundle-specific checks
+ // If running from within a .app bundle, locate the Contents directory.
+ var dirInfo = new DirectoryInfo(Path.GetFullPath(appDir));
+ DirectoryInfo? contentsDir = null;
+ while (dirInfo != null)
+ {
+ if (string.Equals(dirInfo.Name, "Contents", StringComparison.OrdinalIgnoreCase) &&
+ dirInfo.Parent != null &&
+ dirInfo.Parent.Name.EndsWith(".app", StringComparison.OrdinalIgnoreCase))
+ {
+ contentsDir = dirInfo;
+ break;
+ }
+
+ dirInfo = dirInfo.Parent;
+ }
+
+ if (contentsDir != null)
+ {
+ possibleDirs.Add(Path.Combine(contentsDir.FullName, "MacOS"));
+ possibleDirs.Add(Path.Combine(contentsDir.FullName, "tools"));
+ }
+
+ foreach (var dir in possibleDirs)
+ {
+ foreach (var helperName in helperNames)
+ {
+ var fullPath = Path.GetFullPath(Path.Combine(dir, helperName));
+ Console.WriteLine($"Checking bundled helper: {fullPath}");
+ if (File.Exists(fullPath))
+ {
+ Console.WriteLine($"Found bundled helper at: {fullPath}");
+ _logger.LogInformation("Found bundled signature helper at: {HelperPath}", fullPath);
+ return fullPath;
+ }
+ }
+ }
+
+ Console.WriteLine("Bundled signature helper not found in any of the checked paths");
+ _logger.LogInformation("Bundled signature helper not found in any of the checked paths");
+ return null;
+ }
+
+ private string? FindPythonExecutable()
+ {
+ // First, try to find Python in a virtual environment relative to the script
+ var scriptPath = FindScriptPath();
+ if (scriptPath != null)
+ {
+ var scriptDir = Path.GetDirectoryName(scriptPath);
+ if (scriptDir != null)
+ {
+ var repoRoot = Path.GetFullPath(Path.Combine(scriptDir, ".."));
+ var venvPython = Path.Combine(repoRoot, ".venv", "bin", "python");
+ if (File.Exists(venvPython))
+ {
+ _logger.LogInformation("Found Python in virtual environment: {VenvPython}", venvPython);
+ return venvPython;
+ }
+ }
+ }
+
+ // Try common Python executable names in PATH
+ var pythonNames = new[] { "python3", "python", "py" };
+
+ foreach (var name in pythonNames)
+ {
+ try
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = name,
+ Arguments = "--version",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(startInfo);
+ if (process != null)
+ {
+ process.WaitForExit();
+ if (process.ExitCode == 0)
+ {
+ return name;
+ }
+ }
+ }
+ catch
+ {
+ // Continue to next name
+ }
+ }
+
+ return null;
+ }
+
+ private string? FindScriptPath()
+ {
+ // Try to find the script relative to the application directory
+ var appDir = AppDomain.CurrentDomain.BaseDirectory;
+ Console.WriteLine($"App directory: {appDir}");
+
+ var possiblePaths = new[]
+ {
+ Path.Combine(appDir, "scripts", "embed_signature_fields.py"),
+ Path.Combine(appDir, "..", "scripts", "embed_signature_fields.py"),
+ Path.Combine(appDir, "..", "..", "scripts", "embed_signature_fields.py"),
+ Path.Combine(appDir, "..", "..", "..", "scripts", "embed_signature_fields.py"),
+ Path.Combine(appDir, "..", "..", "..", "..", "scripts", "embed_signature_fields.py"),
+ Path.Combine(appDir, "..", "..", "..", "..", "..", "scripts", "embed_signature_fields.py"),
+ Path.Combine(appDir, "..", "..", "..", "..", "..", "..", "scripts", "embed_signature_fields.py"),
+ Path.Combine(appDir, "..", "..", "..", "..", "..", "..", "..", "scripts", "embed_signature_fields.py"),
+ };
+
+ foreach (var path in possiblePaths)
+ {
+ var fullPath = Path.GetFullPath(path);
+ Console.WriteLine($"Checking: {fullPath}");
+ if (File.Exists(fullPath))
+ {
+ Console.WriteLine($"Found script at: {fullPath}");
+ return fullPath;
+ }
+ }
+
+ Console.WriteLine("Script not found in any of the checked paths");
+ return null;
+ }
+
+ private string ConvertFieldsToJson(
+ List<(int pageIndex, SignatureFieldPlacement field, double pageWidth, double pageHeight, double imageWidth, double imageHeight)> fields,
+ Dictionary pageDimensions)
+ {
+ var jsonFields = fields.Select(f =>
+ {
+ // Convert normalized coordinates to PDF points
+ // The normalized coordinates were created using the original image dimensions (in pixels),
+ // but we need to convert them to PDF page dimensions (in points).
+ // PDF uses bottom-left origin, so we need to flip Y coordinate.
+
+ var pageWidth = f.pageWidth;
+ var pageHeight = f.pageHeight;
+ var imageWidth = f.imageWidth;
+ var imageHeight = f.imageHeight;
+
+ Console.WriteLine($"Converting field {f.field.FieldName}:");
+ Console.WriteLine($" Normalized: X={f.field.NormalizedX}, Y={f.field.NormalizedY}, W={f.field.NormalizedWidth}, H={f.field.NormalizedHeight}");
+ Console.WriteLine($" Image dimensions (pixels): W={imageWidth}, H={imageHeight}");
+ Console.WriteLine($" Page dimensions (PDF points): W={pageWidth}, H={pageHeight}");
+
+ // First convert from normalized to image pixel coordinates
+ var (imagePixelX, imagePixelY, imagePixelWidth, imagePixelHeight) = f.field.ToPixels(
+ (float)imageWidth,
+ (float)imageHeight);
+
+ Console.WriteLine($" Image pixel coords: X={imagePixelX}, Y={imagePixelY}, W={imagePixelWidth}, H={imagePixelHeight}");
+
+ // Then scale from image pixels to PDF points
+ var scaleX = pageWidth / imageWidth;
+ var scaleY = pageHeight / imageHeight;
+
+ var pdfX = imagePixelX * scaleX;
+ var pdfWidth = imagePixelWidth * scaleX;
+ var pdfHeight = imagePixelHeight * scaleY;
+
+ // Convert Y coordinate from top-left origin (UI) to bottom-left origin (PDF).
+ //
+ // UI coordinates:
+ // imagePixelY = distance from TOP of the image.
+ // PDF coordinates:
+ // y = distance from BOTTOM of the page.
+ //
+ // We want the lower-left corner of the widget rectangle in PDF user space:
+ // bottomOfFieldFromTop = imagePixelY + imagePixelHeight
+ // bottomOfFieldFromBottom = imageHeight - bottomOfFieldFromTop
+ // pdfY = bottomOfFieldFromBottom * scaleY
+ var pdfY = (imageHeight - imagePixelY - imagePixelHeight) * scaleY;
+
+ Console.WriteLine($" Scale factors: X={scaleX}, Y={scaleY}");
+ Console.WriteLine($" PDF coordinates: X={pdfX}, Y={pdfY}, W={pdfWidth}, H={pdfHeight}");
+
+ return new
+ {
+ name = f.field.FieldName,
+ page = f.pageIndex,
+ x = pdfX,
+ y = pdfY,
+ width = pdfWidth,
+ height = pdfHeight
+ };
+ }).ToList();
+
+ return JsonConvert.SerializeObject(jsonFields);
+ }
+}
diff --git a/NAPS2.Sdk/Pdf/SignatureFieldPlacement.cs b/NAPS2.Sdk/Pdf/SignatureFieldPlacement.cs
new file mode 100644
index 0000000000..592d5565ab
--- /dev/null
+++ b/NAPS2.Sdk/Pdf/SignatureFieldPlacement.cs
@@ -0,0 +1,49 @@
+namespace NAPS2.Pdf;
+
+///
+/// Represents a signature field placement on a PDF page.
+/// Coordinates are stored as normalized fractions (0.0 to 1.0) relative to page dimensions.
+///
+public record SignatureFieldPlacement(
+ string FieldName,
+ float NormalizedX,
+ float NormalizedY,
+ float NormalizedWidth,
+ float NormalizedHeight,
+ float OriginalImageWidth,
+ float OriginalImageHeight)
+{
+ ///
+ /// Creates a signature field placement from pixel coordinates on a page.
+ ///
+ public static SignatureFieldPlacement FromPixels(
+ string fieldName,
+ float pixelX,
+ float pixelY,
+ float pixelWidth,
+ float pixelHeight,
+ float pageWidth,
+ float pageHeight)
+ {
+ return new SignatureFieldPlacement(
+ fieldName,
+ pixelX / pageWidth,
+ pixelY / pageHeight,
+ pixelWidth / pageWidth,
+ pixelHeight / pageHeight,
+ pageWidth,
+ pageHeight);
+ }
+
+ ///
+ /// Converts normalized coordinates to pixel coordinates for a given page size.
+ ///
+ public (float x, float y, float width, float height) ToPixels(float pageWidth, float pageHeight)
+ {
+ return (
+ NormalizedX * pageWidth,
+ NormalizedY * pageHeight,
+ NormalizedWidth * pageWidth,
+ NormalizedHeight * pageHeight);
+ }
+}
diff --git a/NAPS2.Sdk/Scan/Driver.cs b/NAPS2.Sdk/Scan/Driver.cs
index 2367bb07a6..65234a1b6e 100644
--- a/NAPS2.Sdk/Scan/Driver.cs
+++ b/NAPS2.Sdk/Scan/Driver.cs
@@ -24,7 +24,7 @@ public enum Driver
///
/// Use an Apple ImageCaptureCore driver (Mac-only). You will also need to compile against a macOS framework target
- /// (e.g net8-macos) to use this driver type.
+ /// (e.g net9-macos) to use this driver type.
///
Apple,
diff --git a/NAPS2.Sdk/Scan/Internal/ScanDriverFactory.cs b/NAPS2.Sdk/Scan/Internal/ScanDriverFactory.cs
index 88af5e8c3e..3d6d6404c8 100644
--- a/NAPS2.Sdk/Scan/Internal/ScanDriverFactory.cs
+++ b/NAPS2.Sdk/Scan/Internal/ScanDriverFactory.cs
@@ -37,7 +37,7 @@ public IScanDriver Create(ScanOptions options)
default:
throw new DriverNotSupportedException(
$"Unsupported driver: {options.Driver}. " +
- "Make sure you're using the right framework target (e.g. net8-macos for the Apple driver).");
+ "Make sure you're using the right framework target (e.g. net9-macos for the Apple driver).");
}
}
}
\ No newline at end of file
diff --git a/NAPS2.Sdk/Util/AssemblyHelper.cs b/NAPS2.Sdk/Util/AssemblyHelper.cs
index a99a961944..a34f990f25 100644
--- a/NAPS2.Sdk/Util/AssemblyHelper.cs
+++ b/NAPS2.Sdk/Util/AssemblyHelper.cs
@@ -57,6 +57,12 @@ private static string GetAssemblyAttributeValue(Func selector)
public static Version Version => Assembly.GetEntryAssembly()!.GetName().Version!;
+ public static string InformationalVersion =>
+ GetAssemblyAttributeValue(x => x.InformationalVersion);
+
+ public static string DisplayVersion =>
+ string.IsNullOrWhiteSpace(InformationalVersion) ? Version.ToString() : InformationalVersion;
+
public static string Description => GetAssemblyAttributeValue(x => x.Description);
public static string Product => GetAssemblyAttributeValue(x => x.Product);
@@ -64,4 +70,4 @@ private static string GetAssemblyAttributeValue(Func selector)
public static string Copyright => GetAssemblyAttributeValue(x => x.Copyright);
public static string Company => GetAssemblyAttributeValue(x => x.Company);
-}
\ No newline at end of file
+}
diff --git a/NAPS2.Setup/config/windows/setup.template.iss b/NAPS2.Setup/config/windows/setup.template.iss
index e19d26e78e..011f7269e9 100644
--- a/NAPS2.Setup/config/windows/setup.template.iss
+++ b/NAPS2.Setup/config/windows/setup.template.iss
@@ -65,6 +65,7 @@ Type: files; Name: "{app}\*.exe.config"
Type: files; Name: "{app}\*.dll"
Type: files; Name: "{app}\*.json"
Type: filesandordirs; Name: "{app}\lib"
+Type: filesandordirs; Name: "{app}\tools"
; !clean32
[Icons]
@@ -96,4 +97,4 @@ Root: HKCR; Subkey: ".tif\OpenWithProgids"; ValueType: string; ValueName: "{#App
Root: HKCR; Subkey: ".bmp\OpenWithProgids"; ValueType: string; ValueName: "{#AppShortName}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKCR; Subkey: "{#AppShortName}"; ValueType: string; ValueName: ""; ValueData: "{#AppShortName}"; Flags: uninsdeletekey;
Root: HKCR; Subkey: "{#AppShortName}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#ExeName},0"
-Root: HKCR; Subkey: "{#AppShortName}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeName}"" ""%1"""
\ No newline at end of file
+Root: HKCR; Subkey: "{#AppShortName}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeName}"" ""%1"""
diff --git a/NAPS2.Setup/config/windows/setup.template.wxs b/NAPS2.Setup/config/windows/setup.template.wxs
index fccd520f4b..11adee82f4 100644
--- a/NAPS2.Setup/config/windows/setup.template.wxs
+++ b/NAPS2.Setup/config/windows/setup.template.wxs
@@ -22,6 +22,7 @@
+
@@ -43,6 +44,7 @@
+
@@ -111,6 +113,12 @@
+
+
+
+
+
+
diff --git a/NAPS2.Setup/targets/VersionTargets.targets b/NAPS2.Setup/targets/VersionTargets.targets
index e378efef16..fd7f91e15e 100644
--- a/NAPS2.Setup/targets/VersionTargets.targets
+++ b/NAPS2.Setup/targets/VersionTargets.targets
@@ -1,6 +1,12 @@
- 8.2.1
- 8.2.1
+
+ 8.3.0
+ 8.3.0b1
+ V8.3.0-Beta
diff --git a/NAPS2.Tools/Project/Packaging/DebPackager.cs b/NAPS2.Tools/Project/Packaging/DebPackager.cs
index da7491741a..38b9a2f312 100644
--- a/NAPS2.Tools/Project/Packaging/DebPackager.cs
+++ b/NAPS2.Tools/Project/Packaging/DebPackager.cs
@@ -39,6 +39,18 @@ public static void PackageDeb(PackageInfo pkgInfo, bool noSign)
var targetDir = Path.Combine(workingDir, "usr/lib/naps2");
ProjectHelper.CopyDirectory(publishDir, targetDir);
+ // Optional: include the signature helper executable (built via Nuitka) if present.
+ // Runtime lookup prefers appDir/tools (see SignatureFieldEmbedder.FindBundledHelper()).
+ var signatureHelperPath = Path.Combine(Paths.SolutionRoot, "build", "linux", "naps2-signature-helper");
+ if (File.Exists(signatureHelperPath))
+ {
+ var toolsDir = Path.Combine(targetDir, "tools");
+ Directory.CreateDirectory(toolsDir);
+ var destPath = Path.Combine(toolsDir, "naps2-signature-helper");
+ File.Copy(signatureHelperPath, destPath, true);
+ Cli.Run("chmod", $"+x \"{destPath}\"");
+ }
+
// Copy metadata files
var iconDir = Path.Combine(workingDir, "usr/share/icons/hicolor/128x128/apps");
Directory.CreateDirectory(iconDir);
@@ -76,4 +88,4 @@ public static void PackageDeb(PackageInfo pkgInfo, bool noSign)
Output.OperationEnd($"Packaged deb: {debPath}");
}
-}
\ No newline at end of file
+}
diff --git a/NAPS2.Tools/Project/Packaging/MacPackager.cs b/NAPS2.Tools/Project/Packaging/MacPackager.cs
index fae0fc38cb..ff8f51ce69 100644
--- a/NAPS2.Tools/Project/Packaging/MacPackager.cs
+++ b/NAPS2.Tools/Project/Packaging/MacPackager.cs
@@ -36,6 +36,8 @@ public static void Package(PackageInfo packageInfo, bool noSign, bool noNotarize
Cli.Run("dotnet", $"build NAPS2.App.Mac -c Release -r {runtimeId}");
}
+ IncludeSignatureHelper(bundlePath);
+
Output.Verbose("Building package");
var applicationIdentity = noSign ? "" : N2Config.MacApplicationIdentity;
if (string.IsNullOrEmpty(applicationIdentity) && !noSign)
@@ -87,6 +89,31 @@ public static void Package(PackageInfo packageInfo, bool noSign, bool noNotarize
Output.OperationEnd($"Packaged installer: {pkgPath}");
}
+ private static void IncludeSignatureHelper(string bundlePath)
+ {
+ var sourcePath = Path.Combine(Paths.SolutionRoot, "build", "macos", "naps2-signature-helper");
+ if (!File.Exists(sourcePath))
+ {
+ Output.Info($"Skipping signature helper inclusion as it was not found: {sourcePath}");
+ return;
+ }
+
+ var toolsDir = Path.Combine(bundlePath, "Contents", "tools");
+ var destPath = Path.Combine(toolsDir, "naps2-signature-helper");
+
+ try
+ {
+ Directory.CreateDirectory(toolsDir);
+ File.Copy(sourcePath, destPath, true);
+ Cli.Run("chmod", $"+x \"{destPath}\"");
+ Output.Info($"Included signature helper in app bundle: {destPath}");
+ }
+ catch (Exception ex)
+ {
+ Output.Info($"Failed to include signature helper in app bundle; continuing without it. {ex}");
+ }
+ }
+
private static void SignBundleContents(string bundlePath, string signingIdentity)
{
var dirInfo = new DirectoryInfo(bundlePath);
@@ -103,4 +130,4 @@ private static void SignBundleContents(string bundlePath, string signingIdentity
Cli.Run("codesign", $"-s \"{signingIdentity}\" -f --options runtime \"{files}\"");
}
}
-}
\ No newline at end of file
+}
diff --git a/NAPS2.Tools/Project/Packaging/PackageCommand.cs b/NAPS2.Tools/Project/Packaging/PackageCommand.cs
index 714555cfe0..e7ddecae28 100644
--- a/NAPS2.Tools/Project/Packaging/PackageCommand.cs
+++ b/NAPS2.Tools/Project/Packaging/PackageCommand.cs
@@ -92,6 +92,15 @@ private static PackageInfo GetPackageInfo(Platform platform, string? packageName
pkgInfo.AddFile(new PackageFile(Paths.SolutionRoot, "", "LICENSE", "license.txt"));
pkgInfo.AddFile(new PackageFile(Paths.SolutionRoot, "", "CONTRIBUTORS", "contributors.txt"));
+ // Optional: include the signature helper executable (built via Nuitka) if present.
+ // Runtime lookup prefers appDir/tools (see SignatureFieldEmbedder.FindBundledHelper()).
+ var signatureHelperPath = Path.Combine(Paths.SolutionRoot, "build", "windows", "naps2-signature-helper.exe");
+ if (File.Exists(signatureHelperPath))
+ {
+ pkgInfo.AddFile(new PackageFile(Path.GetDirectoryName(signatureHelperPath)!, "tools",
+ Path.GetFileName(signatureHelperPath)));
+ }
+
return pkgInfo;
}
@@ -268,4 +277,4 @@ private static void AddPlatformFile(PackageInfo pkgInfo, string buildPath, strin
pkgInfo.AddFile(new PackageFile(Path.Combine(buildPath, platformPath), Path.Combine("lib", platformPath),
fileName));
}
-}
\ No newline at end of file
+}
diff --git a/NAPS2.Tools/Project/Packaging/RpmPackager.cs b/NAPS2.Tools/Project/Packaging/RpmPackager.cs
index 6b0fb43a8f..510384cc32 100644
--- a/NAPS2.Tools/Project/Packaging/RpmPackager.cs
+++ b/NAPS2.Tools/Project/Packaging/RpmPackager.cs
@@ -44,6 +44,18 @@ public static void PackageRpm(PackageInfo pkgInfo, bool noSign)
var targetDir = Path.Combine(filesDir, "usr/lib/naps2");
ProjectHelper.CopyDirectory(publishDir, targetDir);
+ // Optional: include the signature helper executable (built via Nuitka) if present.
+ // Runtime lookup prefers appDir/tools (see SignatureFieldEmbedder.FindBundledHelper()).
+ var signatureHelperPath = Path.Combine(Paths.SolutionRoot, "build", "linux", "naps2-signature-helper");
+ if (File.Exists(signatureHelperPath))
+ {
+ var toolsDir = Path.Combine(targetDir, "tools");
+ Directory.CreateDirectory(toolsDir);
+ var destPath = Path.Combine(toolsDir, "naps2-signature-helper");
+ File.Copy(signatureHelperPath, destPath, true);
+ Cli.Run("chmod", $"+x \"{destPath}\"");
+ }
+
// Copy metadata files
var iconDir = Path.Combine(filesDir, "usr/share/icons/hicolor/128x128/apps");
Directory.CreateDirectory(iconDir);
@@ -88,4 +100,4 @@ public static void PackageRpm(PackageInfo pkgInfo, bool noSign)
Output.OperationEnd($"Packaged rpm: {rpmPath}");
}
-}
\ No newline at end of file
+}
diff --git a/NAPS2.Tools/Project/Packaging/WixToolsetPackager.cs b/NAPS2.Tools/Project/Packaging/WixToolsetPackager.cs
index 490131ad6e..a991ee43f4 100644
--- a/NAPS2.Tools/Project/Packaging/WixToolsetPackager.cs
+++ b/NAPS2.Tools/Project/Packaging/WixToolsetPackager.cs
@@ -66,6 +66,13 @@ private static string GenerateWxs(PackageInfo packageInfo)
}
template = template.Replace("", libLines.ToString());
+ var toolsLines = new StringBuilder();
+ foreach (var toolsFile in packageInfo.Files.Where(x => x.DestDir == "tools"))
+ {
+ DeclareFile(toolsLines, toolsFile);
+ }
+ template = template.Replace("", toolsLines.ToString());
+
var win32Lines = new StringBuilder();
foreach (var win32File in packageInfo.Files.Where(x => x.DestDir == Path.Combine("lib", "_win32")))
{
@@ -120,4 +127,4 @@ private static string ToId(string raw)
{
return Regex.Replace(raw, @"[^a-zA-Z0-9]+", "_");
}
-}
\ No newline at end of file
+}
diff --git a/docs/SIGNATURE_FIELD_IMPLEMENTATION.md b/docs/SIGNATURE_FIELD_IMPLEMENTATION.md
new file mode 100644
index 0000000000..20448b0e04
--- /dev/null
+++ b/docs/SIGNATURE_FIELD_IMPLEMENTATION.md
@@ -0,0 +1,175 @@
+# PDF Signature Field Feature - Implementation Summary
+
+## Overview
+This document summarizes the implementation of the PDF signature field placement feature for NAPS2.
+
+## Feature Description
+Users can now place signature fields on PDF pages using a mouse-drag interface. The signature fields are persisted in exported PDFs using pyHanko (vendored Python library).
+
+## Files Added
+
+### 1. Data Model
+- **`NAPS2.Sdk/Pdf/SignatureFieldPlacement.cs`**
+ - Record type for storing signature field placements
+ - Uses normalized coordinates (0.0-1.0) for resolution independence
+ - Provides conversion methods between normalized and pixel coordinates
+
+### 2. UI Components
+- **`NAPS2.Lib/EtoForms/Ui/SignatureFieldForm.cs`**
+ - Modal form for placing signature fields via mouse drag
+ - Based on existing `CropForm` pattern
+ - Shows existing fields and allows placing new ones
+ - Stores fields in image's `PostProcessingData`
+
+### 3. PDF Export Integration
+- **`NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs`**
+ - C# helper class to invoke Python script
+ - Finds Python executable and script path
+ - Handles graceful degradation when Python/pyHanko unavailable
+ - Converts normalized coordinates to PDF points
+
+### 4. Python Script
+- **`scripts/embed_signature_fields.py`**
+ - Python script using vendored pyHanko to embed signature fields
+ - Automatically adds vendored pyHanko source to Python path
+ - Takes input PDF, output PDF, and JSON field data
+ - Handles coordinate conversion (PDF uses bottom-left origin)
+ - Provides clear error messages for missing dependencies
+
+### 5. Vendored Dependencies
+- **`third_party/pyHanko/`** (Git submodule)
+ - pyHanko source code vendored as a git submodule
+ - Pinned to commit: b89f139e5c5e0f9895a39686ff2dc4c74dd23ba8
+ - Includes pyHanko and pyhanko-certvalidator packages
+ - Licensed under MIT (see `third_party/pyHanko/LICENSE`)
+
+- **`scripts/requirements-signature-fields.txt`**
+ - Lists runtime dependencies required by pyHanko
+ - Users only need to install these dependencies, not pyHanko itself
+
+### 6. Documentation
+- **`docs/SIGNATURE_FIELD_TESTING.md`**
+ - Comprehensive manual testing procedure
+ - Prerequisites and setup instructions
+ - Expected results and troubleshooting guide
+
+## Files Modified
+
+### 1. Data Model Extension
+- **`NAPS2.Sdk/Images/PostProcessingData.cs`**
+ - Added `SignatureFields` property (nullable list)
+ - Allows storing signature field placements with each image
+
+### 2. PDF Export Pipeline
+- **`NAPS2.Sdk/Pdf/PdfExporter.cs`**
+ - Added `EmbedSignatureFields()` method
+ - Collects signature fields from all pages after PDF creation
+ - Invokes `SignatureFieldEmbedder` to apply fields
+ - Handles both file and stream outputs
+
+### 3. UI Integration
+- **`NAPS2.Lib/EtoForms/Desktop/IDesktopSubFormController.cs`**
+ - Added `ShowSignatureFieldForm()` method declaration
+
+- **`NAPS2.Lib/EtoForms/Desktop/DesktopSubFormController.cs`**
+ - Implemented `ShowSignatureFieldForm()` method
+
+- **`NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs`**
+ - Added `SignatureField` command property
+ - Initialized command with text and icon
+
+- **`NAPS2.Lib/EtoForms/Ui/DesktopForm.cs`**
+ - Added `SignatureField` command to Image menu
+ - Placed after Crop command for logical grouping
+
+## Architecture Decisions
+
+### 1. Coordinate System
+- **Normalized Coordinates**: Fields stored as fractions (0.0-1.0) of page dimensions
+- **Rationale**: Resolution-independent, works across different page sizes and DPI settings
+- **Conversion**: Happens at export time based on actual PDF page dimensions
+
+### 2. Storage Location
+- **PostProcessingData**: Signature fields stored alongside other metadata (barcode, page number, etc.)
+- **Rationale**: Consistent with existing architecture, properly disposed with image lifecycle
+- **Limitation**: Fields are session-only (not persisted when reopening NAPS2)
+
+### 3. Python Integration
+- **Subprocess Invocation**: C# spawns Python process to run pyHanko script
+- **Vendored Source**: pyHanko source is included in the repository as a git submodule
+- **Rationale**: pyHanko is a mature Python library; reimplementing in C# would be complex
+- **Graceful Degradation**: If Python/dependencies unavailable, PDF exports without fields (with warning)
+
+### 4. UI Pattern
+- **Modal Form**: Based on existing `UnaryImageFormBase` pattern (like CropForm)
+- **Mouse Drag**: Familiar interaction pattern for defining rectangular areas
+- **Visual Feedback**: Semi-transparent overlay shows field placement
+
+## Dependencies
+
+### Required for Full Functionality
+- **Python 3.x**: Must be in system PATH
+- **pyHanko runtime dependencies**: Install via `pip install -r scripts/requirements-signature-fields.txt`
+ - asn1crypto, tzlocal, requests, pyyaml, cryptography, lxml, oscrypto, uritools
+- **pyHanko source**: Vendored in `third_party/pyHanko` (no separate installation needed)
+
+### Fallback Behavior
+- If Python not found: Warning logged, PDF exports without signature fields
+- If dependencies not installed: Warning logged, PDF exports without signature fields
+- If script not found: Warning logged, PDF exports without signature fields
+
+## Known Limitations
+
+1. **Session-Only Storage**: Signature fields not persisted when closing/reopening NAPS2
+2. **No Field Editing**: Cannot edit or delete placed fields (must reopen tool to add more)
+3. **Auto-Generated Names**: Field names are GUIDs, not user-customizable
+4. **Single Field Per Session**: MVP allows placing one field at a time per page
+5. **No Visual Indicator**: Thumbnails don't show which pages have signature fields
+
+## Testing
+
+### Manual Testing
+Follow the procedure in [`docs/SIGNATURE_FIELD_TESTING.md`](../docs/SIGNATURE_FIELD_TESTING.md)
+
+### Key Test Cases
+1. Place signature field and export to PDF
+2. Verify field appears in PDF viewer (Adobe Acrobat, Foxit, etc.)
+3. Test with Python/dependencies unavailable (graceful degradation)
+4. Test multiple fields on same page
+5. Test fields on multiple pages
+
+## Future Enhancements
+
+Potential improvements for future versions:
+- Persistent storage of signature fields (save/load with project)
+- Field editing/deletion UI
+- Custom field names and properties
+- Visual indicators in thumbnail view
+- Support for other field types (text, checkbox, radio button)
+- Batch placement across multiple pages
+- Field templates/presets
+
+## Assumptions
+
+As specified in the task:
+1. **Coordinate Mapping**: Normalized fractions ensure accuracy across typical page sizes
+2. **pyHanko Suitability**: Confirmed as appropriate for AcroForm signature field embedding
+3. **Conservative Approach**: Minimal changes to existing code, feature behind UI command
+4. **Error Handling**: Robust error messages when dependencies unavailable
+
+## Build Considerations
+
+- No breaking changes to existing PDF export when feature unused
+- All new files follow existing NAPS2 code conventions
+- Python script is standalone and can be tested independently
+- C# code compiles on all supported platforms (Windows, macOS, Linux)
+
+## Summary
+
+This implementation provides a complete MVP for PDF signature field placement:
+- ✅ UI tool for mouse-drag field placement
+- ✅ Data model with normalized coordinates
+- ✅ PDF export integration with pyHanko
+- ✅ Graceful degradation when dependencies unavailable
+- ✅ Comprehensive testing documentation
+- ✅ No breaking changes to existing functionality
diff --git a/docs/SIGNATURE_FIELD_TESTING.md b/docs/SIGNATURE_FIELD_TESTING.md
new file mode 100644
index 0000000000..e38f6c8ab5
--- /dev/null
+++ b/docs/SIGNATURE_FIELD_TESTING.md
@@ -0,0 +1,184 @@
+# PDF Signature Field Placement - Manual Test Procedure
+
+## Overview
+This document describes how to manually test the PDF signature field placement feature in NAPS2.
+
+## Prerequisites
+
+### Required Software
+1. **NAPS2** - Build and run the application
+2. **Python 3.x** - Required for signature field embedding
+3. **pyHanko dependencies** - Python libraries required by pyHanko (vendored in this repository)
+ ```bash
+ pip install -r scripts/requirements-signature-fields.txt
+ ```
+ Note: pyHanko itself is vendored in `third_party/pyHanko` and does not need to be installed separately.
+4. **PDF Viewer** - Adobe Acrobat Reader, Foxit Reader, or similar that displays form fields
+
+### Optional (for verification)
+- A PDF viewer that highlights form fields (e.g., Adobe Acrobat Reader)
+
+## Test Procedure
+
+### Part 1: Setup and Basic Functionality
+
+1. **Start NAPS2**
+ - Launch the NAPS2 application
+ - Ensure you have at least one scanned or imported image/PDF page
+
+2. **Access the Signature Field Tool**
+ - Select an image/page in the thumbnail list
+ - Go to the **Image** menu
+ - Click on **Place Signature Field** (or use the toolbar button if visible)
+ - The signature field placement dialog should open
+
+3. **Place a Signature Field**
+ - In the signature field dialog, you should see your selected page displayed
+ - Click and drag on the page to create a rectangle
+ - The rectangle should appear with a colored border while dragging
+ - Release the mouse button to finalize the field placement
+ - The field should remain visible as a semi-transparent overlay
+
+4. **Apply the Field**
+ - Click the **OK** button to apply the signature field
+ - The dialog should close
+ - The field placement is now stored with the image
+
+### Part 2: Export and Verification
+
+5. **Export to PDF**
+ - With the image(s) that have signature fields, go to **File** → **Save PDF** (or **Save All as PDF**)
+ - Choose a location and filename for the PDF
+ - Click **Save**
+ - Wait for the export to complete
+
+6. **Check Export Logs** (Optional)
+ - If Python or pyHanko dependencies are not installed, you should see a warning in the logs
+ - The PDF will still be created, but without embedded signature fields
+ - If dependencies are installed, you should see a success message
+
+7. **Verify Signature Fields in PDF**
+ - Open the exported PDF in a PDF viewer that supports form fields
+ - **Adobe Acrobat Reader**:
+ - The signature field should appear as a clickable area
+ - Right-click on the field → **Properties** to see field details
+ - **Foxit Reader**:
+ - Signature fields should be visible and highlighted
+ - **Preview (macOS)**:
+ - May not show signature fields (limited form support)
+
+### Part 3: Multiple Fields and Pages
+
+8. **Place Multiple Fields**
+ - Open the signature field tool again on the same page
+ - Place another signature field in a different location
+ - Click **OK**
+ - Both fields should be stored
+
+9. **Fields on Different Pages**
+ - If you have multiple pages, select a different page
+ - Open the signature field tool
+ - Place a signature field on this page
+ - Export to PDF again
+
+10. **Verify Multiple Fields**
+ - Open the exported PDF
+ - Navigate through pages
+ - Verify that signature fields appear on the correct pages
+ - Each field should be independently clickable
+
+### Part 4: Edge Cases
+
+11. **No Python/pyHanko Dependencies Installed**
+ - Temporarily rename or remove Python from PATH
+ - Place signature fields and export
+ - Verify that:
+ - Export completes without crashing
+ - A warning is logged
+ - PDF is created (without signature fields)
+
+12. **Empty Field Placement**
+ - Open the signature field tool
+ - Click **OK** without placing any field
+ - Verify no error occurs
+
+13. **Very Small Fields**
+ - Place a very small signature field (just a few pixels)
+ - Export and verify it appears in the PDF
+
+14. **Very Large Fields**
+ - Place a signature field covering most of the page
+ - Export and verify it appears correctly
+
+## Expected Results
+
+### Success Criteria
+✅ Signature field tool opens without errors
+✅ User can drag to create a visible rectangle
+✅ Field placement is stored with the image
+✅ PDF export completes successfully
+✅ When pyHanko dependencies are available, signature fields are embedded in PDF
+✅ Signature fields are visible and functional in PDF viewers
+✅ Multiple fields can be placed on the same page
+✅ Fields can be placed on different pages
+✅ Graceful degradation when Python/pyHanko dependencies are unavailable
+
+### Known Limitations
+- Signature fields are not editable after placement (must reopen tool to add more)
+- Field names are auto-generated (not user-customizable in MVP)
+- No visual indicator in thumbnail view showing which pages have signature fields
+- Signature fields are not preserved when reopening NAPS2 (stored in session only)
+
+## Troubleshooting
+
+### Issue: Signature field tool doesn't appear in menu
+- **Solution**: Ensure you have selected at least one image/page first
+
+### Issue: Python not found error
+- **Solution**: Install Python 3.x and ensure it's in your system PATH
+- **Verify**: Run `python --version` or `python3 --version` in terminal
+
+### Issue: pyHanko dependencies not installed error
+- **Solution**: Run `pip install -r scripts/requirements-signature-fields.txt` in terminal
+- **Verify**: Run `python -c "import pyhanko; print('OK')"` (should work with vendored pyHanko)
+
+### Issue: Signature fields don't appear in PDF
+- **Solution**:
+ 1. Check that Python and pyHanko dependencies are installed
+ 2. Check application logs for errors
+ 3. Try a different PDF viewer (some viewers don't display form fields)
+
+### Issue: Script not found error
+- **Solution**: Ensure `scripts/embed_signature_fields.py` exists in the repository
+- The script should be found relative to the application directory
+
+## Test Report Template
+
+```
+Test Date: _______________
+Tester: _______________
+NAPS2 Version: _______________
+Python Version: _______________
+pyHanko Dependencies Installed: Yes / No
+
+Test Results:
+[ ] Part 1: Setup and Basic Functionality - PASS / FAIL
+[ ] Part 2: Export and Verification - PASS / FAIL
+[ ] Part 3: Multiple Fields and Pages - PASS / FAIL
+[ ] Part 4: Edge Cases - PASS / FAIL
+
+Notes:
+_________________________________
+_________________________________
+_________________________________
+```
+
+## Additional Notes
+
+- The signature field feature is designed as an MVP (Minimum Viable Product)
+- Future enhancements may include:
+ - Persistent storage of signature fields
+ - Field editing/deletion UI
+ - Custom field names and properties
+ - Visual indicators in thumbnail view
+ - Support for other field types (text, checkbox, etc.)
diff --git a/docs/signature-helper-build.md b/docs/signature-helper-build.md
new file mode 100644
index 0000000000..1be9e14bbb
--- /dev/null
+++ b/docs/signature-helper-build.md
@@ -0,0 +1,301 @@
+# Building & Updating the Signature Helper (macOS / Linux / Windows)
+
+This document explains how to build and update the **signature helper** executable shipped with NAPS2.
+
+Primary entry points:
+
+* Build wrapper: [`scripts/build_helper.sh`](../scripts/build_helper.sh:1)
+* Build script: [`scripts/build_embedder_helper.py`](../scripts/build_embedder_helper.py:1)
+* Source (Python): [`scripts/embed_signature_fields.py`](../scripts/embed_signature_fields.py:1)
+* Build dependencies: [`pyproject.toml`](../pyproject.toml:1) (`[project.optional-dependencies]` → `build`)
+* macOS packaging hook: [`MacPackager.IncludeSignatureHelper()`](../NAPS2.Tools/Project/Packaging/MacPackager.cs:92)
+
+---
+
+## 1) Overview
+
+### What is the signature helper?
+
+The signature helper is a small executable that embeds **AcroForm signature fields** into a PDF.
+
+Functionally, it is a compiled form of the Python script [`scripts/embed_signature_fields.py`](../scripts/embed_signature_fields.py:1), which uses the vendored pyHanko source tree to modify PDFs.
+
+At runtime, NAPS2 prefers to call the helper (no Python interpreter required). If the helper cannot be found, NAPS2 falls back to running the Python script.
+
+Runtime integration is implemented primarily in [`SignatureFieldEmbedder.EmbedFields()`](../NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs:28), specifically:
+
+* Helper discovery: [`SignatureFieldEmbedder.FindBundledHelper()`](../NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs:242)
+* Fallback script discovery: [`SignatureFieldEmbedder.FindScriptPath()`](../NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs:356)
+* Fallback Python discovery: [`SignatureFieldEmbedder.FindPythonExecutable()`](../NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs:301)
+
+### Why bundle an executable instead of running Python?
+
+Bundling a helper executable has several benefits over invoking a Python script:
+
+* **No Python runtime required** on the end user’s machine.
+* **Predictable dependency set**: the build step bundles the runtime dependencies needed by pyHanko.
+* **Better UX**: fewer “Python not found” / “dependency missing” failures.
+* **App-store/notarization friendly**: packaging can include the helper and code signing can cover it as part of the bundle.
+
+The fallback to Python remains useful for development and for environments where the helper is not present.
+
+---
+
+## 2) Prerequisites
+
+### Required tooling
+
+1. **Python 3.11+**
+ * The repository requires `>=3.11` (see [`pyproject.toml`](../pyproject.toml:6)).
+
+2. **uv** (Astral’s Python package manager)
+ * The wrapper script [`scripts/build_helper.sh`](../scripts/build_helper.sh:1) uses `uv` for venv creation and dependency installation.
+
+3. **Nuitka**
+ * The build script checks for Nuitka at startup (see [`check_prerequisites()`](../scripts/build_embedder_helper.py:30)).
+ * Nuitka is provided via the `build` optional dependency group (see [`pyproject.toml`](../pyproject.toml:18)).
+
+4. **Xcode Command Line Tools**
+ * Required for compiling native extensions and linking the final executable.
+ * Install:
+ ```bash
+ xcode-select --install
+ ```
+
+### Platform notes
+
+* The helper can be built for macOS, Linux, and Windows.
+* On macOS, the build configuration targets Apple Silicon (`arm64`) by default via [`--macos-target-arch=arm64`](../scripts/build_embedder_helper.py:223).
+* On Windows, Nuitka requires a working C toolchain (MSVC Build Tools).
+
+### Git submodules (pyHanko)
+
+The helper bundles vendored pyHanko and pyhanko-certvalidator from the pyHanko git submodule.
+
+* Submodule documentation: [`third_party/README.md`](../third_party/README.md:1)
+* The build script verifies the submodule by checking for the vendored source directories (see [`check_prerequisites()`](../scripts/build_embedder_helper.py:30)):
+ * [`third_party/pyHanko/pkgs/pyhanko/src`](../third_party/pyHanko/pkgs/pyhanko/src)
+ * [`third_party/pyHanko/pkgs/pyhanko-certvalidator/src`](../third_party/pyHanko/pkgs/pyhanko-certvalidator/src)
+
+Initialize submodules:
+
+```bash
+git submodule update --init --recursive
+```
+
+---
+
+## 3) Building the Helper
+
+### Recommended: wrapper script
+
+Use the wrapper [`scripts/build_helper.sh`](../scripts/build_helper.sh:1). It:
+
+* checks that `uv` is installed,
+* creates a `.venv` if missing,
+* installs build dependencies from [`pyproject.toml`](../pyproject.toml:1),
+* runs the build script with `uv run`.
+
+From the repository root:
+
+```bash
+./scripts/build_helper.sh
+```
+
+### Direct: invoke the Python build script
+
+If you want to run the build without the wrapper, ensure you have a venv and build deps installed:
+
+```bash
+uv venv
+uv pip install -e ".[build]"
+uv run python ./scripts/build_embedder_helper.py
+```
+
+### Expected output
+
+The build script writes into the output directory configured in [`build_executable()`](../scripts/build_embedder_helper.py:71) and uses the filename set by [`--output-filename=naps2-signature-helper`](../scripts/build_embedder_helper.py:103).
+
+On success, the script prints:
+
+* “Build completed successfully!”
+* “Executable location: …” (printed by [`build_executable()`](../scripts/build_embedder_helper.py:71))
+
+### Quick verification
+
+1. Confirm the file exists and is executable:
+
+ ```bash
+ ls -la ./build/macos/
+ file ./build/macos/naps2-signature-helper
+ ```
+
+2. Run the helper on any PDF you have available:
+
+ ```bash
+ ./build/macos/naps2-signature-helper \
+ ./input.pdf \
+ ./output.pdf \
+ '[{"name":"Sig1","page":0,"x":72,"y":72,"width":200,"height":50}]'
+ ```
+
+3. Open `output.pdf` in a PDF viewer with form support (Adobe Acrobat Reader, Foxit, etc.) to confirm the signature widget is present.
+
+### Troubleshooting
+
+#### Build fails: “uv is not installed”
+
+* This comes from [`scripts/build_helper.sh`](../scripts/build_helper.sh:38).
+* Install uv (see https://github.com/astral-sh/uv) and retry.
+
+#### Build fails: “Nuitka is not installed”
+
+* This comes from [`check_prerequisites()`](../scripts/build_embedder_helper.py:30).
+* Fix by installing the build dependency group:
+
+ ```bash
+ uv pip install -e ".[build]"
+ ```
+
+#### Build fails: “pyHanko source not found … Initialize submodules …”
+
+* This comes from [`check_prerequisites()`](../scripts/build_embedder_helper.py:41).
+* Fix by initializing submodules:
+
+ ```bash
+ git submodule update --init --recursive
+ ```
+
+#### Build fails on compilation/linking
+
+Common causes:
+
+* Missing Xcode Command Line Tools
+ ```bash
+ xcode-select --install
+ ```
+* A stale or partially-built Nuitka output directory
+ * The build uses [`--remove-output`](../scripts/build_embedder_helper.py:109), but if a previous run was interrupted, remove the output directory and rebuild:
+ ```bash
+ rm -rf ./build/macos
+ ./scripts/build_helper.sh
+ ```
+
+---
+
+## 4) Integration with NAPS2
+
+### How the helper is discovered and used at runtime
+
+At export time, NAPS2 tries to embed signature fields by spawning a helper process.
+
+1. NAPS2 searches for a bundled helper executable first:
+ * See [`SignatureFieldEmbedder.FindBundledHelper()`](../NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs:242).
+ * It searches for the base name `naps2-signature-helper` (and also a Windows-style `.exe` name) and checks several directories relative to `AppDomain.CurrentDomain.BaseDirectory`.
+
+2. If found, NAPS2 invokes the helper with:
+ * `inputPdfPath`, `outputPdfPath`, and a JSON array of field placements.
+ * See the helper invocation code in [`SignatureFieldEmbedder.EmbedFields()`](../NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs:28).
+
+### Fallback behavior (Python script)
+
+If the helper is not found, NAPS2 logs and falls back:
+
+* Fallback branch starts in [`SignatureFieldEmbedder.EmbedFields()`](../NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs:28)
+* Python executable discovery: [`SignatureFieldEmbedder.FindPythonExecutable()`](../NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs:301)
+* Script discovery: [`SignatureFieldEmbedder.FindScriptPath()`](../NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs:356)
+
+If Python or dependencies are missing, NAPS2 will still export a PDF, but **without embedded fields** (it copies the input PDF to the output PDF).
+
+### Where the helper is placed in the `.app` bundle
+
+During macOS packaging, the helper is copied into the app bundle under:
+
+* `NAPS2.app/Contents/tools/naps2-signature-helper`
+
+The copy logic is implemented in [`MacPackager.IncludeSignatureHelper()`](../NAPS2.Tools/Project/Packaging/MacPackager.cs:92):
+
+* Source path (expected build output): `build/macos/naps2-signature-helper` (see [`sourcePath`](../NAPS2.Tools/Project/Packaging/MacPackager.cs:94))
+* Destination inside bundle: `Contents/tools/naps2-signature-helper` (see [`destPath`](../NAPS2.Tools/Project/Packaging/MacPackager.cs:102))
+* The packager also runs `chmod +x` (see [`Cli.Run("chmod", …)`](../NAPS2.Tools/Project/Packaging/MacPackager.cs:108)).
+
+If the helper is not present at packaging time, the packager logs and continues without it.
+
+---
+
+## 5) Updating the Helper
+
+### When you should rebuild
+
+Rebuild the helper when:
+
+* [`scripts/embed_signature_fields.py`](../scripts/embed_signature_fields.py:1) changes.
+* The build configuration changes (e.g., Nuitka flags in [`build_executable()`](../scripts/build_embedder_helper.py:71)).
+* The vendored pyHanko submodule changes (see [`third_party/README.md`](../third_party/README.md:29)).
+* Python runtime dependencies change (see [`pyproject.toml`](../pyproject.toml:7)), especially `cryptography`, `lxml`, or `asn1crypto`.
+
+### Updating dependencies
+
+There are two dependency “layers” to be aware of:
+
+1. **Vendored pyHanko source** (git submodule)
+ * Update instructions: [`third_party/README.md`](../third_party/README.md:29)
+ * After updating, rebuild the helper so the new vendored code is bundled.
+
+2. **Python runtime deps** that pyHanko imports at runtime
+ * These are listed as project dependencies in [`pyproject.toml`](../pyproject.toml:7).
+ * If you adjust versions, rebuild the helper to pick up the changes.
+
+### Testing an updated helper
+
+Minimum “smoke test” after rebuild:
+
+1. Run the helper directly on a PDF (see the verification command in [Quick verification](#quick-verification)).
+2. Export a PDF from NAPS2 with signature fields on macOS and confirm NAPS2 reports it is using the bundled helper (emitted by [`SignatureFieldEmbedder.EmbedFields()`](../NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs:28)).
+3. Verify the output in a viewer that supports form fields.
+
+For end-to-end testing guidance, see [`docs/SIGNATURE_FIELD_TESTING.md`](SIGNATURE_FIELD_TESTING.md:1).
+
+---
+
+## 6) Development Notes
+
+### Architecture-specific builds (arm64 vs universal)
+
+The current build script hardcodes Apple Silicon:
+
+* [`--macos-target-arch=arm64`](../scripts/build_embedder_helper.py:106)
+
+To produce an Intel build, you must adjust the target arch in [`scripts/build_embedder_helper.py`](../scripts/build_embedder_helper.py:1) (e.g., to `x86_64`) and rebuild.
+
+To produce a universal build, the common approach is:
+
+1. Build one helper for `arm64`.
+2. Build one helper for `x86_64`.
+3. Combine them with `lipo`.
+
+Note: This repository currently does not provide an automated “universal helper” script; if you implement one, keep it consistent with the location expected by [`MacPackager.IncludeSignatureHelper()`](../NAPS2.Tools/Project/Packaging/MacPackager.cs:92).
+
+### Build performance tips (ccache)
+
+Nuitka compilation can be slow due to C compilation.
+
+If you have `ccache` available, you can often speed up rebuilds by caching compilation artifacts:
+
+```bash
+brew install ccache
+export CC="ccache clang"
+export CXX="ccache clang++"
+./scripts/build_helper.sh
+```
+
+### Debugging the helper
+
+Helpful toggles live in [`scripts/build_embedder_helper.py`](../scripts/build_embedder_helper.py:1):
+
+* Console output: the build uses [`--disable-console`](../scripts/build_embedder_helper.py:137).
+ * For debugging, remove this flag so you can see stdout/stderr when launching the helper from the Finder.
+* Keep intermediate build artifacts: the build uses [`--remove-output`](../scripts/build_embedder_helper.py:109).
+ * For debugging build failures, temporarily remove this flag so Nuitka’s build directories remain.
+
+At runtime, NAPS2 logs the searched paths and the chosen strategy (helper vs Python) in [`SignatureFieldEmbedder.EmbedFields()`](../NAPS2.Sdk/Pdf/SignatureFieldEmbedder.cs:28). That is typically the fastest way to diagnose “helper not found” or “execution failed” issues.
diff --git a/docs/signature-helper-licenses.md b/docs/signature-helper-licenses.md
new file mode 100644
index 0000000000..6d36649f63
--- /dev/null
+++ b/docs/signature-helper-licenses.md
@@ -0,0 +1,181 @@
+# Signature Helper - License Analysis
+
+This document provides a comprehensive analysis of all licenses for components bundled in the NAPS2 signature helper executable.
+
+## Summary
+
+The NAPS2 signature helper is a standalone executable that bundles several open-source components. All bundled components use permissive licenses (MIT, Apache 2.0, BSD, PSF) that are compatible with NAPS2's GPLv2 license.
+
+**Key Points:**
+- The executable is compiled using Nuitka (Apache 2.0), which converts Python code to C
+- Nuitka itself is NOT included in the executable - only the compiled application code
+- The Python runtime (CPython) IS included and uses the PSF License
+- All dependencies use permissive licenses compatible with GPLv2
+
+## License Compatibility
+
+**NAPS2 License**: GNU General Public License v2.0 (GPLv2)
+
+**Bundled Components**: All use permissive licenses (MIT, Apache 2.0, BSD) which are compatible with GPLv2. The GPLv2 allows linking with MIT/Apache/BSD-licensed code, and the resulting combined work is distributed under GPLv2.
+
+## Component Licenses
+
+### 1. pyHanko
+- **License**: MIT License
+- **Copyright**: Copyright (c) 2020-2023 Matthias Valvekens
+- **Source**: https://github.com/MatthiasValvekens/pyHanko
+- **License File**: [`third_party/pyHanko/LICENSE`](../third_party/pyHanko/LICENSE)
+- **Compatibility**: ✅ MIT is compatible with GPLv2
+
+### 2. pyhanko-certvalidator
+- **License**: MIT License
+- **Copyright**:
+ - Copyright (c) 2015-2018 Will Bond
+ - Copyright (c) 2020-2023 Matthias Valvekens
+- **Source**: https://github.com/MatthiasValvekens/pyHanko (subpackage)
+- **License File**: [`third_party/pyHanko/pkgs/pyhanko-certvalidator/LICENSE`](../third_party/pyHanko/pkgs/pyhanko-certvalidator/LICENSE)
+- **Compatibility**: ✅ MIT is compatible with GPLv2
+
+### 3. asn1crypto
+- **License**: MIT License
+- **Copyright**: Copyright (c) 2015-2024 Will Bond
+- **Source**: https://github.com/wbond/asn1crypto
+- **PyPI**: https://pypi.org/project/asn1crypto/
+- **Compatibility**: ✅ MIT is compatible with GPLv2
+
+### 4. cryptography
+- **License**: Apache License 2.0 and BSD License (dual-licensed)
+- **Copyright**: Copyright (c) Individual contributors
+- **Source**: https://github.com/pyca/cryptography
+- **PyPI**: https://pypi.org/project/cryptography/
+- **Compatibility**: ✅ Apache 2.0 and BSD are compatible with GPLv2
+
+### 5. lxml
+- **License**: BSD License
+- **Copyright**: Copyright (c) 2004 Infrae
+- **Source**: https://github.com/lxml/lxml
+- **PyPI**: https://pypi.org/project/lxml/
+- **Compatibility**: ✅ BSD is compatible with GPLv2
+
+### 6. oscrypto
+- **License**: MIT License
+- **Copyright**: Copyright (c) 2015-2018 Will Bond
+- **Source**: https://github.com/wbond/oscrypto
+- **PyPI**: https://pypi.org/project/oscrypto/
+- **Compatibility**: ✅ MIT is compatible with GPLv2
+
+### 7. requests
+- **License**: Apache License 2.0
+- **Copyright**: Copyright 2019 Kenneth Reitz
+- **Source**: https://github.com/psf/requests
+- **PyPI**: https://pypi.org/project/requests/
+- **Compatibility**: ✅ Apache 2.0 is compatible with GPLv2
+
+### 8. tzlocal
+- **License**: MIT License
+- **Copyright**: Copyright (c) 2011-2017 Lennart Regebro
+- **Source**: https://github.com/regebro/tzlocal
+- **PyPI**: https://pypi.org/project/tzlocal/
+- **Compatibility**: ✅ MIT is compatible with GPLv2
+
+### 9. pyyaml
+- **License**: MIT License
+- **Copyright**: Copyright (c) 2017-2021 Ingy döt Net, Copyright (c) 2006-2016 Kirill Simonov
+- **Source**: https://github.com/yaml/pyyaml
+- **PyPI**: https://pypi.org/project/PyYAML/
+- **Compatibility**: ✅ MIT is compatible with GPLv2
+
+### 10. uritools
+- **License**: MIT License
+- **Copyright**: Copyright (c) 2014-2024 Thomas Kemmer
+- **Source**: https://github.com/tkem/uritools
+- **PyPI**: https://pypi.org/project/uritools/
+- **Compatibility**: ✅ MIT is compatible with GPLv2
+
+### 11. Nuitka (Compiler)
+- **License**: Apache License 2.0
+- **Copyright**: Copyright (c) Kay Hayen and Nuitka Contributors
+- **Source**: https://github.com/Nuitka/Nuitka
+- **PyPI**: https://pypi.org/project/Nuitka/
+- **Purpose**: Python-to-C compiler used to create standalone executables
+- **What's Included in the Executable**:
+ - ✅ Compiled application code (our Python scripts converted to C)
+ - ✅ Python runtime (CPython, PSF License - compatible with GPLv2)
+ - ✅ Application dependencies (listed above)
+ - ❌ Nuitka compiler code itself (NOT included in the executable)
+- **License Implications**:
+ - Nuitka is a build tool, similar to GCC or Clang
+ - The Apache 2.0 license applies to Nuitka's compiler code
+ - Executables created by Nuitka do NOT contain Nuitka code
+ - The executable's license is determined by the application code and bundled dependencies
+ - Nuitka explicitly allows commercial and closed-source use of compiled executables
+- **Compatibility**: ✅ Apache 2.0 is compatible with GPLv2
+- **Reference**: [Nuitka License FAQ](https://nuitka.net/doc/user-manual.html#license)
+
+### 12. Python Runtime (CPython)
+- **License**: Python Software Foundation License (PSF License)
+- **Copyright**: Copyright (c) 2001-2025 Python Software Foundation
+- **Source**: https://www.python.org/
+- **Note**: The Python runtime is embedded in the Nuitka-compiled executable
+- **Compatibility**: ✅ PSF License is compatible with GPLv2
+- **Reference**: [Python License](https://docs.python.org/3/license.html)
+
+## License Obligations
+
+### MIT License Requirements
+For all MIT-licensed components (pyHanko, pyhanko-certvalidator, asn1crypto, oscrypto, tzlocal, pyyaml, uritools):
+- ✅ **Attribution**: Copyright notices are preserved in source code
+- ✅ **License Text**: MIT license text is included in the repository
+- ✅ **No Warranty**: MIT license includes no warranty clause
+
+### Apache 2.0 License Requirements
+For Apache 2.0-licensed components (cryptography, requests):
+- ✅ **Attribution**: Copyright notices are preserved
+- ✅ **License Text**: Apache 2.0 license is referenced
+- ✅ **Notice File**: Any NOTICE files from dependencies are preserved
+- ✅ **Patent Grant**: Apache 2.0 includes explicit patent grant
+
+### BSD License Requirements
+For BSD-licensed components (lxml):
+- ✅ **Attribution**: Copyright notices are preserved
+- ✅ **License Text**: BSD license text is included
+- ✅ **No Endorsement**: BSD license includes no endorsement clause
+
+## Distribution Requirements
+
+When distributing the NAPS2 signature helper executable:
+
+1. **Source Code Availability** (GPLv2 requirement):
+ - ✅ Source code is available at: https://github.com/ronnyhopf/naps2
+ - ✅ Build instructions are provided in [`docs/signature-helper-build.md`](signature-helper-build.md)
+ - ✅ All dependencies are documented in [`scripts/requirements-signature-fields.txt`](../scripts/requirements-signature-fields.txt)
+
+2. **License Notices**:
+ - ✅ NAPS2 LICENSE file (GPLv2) is included in distributions
+ - ✅ Third-party licenses are preserved in the repository
+ - ✅ This license analysis document provides comprehensive attribution
+
+3. **Combined Work License**:
+ - The signature helper executable is a combined work under GPLv2
+ - All bundled MIT/Apache/BSD components remain under their original licenses
+ - The combined work is distributed under GPLv2 terms
+
+## Conclusion
+
+All components bundled in the NAPS2 signature helper use permissive open-source licenses (MIT, Apache 2.0, BSD) that are fully compatible with NAPS2's GPLv2 license. The distribution complies with all license requirements:
+
+- ✅ All copyright notices are preserved
+- ✅ All license texts are available in the repository
+- ✅ Source code is publicly available
+- ✅ Build instructions are documented
+- ✅ No license conflicts exist
+
+The signature helper can be legally distributed as part of NAPS2 under the GPLv2 license.
+
+## References
+
+- [GNU GPL Compatibility](https://www.gnu.org/licenses/license-list.html)
+- [MIT License](https://opensource.org/licenses/MIT)
+- [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)
+- [BSD License](https://opensource.org/licenses/BSD-3-Clause)
+- [NAPS2 License](../LICENSE)
diff --git a/main.py b/main.py
new file mode 100644
index 0000000000..2995a09d82
--- /dev/null
+++ b/main.py
@@ -0,0 +1,6 @@
+def main():
+ print("Hello from naps2!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000..c422316806
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,23 @@
+[project]
+name = "naps2"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+ "asn1crypto>=1.5.1",
+ "cryptography>=43.0.3",
+ "lxml>=5.4.0",
+ "oscrypto>=1.1.0",
+ "pyyaml>=6.0",
+ "requests>=2.31.0",
+ "tzlocal>=4.3",
+ "uritools>=3.0.1",
+]
+
+[project.optional-dependencies]
+# Build dependencies for creating standalone executables
+build = [
+ "nuitka>=2.0",
+ "ordered-set>=4.1.0", # Required by Nuitka
+]
diff --git a/scripts/build_embedder_helper.py b/scripts/build_embedder_helper.py
new file mode 100644
index 0000000000..7c7914d390
--- /dev/null
+++ b/scripts/build_embedder_helper.py
@@ -0,0 +1,247 @@
+#!/usr/bin/env python3
+"""scripts/build_embedder_helper.py
+
+Build script for creating a self-contained executable of the signature field embedder.
+
+This script uses Nuitka to compile [`scripts/embed_signature_fields.py`](scripts/embed_signature_fields.py:1)
+into a single-file executable that bundles all runtime dependencies (including the vendored pyHanko source).
+
+Supported platforms:
+ - macOS (outputs to build/macos/)
+ - Linux (outputs to build/linux/)
+ - Windows (outputs to build/windows/)
+
+Prerequisites:
+ - Python 3.11+
+ - Nuitka installed (recommended: `pip install -e ".[build]"`)
+ - Platform build toolchain:
+ macOS: Xcode Command Line Tools
+ Linux: GCC/Clang toolchain
+ Windows: MSVC Build Tools
+
+Output:
+ - build//naps2-signature-helper[.exe]
+"""
+
+import argparse
+import os
+import subprocess
+import sys
+from pathlib import Path
+from typing import NoReturn
+
+
+def error_exit(message: str, code: int = 1) -> NoReturn:
+ """Print error message and exit with specified code."""
+ print(f"ERROR: {message}", file=sys.stderr)
+ sys.exit(code)
+
+
+def check_prerequisites() -> None:
+ """Validate that all prerequisites are met before building."""
+ # Check if Nuitka is available
+ try:
+ import nuitka # noqa: F401
+ except ImportError:
+ error_exit(
+ "Nuitka is not installed. Install with: uv pip install -e '.[build]'",
+ code=2,
+ )
+
+ # Check if pyHanko submodule is present
+ script_dir = Path(__file__).parent
+ repo_root = script_dir.parent
+ pyhanko_src = repo_root / "third_party" / "pyHanko" / "pkgs" / "pyhanko" / "src"
+ certvalidator_src = (
+ repo_root / "third_party" / "pyHanko" / "pkgs" / "pyhanko-certvalidator" / "src"
+ )
+
+ if not pyhanko_src.exists():
+ error_exit(
+ f"pyHanko source not found at {pyhanko_src}. "
+ "Initialize submodules with: git submodule update --init --recursive",
+ code=3,
+ )
+
+ if not certvalidator_src.exists():
+ error_exit(
+ f"pyhanko-certvalidator source not found at {certvalidator_src}. "
+ "Initialize submodules with: git submodule update --init --recursive",
+ code=3,
+ )
+
+ # Check if source script exists
+ source_script = script_dir / "embed_signature_fields.py"
+ if not source_script.exists():
+ error_exit(f"Source script not found: {source_script}", code=4)
+
+ print("[OK] All prerequisites met")
+
+
+def detect_platform() -> str:
+ if sys.platform.startswith("darwin"):
+ return "macos"
+ if sys.platform.startswith("win"):
+ return "windows"
+ return "linux"
+
+
+def build_executable(platform: str, macos_target_arch: str | None, output_dir: Path | None) -> None:
+ """Build the standalone executable using Nuitka."""
+ script_dir = Path(__file__).parent
+ repo_root = script_dir.parent
+ source_script = script_dir / "embed_signature_fields.py"
+ resolved_output_dir = output_dir or (repo_root / "build" / platform)
+
+ # Create output directory if it doesn't exist
+ resolved_output_dir.mkdir(parents=True, exist_ok=True)
+
+ # Paths to vendored dependencies
+ pyhanko_src = repo_root / "third_party" / "pyHanko" / "pkgs" / "pyhanko" / "src"
+ certvalidator_src = (
+ repo_root / "third_party" / "pyHanko" / "pkgs" / "pyhanko-certvalidator" / "src"
+ )
+
+ print(f"Building executable from: {source_script}")
+ print(f"Platform: {platform}")
+ print(f"Output directory: {resolved_output_dir}")
+ print(f"Including vendored pyHanko from: {pyhanko_src}")
+ print(f"Including vendored certvalidator from: {certvalidator_src}")
+
+ # Nuitka command-line arguments
+ # Using subprocess instead of Python API for better control and error reporting
+ nuitka_args: list[str] = [
+ sys.executable,
+ "-m",
+ "nuitka",
+ # Basic compilation options
+ "--standalone", # Create standalone distribution with all dependencies
+ "--onefile", # Create a single executable file
+ # Output configuration
+ f"--output-dir={resolved_output_dir}",
+ "--output-filename=naps2-signature-helper",
+ # Optimization options
+ "--assume-yes-for-downloads", # Auto-accept dependency downloads
+ "--remove-output", # Remove build directory after successful build
+ # Python path configuration - include vendored dependencies
+ f"--include-package-data=pyhanko",
+ f"--include-package-data=pyhanko_certvalidator",
+ # Follow all imports to ensure complete bundling
+ "--follow-imports",
+ # Include standard library modules that might be needed
+ "--include-module=json",
+ "--include-module=pathlib",
+ "--include-module=sys",
+ "--include-module=os",
+ # Include all pyHanko subpackages
+ "--include-package=pyhanko.pdf_utils",
+ "--include-package=pyhanko.sign",
+ "--include-package=pyhanko_certvalidator",
+ # Include dependencies
+ "--include-package=asn1crypto",
+ "--include-package=cryptography",
+ "--include-package=lxml",
+ "--include-package=oscrypto",
+ "--include-package=requests",
+ "--include-package=tzlocal",
+ "--include-package=uritools",
+ # Exclude unnecessary modules
+ "--nofollow-import-to=tkinter",
+ "--nofollow-import-to=unittest",
+ "--nofollow-import-to=test",
+ # Disable console window (optional, can be removed if debugging needed)
+ "--disable-console",
+ # Show progress
+ "--show-progress",
+ "--show-modules",
+ # Source file
+ str(source_script),
+ ]
+
+ # Platform-specific switches
+ if platform == "macos":
+ arch = macos_target_arch or "arm64"
+ # Insert after module invocation (sys.executable -m nuitka)
+ # Final argv starts with: python -m nuitka --macos-target-arch=...
+ nuitka_args[3:3] = [f"--macos-target-arch={arch}"]
+
+ print("\nStarting Nuitka compilation...")
+ print("This may take several minutes...\n")
+
+ try:
+ # Set PYTHONPATH to include vendored dependencies
+ env = os.environ.copy()
+ pythonpath_parts = [str(pyhanko_src), str(certvalidator_src)]
+ if "PYTHONPATH" in env:
+ pythonpath_parts.append(env["PYTHONPATH"])
+ env["PYTHONPATH"] = os.pathsep.join(pythonpath_parts)
+
+ # Run Nuitka
+ result = subprocess.run(
+ nuitka_args,
+ env=env,
+ check=True,
+ capture_output=False, # Show output in real-time
+ )
+
+ if result.returncode == 0:
+ print("\n[OK] Build completed successfully!")
+ exe_name = "naps2-signature-helper.exe" if platform == "windows" else "naps2-signature-helper"
+ print(f"Executable location: {resolved_output_dir / exe_name}")
+ exe_name = "naps2-signature-helper.exe" if platform == "windows" else "naps2-signature-helper"
+ print("\nUsage:")
+ print(f" {resolved_output_dir / exe_name} ")
+ else:
+ error_exit(f"Nuitka compilation failed with code {result.returncode}", code=5)
+
+ except subprocess.CalledProcessError as e:
+ error_exit(f"Nuitka compilation failed: {e}", code=5)
+ except FileNotFoundError:
+ error_exit(
+ "Python executable not found. Ensure you're running in a proper Python environment.",
+ code=6,
+ )
+ except Exception as e:
+ error_exit(f"Unexpected error during build: {e}", code=7)
+
+
+def main() -> None:
+ """Main entry point for the build script."""
+ print("=" * 70)
+ print("NAPS2 Signature Helper - Build Script")
+ print("=" * 70)
+ print()
+
+ parser = argparse.ArgumentParser(add_help=True)
+ parser.add_argument(
+ "--platform",
+ choices=["macos", "linux", "windows", "auto"],
+ default="auto",
+ help="Target platform (default: auto-detect)",
+ )
+ parser.add_argument(
+ "--macos-target-arch",
+ default="arm64",
+ help="macOS target architecture for Nuitka (default: arm64)",
+ )
+ parser.add_argument(
+ "--output-dir",
+ default=None,
+ help="Override output directory (default: build//)",
+ )
+ args = parser.parse_args()
+
+ platform = detect_platform() if args.platform == "auto" else args.platform
+ out_dir = Path(args.output_dir).expanduser().resolve() if args.output_dir else None
+
+ # Check prerequisites
+ print("Checking prerequisites...")
+ check_prerequisites()
+ print()
+
+ # Build executable
+ build_executable(platform=platform, macos_target_arch=args.macos_target_arch, output_dir=out_dir)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/build_helper.sh b/scripts/build_helper.sh
new file mode 100755
index 0000000000..6f6746bf93
--- /dev/null
+++ b/scripts/build_helper.sh
@@ -0,0 +1,100 @@
+#!/bin/bash
+#
+# Build Helper Script for NAPS2 Signature Helper
+#
+# This script ensures the uv environment is properly activated and runs
+# the Python build script to create a standalone executable.
+#
+# Usage:
+# ./scripts/build_helper.sh
+#
+# Prerequisites:
+# - uv package manager installed
+# - Python 3.11+ with project dependencies installed
+# - Xcode Command Line Tools (for macOS compilation)
+#
+
+set -e # Exit on error
+set -u # Exit on undefined variable
+
+# Color codes for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Get script directory and repository root
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
+
+echo "========================================================================"
+echo "NAPS2 Signature Helper - Build Wrapper"
+echo "========================================================================"
+echo ""
+
+# Change to repository root
+cd "${REPO_ROOT}"
+
+# Check if uv is installed
+if ! command -v uv &> /dev/null; then
+ echo -e "${RED}ERROR: uv is not installed${NC}" >&2
+ echo "Install uv from: https://github.com/astral-sh/uv" >&2
+ exit 1
+fi
+
+echo -e "${GREEN}✓${NC} uv is installed"
+
+# Check if .venv exists
+if [ ! -d ".venv" ]; then
+ echo -e "${YELLOW}⚠${NC} Virtual environment not found. Creating one..."
+ uv venv
+ echo -e "${GREEN}✓${NC} Virtual environment created"
+fi
+
+# Ensure build dependencies are installed
+echo ""
+echo "Installing build dependencies..."
+uv pip install -e ".[build]"
+
+if [ $? -ne 0 ]; then
+ echo -e "${RED}ERROR: Failed to install build dependencies${NC}" >&2
+ exit 2
+fi
+
+echo -e "${GREEN}✓${NC} Build dependencies installed"
+echo ""
+
+# Run the Python build script
+echo "Running build script..."
+echo ""
+
+# Activate virtual environment and run build script
+# Using uv run to ensure correct environment
+uv run python "${SCRIPT_DIR}/build_embedder_helper.py"
+
+BUILD_EXIT_CODE=$?
+
+echo ""
+if [ ${BUILD_EXIT_CODE} -eq 0 ]; then
+ echo "========================================================================"
+ echo -e "${GREEN}✓ Build completed successfully!${NC}"
+ echo "========================================================================"
+ echo ""
+ echo "Executable location: build/macos/naps2-signature-helper"
+ echo ""
+ echo "Test the executable with:"
+ echo " ./build/macos/naps2-signature-helper ''"
+ echo ""
+ exit 0
+else
+ echo "========================================================================"
+ echo -e "${RED}✗ Build failed with exit code ${BUILD_EXIT_CODE}${NC}"
+ echo "========================================================================"
+ echo ""
+ echo "Common issues:"
+ echo " - Ensure Xcode Command Line Tools are installed: xcode-select --install"
+ echo " - Ensure pyHanko submodule is initialized: git submodule update --init --recursive"
+ echo " - Check that all dependencies are installed: uv pip install -e '.[build]'"
+ echo ""
+ exit ${BUILD_EXIT_CODE}
+fi
diff --git a/scripts/embed_signature_fields.py b/scripts/embed_signature_fields.py
new file mode 100644
index 0000000000..3ecb1066ca
--- /dev/null
+++ b/scripts/embed_signature_fields.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python3
+"""
+NAPS2 Signature Field Embedder
+Uses pyHanko to embed signature fields into PDF documents.
+
+Usage:
+ python embed_signature_fields.py
+
+Where fields_json is a JSON array of signature field objects with:
+ - name: field name
+ - page: page number (0-indexed)
+ - x: x coordinate in PDF points (72 points = 1 inch)
+ - y: y coordinate in PDF points (from bottom-left)
+ - width: field width in PDF points
+ - height: field height in PDF points
+"""
+
+import sys
+import json
+import os
+from pathlib import Path
+
+# Add vendored pyHanko to Python path
+# This allows importing pyHanko from the repository without requiring pip install
+script_dir = Path(__file__).parent
+repo_root = script_dir.parent
+pyhanko_src = repo_root / "third_party" / "pyHanko" / "pkgs" / "pyhanko" / "src"
+certvalidator_src = repo_root / "third_party" / "pyHanko" / "pkgs" / "pyhanko-certvalidator" / "src"
+
+# Insert vendored paths at the beginning to prioritize them over system installations
+if pyhanko_src.exists():
+ sys.path.insert(0, str(pyhanko_src))
+if certvalidator_src.exists():
+ sys.path.insert(0, str(certvalidator_src))
+
+def check_dependencies():
+ """Check if pyHanko is installed."""
+ try:
+ import pyhanko
+ from pyhanko.pdf_utils import generic
+ from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
+ from pyhanko.sign.fields import SigFieldSpec
+ return True
+ except ImportError:
+ return False
+
+def embed_signature_fields(input_pdf, output_pdf, fields_data):
+ """
+ Embed signature fields into a PDF using pyHanko.
+
+ Args:
+ input_pdf: Path to input PDF file
+ output_pdf: Path to output PDF file
+ fields_data: List of field dictionaries
+ """
+ from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
+ from pyhanko.sign.fields import SigFieldSpec, append_signature_field
+ from pyhanko.pdf_utils import generic
+ from pyhanko.pdf_utils.content import RawContent
+ from pyhanko.pdf_utils.layout import BoxConstraints
+ from pyhanko.pdf_utils.generic import pdf_name
+
+ with open(input_pdf, 'rb') as inf:
+ writer = IncrementalPdfFileWriter(inf)
+
+ for field in fields_data:
+ # Create signature field specification with empty appearance
+ # pyHanko uses bottom-left origin, coordinates in PDF points
+ width = field['width']
+ height = field['height']
+
+ spec = SigFieldSpec(
+ sig_field_name=field['name'],
+ on_page=field['page'],
+ box=(
+ field['x'],
+ field['y'],
+ field['x'] + width,
+ field['y'] + height
+ ),
+ empty_field_appearance=False # We'll create our own appearance
+ )
+
+ # Add the field to the PDF
+ append_signature_field(writer, spec)
+
+ # Now find the field we just added and enhance its appearance
+ # Get the field reference from the AcroForm
+ fields_array = writer.root['/AcroForm']['/Fields']
+ sig_field_ref = fields_array[-1] # The field we just added
+ sig_field = sig_field_ref.get_object()
+
+ # Create a custom appearance stream with text and border
+ appearance_cmds = [
+ b'q', # Save graphics state
+ # Draw light gray background
+ b'0.95 0.95 0.95 rg',
+ b'0 0 %g %g re f' % (width, height),
+ # Draw border
+ b'0.5 w', # Line width
+ b'0.3 0.3 0.3 RG', # Dark gray border
+ b'0 0 %g %g re S' % (width, height),
+ # Add text "Sign here"
+ b'BT', # Begin text
+ b'/Helvetica 12 Tf', # Font and size
+ b'0.3 0.3 0.3 rg', # Text color (dark gray)
+ b'%g %g Td' % (10, height / 2 - 6), # Position text
+ b'(Sign here) Tj', # Draw text
+ b'ET', # End text
+ b'Q', # Restore graphics state
+ ]
+
+ # Create the appearance stream
+ # Build the form XObject manually since RawContent expects a specific resource format
+ ap_stream = generic.StreamObject(stream_data=b' '.join(appearance_cmds))
+ ap_stream[pdf_name('/Type')] = pdf_name('/XObject')
+ ap_stream[pdf_name('/Subtype')] = pdf_name('/Form')
+ ap_stream[pdf_name('/BBox')] = generic.ArrayObject([
+ generic.NumberObject(0),
+ generic.NumberObject(0),
+ generic.NumberObject(width),
+ generic.NumberObject(height)
+ ])
+ ap_stream[pdf_name('/Resources')] = generic.DictionaryObject({
+ pdf_name('/Font'): generic.DictionaryObject({
+ pdf_name('/Helvetica'): generic.DictionaryObject({
+ pdf_name('/Type'): pdf_name('/Font'),
+ pdf_name('/Subtype'): pdf_name('/Type1'),
+ pdf_name('/BaseFont'): pdf_name('/Helvetica'),
+ })
+ })
+ })
+
+ # Update the appearance dictionary
+ if pdf_name('/AP') not in sig_field:
+ sig_field[pdf_name('/AP')] = generic.DictionaryObject()
+ sig_field[pdf_name('/AP')][pdf_name('/N')] = writer.add_object(ap_stream)
+ writer.update_container(sig_field)
+
+ # Write the modified PDF
+ with open(output_pdf, 'wb') as outf:
+ writer.write(outf)
+
+def main():
+ """Main entry point."""
+ if len(sys.argv) != 4:
+ print("Usage: python embed_signature_fields.py ", file=sys.stderr)
+ sys.exit(1)
+
+ # Check dependencies
+ if not check_dependencies():
+ print("ERROR: pyHanko dependencies are not available.", file=sys.stderr)
+ print("Install dependencies with: pip install -r scripts/requirements-signature-fields.txt", file=sys.stderr)
+ sys.exit(2)
+
+ input_pdf = sys.argv[1]
+ output_pdf = sys.argv[2]
+ fields_json = sys.argv[3]
+
+ # Validate input file exists
+ if not os.path.exists(input_pdf):
+ print(f"ERROR: Input PDF not found: {input_pdf}", file=sys.stderr)
+ sys.exit(3)
+
+ # Parse fields data
+ try:
+ fields_data = json.loads(fields_json)
+ except json.JSONDecodeError as e:
+ print(f"ERROR: Invalid JSON: {e}", file=sys.stderr)
+ sys.exit(4)
+
+ # Embed signature fields
+ try:
+ embed_signature_fields(input_pdf, output_pdf, fields_data)
+ print(f"SUCCESS: Signature fields embedded in {output_pdf}")
+ except Exception as e:
+ print(f"ERROR: Failed to embed signature fields: {e}", file=sys.stderr)
+ import traceback
+ traceback.print_exc(file=sys.stderr)
+ sys.exit(5)
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/requirements-signature-fields.txt b/scripts/requirements-signature-fields.txt
new file mode 100644
index 0000000000..2f275e211b
--- /dev/null
+++ b/scripts/requirements-signature-fields.txt
@@ -0,0 +1,15 @@
+# Runtime dependencies for NAPS2 signature field embedding
+# These are the dependencies required by pyHanko (vendored in third_party/pyHanko)
+# Install with: pip install -r scripts/requirements-signature-fields.txt
+
+# Core dependencies from pyHanko
+asn1crypto>=1.5.1
+tzlocal>=4.3
+requests>=2.31.0
+pyyaml>=6.0
+cryptography>=43.0.3
+lxml>=5.4.0
+
+# Dependencies from pyhanko-certvalidator
+oscrypto>=1.1.0
+uritools>=3.0.1
diff --git a/third_party/README.md b/third_party/README.md
new file mode 100644
index 0000000000..dd87e270fe
--- /dev/null
+++ b/third_party/README.md
@@ -0,0 +1,54 @@
+# Third-Party Dependencies
+
+This directory contains vendored third-party dependencies for NAPS2.
+
+## pyHanko
+
+**Purpose**: PDF signature field embedding
+**Version**: Git submodule at commit b89f139e5c5e0f9895a39686ff2dc4c74dd23ba8
+**License**: MIT
+**Repository**: https://github.com/MatthiasValvekens/pyHanko
+**Documentation**: https://docs.pyhanko.eu/
+
+### Why Vendored?
+
+pyHanko is vendored as a git submodule to:
+1. Ensure consistent behavior across installations
+2. Avoid requiring users to `pip install pyHanko` separately
+3. Pin to a specific, tested version
+4. Simplify the build and deployment process
+
+### Usage
+
+The [`scripts/embed_signature_fields.py`](../scripts/embed_signature_fields.py) script automatically adds the vendored pyHanko source to the Python path. Users only need to install pyHanko's runtime dependencies:
+
+```bash
+pip install -r scripts/requirements-signature-fields.txt
+```
+
+### Updating pyHanko
+
+To update the vendored pyHanko to a newer version:
+
+```bash
+cd third_party/pyHanko
+git fetch origin
+git checkout
+cd ../..
+git add third_party/pyHanko
+git commit -m "Update pyHanko to "
+```
+
+### Initializing Submodules
+
+When cloning the NAPS2 repository, initialize the submodules:
+
+```bash
+git clone
+cd naps2
+git submodule update --init --recursive
+```
+
+## License Information
+
+Each vendored dependency retains its original license. See the LICENSE file in each subdirectory for details.
diff --git a/third_party/pyHanko b/third_party/pyHanko
new file mode 160000
index 0000000000..b89f139e5c
--- /dev/null
+++ b/third_party/pyHanko
@@ -0,0 +1 @@
+Subproject commit b89f139e5c5e0f9895a39686ff2dc4c74dd23ba8
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000000..f08399a4e8
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,593 @@
+version = 1
+revision = 3
+requires-python = ">=3.11"
+
+[[package]]
+name = "asn1crypto"
+version = "1.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080, upload-time = "2022-03-15T14:46:52.889Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "46.0.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" },
+ { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" },
+ { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" },
+ { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" },
+ { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" },
+ { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" },
+ { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" },
+ { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" },
+ { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" },
+ { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" },
+ { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" },
+ { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" },
+ { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" },
+ { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" },
+ { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" },
+ { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" },
+ { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" },
+ { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" },
+ { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" },
+ { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" },
+ { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "lxml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" },
+ { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" },
+ { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" },
+ { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" },
+ { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" },
+ { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" },
+ { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" },
+ { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" },
+ { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" },
+ { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" },
+ { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" },
+ { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" },
+ { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" },
+ { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" },
+ { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" },
+ { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" },
+ { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" },
+ { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" },
+ { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" },
+ { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" },
+ { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" },
+ { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" },
+ { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" },
+ { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" },
+ { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" },
+ { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" },
+ { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" },
+ { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" },
+ { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" },
+ { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" },
+ { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" },
+ { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" },
+]
+
+[[package]]
+name = "naps2"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "asn1crypto" },
+ { name = "cryptography" },
+ { name = "lxml" },
+ { name = "oscrypto" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "tzlocal" },
+ { name = "uritools" },
+]
+
+[package.optional-dependencies]
+build = [
+ { name = "nuitka" },
+ { name = "ordered-set" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "asn1crypto", specifier = ">=1.5.1" },
+ { name = "cryptography", specifier = ">=43.0.3" },
+ { name = "lxml", specifier = ">=5.4.0" },
+ { name = "nuitka", marker = "extra == 'build'", specifier = ">=2.0" },
+ { name = "ordered-set", marker = "extra == 'build'", specifier = ">=4.1.0" },
+ { name = "oscrypto", specifier = ">=1.1.0" },
+ { name = "pyyaml", specifier = ">=6.0" },
+ { name = "requests", specifier = ">=2.31.0" },
+ { name = "tzlocal", specifier = ">=4.3" },
+ { name = "uritools", specifier = ">=3.0.1" },
+]
+provides-extras = ["build"]
+
+[[package]]
+name = "nuitka"
+version = "2.8.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ordered-set" },
+ { name = "zstandard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a6/b8/5c58a2c4d66631ec12eb641c08b7a31116d972c4ccbb0a340a047db51238/nuitka-2.8.10.tar.gz", hash = "sha256:03e4d0756d8a11cb2627da3a2d9b518c802d031bf4f2c629e0a7b8c773497452", size = 4331977, upload-time = "2026-01-23T09:54:56.396Z" }
+
+[[package]]
+name = "ordered-set"
+version = "4.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/ca/bfac8bc689799bcca4157e0e0ced07e70ce125193fc2e166d2e685b7e2fe/ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8", size = 12826, upload-time = "2022-01-26T14:38:56.6Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634, upload-time = "2022-01-26T14:38:48.677Z" },
+]
+
+[[package]]
+name = "oscrypto"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asn1crypto" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/81/a7654e654a4b30eda06ef9ad8c1b45d1534bfd10b5c045d0c0f6b16fecd2/oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4", size = 184590, upload-time = "2022-03-18T01:53:26.889Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085", size = 194553, upload-time = "2022-03-18T01:53:24.559Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
+]
+
+[[package]]
+name = "tzlocal"
+version = "5.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
+]
+
+[[package]]
+name = "uritools"
+version = "6.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dd/f7/6651d145bedd535a5bdd6dad108329ec1fec89d38ec611f8d98834eb5378/uritools-6.0.1.tar.gz", hash = "sha256:2f9e9cb954e7877232b2c863f724a44a06eb98d9c7ebdd69914876e9487b94f8", size = 22857, upload-time = "2025-12-21T18:58:54.446Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/d7/e1542857c3f7615a1a9afa6b602b87cb5a33885db41c686aa7bf5092d4f0/uritools-6.0.1-py3-none-any.whl", hash = "sha256:d9507b82206c857d2f93d8fcc84f3b05ae4174096761102be690aa76a360cc1b", size = 10466, upload-time = "2025-12-21T18:58:52.903Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+]
+
+[[package]]
+name = "zstandard"
+version = "0.25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" },
+ { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" },
+ { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" },
+ { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" },
+ { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" },
+ { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" },
+ { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" },
+ { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" },
+ { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" },
+ { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" },
+ { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" },
+ { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" },
+ { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" },
+ { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" },
+ { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" },
+ { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
+]