[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"pip\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n\n  # Hash-pinned CI dependency files generated by pip-compile.\n  - package-ecosystem: \"pip\"\n    directory: \"/requirements\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "name: CodeQL\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n  schedule:\n    # Weekly Monday at 05:17 UTC. Offset from common cron times to avoid\n    # GitHub's runner congestion at the top of the hour.\n    - cron: '17 5 * * 1'\n\npermissions:\n  contents: read\n\njobs:\n  analyze:\n    name: Analyze (Python)\n    runs-on: ubuntu-latest\n    permissions:\n      security-events: write   # publish findings to the code-scanning dashboard\n      actions: read            # read workflow metadata for analysis tracing\n      contents: read\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3\n        with:\n          languages: python\n          # security-extended adds queries beyond the default suite\n          # (more findings, slightly slower scan; appropriate for a crypto lib).\n          queries: security-extended\n\n      - name: Perform CodeQL analysis\n        uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3\n        with:\n          category: \"/language:python\"\n"
  },
  {
    "path": ".github/workflows/compatibility-test.yml",
    "content": "name: Compatibility Test\n\non:\n  workflow_dispatch: {}\n  push:\n    branches:\n      - main\n  pull_request:\n    types: [opened, synchronize, reopened]\n\npermissions:\n  contents: read\n\njobs:\n  compatibility:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n    \n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n      with:\n        python-version: ${{ matrix.python-version }}\n    \n    - name: Install package\n      run: |\n        python -m pip install --upgrade pip\n        pip install .\n    \n    - name: Test import and basic functionality\n      run: |\n        python -c \"\n        import pyshamir\n        print(f'PyShamir version: {pyshamir.__version__}')\n        \n        # Test basic functionality\n        secret = b'Hello, World!'\n        parts = pyshamir.split(secret, 5, 3)\n        reconstructed = pyshamir.combine(parts[:3])\n        \n        assert reconstructed == secret, 'Secret reconstruction failed'\n        print('OK: Basic functionality test passed')\n        print('OK: Package import and functionality verification completed successfully')\n        \" "
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "# Manual release workflow for pyshamir.\n#\n# Dispatch this workflow with a target semver (e.g. 1.0.5, 1.1.0rc1) to:\n#   1. Validate the version (regex + not-equal-to-current + tag-doesn't-exist)\n#   2. Bump pyshamir/__init__.py and commit to main as github-actions[bot]\n#   3. Create and push the vX.Y.Z tag (atomic with the main push)\n#   4. Build sdist + wheel\n#   5. Publish to PyPI via OIDC trusted publishing\n#   6. Create a GitHub release with auto-generated release notes\n#\n# Restricted to the repository owner via `if: github.actor == github.repository_owner`.\n#\n# Rollback (if a release fails partway):\n#   - PyPI publishes are immutable; if step 5 succeeded but step 6 failed, the\n#     PyPI version exists and only the GitHub release page is missing — re-run\n#     just the gh-release step manually, or create the release from the UI.\n#   - If only step 2/3 succeeded (bump + tag pushed, no PyPI publish), delete\n#     the bad tag (`git push --delete origin vX.Y.Z`) and revert the bump\n#     commit on main, then re-dispatch.\n\nname: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Target version (semver, e.g. 1.0.5 or 1.1.0rc1). The vX.Y.Z tag is created automatically.'\n        required: true\n        type: string\n\npermissions:\n  contents: read\n\nconcurrency:\n  group: release\n  cancel-in-progress: false\n\njobs:\n  bump-and-tag:\n    name: Bump version and tag\n    runs-on: ubuntu-latest\n    if: github.actor == github.repository_owner\n    permissions:\n      contents: write\n    outputs:\n      tag: ${{ steps.bump.outputs.tag }}\n    steps:\n      - name: Checkout main\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: main\n          fetch-depth: 0\n\n      - name: Validate input\n        env:\n          VERSION: ${{ inputs.version }}\n        run: |\n          set -euo pipefail\n          if ! [[ \"$VERSION\" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(rc[0-9]+|a[0-9]+|b[0-9]+)?$ ]]; then\n            echo \"::error::Version must match semver (e.g. 1.0.5 or 1.1.0rc1), got: '$VERSION'\"\n            exit 1\n          fi\n          current=$(grep -E '^__version__\\s*=' pyshamir/__init__.py | sed -E 's/.*\"([^\"]+)\".*/\\1/')\n          if [[ \"$VERSION\" == \"$current\" ]]; then\n            echo \"::error::Version is unchanged from current ($current)\"\n            exit 1\n          fi\n          if git ls-remote --tags --exit-code origin \"refs/tags/v$VERSION\" >/dev/null 2>&1; then\n            echo \"::error::Tag v$VERSION already exists on origin\"\n            exit 1\n          fi\n          echo \"Validation passed: bumping $current -> $VERSION\"\n\n      - name: Bump pyshamir/__init__.py\n        id: bump\n        env:\n          VERSION: ${{ inputs.version }}\n        run: |\n          set -euo pipefail\n          sed -i -E \"s/^__version__\\s*=.*/__version__ = \\\"$VERSION\\\"/\" pyshamir/__init__.py\n          echo \"tag=v$VERSION\" >> \"$GITHUB_OUTPUT\"\n          echo \"--- pyshamir/__init__.py after bump ---\"\n          cat pyshamir/__init__.py\n\n      - name: Commit, tag, and push atomically\n        env:\n          VERSION: ${{ inputs.version }}\n        run: |\n          set -euo pipefail\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"\n          git add pyshamir/__init__.py\n          git commit -m \"chore: bump version to v$VERSION\"\n          git tag \"v$VERSION\"\n          # Atomic push: main commit and tag go together, or neither does.\n          git push --atomic origin main \"refs/tags/v$VERSION\"\n\n  build:\n    name: Build distribution\n    needs: bump-and-tag\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout the new tag\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ needs.bump-and-tag.outputs.tag }}\n\n      - name: Set up Python\n        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.13\"\n\n      - name: Install build (hash-pinned)\n        run: |\n          python -m pip install --upgrade pip\n          pip install --require-hashes -r requirements/build.txt\n\n      - name: Build sdist + wheel\n        run: python -m build\n\n      - name: Upload dist artifact\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: dist\n          path: dist/\n\n  publish:\n    name: Publish to PyPI via OIDC\n    needs: build\n    runs-on: ubuntu-latest\n    environment:\n      name: pypi\n      url: https://pypi.org/p/pyshamir\n    permissions:\n      id-token: write\n    steps:\n      - name: Download dist artifact\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: dist\n          path: dist/\n\n      - name: Publish to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n\n  gh-release:\n    name: Create GitHub release\n    needs: [bump-and-tag, build]\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write     # for the release + asset upload\n      id-token: write     # sigstore keyless signing (Fulcio cert via OIDC)\n    steps:\n      - name: Download dist artifact\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: dist\n          path: dist/\n\n      - name: Sign sdist + wheel with Sigstore\n        uses: sigstore/gh-action-sigstore-python@04cffa1d795717b140764e8b640de88853c92acc # v3.3.0\n        with:\n          inputs: ./dist/*.tar.gz ./dist/*.whl\n\n      - name: Create GitHub release with auto-generated notes\n        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0\n        with:\n          tag_name: ${{ needs.bump-and-tag.outputs.tag }}\n          files: |\n            dist/*.tar.gz\n            dist/*.whl\n            dist/*.sigstore.json\n          generate_release_notes: true\n"
  },
  {
    "path": ".github/workflows/scorecard.yml",
    "content": "# This workflow uses actions that are not certified by GitHub. They are provided\n# by a third-party and are governed by separate terms of service, privacy\n# policy, and support documentation.\n\nname: Scorecard supply-chain security\non:\n  # For Branch-Protection check. Only the default branch is supported. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection\n  branch_protection_rule:\n  # To guarantee Maintained check is occasionally updated. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained\n  schedule:\n    - cron: '23 14 * * 0'\n  push:\n    branches: [ \"main\" ]\n\n# Declare default permissions as read only.\npermissions: read-all\n\njobs:\n  analysis:\n    name: Scorecard analysis\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to upload the results to code-scanning dashboard.\n      security-events: write\n      # Needed to publish results and get a badge (see publish_results below).\n      id-token: write\n      # Uncomment the permissions below if installing in a private repository.\n      # contents: read\n      # actions: read\n\n    steps:\n      - name: \"Checkout code\"\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: \"Run analysis\"\n        uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3\n        with:\n          results_file: results.sarif\n          results_format: sarif\n          # (Optional) \"write\" PAT token. Uncomment the `repo_token` line below if:\n          # - you want to enable the Branch-Protection check on a *public* repository, or\n          # - you are installing Scorecard on a *private* repository\n          # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.\n          # repo_token: ${{ secrets.SCORECARD_TOKEN }}\n\n          # Public repositories:\n          #   - Publish results to OpenSSF REST API for easy access by consumers\n          #   - Allows the repository to include the Scorecard badge.\n          #   - See https://github.com/ossf/scorecard-action#publishing-results.\n          # For private repositories:\n          #   - `publish_results` will always be set to `false`, regardless\n          #     of the value entered here.\n          publish_results: true\n\n      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF\n      # format to the repository Actions tab.\n      - name: \"Upload artifact\"\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: SARIF file\n          path: results.sarif\n          retention-days: 5\n\n      # Upload the results to GitHub's code scanning dashboard.\n      - name: \"Upload to code-scanning\"\n        uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3\n        with:\n          sarif_file: results.sarif\n"
  },
  {
    "path": ".github/workflows/unit-test-and-reporting.yml",
    "content": "name: Unit Test & Reporting\n\non:\n  workflow_dispatch: {}\n  push:\n    branches:\n      - main\n  pull_request:\n    types: [opened, synchronize, reopened]\n\npermissions:\n  contents: read\n\njobs:\n  test:\n    name: Test (Python ${{ matrix.python-version }})\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n    steps:\n      - name: Fetch files\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install test dependencies (hash-pinned)\n        run: |\n          python -m pip install --upgrade pip\n          pip install --require-hashes -r requirements/test.txt\n      - name: Run pytest with coverage\n        run: pytest --cov=pyshamir --cov-report=term-missing\n\n  lint:\n    name: Lint and type-check\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.13\"\n      - name: Install ruff and mypy (hash-pinned)\n        run: |\n          python -m pip install --upgrade pip\n          pip install --require-hashes -r requirements/lint.txt\n      - name: ruff check\n        run: ruff check pyshamir tests\n      - name: ruff format --check\n        run: ruff format --check pyshamir tests\n      - name: mypy --strict\n        run: mypy pyshamir\n"
  },
  {
    "path": ".gitignore",
    "content": "# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,intellij,vim,emacs,git,python\n# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,visualstudiocode,intellij,vim,emacs,git,python\n\n### Emacs ###\n# -*- mode: gitignore; -*-\n*~\n\\#*\\#\n/.emacs.desktop\n/.emacs.desktop.lock\n*.elc\nauto-save-list\ntramp\n.\\#*\n\n# Org-mode\n.org-id-locations\n*_archive\n\n# flymake-mode\n*_flymake.*\n\n# eshell files\n/eshell/history\n/eshell/lastdir\n\n# elpa packages\n/elpa/\n\n# reftex files\n*.rel\n\n# AUCTeX auto folder\n/auto/\n\n# cask packages\n.cask/\ndist/\n\n# Flycheck\nflycheck_*.el\n\n# server auth directory\n/server/\n\n# projectiles files\n.projectile\n\n# directory configuration\n.dir-locals.el\n\n# network security\n/network-security.data\n\n\n### Git ###\n# Created by git for backups. To disable backups in Git:\n# $ git config --global mergetool.keepBackup false\n*.orig\n\n# Created by git when using merge tools for conflicts\n*.BACKUP.*\n*.BASE.*\n*.LOCAL.*\n*.REMOTE.*\n*_BACKUP_*.txt\n*_BASE_*.txt\n*_LOCAL_*.txt\n*_REMOTE_*.txt\n\n### Intellij ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### Intellij Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# Azure Toolkit for IntelliJ plugin\n# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij\n.idea/**/azureSettings.xml\n\n### Linux ###\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n### macOS ###\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\r\r\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### macOS Patch ###\n# iCloud generated files\n*.icloud\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\n### Vim ###\n# Swap\n[._]*.s[a-v][a-z]\n!*.svg  # comment out if you don't need vector files\n[._]*.sw[a-p]\n[._]s[a-rt-v][a-z]\n[._]ss[a-gi-z]\n[._]sw[a-p]\n\n# Session\nSession.vim\nSessionx.vim\n\n# Temporary\n.netrwhist\n# Auto-generated tag files\ntags\n# Persistent undo\n[._]*.un~\n\n### VisualStudioCode ###\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n!.vscode/*.code-snippets\n\n# Local History for Visual Studio Code\n.history/\n\n# Built Visual Studio Code Extensions\n*.vsix\n\n### VisualStudioCode Patch ###\n# Ignore all local history of files\n.history\n.ionide\n\n### Windows ###\n# Windows thumbnail cache files\nThumbs.db\nThumbs.db:encryptable\nehthumbs.db\nehthumbs_vista.db\n\n# Dump file\n*.stackdump\n\n# Folder config file\n[Dd]esktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,intellij,vim,emacs,git,python\n\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v5.0.0\n    hooks:\n      - id: trailing-whitespace\n      - id: end-of-file-fixer\n      - id: check-yaml\n      - id: check-toml\n      - id: check-merge-conflict\n\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.15.12\n    hooks:\n      - id: ruff\n        args: [--fix]\n      - id: ruff-format\n\n  - repo: https://github.com/pre-commit/mirrors-mypy\n    rev: v1.20.2\n    hooks:\n      - id: mypy\n        files: ^pyshamir/\n        args: [--strict]\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project\n\nPython port of HashiCorp Vault's `shamir` package (Go) implementing Shamir's Secret Sharing. The public API is just `split(secret, parts, threshold)` and `combine(parts)`, exported from `pyshamir/__init__.py`.\n\nWhen behavior of the algorithm is ambiguous, the Go original is the source of truth: https://github.com/hashicorp/vault/tree/main/shamir. Match its semantics rather than inventing new ones.\n\n## Commands\n\nTests use `pytest` with `pytest-cov`. Lint is `ruff` (check + format), type-check is `mypy --strict`. All tool config lives in `pyproject.toml`.\n\n```sh\n# Run all tests\npytest\n\n# Run a single test\npytest tests/test_utils.py::test_mul_known_vectors\n\n# Run tests with coverage\npytest --cov=pyshamir --cov-report=term-missing\n\n# Lint + format check\nruff check pyshamir tests\nruff format --check pyshamir tests\n\n# Type-check (strict)\nmypy pyshamir\n\n# Multi-version matrix via tox\ntox                    # py39..py313 + lint + type\ntox -e py311           # single Python\ntox -e lint            # ruff only\ntox -e type            # mypy only\ntox -e coverage        # writes coverage.xml + htmlcov/\n```\n\nPre-commit hooks (ruff, ruff-format, mypy --strict, plus standard whitespace/yaml hooks) are configured in `.pre-commit-config.yaml`. Install with `pre-commit install` after cloning.\n\n## Architecture\n\nTwo-file core; treat them as a unit when changing the algorithm.\n\n**`pyshamir/shamir.py`** — public `split` / `combine`. Wire format: each \"part\" is a `bytearray` of length `len(secret) + 1`; the **final byte** holds the x-coordinate (offset by +1 so it's never 0), and bytes `0..len(secret)-1` hold the polynomial evaluations at that x for each secret byte. `combine` reverses this by reading the last byte as x, then Lagrange-interpolating each byte position back to x=0. Both `parts` and `threshold` must be ≥2 and ≤255.\n\n**`pyshamir/_utils.py`** — GF(256) finite field arithmetic and the `Polynomial` class. `add` is XOR; `mul` implements the Rijndael/AES reduction polynomial (0x1B) and uses `ctypes.c_uint8` for exact 8-bit wraparound that matches the Go reference. `inverse` is a fixed exponentiation chain (a^254). `make_polynomial` builds a degree-`(threshold-1)` polynomial with a fixed intercept (the secret byte) and **cryptographically random coefficients via `secrets.token_bytes`** — do not swap to `random` or any non-CSPRNG source. `generate_x_coordinates` likewise uses `secrets.SystemRandom().shuffle`.\n\n`Polynomial.evaluate` uses Horner's method over GF(256). `interpolate_polynomial` is Lagrange interpolation in GF(256) — note `add` is XOR so `add(a, b)` doubles as both addition and subtraction.\n\n`tests/test_utils.py` exercises the GF(256) primitives directly (FIPS 197 worked vectors, Rijndael overflow, the `inverse(0)` edge case, a make/sample/interpolate round-trip). When changing math in `_utils.py`, run that file specifically — failures land closer to the broken primitive than they would going through `split`/`combine`.\n\n## Conventions\n\n- **PEP-585 generic type hints are allowed** (`list[bytearray]`, `dict[int, bool]`). They work under py3.9 because both source files start with `from __future__ import annotations`, which defers all annotation evaluation to strings. Don't add them to a file that's missing the `__future__` import without also adding the import.\n- **`mypy --strict` must stay green.** New functions need full type annotations; new module-level re-exports need an `__all__` entry in `pyshamir/__init__.py`.\n- **Don't reword exception messages.** Tests in `tests/test_shamir.py` match them with `pytest.raises(..., match=...)`; changing an error string will break tests in another file.\n- **Version source**: `__version__` in `pyshamir/__init__.py` is read at build time by `pyproject.toml`'s `[tool.setuptools.dynamic] version = { attr = ... }`. Bumping the version is a one-line change there; tag release commits as `chore: bump version to vX.Y.Z`.\n- **License is MPL-2.0** (since commit 983d9f1). The SPDX expression in `pyproject.toml` and the matching `LICENSE` file are the canonical source — don't add `License :: ...` classifiers (PEP 639 reserves them as mutually exclusive with the SPDX form).\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nsgovind.dev@outlook.com.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to contribute\n\nPlease read these guidelines before contributing to pyshamir:\n\n - [Question or Problem?](#question)\n - [Issues and Bugs](#issue)\n - [Submitting a Pull Request](#pullrequest)\n - [Contributor License Agreement](#cla)\n\n\n## <a name=\"question\"></a> Got a Question or Problem?\n\nYou can start a discussion thread on GitHub Discussions linked with this library found at [pyshamir Discussions](https://github.com/konidev20/pyshamir/discussions)\n\nGitHub issues are only for [reporting bugs](#issue) and [feature requests](#feature), not\nquestions or help.\n\n## <a name=\"issue\"></a> Found an Issue?\n\nIf you find a bug in the source code or a mistake in the documentation, you can help by\nsubmitting an issue to the [GitHub Repository][github]. Even better you can submit a Pull Request\nwith a fix.\n\nWhen submitting an issue please include the following information:\n\n- A description of the issue which includes the pyshamir version and Python version\n- The exception message \n- If possible, please include code that reproduces the issue. [DropBox][dropbox] or GitHub's\n[Gist][gist] can be used to share large code samples, or you could\n[submit a pull request](#pullrequest) with the issue reproduced in a new test.\n\nThe more information you include about the issue, the more likely it is to be fixed!\n\n## <a name=\"pullrequest\"></a> Submitting a Pull Request\n\nWhen submitting a pull request to the [GitHub Repository][github] make sure to do the following:\n\n- Please adhere to the existing coding pattern in the project. It should be straight-forward to absorb.\n- Ensure that the unit test coverage is more than 80%\n- Write appropriate code samples in the documentation. (README.md)\n- Add the summary of the issue in the changelog\n\nRead [GitHub Help][pullrequesthelp] for more details about creating pull requests.\n\n## <a name=\"cla\"></a> Contributor License Agreement\n\n**Thank you for submitting your contributions to this project.**\n\nBy signing this CLA, you agree that the following terms apply to all of your past, present and future contributions\nto the project.\n\n### License.\n\nYou hereby represent that all present, past and future contributions are governed by the\n[Mozilla Public License 2.0](https://opensource.org/licenses/MPL-2.0)\ncopyright statement.\n\nThis entails that to the extent possible under law, you transfer all copyright and related or neighboring rights\nof the code or documents you contribute to the project itself or its maintainers.\nFurthermore you also represent that you have the authority to perform the above waiver\nwith respect to the entirety of you contributions.\n\n### Moral Rights.\n\nTo the fullest extent permitted under applicable law, you hereby waive, and agree not to\nassert, all of your “moral rights” in or relating to your contributions for the benefit of the project.\n\n### Third Party Content.\n\nIf your Contribution includes or is based on any source code, object code, bug fixes, configuration changes, tools,\nspecifications, documentation, data, materials, feedback, information or other works of authorship that were not\nauthored by you (“Third Party Content”) or if you are aware of any third party intellectual property or proprietary\nrights associated with your Contribution (“Third Party Rights”),\nthen you agree to include with the submission of your Contribution full details respecting such Third Party\nContent and Third Party Rights, including, without limitation, identification of which aspects of your\nContribution contain Third Party Content or are associated with Third Party Rights, the owner/author of the\nThird Party Content and Third Party Rights, where you obtained the Third Party Content, and any applicable\nthird party license terms or restrictions respecting the Third Party Content and Third Party Rights. For greater\ncertainty, the foregoing obligations respecting the identification of Third Party Content and Third Party Rights\ndo not apply to any portion of a Project that is incorporated into your Contribution to that same Project.\n\n### Representations.\n\nYou represent that, other than the Third Party Content and Third Party Rights identified by\nyou in accordance with this Agreement, you are the sole author of your Contributions and are legally entitled\nto grant the foregoing licenses and waivers in respect of your Contributions. If your Contributions were\ncreated in the course of your employment with your past or present employer(s), you represent that such\nemployer(s) has authorized you to make your Contributions on behalf of such employer(s) or such employer\n(s) has waived all of their right, title or interest in or to your Contributions.\n\n### Disclaimer.\n\nTo the fullest extent permitted under applicable law, your Contributions are provided on an \"as is\"\nbasis, without any warranties or conditions, express or implied, including, without limitation, any implied\nwarranties or conditions of non-infringement, merchantability or fitness for a particular purpose. You are not\nrequired to provide support for your Contributions, except to the extent you desire to provide support.\n\n### No Obligation.\n\nYou acknowledge that the maintainers of this project are under no obligation to use or incorporate your contributions\ninto the project. The decision to use or incorporate your contributions into the project will be made at the\nsole discretion of the maintainers or their authorized delegates.\n\n\n[github]: https://github.com/konidev20/pyshamir\n[dropbox]: https://www.dropbox.com\n[gist]: https://gist.github.com\n[pullrequesthelp]: https://help.github.com/articles/using-pull-requests\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2015 HashiCorp, Inc.\n\nMozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n     means each individual or legal entity that creates, contributes to the\n     creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n\n     means the combination of the Contributions of others (if any) used by a\n     Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n\n     means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n\n     means Source Code Form to which the initial Contributor has attached the\n     notice in Exhibit A, the Executable Form of such Source Code Form, and\n     Modifications of such Source Code Form, in each case including portions\n     thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n     means\n\n     a. that the initial Contributor has attached the notice described in\n        Exhibit B to the Covered Software; or\n\n     b. that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the terms of\n        a Secondary License.\n\n1.6. \"Executable Form\"\n\n     means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n\n     means a work that combines Covered Software with other material, in a\n     separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n\n     means this document.\n\n1.9. \"Licensable\"\n\n     means having the right to grant, to the maximum extent possible, whether\n     at the time of the initial grant or subsequently, any and all of the\n     rights conveyed by this License.\n\n1.10. \"Modifications\"\n\n     means any of the following:\n\n     a. any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered Software; or\n\n     b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. \"Patent Claims\" of a Contributor\n\n      means any patent claim(s), including without limitation, method,\n      process, and apparatus claims, in any patent Licensable by such\n      Contributor that would be infringed, but for the grant of the License,\n      by the making, using, selling, offering for sale, having made, import,\n      or transfer of either its Contributions or its Contributor Version.\n\n1.12. \"Secondary License\"\n\n      means either the GNU General Public License, Version 2.0, the GNU Lesser\n      General Public License, Version 2.1, the GNU Affero General Public\n      License, Version 3.0, or any later versions of those licenses.\n\n1.13. \"Source Code Form\"\n\n      means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n\n      means an individual or a legal entity exercising rights under this\n      License. For legal entities, \"You\" includes any entity that controls, is\n      controlled by, or is under common control with You. For purposes of this\n      definition, \"control\" means (a) the power, direct or indirect, to cause\n      the direction or management of such entity, whether by contract or\n      otherwise, or (b) ownership of more than fifty percent (50%) of the\n      outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n     Each Contributor hereby grants You a world-wide, royalty-free,\n     non-exclusive license:\n\n     a. under intellectual property rights (other than patent or trademark)\n        Licensable by such Contributor to use, reproduce, make available,\n        modify, display, perform, distribute, and otherwise exploit its\n        Contributions, either on an unmodified basis, with Modifications, or\n        as part of a Larger Work; and\n\n     b. under Patent Claims of such Contributor to make, use, sell, offer for\n        sale, have made, import, and otherwise transfer either its\n        Contributions or its Contributor Version.\n\n2.2. Effective Date\n\n     The licenses granted in Section 2.1 with respect to any Contribution\n     become effective for each Contribution on the date the Contributor first\n     distributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\n     The licenses granted in this Section 2 are the only rights granted under\n     this License. No additional rights or licenses will be implied from the\n     distribution or licensing of Covered Software under this License.\n     Notwithstanding Section 2.1(b) above, no patent license is granted by a\n     Contributor:\n\n     a. for any code that a Contributor has removed from Covered Software; or\n\n     b. for infringements caused by: (i) Your and any other third party's\n        modifications of Covered Software, or (ii) the combination of its\n        Contributions with other software (except as part of its Contributor\n        Version); or\n\n     c. under Patent Claims infringed by Covered Software in the absence of\n        its Contributions.\n\n     This License does not grant any rights in the trademarks, service marks,\n     or logos of any Contributor (except as may be necessary to comply with\n     the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n     No Contributor makes additional grants as a result of Your choice to\n     distribute the Covered Software under a subsequent version of this\n     License (see Section 10.2) or under the terms of a Secondary License (if\n     permitted under the terms of Section 3.3).\n\n2.5. Representation\n\n     Each Contributor represents that the Contributor believes its\n     Contributions are its original creation(s) or it has sufficient rights to\n     grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n     This License is not intended to limit any rights You have under\n     applicable copyright doctrines of fair use, fair dealing, or other\n     equivalents.\n\n2.7. Conditions\n\n     Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n     Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n     All distribution of Covered Software in Source Code Form, including any\n     Modifications that You create or to which You contribute, must be under\n     the terms of this License. You must inform recipients that the Source\n     Code Form of the Covered Software is governed by the terms of this\n     License, and how they can obtain a copy of this License. You may not\n     attempt to alter or restrict the recipients' rights in the Source Code\n     Form.\n\n3.2. Distribution of Executable Form\n\n     If You distribute Covered Software in Executable Form then:\n\n     a. such Covered Software must also be made available in Source Code Form,\n        as described in Section 3.1, and You must inform recipients of the\n        Executable Form how they can obtain a copy of such Source Code Form by\n        reasonable means in a timely manner, at a charge no more than the cost\n        of distribution to the recipient; and\n\n     b. You may distribute such Executable Form under the terms of this\n        License, or sublicense it under different terms, provided that the\n        license for the Executable Form does not attempt to limit or alter the\n        recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n     You may create and distribute a Larger Work under terms of Your choice,\n     provided that You also comply with the requirements of this License for\n     the Covered Software. If the Larger Work is a combination of Covered\n     Software with a work governed by one or more Secondary Licenses, and the\n     Covered Software is not Incompatible With Secondary Licenses, this\n     License permits You to additionally distribute such Covered Software\n     under the terms of such Secondary License(s), so that the recipient of\n     the Larger Work may, at their option, further distribute the Covered\n     Software under the terms of either this License or such Secondary\n     License(s).\n\n3.4. Notices\n\n     You may not remove or alter the substance of any license notices\n     (including copyright notices, patent notices, disclaimers of warranty, or\n     limitations of liability) contained within the Source Code Form of the\n     Covered Software, except that You may alter any license notices to the\n     extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n     You may choose to offer, and to charge a fee for, warranty, support,\n     indemnity or liability obligations to one or more recipients of Covered\n     Software. However, You may do so only on Your own behalf, and not on\n     behalf of any Contributor. You must make it absolutely clear that any\n     such warranty, support, indemnity, or liability obligation is offered by\n     You alone, and You hereby agree to indemnify every Contributor for any\n     liability incurred by such Contributor as a result of warranty, support,\n     indemnity or liability terms You offer. You may include additional\n     disclaimers of warranty and limitations of liability specific to any\n     jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n   If it is impossible for You to comply with any of the terms of this License\n   with respect to some or all of the Covered Software due to statute,\n   judicial order, or regulation then You must: (a) comply with the terms of\n   this License to the maximum extent possible; and (b) describe the\n   limitations and the code they affect. Such description must be placed in a\n   text file included with all distributions of the Covered Software under\n   this License. Except to the extent prohibited by statute or regulation,\n   such description must be sufficiently detailed for a recipient of ordinary\n   skill to be able to understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n     fail to comply with any of its terms. However, if You become compliant,\n     then the rights granted under this License from a particular Contributor\n     are reinstated (a) provisionally, unless and until such Contributor\n     explicitly and finally terminates Your grants, and (b) on an ongoing\n     basis, if such Contributor fails to notify You of the non-compliance by\n     some reasonable means prior to 60 days after You have come back into\n     compliance. Moreover, Your grants from a particular Contributor are\n     reinstated on an ongoing basis if such Contributor notifies You of the\n     non-compliance by some reasonable means, this is the first time You have\n     received notice of non-compliance with this License from such\n     Contributor, and You become compliant prior to 30 days after Your receipt\n     of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n     infringement claim (excluding declaratory judgment actions,\n     counter-claims, and cross-claims) alleging that a Contributor Version\n     directly or indirectly infringes any patent, then the rights granted to\n     You by any and all Contributors for the Covered Software under Section\n     2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n     license agreements (excluding distributors and resellers) which have been\n     validly granted by You or Your distributors under this License prior to\n     termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n   Covered Software is provided under this License on an \"as is\" basis,\n   without warranty of any kind, either expressed, implied, or statutory,\n   including, without limitation, warranties that the Covered Software is free\n   of defects, merchantable, fit for a particular purpose or non-infringing.\n   The entire risk as to the quality and performance of the Covered Software\n   is with You. Should any Covered Software prove defective in any respect,\n   You (not any Contributor) assume the cost of any necessary servicing,\n   repair, or correction. This disclaimer of warranty constitutes an essential\n   part of this License. No use of  any Covered Software is authorized under\n   this License except under this disclaimer.\n\n7. Limitation of Liability\n\n   Under no circumstances and under no legal theory, whether tort (including\n   negligence), contract, or otherwise, shall any Contributor, or anyone who\n   distributes Covered Software as permitted above, be liable to You for any\n   direct, indirect, special, incidental, or consequential damages of any\n   character including, without limitation, damages for lost profits, loss of\n   goodwill, work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses, even if such party shall have been\n   informed of the possibility of such damages. This limitation of liability\n   shall not apply to liability for death or personal injury resulting from\n   such party's negligence to the extent applicable law prohibits such\n   limitation. Some jurisdictions do not allow the exclusion or limitation of\n   incidental or consequential damages, so this exclusion and limitation may\n   not apply to You.\n\n8. Litigation\n\n   Any litigation relating to this License may be brought only in the courts\n   of a jurisdiction where the defendant maintains its principal place of\n   business and such litigation shall be governed by laws of that\n   jurisdiction, without reference to its conflict-of-law provisions. Nothing\n   in this Section shall prevent a party's ability to bring cross-claims or\n   counter-claims.\n\n9. Miscellaneous\n\n   This License represents the complete agreement concerning the subject\n   matter hereof. If any provision of this License is held to be\n   unenforceable, such provision shall be reformed only to the extent\n   necessary to make it enforceable. Any law or regulation which provides that\n   the language of a contract shall be construed against the drafter shall not\n   be used to construe this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n      Mozilla Foundation is the license steward. Except as provided in Section\n      10.3, no one other than the license steward has the right to modify or\n      publish new versions of this License. Each version will be given a\n      distinguishing version number.\n\n10.2. Effect of New Versions\n\n      You may distribute the Covered Software under the terms of the version\n      of the License under which You originally received the Covered Software,\n      or under the terms of any subsequent version published by the license\n      steward.\n\n10.3. Modified Versions\n\n      If you create software not governed by this License, and you want to\n      create a new license for such software, you may create and use a\n      modified version of this License if you rename the license and remove\n      any references to the name of the license steward (except to note that\n      such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\n      Licenses If You choose to distribute Source Code Form that is\n      Incompatible With Secondary Licenses under the terms of this version of\n      the License, the notice described in Exhibit B of this License must be\n      attached.\n\nExhibit A - Source Code Form License Notice\n\n      This Source Code Form is subject to the\n      terms of the Mozilla Public License, v.\n      2.0. If a copy of the MPL was not\n      distributed with this file, You can\n      obtain one at\n      http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file,\nthen You may include the notice in a location (such as a LICENSE file in a\nrelevant directory) where a recipient would be likely to look for such a\nnotice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n\n      This Source Code Form is \"Incompatible\n      With Secondary Licenses\", as defined by\n      the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "README.md",
    "content": "![pyshamir banner](https://user-images.githubusercontent.com/5201843/232241639-22034903-87c2-4bf0-9b36-2ae9a8481b71.png)\n\n## Description\n\nPython port of Shamir key Split and Combine methods from Hashicorp Vault.\n\n## Requirements\n\n- Python 3.9 or higher\n\n## Installation\n\n```sh\npip install pyshamir \n```\n\n## Usage\n\n### Split & Combine\n\n```py\nfrom pyshamir import split, combine\nimport secrets\n\n# generate a random secret, here secret is a 32 bytes\nsecret = secrets.token_bytes(32)\n\n# set the number of shares; i.e. the number of parts to split the secret into\nnum_of_shares = 5\n\n# threshold is minimum number of keys required to get back the secret\nthreshold = 3\n\n# split to get a list of bytearrays which can be combined later to get back the secret\nparts = split(secret, num_of_shares, threshold)\n\n# Now, the parts be combined to get back the secret\nrecomb_secret = combine(parts)\n```\n\n## References\n\n1. [Shamir Secret Sharing | Wikipedia](https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing)\n2. [Go Implementation | HashiCorp Vault](https://github.com/hashicorp/vault/tree/main/shamir)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 1.0.x   | :white_check_mark: |\n\n## Reporting a Vulnerability\n\nPlease report security vulnerabilities privately via GitHub's [private vulnerability reporting](https://github.com/konidev20/pyshamir/security/advisories/new) so the issue can be triaged and patched before public disclosure. Do **not** open a public issue for security reports.\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=77.0.0\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"pyshamir\"\ndescription = \"Python port of Shamir key Split and Combine methods from Hashicorp Vault.\"\nreadme = \"README.md\"\nlicense = \"MPL-2.0\"\nlicense-files = [\"LICENSE\"]\nrequires-python = \">=3.9\"\nauthors = [{ name = \"Srigovind Nayak\", email = \"sgovind.dev@outlook.com\" }]\nmaintainers = [{ name = \"Srigovind Nayak\", email = \"sgovind.dev@outlook.com\" }]\nkeywords = [\"shamir\", \"pyshamir\"]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Intended Audience :: Developers\",\n    \"Operating System :: OS Independent\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Topic :: Security :: Cryptography\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n]\ndynamic = [\"version\"]\n\n[project.optional-dependencies]\ntests = [\"pytest\", \"pytest-cov\", \"hypothesis\"]\n\n[project.urls]\nHomepage = \"https://github.com/konidev20/pyshamir\"\nDocumentation = \"https://github.com/konidev20/pyshamir\"\n\"Source Code\" = \"https://github.com/konidev20/pyshamir\"\n\"Issue Tracker\" = \"https://github.com/konidev20/pyshamir/issues\"\n\n[tool.setuptools]\npackages = [\"pyshamir\"]\n\n[tool.setuptools.dynamic]\nversion = { attr = \"pyshamir.__version__\" }\n\n[tool.ruff]\nline-length = 88\ntarget-version = \"py39\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\", \"UP\", \"B\"]\nextend-ignore = [\"E203\"]\n\n[tool.mypy]\npython_version = \"3.9\"\nstrict = true\nfiles = [\"pyshamir\"]\n\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\naddopts = \"--strict-markers\"\n"
  },
  {
    "path": "pyshamir/__init__.py",
    "content": "from .shamir import combine, split\n\n__all__ = [\"combine\", \"split\"]\n__version__ = \"1.0.4\"\n"
  },
  {
    "path": "pyshamir/_utils.py",
    "content": "from __future__ import annotations\n\nimport secrets\nfrom ctypes import c_uint8\n\n\ndef add(a: int, b: int) -> int:\n    \"\"\"\n    Adds two numbers in the finite field GF(256)\n    \"\"\"\n    out = a ^ b\n    return out\n\n\ndef inverse(a: int) -> int:\n    b = mul(a, a)\n    c = mul(a, b)\n    b = mul(c, c)\n    b = mul(b, b)\n    c = mul(b, c)\n    b = mul(b, b)\n    b = mul(b, b)\n    b = mul(b, c)\n    b = mul(b, b)\n    b = mul(a, b)\n    return mul(b, b)\n\n\ndef mul(a: int, b: int) -> int:\n    a_u = c_uint8(a)\n    b_u = c_uint8(b)\n    r = c_uint8(0)\n    i = 8\n\n    while i > 0:\n        i -= 1\n        p1 = -(c_uint8(b_u.value >> i).value & 1) & a_u.value\n        p2 = -(c_uint8(r.value >> 7).value) & 0x1B\n        p3 = c_uint8(2 * r.value)\n        r = c_uint8(p1 ^ p2 ^ p3.value)\n\n    return r.value\n\n\ndef div(a: int, b: int) -> int:\n    a_u = c_uint8(a)\n    b_u = c_uint8(b)\n\n    if b_u.value == 0:\n        raise ZeroDivisionError(\"Divide by zero\")\n    ret = mul(a_u.value, inverse(b_u.value))\n\n    if a_u.value == 0:\n        return 0\n\n    return ret\n\n\nclass Polynomial:\n    \"\"\"Polynomial in GF(256), evaluated via Horner's method.\"\"\"\n\n    def __init__(self, degree: int) -> None:\n        self.coefficients = bytearray(degree + 1)\n\n    def evaluate(self, x: int) -> int:\n        # origin case\n        if x == 0:\n            return self.coefficients[0]\n\n        # compute the polynomial value using Horner's method\n        degree = len(self.coefficients) - 1\n        out = self.coefficients[degree]\n        for i in range(degree - 1, -1, -1):\n            coeff = self.coefficients[i]\n            out = add(mul(out, x), coeff)\n        return out\n\n\ndef make_polynomial(intercept: int, degree: int) -> Polynomial:\n    \"\"\"\n    Creates a random polynomial with the given intercept and degree\n    :param intercept:\n    :param degree:\n    :return:\n    \"\"\"\n    polynomial_instance = Polynomial(degree)\n\n    # Set the intercept\n    polynomial_instance.coefficients[0] = intercept\n\n    # assign random co-efficients to the polynomial\n    polynomial_instance.coefficients[1:] = secrets.token_bytes(degree)\n\n    return polynomial_instance\n\n\ndef interpolate_polynomial(x_samples: bytearray, y_samples: bytearray, x: int) -> int:\n    \"\"\"Lagrange-interpolate N sample points in GF(256), returning the value at x.\"\"\"\n    limit = len(x_samples)\n    result = 0\n    for i in range(limit):\n        basis = 1\n        for j in range(limit):\n            if i != j:\n                num = add(x, x_samples[j])\n                den = add(x_samples[i], x_samples[j])\n                term = div(num, den)\n                basis = mul(basis, term)\n        group = mul(y_samples[i], basis)\n        result = add(result, group)\n    return result\n\n\ndef generate_x_coordinates(n: int) -> list[int]:\n    x_coordinates = list(range(n))\n    secrets.SystemRandom().shuffle(x_coordinates)\n    return x_coordinates\n"
  },
  {
    "path": "pyshamir/shamir.py",
    "content": "from __future__ import annotations\n\nfrom ._utils import generate_x_coordinates, interpolate_polynomial, make_polynomial\n\n\ndef combine(parts: list[bytearray]) -> bytearray:\n    \"\"\"\n    Takes a list of parts and returns the secret\n    :param parts:\n    :return:\n    \"\"\"\n    # Verify enough parts are present\n    if parts is None:\n        raise ValueError(\"Not enough parts to combine\")\n    if len(parts) < 2:\n        raise ValueError(\"Not enough parts to combine\")\n\n    # Verify all parts are all the same length\n    first_part_len = len(parts[0])\n\n    if first_part_len < 2:\n        raise ValueError(\"Part is too short\")\n\n    for part in parts:\n        if len(part) != first_part_len:\n            raise ValueError(\"Parts are not the same length\")\n\n    # Create a buffer to store the reconstructed secret\n    secret = bytearray(first_part_len - 1)\n\n    # Buffer to store the samples\n    x_samples = bytearray(len(parts))\n    y_samples = bytearray(len(parts))\n\n    # Record x for each sample; duplicate x-coordinates would break interpolation.\n    check_map: dict[int, bool] = {}\n\n    for i, part in enumerate(parts):\n        samp = part[first_part_len - 1]\n        if samp in check_map:\n            raise ValueError(\"Duplicate sample\")\n        check_map[samp] = True\n        x_samples[i] = samp\n\n    # Reconstruct each  byte\n    for idx, _ in enumerate(secret):\n        for i, part in enumerate(parts):\n            y_samples[i] = part[idx]\n\n        # interpolate the polynomial and compute the vault at 0\n        val = interpolate_polynomial(x_samples, y_samples, 0)\n\n        # Evaluate the 0th value to get the intercept\n        secret[idx] = val\n\n    return secret\n\n\ndef split(secret: bytes, parts: int, threshold: int) -> list[bytearray]:\n    \"\"\"\n    Takes a secret and splits it into parts\n    :param secret:\n    :param parts:\n    :param threshold:\n    :return:\n    \"\"\"\n    # Sanity check the input\n    if parts < 2 or threshold < 2:\n        raise ValueError(\"Parts and threshold must be greater than 1\")\n    if parts < threshold:\n        raise ValueError(\"Parts must be greater than threshold\")\n    if parts > 255:\n        raise ValueError(\"Parts must be less than 256\")\n    if secret is None:\n        raise ValueError(\"Secret must be at least 1 byte long\")\n    if len(secret) < 1:\n        raise ValueError(\"Secret must be at least 1 byte long\")\n\n    # Generate random list of x coordinates\n    x_coordinates = generate_x_coordinates(255)\n\n    # Allocate output buffers; the final byte of each holds the (offset) x-coordinate.\n    # Random x's ensure repeated splits of the same secret yield different parts.\n    output = [bytearray() for _ in range(parts)]\n    for i in range(len(output)):\n        output[i] = bytearray(len(secret) + 1)\n        output[i][len(secret)] = int(x_coordinates[i]) + 1\n\n    for i, val in enumerate(secret):\n        polynomial_instance = make_polynomial(val, int(threshold - 1))\n\n        for j in range(parts):\n            x = int(x_coordinates[j]) + 1\n            y = polynomial_instance.evaluate(x)\n            output[j][i] = y\n\n    return output\n"
  },
  {
    "path": "requirements/build.in",
    "content": "build\n"
  },
  {
    "path": "requirements/build.txt",
    "content": "# This file was autogenerated by uv via the following command:\n#    uv pip compile --python-version 3.9 --generate-hashes --strip-extras --output-file=build.txt build.in\nbuild==1.4.4 \\\n    --hash=sha256:8c3f48a6090b39edec1a273d2d57949aaf13723b01e02f9d518396887519f64d \\\n    --hash=sha256:f832ae053061f3fb524af812dc94b8b84bac6880cd587630e3b5d91a6a9c1703\n    # via -r build.in\nimportlib-metadata==8.7.1 \\\n    --hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \\\n    --hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151\n    # via build\npackaging==26.2 \\\n    --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \\\n    --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661\n    # via build\npyproject-hooks==1.2.0 \\\n    --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \\\n    --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913\n    # via build\ntomli==2.4.1 \\\n    --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \\\n    --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \\\n    --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \\\n    --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \\\n    --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \\\n    --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \\\n    --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \\\n    --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \\\n    --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \\\n    --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \\\n    --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \\\n    --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \\\n    --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \\\n    --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \\\n    --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \\\n    --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \\\n    --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \\\n    --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \\\n    --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \\\n    --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \\\n    --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \\\n    --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \\\n    --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \\\n    --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \\\n    --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \\\n    --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \\\n    --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \\\n    --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \\\n    --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \\\n    --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \\\n    --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \\\n    --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \\\n    --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \\\n    --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \\\n    --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \\\n    --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \\\n    --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \\\n    --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \\\n    --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \\\n    --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \\\n    --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \\\n    --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \\\n    --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \\\n    --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \\\n    --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \\\n    --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \\\n    --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049\n    # via build\nzipp==3.23.1 \\\n    --hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \\\n    --hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110\n    # via importlib-metadata\n"
  },
  {
    "path": "requirements/lint.in",
    "content": "ruff\nmypy\n"
  },
  {
    "path": "requirements/lint.txt",
    "content": "# This file was autogenerated by uv via the following command:\n#    uv pip compile --python-version 3.9 --generate-hashes --strip-extras --output-file=lint.txt lint.in\nlibrt==0.9.0 \\\n    --hash=sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d \\\n    --hash=sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d \\\n    --hash=sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38 \\\n    --hash=sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8 \\\n    --hash=sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a \\\n    --hash=sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb \\\n    --hash=sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6 \\\n    --hash=sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499 \\\n    --hash=sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2 \\\n    --hash=sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285 \\\n    --hash=sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5 \\\n    --hash=sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0 \\\n    --hash=sha256:2d03fa4fd277a7974c1978c92c374c57f44edeee163d147b477b143446ad1bf6 \\\n    --hash=sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443 \\\n    --hash=sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b \\\n    --hash=sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745 \\\n    --hash=sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb \\\n    --hash=sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228 \\\n    --hash=sha256:42ff8a962554c350d4a83cf47d9b7b78b0e6ff7943e87df7cdfc97c07f3c016f \\\n    --hash=sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c \\\n    --hash=sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845 \\\n    --hash=sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5 \\\n    --hash=sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c \\\n    --hash=sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f \\\n    --hash=sha256:5112c2fb7c2eefefaeaf5c97fec81343ef44ee86a30dcfaa8223822fba6467b4 \\\n    --hash=sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54 \\\n    --hash=sha256:54d412e47c21b85865676ed0724e37a89e9593c2eee1e7367adf85bfad56ffb1 \\\n    --hash=sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236 \\\n    --hash=sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f \\\n    --hash=sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27 \\\n    --hash=sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b \\\n    --hash=sha256:657f8ba7b9eaaa82759a104137aed2a3ef7bc46ccfd43e0d89b04005b3e0a4cc \\\n    --hash=sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858 \\\n    --hash=sha256:6788207daa0c19955d2b668f3294a368d19f67d9b5f274553fd073c1260cbb9f \\\n    --hash=sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b \\\n    --hash=sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938 \\\n    --hash=sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a \\\n    --hash=sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b \\\n    --hash=sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d \\\n    --hash=sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f \\\n    --hash=sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71 \\\n    --hash=sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22 \\\n    --hash=sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8 \\\n    --hash=sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990 \\\n    --hash=sha256:81107843ed1836874b46b310f9b1816abcb89912af627868522461c3b7333c0f \\\n    --hash=sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2 \\\n    --hash=sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd \\\n    --hash=sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076 \\\n    --hash=sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671 \\\n    --hash=sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9 \\\n    --hash=sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15 \\\n    --hash=sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4 \\\n    --hash=sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f \\\n    --hash=sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8 \\\n    --hash=sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d \\\n    --hash=sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265 \\\n    --hash=sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61 \\\n    --hash=sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519 \\\n    --hash=sha256:a81eea9b999b985e4bacc650c4312805ea7008fd5e45e1bf221310176a7bcb3a \\\n    --hash=sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40 \\\n    --hash=sha256:aa95738a68cedd3a6f5492feddc513e2e166b50602958139e47bbdd82da0f5a7 \\\n    --hash=sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f \\\n    --hash=sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e \\\n    --hash=sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9 \\\n    --hash=sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a \\\n    --hash=sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3 \\\n    --hash=sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee \\\n    --hash=sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11 \\\n    --hash=sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4 \\\n    --hash=sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283 \\\n    --hash=sha256:d9da80e5b04acce03ced8ba6479a71c2a2edf535c2acc0d09c80d2f80f3bad15 \\\n    --hash=sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084 \\\n    --hash=sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e \\\n    --hash=sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882 \\\n    --hash=sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f \\\n    --hash=sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e \\\n    --hash=sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774 \\\n    --hash=sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce \\\n    --hash=sha256:eea1b54943475f51698f85fa230c65ccac769f1e603b981be060ac5763d90927 \\\n    --hash=sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6 \\\n    --hash=sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118 \\\n    --hash=sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4 \\\n    --hash=sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e \\\n    --hash=sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1 \\\n    --hash=sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f \\\n    --hash=sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2 \\\n    --hash=sha256:f48c963a76d71b9d7927eb817b543d0dccd52ab6648b99d37bd54f4cd475d856 \\\n    --hash=sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1 \\\n    --hash=sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f \\\n    --hash=sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a\n    # via mypy\nmypy==1.19.1 \\\n    --hash=sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd \\\n    --hash=sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b \\\n    --hash=sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1 \\\n    --hash=sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba \\\n    --hash=sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b \\\n    --hash=sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045 \\\n    --hash=sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac \\\n    --hash=sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6 \\\n    --hash=sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a \\\n    --hash=sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24 \\\n    --hash=sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957 \\\n    --hash=sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042 \\\n    --hash=sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e \\\n    --hash=sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec \\\n    --hash=sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3 \\\n    --hash=sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718 \\\n    --hash=sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f \\\n    --hash=sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331 \\\n    --hash=sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1 \\\n    --hash=sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1 \\\n    --hash=sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13 \\\n    --hash=sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67 \\\n    --hash=sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2 \\\n    --hash=sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a \\\n    --hash=sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b \\\n    --hash=sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8 \\\n    --hash=sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376 \\\n    --hash=sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef \\\n    --hash=sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288 \\\n    --hash=sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75 \\\n    --hash=sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74 \\\n    --hash=sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250 \\\n    --hash=sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab \\\n    --hash=sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6 \\\n    --hash=sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247 \\\n    --hash=sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925 \\\n    --hash=sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e \\\n    --hash=sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e\n    # via -r lint.in\nmypy-extensions==1.1.0 \\\n    --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \\\n    --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558\n    # via mypy\npathspec==1.1.0 \\\n    --hash=sha256:574b128f7456bd899045ccd142dd446af7e6cfd0072d63ad73fbc55fbb4aaa42 \\\n    --hash=sha256:f5d7c555da02fd8dde3e4a2354b6aba817a89112fa8f333f7917a2a4834dd080\n    # via mypy\nruff==0.15.12 \\\n    --hash=sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b \\\n    --hash=sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33 \\\n    --hash=sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0 \\\n    --hash=sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002 \\\n    --hash=sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339 \\\n    --hash=sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e \\\n    --hash=sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847 \\\n    --hash=sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f \\\n    --hash=sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6 \\\n    --hash=sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d \\\n    --hash=sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20 \\\n    --hash=sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd \\\n    --hash=sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c \\\n    --hash=sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5 \\\n    --hash=sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6 \\\n    --hash=sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c \\\n    --hash=sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5 \\\n    --hash=sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5\n    # via -r lint.in\ntomli==2.4.1 \\\n    --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \\\n    --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \\\n    --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \\\n    --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \\\n    --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \\\n    --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \\\n    --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \\\n    --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \\\n    --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \\\n    --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \\\n    --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \\\n    --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \\\n    --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \\\n    --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \\\n    --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \\\n    --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \\\n    --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \\\n    --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \\\n    --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \\\n    --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \\\n    --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \\\n    --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \\\n    --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \\\n    --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \\\n    --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \\\n    --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \\\n    --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \\\n    --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \\\n    --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \\\n    --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \\\n    --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \\\n    --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \\\n    --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \\\n    --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \\\n    --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \\\n    --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \\\n    --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \\\n    --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \\\n    --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \\\n    --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \\\n    --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \\\n    --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \\\n    --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \\\n    --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \\\n    --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \\\n    --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \\\n    --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049\n    # via mypy\ntyping-extensions==4.15.0 \\\n    --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \\\n    --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548\n    # via mypy\n"
  },
  {
    "path": "requirements/test.in",
    "content": "pytest\npytest-cov\nhypothesis\n"
  },
  {
    "path": "requirements/test.txt",
    "content": "# This file was autogenerated by uv via the following command:\n#    uv pip compile --python-version 3.9 --generate-hashes --strip-extras --output-file=test.txt test.in\nattrs==26.1.0 \\\n    --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \\\n    --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32\n    # via hypothesis\ncoverage==7.10.7 \\\n    --hash=sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9 \\\n    --hash=sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880 \\\n    --hash=sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999 \\\n    --hash=sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1 \\\n    --hash=sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13 \\\n    --hash=sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b \\\n    --hash=sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82 \\\n    --hash=sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973 \\\n    --hash=sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f \\\n    --hash=sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681 \\\n    --hash=sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0 \\\n    --hash=sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541 \\\n    --hash=sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32 \\\n    --hash=sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17 \\\n    --hash=sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a \\\n    --hash=sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40 \\\n    --hash=sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd \\\n    --hash=sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6 \\\n    --hash=sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7 \\\n    --hash=sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb \\\n    --hash=sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f \\\n    --hash=sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d \\\n    --hash=sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe \\\n    --hash=sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c \\\n    --hash=sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807 \\\n    --hash=sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab \\\n    --hash=sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2 \\\n    --hash=sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546 \\\n    --hash=sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e \\\n    --hash=sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65 \\\n    --hash=sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396 \\\n    --hash=sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431 \\\n    --hash=sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb \\\n    --hash=sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699 \\\n    --hash=sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0 \\\n    --hash=sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f \\\n    --hash=sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a \\\n    --hash=sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235 \\\n    --hash=sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911 \\\n    --hash=sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23 \\\n    --hash=sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87 \\\n    --hash=sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699 \\\n    --hash=sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a \\\n    --hash=sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b \\\n    --hash=sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256 \\\n    --hash=sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a \\\n    --hash=sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417 \\\n    --hash=sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0 \\\n    --hash=sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a \\\n    --hash=sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360 \\\n    --hash=sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0 \\\n    --hash=sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b \\\n    --hash=sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb \\\n    --hash=sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2 \\\n    --hash=sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d \\\n    --hash=sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a \\\n    --hash=sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e \\\n    --hash=sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69 \\\n    --hash=sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14 \\\n    --hash=sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d \\\n    --hash=sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f \\\n    --hash=sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2 \\\n    --hash=sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c \\\n    --hash=sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0 \\\n    --hash=sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399 \\\n    --hash=sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59 \\\n    --hash=sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63 \\\n    --hash=sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b \\\n    --hash=sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2 \\\n    --hash=sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e \\\n    --hash=sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0 \\\n    --hash=sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520 \\\n    --hash=sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df \\\n    --hash=sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c \\\n    --hash=sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b \\\n    --hash=sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2 \\\n    --hash=sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f \\\n    --hash=sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61 \\\n    --hash=sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a \\\n    --hash=sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59 \\\n    --hash=sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c \\\n    --hash=sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf \\\n    --hash=sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07 \\\n    --hash=sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6 \\\n    --hash=sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e \\\n    --hash=sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594 \\\n    --hash=sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49 \\\n    --hash=sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843 \\\n    --hash=sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14 \\\n    --hash=sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3 \\\n    --hash=sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1 \\\n    --hash=sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698 \\\n    --hash=sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15 \\\n    --hash=sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d \\\n    --hash=sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5 \\\n    --hash=sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e \\\n    --hash=sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0 \\\n    --hash=sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b \\\n    --hash=sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239 \\\n    --hash=sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba \\\n    --hash=sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4 \\\n    --hash=sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260 \\\n    --hash=sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a \\\n    --hash=sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3\n    # via pytest-cov\nexceptiongroup==1.3.1 \\\n    --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \\\n    --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598\n    # via\n    #   hypothesis\n    #   pytest\nhypothesis==6.141.1 \\\n    --hash=sha256:8ef356e1e18fbeaa8015aab3c805303b7fe4b868e5b506e87ad83c0bf951f46f \\\n    --hash=sha256:a5b3c39c16d98b7b4c3c5c8d4262e511e3b2255e6814ced8023af49087ad60b3\n    # via -r test.in\niniconfig==2.1.0 \\\n    --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \\\n    --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760\n    # via pytest\npackaging==26.2 \\\n    --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \\\n    --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661\n    # via pytest\npluggy==1.6.0 \\\n    --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \\\n    --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746\n    # via\n    #   pytest\n    #   pytest-cov\npygments==2.20.0 \\\n    --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \\\n    --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176\n    # via pytest\npytest==8.4.2 \\\n    --hash=sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01 \\\n    --hash=sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79\n    # via\n    #   -r test.in\n    #   pytest-cov\npytest-cov==7.1.0 \\\n    --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \\\n    --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678\n    # via -r test.in\nsortedcontainers==2.4.0 \\\n    --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \\\n    --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0\n    # via hypothesis\ntomli==2.4.1 \\\n    --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \\\n    --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \\\n    --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \\\n    --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \\\n    --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \\\n    --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \\\n    --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \\\n    --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \\\n    --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \\\n    --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \\\n    --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \\\n    --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \\\n    --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \\\n    --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \\\n    --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \\\n    --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \\\n    --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \\\n    --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \\\n    --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \\\n    --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \\\n    --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \\\n    --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \\\n    --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \\\n    --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \\\n    --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \\\n    --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \\\n    --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \\\n    --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \\\n    --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \\\n    --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \\\n    --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \\\n    --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \\\n    --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \\\n    --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \\\n    --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \\\n    --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \\\n    --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \\\n    --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \\\n    --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \\\n    --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \\\n    --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \\\n    --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \\\n    --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \\\n    --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \\\n    --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \\\n    --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \\\n    --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049\n    # via\n    #   coverage\n    #   pytest\ntyping-extensions==4.15.0 \\\n    --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \\\n    --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548\n    # via exceptiongroup\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/test_shamir.py",
    "content": "from __future__ import annotations\n\nimport itertools\nfrom base64 import b64decode\n\nimport pytest\n\nfrom pyshamir import combine, split\n\nSPLIT_SECRET = b64decode(\"a+m4G0kEkKDQK4MFGz7L0vLz5oViQkDSLThiC4zDRZU=\")\nCOMBINE_SECRET = b64decode(\"esfX3MUC++BrcwkiwsKtK6M5Pi5yvuc/A/6LweWJ5FA=\")\n\n\ndef test_split_returns_correct_part_count_and_length():\n    parts = split(SPLIT_SECRET, 5, 3)\n    assert len(parts) == 5\n    for part in parts:\n        assert len(part) == len(SPLIT_SECRET) + 1\n\n\ndef test_split_produces_distinct_parts():\n    parts = split(SPLIT_SECRET, 5, 3)\n    for i in range(len(parts) - 1):\n        assert parts[i].hex() != parts[i + 1].hex()\n\n\n@pytest.mark.parametrize(\n    \"parts, threshold, message\",\n    [\n        (0, 0, \"Parts and threshold must be greater than 1\"),\n        (2, 3, \"Parts must be greater than threshold\"),\n        (1000, 3, \"Parts must be less than 256\"),\n    ],\n)\ndef test_split_invalid_arguments(parts, threshold, message):\n    with pytest.raises(ValueError, match=message):\n        split(SPLIT_SECRET, parts, threshold)\n\n\n@pytest.mark.parametrize(\"secret\", [None, bytearray(b\"\")], ids=[\"none\", \"empty\"])\ndef test_split_invalid_secret(secret):\n    with pytest.raises(ValueError, match=\"Secret must be at least 1 byte long\"):\n        split(secret, 5, 3)\n\n\n@pytest.mark.parametrize(\n    \"indices\",\n    list(itertools.combinations(range(5), 3)),\n    ids=lambda x: \"+\".join(str(i) for i in x),\n)\ndef test_combine_recovers_secret_from_any_threshold_subset(indices):\n    parts = split(COMBINE_SECRET, 5, 3)\n    selected = [parts[i] for i in indices]\n    assert combine(selected) == COMBINE_SECRET\n\n\n@pytest.mark.parametrize(\"parts\", [None, bytearray()], ids=[\"none\", \"empty\"])\ndef test_combine_with_too_few_parts(parts):\n    with pytest.raises(ValueError, match=\"Not enough parts to combine\"):\n        combine(parts)\n\n\ndef test_combine_with_mismatched_part_lengths():\n    with pytest.raises(ValueError, match=\"Parts are not the same length\"):\n        combine([bytearray(b\"ab\"), bytearray(b\"abc\")])\n\n\ndef test_combine_with_too_short_parts():\n    with pytest.raises(ValueError, match=\"Part is too short\"):\n        combine([bytearray(b\"a\"), bytearray(b\"b\")])\n\n\ndef test_combine_with_duplicate_samples():\n    with pytest.raises(ValueError, match=\"Duplicate sample\"):\n        combine([bytearray(b\"ab\"), bytearray(b\"ab\")])\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "\"\"\"Direct tests for the GF(256) primitives in pyshamir._utils.\n\nThese functions are exercised transitively through split/combine in\ntest_shamir.py, but direct tests pin the cryptographic invariants\n(field axioms, AES Rijndael polynomial reduction, Horner evaluation,\nLagrange interpolation) and surface failures closer to their root cause.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport pytest\n\nfrom pyshamir._utils import (\n    Polynomial,\n    add,\n    div,\n    generate_x_coordinates,\n    interpolate_polynomial,\n    inverse,\n    make_polynomial,\n    mul,\n)\n\n# ----- add -----\n\n\n@pytest.mark.parametrize(\n    \"a, b\", [(a, b) for a in [0, 1, 0x55, 0xAA, 0xFF] for b in [0, 1, 0x55, 0xAA, 0xFF]]\n)\ndef test_add_is_xor(a, b):\n    assert add(a, b) == a ^ b\n\n\ndef test_add_zero_is_identity():\n    for a in range(256):\n        assert add(a, 0) == a\n        assert add(0, a) == a\n\n\ndef test_add_self_is_zero():\n    for a in range(256):\n        assert add(a, a) == 0\n\n\n# ----- mul -----\n\n\ndef test_mul_zero_annihilates():\n    for a in range(256):\n        assert mul(a, 0) == 0\n        assert mul(0, a) == 0\n\n\ndef test_mul_one_is_identity():\n    for a in range(256):\n        assert mul(a, 1) == a\n        assert mul(1, a) == a\n\n\n@pytest.mark.parametrize(\n    \"a, b, expected\",\n    [\n        # mul(2, x) doubles in GF(256); high-bit overflow XORs the Rijndael 0x1B.\n        (2, 1, 2),\n        (2, 0x40, 0x80),\n        (2, 0x80, 0x1B),\n        # 3 = 2 ^ 1, so mul(3, 7) = mul(2, 7) ^ mul(1, 7) = 14 ^ 7 = 9.\n        (3, 7, 9),\n        # FIPS 197 §4.2 worked example: 0x57 * 0x83 = 0xC1.\n        (0x57, 0x83, 0xC1),\n    ],\n)\ndef test_mul_known_vectors(a, b, expected):\n    assert mul(a, b) == expected\n\n\ndef test_mul_is_commutative():\n    for a in [0x12, 0x34, 0x56, 0x78]:\n        for b in [0x9A, 0xBC, 0xDE, 0xF0]:\n            assert mul(a, b) == mul(b, a)\n\n\n# ----- inverse -----\n\n\ndef test_inverse_times_self_is_one():\n    for a in range(1, 256):\n        assert mul(a, inverse(a)) == 1\n\n\ndef test_inverse_of_zero_is_zero():\n    # 0 has no true multiplicative inverse; the exponentiation chain\n    # collapses to 0, matching the Go reference.\n    assert inverse(0) == 0\n\n\n# ----- div -----\n\n\ndef test_div_by_zero_raises():\n    with pytest.raises(ZeroDivisionError, match=\"Divide by zero\"):\n        div(5, 0)\n\n\ndef test_div_zero_numerator_returns_zero():\n    for b in range(1, 256):\n        assert div(0, b) == 0\n\n\ndef test_div_inverts_mul():\n    for a in [0x01, 0x42, 0xAB, 0xFF]:\n        for b in [0x01, 0x42, 0xAB, 0xFF]:\n            assert div(mul(a, b), b) == a\n\n\n# ----- Polynomial.evaluate -----\n\n\ndef test_polynomial_evaluate_at_zero_returns_intercept():\n    p = Polynomial(3)\n    p.coefficients[0] = 0x42\n    p.coefficients[1] = 0x11\n    p.coefficients[2] = 0x22\n    p.coefficients[3] = 0x33\n    assert p.evaluate(0) == 0x42\n\n\n@pytest.mark.parametrize(\"x\", [1, 2, 0x55, 0xAA, 0xFF])\ndef test_polynomial_evaluate_matches_manual_horner(x):\n    p = Polynomial(3)\n    p.coefficients[0] = 0x05\n    p.coefficients[1] = 0x07\n    p.coefficients[2] = 0x0B\n    p.coefficients[3] = 0x0D\n\n    # Horner: ((c3 * x + c2) * x + c1) * x + c0, with + as XOR in GF(256).\n    expected = p.coefficients[3]\n    expected = add(mul(expected, x), p.coefficients[2])\n    expected = add(mul(expected, x), p.coefficients[1])\n    expected = add(mul(expected, x), p.coefficients[0])\n\n    assert p.evaluate(x) == expected\n\n\n# ----- make_polynomial -----\n\n\ndef test_make_polynomial_sets_intercept_and_size():\n    poly = make_polynomial(0x77, 5)\n    assert poly.coefficients[0] == 0x77\n    assert len(poly.coefficients) == 6  # degree + 1\n\n\n# ----- interpolate_polynomial round-trip -----\n\n\n@pytest.mark.parametrize(\"intercept\", [0x00, 0x01, 0x55, 0xAA, 0xFF])\ndef test_interpolate_recovers_intercept(intercept):\n    \"\"\"Build a degree-3 polynomial, sample 4 distinct x's, interpolate at x=0.\"\"\"\n    poly = make_polynomial(intercept, 3)\n    xs = bytearray([1, 2, 3, 4])\n    ys = bytearray([poly.evaluate(x) for x in xs])\n    assert interpolate_polynomial(xs, ys, 0) == intercept\n\n\n# ----- generate_x_coordinates -----\n\n\ndef test_generate_x_coordinates_returns_permutation():\n    n = 100\n    xs = generate_x_coordinates(n)\n    assert len(xs) == n\n    assert sorted(xs) == list(range(n))\n\n\ndef test_generate_x_coordinates_is_shuffled():\n    # Two independent calls on n=100 collide with probability 1/100! ≈ 0.\n    assert generate_x_coordinates(100) != generate_x_coordinates(100)\n"
  },
  {
    "path": "tests/test_utils_properties.py",
    "content": "\"\"\"Hypothesis-based property tests for the GF(256) primitives in pyshamir._utils.\n\nThese complement the explicit-value tests in test_utils.py by sweeping random\ninputs across the full byte domain and shrinking failures to minimal\ncounterexamples. Each test pins an algebraic invariant of the field; together\nthey form a partial fuzzing harness recognized by OpenSSF Scorecard's\n`Fuzzing` check.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom hypothesis import given, settings\nfrom hypothesis import strategies as st\n\nfrom pyshamir._utils import (\n    add,\n    div,\n    interpolate_polynomial,\n    inverse,\n    make_polynomial,\n    mul,\n)\n\nbyte = st.integers(min_value=0, max_value=255)\nnonzero_byte = st.integers(min_value=1, max_value=255)\nsmall_degree = st.integers(min_value=2, max_value=8)\n\n\n# ----- add (XOR) field axioms -----\n\n\n@given(a=byte, b=byte)\ndef test_add_is_commutative(a: int, b: int) -> None:\n    assert add(a, b) == add(b, a)\n\n\n@given(a=byte, b=byte, c=byte)\ndef test_add_is_associative(a: int, b: int, c: int) -> None:\n    assert add(add(a, b), c) == add(a, add(b, c))\n\n\n@given(a=byte)\ndef test_add_zero_is_identity(a: int) -> None:\n    assert add(a, 0) == a\n\n\n# ----- mul field axioms -----\n\n\n@given(a=byte, b=byte)\ndef test_mul_is_commutative(a: int, b: int) -> None:\n    assert mul(a, b) == mul(b, a)\n\n\n@given(a=byte, b=byte, c=byte)\ndef test_mul_distributes_over_add(a: int, b: int, c: int) -> None:\n    assert mul(a, add(b, c)) == add(mul(a, b), mul(a, c))\n\n\n@given(a=byte)\ndef test_mul_one_is_identity(a: int) -> None:\n    assert mul(a, 1) == a\n\n\n# ----- inverse / div -----\n\n\n@given(a=nonzero_byte)\ndef test_inverse_is_multiplicative_inverse(a: int) -> None:\n    assert mul(a, inverse(a)) == 1\n\n\n@given(a=byte, b=nonzero_byte)\ndef test_div_inverts_mul(a: int, b: int) -> None:\n    assert div(mul(a, b), b) == a\n\n\n# ----- Polynomial / Lagrange round-trip -----\n\n\n@settings(max_examples=200)\n@given(intercept=byte, degree=small_degree)\ndef test_interpolate_recovers_intercept(intercept: int, degree: int) -> None:\n    \"\"\"For any random polynomial of degree d with intercept I,\n    sampling at d+1 distinct non-zero x's and interpolating at x=0 yields I.\n    \"\"\"\n    poly = make_polynomial(intercept, degree)\n    xs = bytearray(range(1, degree + 2))\n    ys = bytearray([poly.evaluate(x) for x in xs])\n    assert interpolate_polynomial(xs, ys, 0) == intercept\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist = py39,py310,py311,py312,py313,lint,type\nskip_missing_interpreters = true\n\n[testenv]\ndeps =\n    pytest\n    pytest-cov\n    hypothesis\ncommands = pytest --cov=pyshamir --cov-report=term-missing\n\n[testenv:lint]\ndeps = ruff\ncommands =\n    ruff check pyshamir tests\n    ruff format --check pyshamir tests\n\n[testenv:type]\ndeps = mypy\ncommands = mypy pyshamir\n\n[testenv:coverage]\ndeps =\n    pytest\n    pytest-cov\n    hypothesis\ncommands = pytest --cov=pyshamir --cov-report=xml --cov-report=html --cov-report=term-missing\n"
  }
]