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" }, +]