Full Code of konidev20/pyshamir for AI

main 605ed8a538be cached
31 files
109.5 KB
39.0k tokens
48 symbols
1 requests
Download .txt
Repository: konidev20/pyshamir
Branch: main
Commit: 605ed8a538be
Files: 31
Total size: 109.5 KB

Directory structure:
gitextract_tvko4ywd/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── codeql.yml
│       ├── compatibility-test.yml
│       ├── release.yml
│       ├── scorecard.yml
│       └── unit-test-and-reporting.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── pyproject.toml
├── pyshamir/
│   ├── __init__.py
│   ├── _utils.py
│   └── shamir.py
├── requirements/
│   ├── build.in
│   ├── build.txt
│   ├── lint.in
│   ├── lint.txt
│   ├── test.in
│   └── test.txt
├── tests/
│   ├── __init__.py
│   ├── test_shamir.py
│   ├── test_utils.py
│   └── test_utils_properties.py
└── tox.ini

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "weekly"

  # Hash-pinned CI dependency files generated by pip-compile.
  - package-ecosystem: "pip"
    directory: "/requirements"
    schedule:
      interval: "weekly"


================================================
FILE: .github/workflows/codeql.yml
================================================
name: CodeQL

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    # Weekly Monday at 05:17 UTC. Offset from common cron times to avoid
    # GitHub's runner congestion at the top of the hour.
    - cron: '17 5 * * 1'

permissions:
  contents: read

jobs:
  analyze:
    name: Analyze (Python)
    runs-on: ubuntu-latest
    permissions:
      security-events: write   # publish findings to the code-scanning dashboard
      actions: read            # read workflow metadata for analysis tracing
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Initialize CodeQL
        uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
        with:
          languages: python
          # security-extended adds queries beyond the default suite
          # (more findings, slightly slower scan; appropriate for a crypto lib).
          queries: security-extended

      - name: Perform CodeQL analysis
        uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
        with:
          category: "/language:python"


================================================
FILE: .github/workflows/compatibility-test.yml
================================================
name: Compatibility Test

on:
  workflow_dispatch: {}
  push:
    branches:
      - main
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read

jobs:
  compatibility:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
    steps:
    - name: Checkout code
      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
    
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
      with:
        python-version: ${{ matrix.python-version }}
    
    - name: Install package
      run: |
        python -m pip install --upgrade pip
        pip install .
    
    - name: Test import and basic functionality
      run: |
        python -c "
        import pyshamir
        print(f'PyShamir version: {pyshamir.__version__}')
        
        # Test basic functionality
        secret = b'Hello, World!'
        parts = pyshamir.split(secret, 5, 3)
        reconstructed = pyshamir.combine(parts[:3])
        
        assert reconstructed == secret, 'Secret reconstruction failed'
        print('OK: Basic functionality test passed')
        print('OK: Package import and functionality verification completed successfully')
        " 

================================================
FILE: .github/workflows/release.yml
================================================
# Manual release workflow for pyshamir.
#
# Dispatch this workflow with a target semver (e.g. 1.0.5, 1.1.0rc1) to:
#   1. Validate the version (regex + not-equal-to-current + tag-doesn't-exist)
#   2. Bump pyshamir/__init__.py and commit to main as github-actions[bot]
#   3. Create and push the vX.Y.Z tag (atomic with the main push)
#   4. Build sdist + wheel
#   5. Publish to PyPI via OIDC trusted publishing
#   6. Create a GitHub release with auto-generated release notes
#
# Restricted to the repository owner via `if: github.actor == github.repository_owner`.
#
# Rollback (if a release fails partway):
#   - PyPI publishes are immutable; if step 5 succeeded but step 6 failed, the
#     PyPI version exists and only the GitHub release page is missing — re-run
#     just the gh-release step manually, or create the release from the UI.
#   - If only step 2/3 succeeded (bump + tag pushed, no PyPI publish), delete
#     the bad tag (`git push --delete origin vX.Y.Z`) and revert the bump
#     commit on main, then re-dispatch.

name: Release

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Target version (semver, e.g. 1.0.5 or 1.1.0rc1). The vX.Y.Z tag is created automatically.'
        required: true
        type: string

permissions:
  contents: read

concurrency:
  group: release
  cancel-in-progress: false

jobs:
  bump-and-tag:
    name: Bump version and tag
    runs-on: ubuntu-latest
    if: github.actor == github.repository_owner
    permissions:
      contents: write
    outputs:
      tag: ${{ steps.bump.outputs.tag }}
    steps:
      - name: Checkout main
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          ref: main
          fetch-depth: 0

      - name: Validate input
        env:
          VERSION: ${{ inputs.version }}
        run: |
          set -euo pipefail
          if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(rc[0-9]+|a[0-9]+|b[0-9]+)?$ ]]; then
            echo "::error::Version must match semver (e.g. 1.0.5 or 1.1.0rc1), got: '$VERSION'"
            exit 1
          fi
          current=$(grep -E '^__version__\s*=' pyshamir/__init__.py | sed -E 's/.*"([^"]+)".*/\1/')
          if [[ "$VERSION" == "$current" ]]; then
            echo "::error::Version is unchanged from current ($current)"
            exit 1
          fi
          if git ls-remote --tags --exit-code origin "refs/tags/v$VERSION" >/dev/null 2>&1; then
            echo "::error::Tag v$VERSION already exists on origin"
            exit 1
          fi
          echo "Validation passed: bumping $current -> $VERSION"

      - name: Bump pyshamir/__init__.py
        id: bump
        env:
          VERSION: ${{ inputs.version }}
        run: |
          set -euo pipefail
          sed -i -E "s/^__version__\s*=.*/__version__ = \"$VERSION\"/" pyshamir/__init__.py
          echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
          echo "--- pyshamir/__init__.py after bump ---"
          cat pyshamir/__init__.py

      - name: Commit, tag, and push atomically
        env:
          VERSION: ${{ inputs.version }}
        run: |
          set -euo pipefail
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add pyshamir/__init__.py
          git commit -m "chore: bump version to v$VERSION"
          git tag "v$VERSION"
          # Atomic push: main commit and tag go together, or neither does.
          git push --atomic origin main "refs/tags/v$VERSION"

  build:
    name: Build distribution
    needs: bump-and-tag
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the new tag
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          ref: ${{ needs.bump-and-tag.outputs.tag }}

      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.13"

      - name: Install build (hash-pinned)
        run: |
          python -m pip install --upgrade pip
          pip install --require-hashes -r requirements/build.txt

      - name: Build sdist + wheel
        run: python -m build

      - name: Upload dist artifact
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: dist
          path: dist/

  publish:
    name: Publish to PyPI via OIDC
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/pyshamir
    permissions:
      id-token: write
    steps:
      - name: Download dist artifact
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: dist
          path: dist/

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1

  gh-release:
    name: Create GitHub release
    needs: [bump-and-tag, build]
    runs-on: ubuntu-latest
    permissions:
      contents: write     # for the release + asset upload
      id-token: write     # sigstore keyless signing (Fulcio cert via OIDC)
    steps:
      - name: Download dist artifact
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: dist
          path: dist/

      - name: Sign sdist + wheel with Sigstore
        uses: sigstore/gh-action-sigstore-python@04cffa1d795717b140764e8b640de88853c92acc # v3.3.0
        with:
          inputs: ./dist/*.tar.gz ./dist/*.whl

      - name: Create GitHub release with auto-generated notes
        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
        with:
          tag_name: ${{ needs.bump-and-tag.outputs.tag }}
          files: |
            dist/*.tar.gz
            dist/*.whl
            dist/*.sigstore.json
          generate_release_notes: true


================================================
FILE: .github/workflows/scorecard.yml
================================================
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.

name: Scorecard supply-chain security
on:
  # For Branch-Protection check. Only the default branch is supported. See
  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
  branch_protection_rule:
  # To guarantee Maintained check is occasionally updated. See
  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
  schedule:
    - cron: '23 14 * * 0'
  push:
    branches: [ "main" ]

# Declare default permissions as read only.
permissions: read-all

jobs:
  analysis:
    name: Scorecard analysis
    runs-on: ubuntu-latest
    permissions:
      # Needed to upload the results to code-scanning dashboard.
      security-events: write
      # Needed to publish results and get a badge (see publish_results below).
      id-token: write
      # Uncomment the permissions below if installing in a private repository.
      # contents: read
      # actions: read

    steps:
      - name: "Checkout code"
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: "Run analysis"
        uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
        with:
          results_file: results.sarif
          results_format: sarif
          # (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
          # - you want to enable the Branch-Protection check on a *public* repository, or
          # - you are installing Scorecard on a *private* repository
          # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
          # repo_token: ${{ secrets.SCORECARD_TOKEN }}

          # Public repositories:
          #   - Publish results to OpenSSF REST API for easy access by consumers
          #   - Allows the repository to include the Scorecard badge.
          #   - See https://github.com/ossf/scorecard-action#publishing-results.
          # For private repositories:
          #   - `publish_results` will always be set to `false`, regardless
          #     of the value entered here.
          publish_results: true

      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
      # format to the repository Actions tab.
      - name: "Upload artifact"
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: SARIF file
          path: results.sarif
          retention-days: 5

      # Upload the results to GitHub's code scanning dashboard.
      - name: "Upload to code-scanning"
        uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
        with:
          sarif_file: results.sarif


================================================
FILE: .github/workflows/unit-test-and-reporting.yml
================================================
name: Unit Test & Reporting

on:
  workflow_dispatch: {}
  push:
    branches:
      - main
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read

jobs:
  test:
    name: Test (Python ${{ matrix.python-version }})
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
    steps:
      - name: Fetch files
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install test dependencies (hash-pinned)
        run: |
          python -m pip install --upgrade pip
          pip install --require-hashes -r requirements/test.txt
      - name: Run pytest with coverage
        run: pytest --cov=pyshamir --cov-report=term-missing

  lint:
    name: Lint and type-check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.13"
      - name: Install ruff and mypy (hash-pinned)
        run: |
          python -m pip install --upgrade pip
          pip install --require-hashes -r requirements/lint.txt
      - name: ruff check
        run: ruff check pyshamir tests
      - name: ruff format --check
        run: ruff format --check pyshamir tests
      - name: mypy --strict
        run: mypy pyshamir


================================================
FILE: .gitignore
================================================
# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,intellij,vim,emacs,git,python
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,visualstudiocode,intellij,vim,emacs,git,python

### Emacs ###
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*

# Org-mode
.org-id-locations
*_archive

# flymake-mode
*_flymake.*

# eshell files
/eshell/history
/eshell/lastdir

# elpa packages
/elpa/

# reftex files
*.rel

# AUCTeX auto folder
/auto/

# cask packages
.cask/
dist/

# Flycheck
flycheck_*.el

# server auth directory
/server/

# projectiles files
.projectile

# directory configuration
.dir-locals.el

# network security
/network-security.data


### Git ###
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
*.orig

# Created by git when using merge tools for conflicts
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt

### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# AWS User-specific
.idea/**/aws.xml

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn.  Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# SonarLint plugin
.idea/sonarlint/

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr

# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/

# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml

# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/

# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$

# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml

# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml

### Linux ###

# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*

# KDE directory preferences
.directory

# Linux trash folder which might appear on any partition or disk
.Trash-*

# .nfs files are created when an open file is removed but is still being accessed
.nfs*

### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon


# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### macOS Patch ###
# iCloud generated files
*.icloud

### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/#use-with-ide
.pdm.toml

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg  # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]

# Session
Session.vim
Sessionx.vim

# Temporary
.netrwhist
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~

### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets

# Local History for Visual Studio Code
.history/

# Built Visual Studio Code Extensions
*.vsix

### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide

### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db

# Dump file
*.stackdump

# Folder config file
[Dd]esktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp

# Windows shortcuts
*.lnk

# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,intellij,vim,emacs,git,python



================================================
FILE: .pre-commit-config.yaml
================================================
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-toml
      - id: check-merge-conflict

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.12
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.20.2
    hooks:
      - id: mypy
        files: ^pyshamir/
        args: [--strict]


================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project

Python 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`.

When 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.

## Commands

Tests use `pytest` with `pytest-cov`. Lint is `ruff` (check + format), type-check is `mypy --strict`. All tool config lives in `pyproject.toml`.

```sh
# Run all tests
pytest

# Run a single test
pytest tests/test_utils.py::test_mul_known_vectors

# Run tests with coverage
pytest --cov=pyshamir --cov-report=term-missing

# Lint + format check
ruff check pyshamir tests
ruff format --check pyshamir tests

# Type-check (strict)
mypy pyshamir

# Multi-version matrix via tox
tox                    # py39..py313 + lint + type
tox -e py311           # single Python
tox -e lint            # ruff only
tox -e type            # mypy only
tox -e coverage        # writes coverage.xml + htmlcov/
```

Pre-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.

## Architecture

Two-file core; treat them as a unit when changing the algorithm.

**`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.

**`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`.

`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.

`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`.

## Conventions

- **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.
- **`mypy --strict` must stay green.** New functions need full type annotations; new module-level re-exports need an `__all__` entry in `pyshamir/__init__.py`.
- **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.
- **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`.
- **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).


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or
  advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
  address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
sgovind.dev@outlook.com.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior,  harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.

Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.


================================================
FILE: CONTRIBUTING.md
================================================
# How to contribute

Please read these guidelines before contributing to pyshamir:

 - [Question or Problem?](#question)
 - [Issues and Bugs](#issue)
 - [Submitting a Pull Request](#pullrequest)
 - [Contributor License Agreement](#cla)


## <a name="question"></a> Got a Question or Problem?

You can start a discussion thread on GitHub Discussions linked with this library found at [pyshamir Discussions](https://github.com/konidev20/pyshamir/discussions)

GitHub issues are only for [reporting bugs](#issue) and [feature requests](#feature), not
questions or help.

## <a name="issue"></a> Found an Issue?

If you find a bug in the source code or a mistake in the documentation, you can help by
submitting an issue to the [GitHub Repository][github]. Even better you can submit a Pull Request
with a fix.

When submitting an issue please include the following information:

- A description of the issue which includes the pyshamir version and Python version
- The exception message 
- If possible, please include code that reproduces the issue. [DropBox][dropbox] or GitHub's
[Gist][gist] can be used to share large code samples, or you could
[submit a pull request](#pullrequest) with the issue reproduced in a new test.

The more information you include about the issue, the more likely it is to be fixed!

## <a name="pullrequest"></a> Submitting a Pull Request

When submitting a pull request to the [GitHub Repository][github] make sure to do the following:

- Please adhere to the existing coding pattern in the project. It should be straight-forward to absorb.
- Ensure that the unit test coverage is more than 80%
- Write appropriate code samples in the documentation. (README.md)
- Add the summary of the issue in the changelog

Read [GitHub Help][pullrequesthelp] for more details about creating pull requests.

## <a name="cla"></a> Contributor License Agreement

**Thank you for submitting your contributions to this project.**

By signing this CLA, you agree that the following terms apply to all of your past, present and future contributions
to the project.

### License.

You hereby represent that all present, past and future contributions are governed by the
[Mozilla Public License 2.0](https://opensource.org/licenses/MPL-2.0)
copyright statement.

This entails that to the extent possible under law, you transfer all copyright and related or neighboring rights
of the code or documents you contribute to the project itself or its maintainers.
Furthermore you also represent that you have the authority to perform the above waiver
with respect to the entirety of you contributions.

### Moral Rights.

To the fullest extent permitted under applicable law, you hereby waive, and agree not to
assert, all of your “moral rights” in or relating to your contributions for the benefit of the project.

### Third Party Content.

If your Contribution includes or is based on any source code, object code, bug fixes, configuration changes, tools,
specifications, documentation, data, materials, feedback, information or other works of authorship that were not
authored by you (“Third Party Content”) or if you are aware of any third party intellectual property or proprietary
rights associated with your Contribution (“Third Party Rights”),
then you agree to include with the submission of your Contribution full details respecting such Third Party
Content and Third Party Rights, including, without limitation, identification of which aspects of your
Contribution contain Third Party Content or are associated with Third Party Rights, the owner/author of the
Third Party Content and Third Party Rights, where you obtained the Third Party Content, and any applicable
third party license terms or restrictions respecting the Third Party Content and Third Party Rights. For greater
certainty, the foregoing obligations respecting the identification of Third Party Content and Third Party Rights
do not apply to any portion of a Project that is incorporated into your Contribution to that same Project.

### Representations.

You represent that, other than the Third Party Content and Third Party Rights identified by
you in accordance with this Agreement, you are the sole author of your Contributions and are legally entitled
to grant the foregoing licenses and waivers in respect of your Contributions. If your Contributions were
created in the course of your employment with your past or present employer(s), you represent that such
employer(s) has authorized you to make your Contributions on behalf of such employer(s) or such employer
(s) has waived all of their right, title or interest in or to your Contributions.

### Disclaimer.

To the fullest extent permitted under applicable law, your Contributions are provided on an "as is"
basis, without any warranties or conditions, express or implied, including, without limitation, any implied
warranties or conditions of non-infringement, merchantability or fitness for a particular purpose. You are not
required to provide support for your Contributions, except to the extent you desire to provide support.

### No Obligation.

You acknowledge that the maintainers of this project are under no obligation to use or incorporate your contributions
into the project. The decision to use or incorporate your contributions into the project will be made at the
sole discretion of the maintainers or their authorized delegates.


[github]: https://github.com/konidev20/pyshamir
[dropbox]: https://www.dropbox.com
[gist]: https://gist.github.com
[pullrequesthelp]: https://help.github.com/articles/using-pull-requests


================================================
FILE: LICENSE
================================================
Copyright (c) 2015 HashiCorp, Inc.

Mozilla Public License, version 2.0

1. Definitions

1.1. "Contributor"

     means each individual or legal entity that creates, contributes to the
     creation of, or owns Covered Software.

1.2. "Contributor Version"

     means the combination of the Contributions of others (if any) used by a
     Contributor and that particular Contributor's Contribution.

1.3. "Contribution"

     means Covered Software of a particular Contributor.

1.4. "Covered Software"

     means Source Code Form to which the initial Contributor has attached the
     notice in Exhibit A, the Executable Form of such Source Code Form, and
     Modifications of such Source Code Form, in each case including portions
     thereof.

1.5. "Incompatible With Secondary Licenses"
     means

     a. that the initial Contributor has attached the notice described in
        Exhibit B to the Covered Software; or

     b. that the Covered Software was made available under the terms of
        version 1.1 or earlier of the License, but not also under the terms of
        a Secondary License.

1.6. "Executable Form"

     means any form of the work other than Source Code Form.

1.7. "Larger Work"

     means a work that combines Covered Software with other material, in a
     separate file or files, that is not Covered Software.

1.8. "License"

     means this document.

1.9. "Licensable"

     means having the right to grant, to the maximum extent possible, whether
     at the time of the initial grant or subsequently, any and all of the
     rights conveyed by this License.

1.10. "Modifications"

     means any of the following:

     a. any file in Source Code Form that results from an addition to,
        deletion from, or modification of the contents of Covered Software; or

     b. any new file in Source Code Form that contains any Covered Software.

1.11. "Patent Claims" of a Contributor

      means any patent claim(s), including without limitation, method,
      process, and apparatus claims, in any patent Licensable by such
      Contributor that would be infringed, but for the grant of the License,
      by the making, using, selling, offering for sale, having made, import,
      or transfer of either its Contributions or its Contributor Version.

1.12. "Secondary License"

      means either the GNU General Public License, Version 2.0, the GNU Lesser
      General Public License, Version 2.1, the GNU Affero General Public
      License, Version 3.0, or any later versions of those licenses.

1.13. "Source Code Form"

      means the form of the work preferred for making modifications.

1.14. "You" (or "Your")

      means an individual or a legal entity exercising rights under this
      License. For legal entities, "You" includes any entity that controls, is
      controlled by, or is under common control with You. For purposes of this
      definition, "control" means (a) the power, direct or indirect, to cause
      the direction or management of such entity, whether by contract or
      otherwise, or (b) ownership of more than fifty percent (50%) of the
      outstanding shares or beneficial ownership of such entity.


2. License Grants and Conditions

2.1. Grants

     Each Contributor hereby grants You a world-wide, royalty-free,
     non-exclusive license:

     a. under intellectual property rights (other than patent or trademark)
        Licensable by such Contributor to use, reproduce, make available,
        modify, display, perform, distribute, and otherwise exploit its
        Contributions, either on an unmodified basis, with Modifications, or
        as part of a Larger Work; and

     b. under Patent Claims of such Contributor to make, use, sell, offer for
        sale, have made, import, and otherwise transfer either its
        Contributions or its Contributor Version.

2.2. Effective Date

     The licenses granted in Section 2.1 with respect to any Contribution
     become effective for each Contribution on the date the Contributor first
     distributes such Contribution.

2.3. Limitations on Grant Scope

     The licenses granted in this Section 2 are the only rights granted under
     this License. No additional rights or licenses will be implied from the
     distribution or licensing of Covered Software under this License.
     Notwithstanding Section 2.1(b) above, no patent license is granted by a
     Contributor:

     a. for any code that a Contributor has removed from Covered Software; or

     b. for infringements caused by: (i) Your and any other third party's
        modifications of Covered Software, or (ii) the combination of its
        Contributions with other software (except as part of its Contributor
        Version); or

     c. under Patent Claims infringed by Covered Software in the absence of
        its Contributions.

     This License does not grant any rights in the trademarks, service marks,
     or logos of any Contributor (except as may be necessary to comply with
     the notice requirements in Section 3.4).

2.4. Subsequent Licenses

     No Contributor makes additional grants as a result of Your choice to
     distribute the Covered Software under a subsequent version of this
     License (see Section 10.2) or under the terms of a Secondary License (if
     permitted under the terms of Section 3.3).

2.5. Representation

     Each Contributor represents that the Contributor believes its
     Contributions are its original creation(s) or it has sufficient rights to
     grant the rights to its Contributions conveyed by this License.

2.6. Fair Use

     This License is not intended to limit any rights You have under
     applicable copyright doctrines of fair use, fair dealing, or other
     equivalents.

2.7. Conditions

     Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
     Section 2.1.


3. Responsibilities

3.1. Distribution of Source Form

     All distribution of Covered Software in Source Code Form, including any
     Modifications that You create or to which You contribute, must be under
     the terms of this License. You must inform recipients that the Source
     Code Form of the Covered Software is governed by the terms of this
     License, and how they can obtain a copy of this License. You may not
     attempt to alter or restrict the recipients' rights in the Source Code
     Form.

3.2. Distribution of Executable Form

     If You distribute Covered Software in Executable Form then:

     a. such Covered Software must also be made available in Source Code Form,
        as described in Section 3.1, and You must inform recipients of the
        Executable Form how they can obtain a copy of such Source Code Form by
        reasonable means in a timely manner, at a charge no more than the cost
        of distribution to the recipient; and

     b. You may distribute such Executable Form under the terms of this
        License, or sublicense it under different terms, provided that the
        license for the Executable Form does not attempt to limit or alter the
        recipients' rights in the Source Code Form under this License.

3.3. Distribution of a Larger Work

     You may create and distribute a Larger Work under terms of Your choice,
     provided that You also comply with the requirements of this License for
     the Covered Software. If the Larger Work is a combination of Covered
     Software with a work governed by one or more Secondary Licenses, and the
     Covered Software is not Incompatible With Secondary Licenses, this
     License permits You to additionally distribute such Covered Software
     under the terms of such Secondary License(s), so that the recipient of
     the Larger Work may, at their option, further distribute the Covered
     Software under the terms of either this License or such Secondary
     License(s).

3.4. Notices

     You may not remove or alter the substance of any license notices
     (including copyright notices, patent notices, disclaimers of warranty, or
     limitations of liability) contained within the Source Code Form of the
     Covered Software, except that You may alter any license notices to the
     extent required to remedy known factual inaccuracies.

3.5. Application of Additional Terms

     You may choose to offer, and to charge a fee for, warranty, support,
     indemnity or liability obligations to one or more recipients of Covered
     Software. However, You may do so only on Your own behalf, and not on
     behalf of any Contributor. You must make it absolutely clear that any
     such warranty, support, indemnity, or liability obligation is offered by
     You alone, and You hereby agree to indemnify every Contributor for any
     liability incurred by such Contributor as a result of warranty, support,
     indemnity or liability terms You offer. You may include additional
     disclaimers of warranty and limitations of liability specific to any
     jurisdiction.

4. Inability to Comply Due to Statute or Regulation

   If it is impossible for You to comply with any of the terms of this License
   with respect to some or all of the Covered Software due to statute,
   judicial order, or regulation then You must: (a) comply with the terms of
   this License to the maximum extent possible; and (b) describe the
   limitations and the code they affect. Such description must be placed in a
   text file included with all distributions of the Covered Software under
   this License. Except to the extent prohibited by statute or regulation,
   such description must be sufficiently detailed for a recipient of ordinary
   skill to be able to understand it.

5. Termination

5.1. The rights granted under this License will terminate automatically if You
     fail to comply with any of its terms. However, if You become compliant,
     then the rights granted under this License from a particular Contributor
     are reinstated (a) provisionally, unless and until such Contributor
     explicitly and finally terminates Your grants, and (b) on an ongoing
     basis, if such Contributor fails to notify You of the non-compliance by
     some reasonable means prior to 60 days after You have come back into
     compliance. Moreover, Your grants from a particular Contributor are
     reinstated on an ongoing basis if such Contributor notifies You of the
     non-compliance by some reasonable means, this is the first time You have
     received notice of non-compliance with this License from such
     Contributor, and You become compliant prior to 30 days after Your receipt
     of the notice.

5.2. If You initiate litigation against any entity by asserting a patent
     infringement claim (excluding declaratory judgment actions,
     counter-claims, and cross-claims) alleging that a Contributor Version
     directly or indirectly infringes any patent, then the rights granted to
     You by any and all Contributors for the Covered Software under Section
     2.1 of this License shall terminate.

5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
     license agreements (excluding distributors and resellers) which have been
     validly granted by You or Your distributors under this License prior to
     termination shall survive termination.

6. Disclaimer of Warranty

   Covered Software is provided under this License on an "as is" basis,
   without warranty of any kind, either expressed, implied, or statutory,
   including, without limitation, warranties that the Covered Software is free
   of defects, merchantable, fit for a particular purpose or non-infringing.
   The entire risk as to the quality and performance of the Covered Software
   is with You. Should any Covered Software prove defective in any respect,
   You (not any Contributor) assume the cost of any necessary servicing,
   repair, or correction. This disclaimer of warranty constitutes an essential
   part of this License. No use of  any Covered Software is authorized under
   this License except under this disclaimer.

7. Limitation of Liability

   Under no circumstances and under no legal theory, whether tort (including
   negligence), contract, or otherwise, shall any Contributor, or anyone who
   distributes Covered Software as permitted above, be liable to You for any
   direct, indirect, special, incidental, or consequential damages of any
   character including, without limitation, damages for lost profits, loss of
   goodwill, work stoppage, computer failure or malfunction, or any and all
   other commercial damages or losses, even if such party shall have been
   informed of the possibility of such damages. This limitation of liability
   shall not apply to liability for death or personal injury resulting from
   such party's negligence to the extent applicable law prohibits such
   limitation. Some jurisdictions do not allow the exclusion or limitation of
   incidental or consequential damages, so this exclusion and limitation may
   not apply to You.

8. Litigation

   Any litigation relating to this License may be brought only in the courts
   of a jurisdiction where the defendant maintains its principal place of
   business and such litigation shall be governed by laws of that
   jurisdiction, without reference to its conflict-of-law provisions. Nothing
   in this Section shall prevent a party's ability to bring cross-claims or
   counter-claims.

9. Miscellaneous

   This License represents the complete agreement concerning the subject
   matter hereof. If any provision of this License is held to be
   unenforceable, such provision shall be reformed only to the extent
   necessary to make it enforceable. Any law or regulation which provides that
   the language of a contract shall be construed against the drafter shall not
   be used to construe this License against a Contributor.


10. Versions of the License

10.1. New Versions

      Mozilla Foundation is the license steward. Except as provided in Section
      10.3, no one other than the license steward has the right to modify or
      publish new versions of this License. Each version will be given a
      distinguishing version number.

10.2. Effect of New Versions

      You may distribute the Covered Software under the terms of the version
      of the License under which You originally received the Covered Software,
      or under the terms of any subsequent version published by the license
      steward.

10.3. Modified Versions

      If you create software not governed by this License, and you want to
      create a new license for such software, you may create and use a
      modified version of this License if you rename the license and remove
      any references to the name of the license steward (except to note that
      such modified license differs from this License).

10.4. Distributing Source Code Form that is Incompatible With Secondary
      Licenses If You choose to distribute Source Code Form that is
      Incompatible With Secondary Licenses under the terms of this version of
      the License, the notice described in Exhibit B of this License must be
      attached.

Exhibit A - Source Code Form License Notice

      This Source Code Form is subject to the
      terms of the Mozilla Public License, v.
      2.0. If a copy of the MPL was not
      distributed with this file, You can
      obtain one at
      http://mozilla.org/MPL/2.0/.

If it is not possible or desirable to put the notice in a particular file,
then You may include the notice in a location (such as a LICENSE file in a
relevant directory) where a recipient would be likely to look for such a
notice.

You may add additional accurate notices of copyright ownership.

Exhibit B - "Incompatible With Secondary Licenses" Notice

      This Source Code Form is "Incompatible
      With Secondary Licenses", as defined by
      the Mozilla Public License, v. 2.0.


================================================
FILE: README.md
================================================
![pyshamir banner](https://user-images.githubusercontent.com/5201843/232241639-22034903-87c2-4bf0-9b36-2ae9a8481b71.png)

## Description

Python port of Shamir key Split and Combine methods from Hashicorp Vault.

## Requirements

- Python 3.9 or higher

## Installation

```sh
pip install pyshamir 
```

## Usage

### Split & Combine

```py
from pyshamir import split, combine
import secrets

# generate a random secret, here secret is a 32 bytes
secret = secrets.token_bytes(32)

# set the number of shares; i.e. the number of parts to split the secret into
num_of_shares = 5

# threshold is minimum number of keys required to get back the secret
threshold = 3

# split to get a list of bytearrays which can be combined later to get back the secret
parts = split(secret, num_of_shares, threshold)

# Now, the parts be combined to get back the secret
recomb_secret = combine(parts)
```

## References

1. [Shamir Secret Sharing | Wikipedia](https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing)
2. [Go Implementation | HashiCorp Vault](https://github.com/hashicorp/vault/tree/main/shamir)


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

| Version | Supported          |
| ------- | ------------------ |
| 1.0.x   | :white_check_mark: |

## Reporting a Vulnerability

Please 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.


================================================
FILE: pyproject.toml
================================================
[build-system]
requires = ["setuptools>=77.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "pyshamir"
description = "Python port of Shamir key Split and Combine methods from Hashicorp Vault."
readme = "README.md"
license = "MPL-2.0"
license-files = ["LICENSE"]
requires-python = ">=3.9"
authors = [{ name = "Srigovind Nayak", email = "sgovind.dev@outlook.com" }]
maintainers = [{ name = "Srigovind Nayak", email = "sgovind.dev@outlook.com" }]
keywords = ["shamir", "pyshamir"]
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Intended Audience :: Developers",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Topic :: Security :: Cryptography",
    "Topic :: Software Development :: Libraries :: Python Modules",
]
dynamic = ["version"]

[project.optional-dependencies]
tests = ["pytest", "pytest-cov", "hypothesis"]

[project.urls]
Homepage = "https://github.com/konidev20/pyshamir"
Documentation = "https://github.com/konidev20/pyshamir"
"Source Code" = "https://github.com/konidev20/pyshamir"
"Issue Tracker" = "https://github.com/konidev20/pyshamir/issues"

[tool.setuptools]
packages = ["pyshamir"]

[tool.setuptools.dynamic]
version = { attr = "pyshamir.__version__" }

[tool.ruff]
line-length = 88
target-version = "py39"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
extend-ignore = ["E203"]

[tool.mypy]
python_version = "3.9"
strict = true
files = ["pyshamir"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--strict-markers"


================================================
FILE: pyshamir/__init__.py
================================================
from .shamir import combine, split

__all__ = ["combine", "split"]
__version__ = "1.0.4"


================================================
FILE: pyshamir/_utils.py
================================================
from __future__ import annotations

import secrets
from ctypes import c_uint8


def add(a: int, b: int) -> int:
    """
    Adds two numbers in the finite field GF(256)
    """
    out = a ^ b
    return out


def inverse(a: int) -> int:
    b = mul(a, a)
    c = mul(a, b)
    b = mul(c, c)
    b = mul(b, b)
    c = mul(b, c)
    b = mul(b, b)
    b = mul(b, b)
    b = mul(b, c)
    b = mul(b, b)
    b = mul(a, b)
    return mul(b, b)


def mul(a: int, b: int) -> int:
    a_u = c_uint8(a)
    b_u = c_uint8(b)
    r = c_uint8(0)
    i = 8

    while i > 0:
        i -= 1
        p1 = -(c_uint8(b_u.value >> i).value & 1) & a_u.value
        p2 = -(c_uint8(r.value >> 7).value) & 0x1B
        p3 = c_uint8(2 * r.value)
        r = c_uint8(p1 ^ p2 ^ p3.value)

    return r.value


def div(a: int, b: int) -> int:
    a_u = c_uint8(a)
    b_u = c_uint8(b)

    if b_u.value == 0:
        raise ZeroDivisionError("Divide by zero")
    ret = mul(a_u.value, inverse(b_u.value))

    if a_u.value == 0:
        return 0

    return ret


class Polynomial:
    """Polynomial in GF(256), evaluated via Horner's method."""

    def __init__(self, degree: int) -> None:
        self.coefficients = bytearray(degree + 1)

    def evaluate(self, x: int) -> int:
        # origin case
        if x == 0:
            return self.coefficients[0]

        # compute the polynomial value using Horner's method
        degree = len(self.coefficients) - 1
        out = self.coefficients[degree]
        for i in range(degree - 1, -1, -1):
            coeff = self.coefficients[i]
            out = add(mul(out, x), coeff)
        return out


def make_polynomial(intercept: int, degree: int) -> Polynomial:
    """
    Creates a random polynomial with the given intercept and degree
    :param intercept:
    :param degree:
    :return:
    """
    polynomial_instance = Polynomial(degree)

    # Set the intercept
    polynomial_instance.coefficients[0] = intercept

    # assign random co-efficients to the polynomial
    polynomial_instance.coefficients[1:] = secrets.token_bytes(degree)

    return polynomial_instance


def interpolate_polynomial(x_samples: bytearray, y_samples: bytearray, x: int) -> int:
    """Lagrange-interpolate N sample points in GF(256), returning the value at x."""
    limit = len(x_samples)
    result = 0
    for i in range(limit):
        basis = 1
        for j in range(limit):
            if i != j:
                num = add(x, x_samples[j])
                den = add(x_samples[i], x_samples[j])
                term = div(num, den)
                basis = mul(basis, term)
        group = mul(y_samples[i], basis)
        result = add(result, group)
    return result


def generate_x_coordinates(n: int) -> list[int]:
    x_coordinates = list(range(n))
    secrets.SystemRandom().shuffle(x_coordinates)
    return x_coordinates


================================================
FILE: pyshamir/shamir.py
================================================
from __future__ import annotations

from ._utils import generate_x_coordinates, interpolate_polynomial, make_polynomial


def combine(parts: list[bytearray]) -> bytearray:
    """
    Takes a list of parts and returns the secret
    :param parts:
    :return:
    """
    # Verify enough parts are present
    if parts is None:
        raise ValueError("Not enough parts to combine")
    if len(parts) < 2:
        raise ValueError("Not enough parts to combine")

    # Verify all parts are all the same length
    first_part_len = len(parts[0])

    if first_part_len < 2:
        raise ValueError("Part is too short")

    for part in parts:
        if len(part) != first_part_len:
            raise ValueError("Parts are not the same length")

    # Create a buffer to store the reconstructed secret
    secret = bytearray(first_part_len - 1)

    # Buffer to store the samples
    x_samples = bytearray(len(parts))
    y_samples = bytearray(len(parts))

    # Record x for each sample; duplicate x-coordinates would break interpolation.
    check_map: dict[int, bool] = {}

    for i, part in enumerate(parts):
        samp = part[first_part_len - 1]
        if samp in check_map:
            raise ValueError("Duplicate sample")
        check_map[samp] = True
        x_samples[i] = samp

    # Reconstruct each  byte
    for idx, _ in enumerate(secret):
        for i, part in enumerate(parts):
            y_samples[i] = part[idx]

        # interpolate the polynomial and compute the vault at 0
        val = interpolate_polynomial(x_samples, y_samples, 0)

        # Evaluate the 0th value to get the intercept
        secret[idx] = val

    return secret


def split(secret: bytes, parts: int, threshold: int) -> list[bytearray]:
    """
    Takes a secret and splits it into parts
    :param secret:
    :param parts:
    :param threshold:
    :return:
    """
    # Sanity check the input
    if parts < 2 or threshold < 2:
        raise ValueError("Parts and threshold must be greater than 1")
    if parts < threshold:
        raise ValueError("Parts must be greater than threshold")
    if parts > 255:
        raise ValueError("Parts must be less than 256")
    if secret is None:
        raise ValueError("Secret must be at least 1 byte long")
    if len(secret) < 1:
        raise ValueError("Secret must be at least 1 byte long")

    # Generate random list of x coordinates
    x_coordinates = generate_x_coordinates(255)

    # Allocate output buffers; the final byte of each holds the (offset) x-coordinate.
    # Random x's ensure repeated splits of the same secret yield different parts.
    output = [bytearray() for _ in range(parts)]
    for i in range(len(output)):
        output[i] = bytearray(len(secret) + 1)
        output[i][len(secret)] = int(x_coordinates[i]) + 1

    for i, val in enumerate(secret):
        polynomial_instance = make_polynomial(val, int(threshold - 1))

        for j in range(parts):
            x = int(x_coordinates[j]) + 1
            y = polynomial_instance.evaluate(x)
            output[j][i] = y

    return output


================================================
FILE: requirements/build.in
================================================
build


================================================
FILE: requirements/build.txt
================================================
# This file was autogenerated by uv via the following command:
#    uv pip compile --python-version 3.9 --generate-hashes --strip-extras --output-file=build.txt build.in
build==1.4.4 \
    --hash=sha256:8c3f48a6090b39edec1a273d2d57949aaf13723b01e02f9d518396887519f64d \
    --hash=sha256:f832ae053061f3fb524af812dc94b8b84bac6880cd587630e3b5d91a6a9c1703
    # via -r build.in
importlib-metadata==8.7.1 \
    --hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \
    --hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151
    # via build
packaging==26.2 \
    --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
    --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
    # via build
pyproject-hooks==1.2.0 \
    --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \
    --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913
    # via build
tomli==2.4.1 \
    --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
    --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
    --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \
    --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \
    --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \
    --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \
    --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \
    --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \
    --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \
    --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \
    --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \
    --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \
    --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \
    --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \
    --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \
    --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \
    --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \
    --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \
    --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \
    --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \
    --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \
    --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \
    --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \
    --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \
    --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \
    --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \
    --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \
    --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \
    --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \
    --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \
    --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \
    --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \
    --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \
    --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \
    --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \
    --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \
    --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \
    --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \
    --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \
    --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \
    --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \
    --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \
    --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \
    --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \
    --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \
    --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \
    --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049
    # via build
zipp==3.23.1 \
    --hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \
    --hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110
    # via importlib-metadata


================================================
FILE: requirements/lint.in
================================================
ruff
mypy


================================================
FILE: requirements/lint.txt
================================================
# This file was autogenerated by uv via the following command:
#    uv pip compile --python-version 3.9 --generate-hashes --strip-extras --output-file=lint.txt lint.in
librt==0.9.0 \
    --hash=sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d \
    --hash=sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d \
    --hash=sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38 \
    --hash=sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8 \
    --hash=sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a \
    --hash=sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb \
    --hash=sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6 \
    --hash=sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499 \
    --hash=sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2 \
    --hash=sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285 \
    --hash=sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5 \
    --hash=sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0 \
    --hash=sha256:2d03fa4fd277a7974c1978c92c374c57f44edeee163d147b477b143446ad1bf6 \
    --hash=sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443 \
    --hash=sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b \
    --hash=sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745 \
    --hash=sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb \
    --hash=sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228 \
    --hash=sha256:42ff8a962554c350d4a83cf47d9b7b78b0e6ff7943e87df7cdfc97c07f3c016f \
    --hash=sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c \
    --hash=sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845 \
    --hash=sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5 \
    --hash=sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c \
    --hash=sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f \
    --hash=sha256:5112c2fb7c2eefefaeaf5c97fec81343ef44ee86a30dcfaa8223822fba6467b4 \
    --hash=sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54 \
    --hash=sha256:54d412e47c21b85865676ed0724e37a89e9593c2eee1e7367adf85bfad56ffb1 \
    --hash=sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236 \
    --hash=sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f \
    --hash=sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27 \
    --hash=sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b \
    --hash=sha256:657f8ba7b9eaaa82759a104137aed2a3ef7bc46ccfd43e0d89b04005b3e0a4cc \
    --hash=sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858 \
    --hash=sha256:6788207daa0c19955d2b668f3294a368d19f67d9b5f274553fd073c1260cbb9f \
    --hash=sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b \
    --hash=sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938 \
    --hash=sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a \
    --hash=sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b \
    --hash=sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d \
    --hash=sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f \
    --hash=sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71 \
    --hash=sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22 \
    --hash=sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8 \
    --hash=sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990 \
    --hash=sha256:81107843ed1836874b46b310f9b1816abcb89912af627868522461c3b7333c0f \
    --hash=sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2 \
    --hash=sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd \
    --hash=sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076 \
    --hash=sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671 \
    --hash=sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9 \
    --hash=sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15 \
    --hash=sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4 \
    --hash=sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f \
    --hash=sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8 \
    --hash=sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d \
    --hash=sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265 \
    --hash=sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61 \
    --hash=sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519 \
    --hash=sha256:a81eea9b999b985e4bacc650c4312805ea7008fd5e45e1bf221310176a7bcb3a \
    --hash=sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40 \
    --hash=sha256:aa95738a68cedd3a6f5492feddc513e2e166b50602958139e47bbdd82da0f5a7 \
    --hash=sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f \
    --hash=sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e \
    --hash=sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9 \
    --hash=sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a \
    --hash=sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3 \
    --hash=sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee \
    --hash=sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11 \
    --hash=sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4 \
    --hash=sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283 \
    --hash=sha256:d9da80e5b04acce03ced8ba6479a71c2a2edf535c2acc0d09c80d2f80f3bad15 \
    --hash=sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084 \
    --hash=sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e \
    --hash=sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882 \
    --hash=sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f \
    --hash=sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e \
    --hash=sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774 \
    --hash=sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce \
    --hash=sha256:eea1b54943475f51698f85fa230c65ccac769f1e603b981be060ac5763d90927 \
    --hash=sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6 \
    --hash=sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118 \
    --hash=sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4 \
    --hash=sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e \
    --hash=sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1 \
    --hash=sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f \
    --hash=sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2 \
    --hash=sha256:f48c963a76d71b9d7927eb817b543d0dccd52ab6648b99d37bd54f4cd475d856 \
    --hash=sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1 \
    --hash=sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f \
    --hash=sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a
    # via mypy
mypy==1.19.1 \
    --hash=sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd \
    --hash=sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b \
    --hash=sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1 \
    --hash=sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba \
    --hash=sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b \
    --hash=sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045 \
    --hash=sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac \
    --hash=sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6 \
    --hash=sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a \
    --hash=sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24 \
    --hash=sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957 \
    --hash=sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042 \
    --hash=sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e \
    --hash=sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec \
    --hash=sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3 \
    --hash=sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718 \
    --hash=sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f \
    --hash=sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331 \
    --hash=sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1 \
    --hash=sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1 \
    --hash=sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13 \
    --hash=sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67 \
    --hash=sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2 \
    --hash=sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a \
    --hash=sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b \
    --hash=sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8 \
    --hash=sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376 \
    --hash=sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef \
    --hash=sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288 \
    --hash=sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75 \
    --hash=sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74 \
    --hash=sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250 \
    --hash=sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab \
    --hash=sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6 \
    --hash=sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247 \
    --hash=sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925 \
    --hash=sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e \
    --hash=sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e
    # via -r lint.in
mypy-extensions==1.1.0 \
    --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
    --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558
    # via mypy
pathspec==1.1.0 \
    --hash=sha256:574b128f7456bd899045ccd142dd446af7e6cfd0072d63ad73fbc55fbb4aaa42 \
    --hash=sha256:f5d7c555da02fd8dde3e4a2354b6aba817a89112fa8f333f7917a2a4834dd080
    # via mypy
ruff==0.15.12 \
    --hash=sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b \
    --hash=sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33 \
    --hash=sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0 \
    --hash=sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002 \
    --hash=sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339 \
    --hash=sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e \
    --hash=sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847 \
    --hash=sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f \
    --hash=sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6 \
    --hash=sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d \
    --hash=sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20 \
    --hash=sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd \
    --hash=sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c \
    --hash=sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5 \
    --hash=sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6 \
    --hash=sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c \
    --hash=sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5 \
    --hash=sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5
    # via -r lint.in
tomli==2.4.1 \
    --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
    --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
    --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \
    --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \
    --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \
    --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \
    --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \
    --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \
    --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \
    --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \
    --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \
    --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \
    --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \
    --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \
    --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \
    --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \
    --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \
    --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \
    --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \
    --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \
    --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \
    --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \
    --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \
    --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \
    --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \
    --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \
    --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \
    --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \
    --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \
    --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \
    --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \
    --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \
    --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \
    --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \
    --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \
    --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \
    --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \
    --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \
    --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \
    --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \
    --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \
    --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \
    --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \
    --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \
    --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \
    --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \
    --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049
    # via mypy
typing-extensions==4.15.0 \
    --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
    --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
    # via mypy


================================================
FILE: requirements/test.in
================================================
pytest
pytest-cov
hypothesis


================================================
FILE: requirements/test.txt
================================================
# This file was autogenerated by uv via the following command:
#    uv pip compile --python-version 3.9 --generate-hashes --strip-extras --output-file=test.txt test.in
attrs==26.1.0 \
    --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
    --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
    # via hypothesis
coverage==7.10.7 \
    --hash=sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9 \
    --hash=sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880 \
    --hash=sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999 \
    --hash=sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1 \
    --hash=sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13 \
    --hash=sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b \
    --hash=sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82 \
    --hash=sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973 \
    --hash=sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f \
    --hash=sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681 \
    --hash=sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0 \
    --hash=sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541 \
    --hash=sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32 \
    --hash=sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17 \
    --hash=sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a \
    --hash=sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40 \
    --hash=sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd \
    --hash=sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6 \
    --hash=sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7 \
    --hash=sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb \
    --hash=sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f \
    --hash=sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d \
    --hash=sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe \
    --hash=sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c \
    --hash=sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807 \
    --hash=sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab \
    --hash=sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2 \
    --hash=sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546 \
    --hash=sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e \
    --hash=sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65 \
    --hash=sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396 \
    --hash=sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431 \
    --hash=sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb \
    --hash=sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699 \
    --hash=sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0 \
    --hash=sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f \
    --hash=sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a \
    --hash=sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235 \
    --hash=sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911 \
    --hash=sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23 \
    --hash=sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87 \
    --hash=sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699 \
    --hash=sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a \
    --hash=sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b \
    --hash=sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256 \
    --hash=sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a \
    --hash=sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417 \
    --hash=sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0 \
    --hash=sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a \
    --hash=sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360 \
    --hash=sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0 \
    --hash=sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b \
    --hash=sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb \
    --hash=sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2 \
    --hash=sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d \
    --hash=sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a \
    --hash=sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e \
    --hash=sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69 \
    --hash=sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14 \
    --hash=sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d \
    --hash=sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f \
    --hash=sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2 \
    --hash=sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c \
    --hash=sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0 \
    --hash=sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399 \
    --hash=sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59 \
    --hash=sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63 \
    --hash=sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b \
    --hash=sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2 \
    --hash=sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e \
    --hash=sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0 \
    --hash=sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520 \
    --hash=sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df \
    --hash=sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c \
    --hash=sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b \
    --hash=sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2 \
    --hash=sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f \
    --hash=sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61 \
    --hash=sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a \
    --hash=sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59 \
    --hash=sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c \
    --hash=sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf \
    --hash=sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07 \
    --hash=sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6 \
    --hash=sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e \
    --hash=sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594 \
    --hash=sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49 \
    --hash=sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843 \
    --hash=sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14 \
    --hash=sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3 \
    --hash=sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1 \
    --hash=sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698 \
    --hash=sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15 \
    --hash=sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d \
    --hash=sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5 \
    --hash=sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e \
    --hash=sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0 \
    --hash=sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b \
    --hash=sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239 \
    --hash=sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba \
    --hash=sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4 \
    --hash=sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260 \
    --hash=sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a \
    --hash=sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3
    # via pytest-cov
exceptiongroup==1.3.1 \
    --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \
    --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598
    # via
    #   hypothesis
    #   pytest
hypothesis==6.141.1 \
    --hash=sha256:8ef356e1e18fbeaa8015aab3c805303b7fe4b868e5b506e87ad83c0bf951f46f \
    --hash=sha256:a5b3c39c16d98b7b4c3c5c8d4262e511e3b2255e6814ced8023af49087ad60b3
    # via -r test.in
iniconfig==2.1.0 \
    --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \
    --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760
    # via pytest
packaging==26.2 \
    --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
    --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
    # via pytest
pluggy==1.6.0 \
    --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
    --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
    # via
    #   pytest
    #   pytest-cov
pygments==2.20.0 \
    --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
    --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
    # via pytest
pytest==8.4.2 \
    --hash=sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01 \
    --hash=sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79
    # via
    #   -r test.in
    #   pytest-cov
pytest-cov==7.1.0 \
    --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \
    --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678
    # via -r test.in
sortedcontainers==2.4.0 \
    --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
    --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
    # via hypothesis
tomli==2.4.1 \
    --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
    --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
    --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \
    --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \
    --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \
    --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \
    --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \
    --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \
    --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \
    --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \
    --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \
    --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \
    --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \
    --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \
    --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \
    --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \
    --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \
    --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \
    --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \
    --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \
    --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \
    --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \
    --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \
    --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \
    --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \
    --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \
    --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \
    --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \
    --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \
    --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \
    --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \
    --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \
    --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \
    --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \
    --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \
    --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \
    --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \
    --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \
    --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \
    --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \
    --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \
    --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \
    --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \
    --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \
    --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \
    --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \
    --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049
    # via
    #   coverage
    #   pytest
typing-extensions==4.15.0 \
    --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
    --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
    # via exceptiongroup


================================================
FILE: tests/__init__.py
================================================


================================================
FILE: tests/test_shamir.py
================================================
from __future__ import annotations

import itertools
from base64 import b64decode

import pytest

from pyshamir import combine, split

SPLIT_SECRET = b64decode("a+m4G0kEkKDQK4MFGz7L0vLz5oViQkDSLThiC4zDRZU=")
COMBINE_SECRET = b64decode("esfX3MUC++BrcwkiwsKtK6M5Pi5yvuc/A/6LweWJ5FA=")


def test_split_returns_correct_part_count_and_length():
    parts = split(SPLIT_SECRET, 5, 3)
    assert len(parts) == 5
    for part in parts:
        assert len(part) == len(SPLIT_SECRET) + 1


def test_split_produces_distinct_parts():
    parts = split(SPLIT_SECRET, 5, 3)
    for i in range(len(parts) - 1):
        assert parts[i].hex() != parts[i + 1].hex()


@pytest.mark.parametrize(
    "parts, threshold, message",
    [
        (0, 0, "Parts and threshold must be greater than 1"),
        (2, 3, "Parts must be greater than threshold"),
        (1000, 3, "Parts must be less than 256"),
    ],
)
def test_split_invalid_arguments(parts, threshold, message):
    with pytest.raises(ValueError, match=message):
        split(SPLIT_SECRET, parts, threshold)


@pytest.mark.parametrize("secret", [None, bytearray(b"")], ids=["none", "empty"])
def test_split_invalid_secret(secret):
    with pytest.raises(ValueError, match="Secret must be at least 1 byte long"):
        split(secret, 5, 3)


@pytest.mark.parametrize(
    "indices",
    list(itertools.combinations(range(5), 3)),
    ids=lambda x: "+".join(str(i) for i in x),
)
def test_combine_recovers_secret_from_any_threshold_subset(indices):
    parts = split(COMBINE_SECRET, 5, 3)
    selected = [parts[i] for i in indices]
    assert combine(selected) == COMBINE_SECRET


@pytest.mark.parametrize("parts", [None, bytearray()], ids=["none", "empty"])
def test_combine_with_too_few_parts(parts):
    with pytest.raises(ValueError, match="Not enough parts to combine"):
        combine(parts)


def test_combine_with_mismatched_part_lengths():
    with pytest.raises(ValueError, match="Parts are not the same length"):
        combine([bytearray(b"ab"), bytearray(b"abc")])


def test_combine_with_too_short_parts():
    with pytest.raises(ValueError, match="Part is too short"):
        combine([bytearray(b"a"), bytearray(b"b")])


def test_combine_with_duplicate_samples():
    with pytest.raises(ValueError, match="Duplicate sample"):
        combine([bytearray(b"ab"), bytearray(b"ab")])


================================================
FILE: tests/test_utils.py
================================================
"""Direct tests for the GF(256) primitives in pyshamir._utils.

These functions are exercised transitively through split/combine in
test_shamir.py, but direct tests pin the cryptographic invariants
(field axioms, AES Rijndael polynomial reduction, Horner evaluation,
Lagrange interpolation) and surface failures closer to their root cause.
"""

from __future__ import annotations

import pytest

from pyshamir._utils import (
    Polynomial,
    add,
    div,
    generate_x_coordinates,
    interpolate_polynomial,
    inverse,
    make_polynomial,
    mul,
)

# ----- add -----


@pytest.mark.parametrize(
    "a, b", [(a, b) for a in [0, 1, 0x55, 0xAA, 0xFF] for b in [0, 1, 0x55, 0xAA, 0xFF]]
)
def test_add_is_xor(a, b):
    assert add(a, b) == a ^ b


def test_add_zero_is_identity():
    for a in range(256):
        assert add(a, 0) == a
        assert add(0, a) == a


def test_add_self_is_zero():
    for a in range(256):
        assert add(a, a) == 0


# ----- mul -----


def test_mul_zero_annihilates():
    for a in range(256):
        assert mul(a, 0) == 0
        assert mul(0, a) == 0


def test_mul_one_is_identity():
    for a in range(256):
        assert mul(a, 1) == a
        assert mul(1, a) == a


@pytest.mark.parametrize(
    "a, b, expected",
    [
        # mul(2, x) doubles in GF(256); high-bit overflow XORs the Rijndael 0x1B.
        (2, 1, 2),
        (2, 0x40, 0x80),
        (2, 0x80, 0x1B),
        # 3 = 2 ^ 1, so mul(3, 7) = mul(2, 7) ^ mul(1, 7) = 14 ^ 7 = 9.
        (3, 7, 9),
        # FIPS 197 §4.2 worked example: 0x57 * 0x83 = 0xC1.
        (0x57, 0x83, 0xC1),
    ],
)
def test_mul_known_vectors(a, b, expected):
    assert mul(a, b) == expected


def test_mul_is_commutative():
    for a in [0x12, 0x34, 0x56, 0x78]:
        for b in [0x9A, 0xBC, 0xDE, 0xF0]:
            assert mul(a, b) == mul(b, a)


# ----- inverse -----


def test_inverse_times_self_is_one():
    for a in range(1, 256):
        assert mul(a, inverse(a)) == 1


def test_inverse_of_zero_is_zero():
    # 0 has no true multiplicative inverse; the exponentiation chain
    # collapses to 0, matching the Go reference.
    assert inverse(0) == 0


# ----- div -----


def test_div_by_zero_raises():
    with pytest.raises(ZeroDivisionError, match="Divide by zero"):
        div(5, 0)


def test_div_zero_numerator_returns_zero():
    for b in range(1, 256):
        assert div(0, b) == 0


def test_div_inverts_mul():
    for a in [0x01, 0x42, 0xAB, 0xFF]:
        for b in [0x01, 0x42, 0xAB, 0xFF]:
            assert div(mul(a, b), b) == a


# ----- Polynomial.evaluate -----


def test_polynomial_evaluate_at_zero_returns_intercept():
    p = Polynomial(3)
    p.coefficients[0] = 0x42
    p.coefficients[1] = 0x11
    p.coefficients[2] = 0x22
    p.coefficients[3] = 0x33
    assert p.evaluate(0) == 0x42


@pytest.mark.parametrize("x", [1, 2, 0x55, 0xAA, 0xFF])
def test_polynomial_evaluate_matches_manual_horner(x):
    p = Polynomial(3)
    p.coefficients[0] = 0x05
    p.coefficients[1] = 0x07
    p.coefficients[2] = 0x0B
    p.coefficients[3] = 0x0D

    # Horner: ((c3 * x + c2) * x + c1) * x + c0, with + as XOR in GF(256).
    expected = p.coefficients[3]
    expected = add(mul(expected, x), p.coefficients[2])
    expected = add(mul(expected, x), p.coefficients[1])
    expected = add(mul(expected, x), p.coefficients[0])

    assert p.evaluate(x) == expected


# ----- make_polynomial -----


def test_make_polynomial_sets_intercept_and_size():
    poly = make_polynomial(0x77, 5)
    assert poly.coefficients[0] == 0x77
    assert len(poly.coefficients) == 6  # degree + 1


# ----- interpolate_polynomial round-trip -----


@pytest.mark.parametrize("intercept", [0x00, 0x01, 0x55, 0xAA, 0xFF])
def test_interpolate_recovers_intercept(intercept):
    """Build a degree-3 polynomial, sample 4 distinct x's, interpolate at x=0."""
    poly = make_polynomial(intercept, 3)
    xs = bytearray([1, 2, 3, 4])
    ys = bytearray([poly.evaluate(x) for x in xs])
    assert interpolate_polynomial(xs, ys, 0) == intercept


# ----- generate_x_coordinates -----


def test_generate_x_coordinates_returns_permutation():
    n = 100
    xs = generate_x_coordinates(n)
    assert len(xs) == n
    assert sorted(xs) == list(range(n))


def test_generate_x_coordinates_is_shuffled():
    # Two independent calls on n=100 collide with probability 1/100! ≈ 0.
    assert generate_x_coordinates(100) != generate_x_coordinates(100)


================================================
FILE: tests/test_utils_properties.py
================================================
"""Hypothesis-based property tests for the GF(256) primitives in pyshamir._utils.

These complement the explicit-value tests in test_utils.py by sweeping random
inputs across the full byte domain and shrinking failures to minimal
counterexamples. Each test pins an algebraic invariant of the field; together
they form a partial fuzzing harness recognized by OpenSSF Scorecard's
`Fuzzing` check.
"""

from __future__ import annotations

from hypothesis import given, settings
from hypothesis import strategies as st

from pyshamir._utils import (
    add,
    div,
    interpolate_polynomial,
    inverse,
    make_polynomial,
    mul,
)

byte = st.integers(min_value=0, max_value=255)
nonzero_byte = st.integers(min_value=1, max_value=255)
small_degree = st.integers(min_value=2, max_value=8)


# ----- add (XOR) field axioms -----


@given(a=byte, b=byte)
def test_add_is_commutative(a: int, b: int) -> None:
    assert add(a, b) == add(b, a)


@given(a=byte, b=byte, c=byte)
def test_add_is_associative(a: int, b: int, c: int) -> None:
    assert add(add(a, b), c) == add(a, add(b, c))


@given(a=byte)
def test_add_zero_is_identity(a: int) -> None:
    assert add(a, 0) == a


# ----- mul field axioms -----


@given(a=byte, b=byte)
def test_mul_is_commutative(a: int, b: int) -> None:
    assert mul(a, b) == mul(b, a)


@given(a=byte, b=byte, c=byte)
def test_mul_distributes_over_add(a: int, b: int, c: int) -> None:
    assert mul(a, add(b, c)) == add(mul(a, b), mul(a, c))


@given(a=byte)
def test_mul_one_is_identity(a: int) -> None:
    assert mul(a, 1) == a


# ----- inverse / div -----


@given(a=nonzero_byte)
def test_inverse_is_multiplicative_inverse(a: int) -> None:
    assert mul(a, inverse(a)) == 1


@given(a=byte, b=nonzero_byte)
def test_div_inverts_mul(a: int, b: int) -> None:
    assert div(mul(a, b), b) == a


# ----- Polynomial / Lagrange round-trip -----


@settings(max_examples=200)
@given(intercept=byte, degree=small_degree)
def test_interpolate_recovers_intercept(intercept: int, degree: int) -> None:
    """For any random polynomial of degree d with intercept I,
    sampling at d+1 distinct non-zero x's and interpolating at x=0 yields I.
    """
    poly = make_polynomial(intercept, degree)
    xs = bytearray(range(1, degree + 2))
    ys = bytearray([poly.evaluate(x) for x in xs])
    assert interpolate_polynomial(xs, ys, 0) == intercept


================================================
FILE: tox.ini
================================================
[tox]
envlist = py39,py310,py311,py312,py313,lint,type
skip_missing_interpreters = true

[testenv]
deps =
    pytest
    pytest-cov
    hypothesis
commands = pytest --cov=pyshamir --cov-report=term-missing

[testenv:lint]
deps = ruff
commands =
    ruff check pyshamir tests
    ruff format --check pyshamir tests

[testenv:type]
deps = mypy
commands = mypy pyshamir

[testenv:coverage]
deps =
    pytest
    pytest-cov
    hypothesis
commands = pytest --cov=pyshamir --cov-report=xml --cov-report=html --cov-report=term-missing
Download .txt
gitextract_tvko4ywd/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── codeql.yml
│       ├── compatibility-test.yml
│       ├── release.yml
│       ├── scorecard.yml
│       └── unit-test-and-reporting.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── pyproject.toml
├── pyshamir/
│   ├── __init__.py
│   ├── _utils.py
│   └── shamir.py
├── requirements/
│   ├── build.in
│   ├── build.txt
│   ├── lint.in
│   ├── lint.txt
│   ├── test.in
│   └── test.txt
├── tests/
│   ├── __init__.py
│   ├── test_shamir.py
│   ├── test_utils.py
│   └── test_utils_properties.py
└── tox.ini
Download .txt
SYMBOL INDEX (48 symbols across 5 files)

FILE: pyshamir/_utils.py
  function add (line 7) | def add(a: int, b: int) -> int:
  function inverse (line 15) | def inverse(a: int) -> int:
  function mul (line 29) | def mul(a: int, b: int) -> int:
  function div (line 45) | def div(a: int, b: int) -> int:
  class Polynomial (line 59) | class Polynomial:
    method __init__ (line 62) | def __init__(self, degree: int) -> None:
    method evaluate (line 65) | def evaluate(self, x: int) -> int:
  function make_polynomial (line 79) | def make_polynomial(intercept: int, degree: int) -> Polynomial:
  function interpolate_polynomial (line 97) | def interpolate_polynomial(x_samples: bytearray, y_samples: bytearray, x...
  function generate_x_coordinates (line 114) | def generate_x_coordinates(n: int) -> list[int]:

FILE: pyshamir/shamir.py
  function combine (line 6) | def combine(parts: list[bytearray]) -> bytearray:
  function split (line 59) | def split(secret: bytes, parts: int, threshold: int) -> list[bytearray]:

FILE: tests/test_shamir.py
  function test_split_returns_correct_part_count_and_length (line 14) | def test_split_returns_correct_part_count_and_length():
  function test_split_produces_distinct_parts (line 21) | def test_split_produces_distinct_parts():
  function test_split_invalid_arguments (line 35) | def test_split_invalid_arguments(parts, threshold, message):
  function test_split_invalid_secret (line 41) | def test_split_invalid_secret(secret):
  function test_combine_recovers_secret_from_any_threshold_subset (line 51) | def test_combine_recovers_secret_from_any_threshold_subset(indices):
  function test_combine_with_too_few_parts (line 58) | def test_combine_with_too_few_parts(parts):
  function test_combine_with_mismatched_part_lengths (line 63) | def test_combine_with_mismatched_part_lengths():
  function test_combine_with_too_short_parts (line 68) | def test_combine_with_too_short_parts():
  function test_combine_with_duplicate_samples (line 73) | def test_combine_with_duplicate_samples():

FILE: tests/test_utils.py
  function test_add_is_xor (line 30) | def test_add_is_xor(a, b):
  function test_add_zero_is_identity (line 34) | def test_add_zero_is_identity():
  function test_add_self_is_zero (line 40) | def test_add_self_is_zero():
  function test_mul_zero_annihilates (line 48) | def test_mul_zero_annihilates():
  function test_mul_one_is_identity (line 54) | def test_mul_one_is_identity():
  function test_mul_known_vectors (line 73) | def test_mul_known_vectors(a, b, expected):
  function test_mul_is_commutative (line 77) | def test_mul_is_commutative():
  function test_inverse_times_self_is_one (line 86) | def test_inverse_times_self_is_one():
  function test_inverse_of_zero_is_zero (line 91) | def test_inverse_of_zero_is_zero():
  function test_div_by_zero_raises (line 100) | def test_div_by_zero_raises():
  function test_div_zero_numerator_returns_zero (line 105) | def test_div_zero_numerator_returns_zero():
  function test_div_inverts_mul (line 110) | def test_div_inverts_mul():
  function test_polynomial_evaluate_at_zero_returns_intercept (line 119) | def test_polynomial_evaluate_at_zero_returns_intercept():
  function test_polynomial_evaluate_matches_manual_horner (line 129) | def test_polynomial_evaluate_matches_manual_horner(x):
  function test_make_polynomial_sets_intercept_and_size (line 148) | def test_make_polynomial_sets_intercept_and_size():
  function test_interpolate_recovers_intercept (line 158) | def test_interpolate_recovers_intercept(intercept):
  function test_generate_x_coordinates_returns_permutation (line 169) | def test_generate_x_coordinates_returns_permutation():
  function test_generate_x_coordinates_is_shuffled (line 176) | def test_generate_x_coordinates_is_shuffled():

FILE: tests/test_utils_properties.py
  function test_add_is_commutative (line 33) | def test_add_is_commutative(a: int, b: int) -> None:
  function test_add_is_associative (line 38) | def test_add_is_associative(a: int, b: int, c: int) -> None:
  function test_add_zero_is_identity (line 43) | def test_add_zero_is_identity(a: int) -> None:
  function test_mul_is_commutative (line 51) | def test_mul_is_commutative(a: int, b: int) -> None:
  function test_mul_distributes_over_add (line 56) | def test_mul_distributes_over_add(a: int, b: int, c: int) -> None:
  function test_mul_one_is_identity (line 61) | def test_mul_one_is_identity(a: int) -> None:
  function test_inverse_is_multiplicative_inverse (line 69) | def test_inverse_is_multiplicative_inverse(a: int) -> None:
  function test_div_inverts_mul (line 74) | def test_div_inverts_mul(a: int, b: int) -> None:
  function test_interpolate_recovers_intercept (line 83) | def test_interpolate_recovers_intercept(intercept: int, degree: int) -> ...
Condensed preview — 31 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (117K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 834,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 595,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 368,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n\n "
  },
  {
    "path": ".github/workflows/codeql.yml",
    "chars": 1204,
    "preview": "name: CodeQL\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n  schedule:\n    # Weekly Monday at 0"
  },
  {
    "path": ".github/workflows/compatibility-test.yml",
    "chars": 1419,
    "preview": "name: Compatibility Test\n\non:\n  workflow_dispatch: {}\n  push:\n    branches:\n      - main\n  pull_request:\n    types: [ope"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 5928,
    "preview": "# 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"
  },
  {
    "path": ".github/workflows/scorecard.yml",
    "chars": 2979,
    "preview": "# This workflow uses actions that are not certified by GitHub. They are provided\n# by a third-party and are governed by "
  },
  {
    "path": ".github/workflows/unit-test-and-reporting.yml",
    "chars": 1643,
    "preview": "name: Unit Test & Reporting\n\non:\n  workflow_dispatch: {}\n  push:\n    branches:\n      - main\n  pull_request:\n    types: ["
  },
  {
    "path": ".gitignore",
    "chars": 8496,
    "preview": "# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,intellij,vim,emacs,git"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 530,
    "preview": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v5.0.0\n    hooks:\n      - id: trailing-whitespa"
  },
  {
    "path": "CLAUDE.md",
    "chars": 4297,
    "preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5225,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 5578,
    "preview": "# How to contribute\n\nPlease read these guidelines before contributing to pyshamir:\n\n - [Question or Problem?](#question)"
  },
  {
    "path": "LICENSE",
    "chars": 15957,
    "preview": "Copyright (c) 2015 HashiCorp, Inc.\n\nMozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n     means "
  },
  {
    "path": "README.md",
    "chars": 1092,
    "preview": "![pyshamir banner](https://user-images.githubusercontent.com/5201843/232241639-22034903-87c2-4bf0-9b36-2ae9a8481b71.png)"
  },
  {
    "path": "SECURITY.md",
    "chars": 450,
    "preview": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 1.0.x   | "
  },
  {
    "path": "pyproject.toml",
    "chars": 1732,
    "preview": "[build-system]\nrequires = [\"setuptools>=77.0.0\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"pys"
  },
  {
    "path": "pyshamir/__init__.py",
    "chars": 89,
    "preview": "from .shamir import combine, split\n\n__all__ = [\"combine\", \"split\"]\n__version__ = \"1.0.4\"\n"
  },
  {
    "path": "pyshamir/_utils.py",
    "chars": 2857,
    "preview": "from __future__ import annotations\n\nimport secrets\nfrom ctypes import c_uint8\n\n\ndef add(a: int, b: int) -> int:\n    \"\"\"\n"
  },
  {
    "path": "pyshamir/shamir.py",
    "chars": 3079,
    "preview": "from __future__ import annotations\n\nfrom ._utils import generate_x_coordinates, interpolate_polynomial, make_polynomial\n"
  },
  {
    "path": "requirements/build.in",
    "chars": 6,
    "preview": "build\n"
  },
  {
    "path": "requirements/build.txt",
    "chars": 5234,
    "preview": "# This file was autogenerated by uv via the following command:\n#    uv pip compile --python-version 3.9 --generate-hashe"
  },
  {
    "path": "requirements/lint.in",
    "chars": 10,
    "preview": "ruff\nmypy\n"
  },
  {
    "path": "requirements/lint.txt",
    "chars": 17318,
    "preview": "# This file was autogenerated by uv via the following command:\n#    uv pip compile --python-version 3.9 --generate-hashe"
  },
  {
    "path": "requirements/test.in",
    "chars": 29,
    "preview": "pytest\npytest-cov\nhypothesis\n"
  },
  {
    "path": "requirements/test.txt",
    "chars": 15464,
    "preview": "# This file was autogenerated by uv via the following command:\n#    uv pip compile --python-version 3.9 --generate-hashe"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/test_shamir.py",
    "chars": 2341,
    "preview": "from __future__ import annotations\n\nimport itertools\nfrom base64 import b64decode\n\nimport pytest\n\nfrom pyshamir import c"
  },
  {
    "path": "tests/test_utils.py",
    "chars": 4447,
    "preview": "\"\"\"Direct tests for the GF(256) primitives in pyshamir._utils.\n\nThese functions are exercised transitively through split"
  },
  {
    "path": "tests/test_utils_properties.py",
    "chars": 2382,
    "preview": "\"\"\"Hypothesis-based property tests for the GF(256) primitives in pyshamir._utils.\n\nThese complement the explicit-value t"
  },
  {
    "path": "tox.ini",
    "chars": 529,
    "preview": "[tox]\nenvlist = py39,py310,py311,py312,py313,lint,type\nskip_missing_interpreters = true\n\n[testenv]\ndeps =\n    pytest\n   "
  }
]

About this extraction

This page contains the full source code of the konidev20/pyshamir GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 31 files (109.5 KB), approximately 39.0k tokens, and a symbol index with 48 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!