diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..a74ab68 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,318 @@ +# GitHub Actions Workflows + +## Overview + +This repository has two main workflows: + +1. **Build GmSSL Libraries** (`build-gmssl-libs.yml`) - Updates bundled GmSSL libraries +2. **Release to GitHub** (`release.yml`) - Publishes Python packages to GitHub Releases + +--- + +## Build GmSSL Libraries + +### Purpose + +The `build-gmssl-libs.yml` workflow builds GmSSL dynamic libraries for multiple platforms and creates a PR to update the bundled libraries in `src/gmssl/_libs/`. + +### Supported Platforms + +- **Linux**: x86_64, aarch64 (`libgmssl.so.3`) + - Built with manylinux_2_28 (glibc 2.28+) + - Compatible with Ubuntu 20.04+, Debian 11+, RHEL 8+, etc. +- **macOS**: Universal binary arm64 + x86_64 (`libgmssl.3.dylib`) + - Compatible with macOS 11.0+ +- **Windows**: x86_64 (`gmssl.dll`) + - Compatible with Windows 10+ + +### How to Use + +1. Go to **Actions** tab in GitHub +2. Select **Build GmSSL Libraries** workflow +3. Click **Run workflow** +4. Enter parameters: + - **GmSSL version**: e.g., `3.1.1` (default) + - **GmSSL repository**: e.g., `https://github.com/guanzhi/GmSSL.git` (default) +5. Click **Run workflow** button + +### What Happens + +1. **Build Stage**: Compiles GmSSL for each platform in parallel + - Linux x86_64: manylinux_2_28 container (glibc 2.28) - wide compatibility + - Linux aarch64: manylinux_2_28 container (glibc 2.28) - wide compatibility + - macOS arm64: macOS latest (M1/M2) + - macOS x86_64: macOS 13 (Intel) + - Windows x86_64: Windows latest with MSVC + +2. **Artifact Stage**: Uploads compiled libraries as artifacts + - Retention: 7 days + - Can be downloaded manually if needed + +3. **Test Stage**: Automatically tests libraries on multiple platforms + - **Linux**: Tests with Python 3.8, 3.9, 3.10, 3.11, 3.12 + - **macOS**: Tests with Python 3.12 (universal binary) + - **Windows**: Tests with Python 3.12 + - Runs full test suite (`pytest tests/ -v`) + - Verifies library loading and version info + - **Fail-fast disabled**: All combinations tested even if one fails + +4. **PR Stage**: Creates a pull request with updated libraries (only if tests pass) + - Branch: `update-gmssl-libs-{version}` + - Includes all platform libraries + - macOS: Creates universal binary (arm64 + x86_64) + - **All tests must pass** before PR is created + +### Testing the PR + +**Automated Testing**: The workflow automatically tests all libraries before creating the PR. You can review test results in the Actions tab. + +**Manual Testing** (optional): If you want to test locally: + +```bash +# Clone the PR branch +git fetch origin update-gmssl-libs-{version} +git checkout update-gmssl-libs-{version} + +# Run tests (library is self-contained, no environment setup needed) +pytest tests/ -v + +# Build wheel and verify library is included +python -m build --wheel +unzip -l dist/gmssl_python-*.whl | grep -E "(\.dylib|\.so|\.dll)" +``` + +**Test Coverage**: +- ✅ Linux x86_64: Python 3.8, 3.9, 3.10, 3.11, 3.12 +- ✅ macOS universal: Python 3.12 +- ✅ Windows x86_64: Python 3.12 +- ✅ All 19 test cases from `tests/test_gmssl.py` + +### Manual Library Update + +If you need to update libraries manually: + +1. Build GmSSL on your platform: + ```bash + git clone https://github.com/guanzhi/GmSSL.git + cd GmSSL + mkdir build && cd build + cmake .. -DCMAKE_BUILD_TYPE=Release + make -j$(nproc) + ``` + +2. Copy library to package: + ```bash + # Linux + cp bin/libgmssl.so.3 ../GmSSL-Python/src/gmssl/_libs/ + + # macOS + cp bin/libgmssl.3.dylib ../GmSSL-Python/src/gmssl/_libs/ + + # Windows + cp bin/Release/gmssl.dll ../GmSSL-Python/src/gmssl/_libs/ + ``` + +3. Verify and commit: + ```bash + cd ../GmSSL-Python + git add src/gmssl/_libs/ + git commit -m "chore: update bundled GmSSL library for {platform}" + ``` + +### Troubleshooting + +**Q: Workflow fails on Windows build** +- Check if GmSSL CMake configuration supports Windows +- Verify MSVC toolchain is properly set up +- Check GmSSL version compatibility + +**Q: macOS universal binary creation fails** +- Fallback: Uses arm64 library only +- Can manually create universal binary with `lipo` command + +**Q: Library dependencies missing** +- GmSSL should only depend on system libraries +- Check with `ldd` (Linux), `otool -L` (macOS), or `dumpbin /dependents` (Windows) + +**Q: Tests fail on specific platform** +- Check the Actions tab for detailed test output +- Common issues: + - Library not found: Verify artifact download step + - Import errors: Check Python version compatibility + - Test failures: May indicate library build issues + +### Debug Information + +The workflow includes extensive debug output to help diagnose issues: + +**Library verification:** +- File type and architecture information +- Symbol table checks (especially SM9 functions: `sm9_sign_*`) +- Dependencies verification (ldd/otool) +- Libraries are NOT stripped (preserving all symbols for debugging) + +**Test debugging:** +- Verbose test output with `-v -s --tb=short` +- Separate SM9 test run with full traceback (`--tb=long`) +- Tests continue even on failure (`continue-on-error: true`) +- Shows loaded library path and version + +**Known Issues:** +- SM9 tests may fail in CI environments (under investigation) +- If SM9 tests fail, check the debug output for: + - Symbol table contains `sm9_sign_*` functions + - Library size (unstripped libraries are larger) + - Python version and platform combination + - Loaded library path matches expected bundled library +- PR will not be created if any tests fail + +**Q: Want to skip tests and create PR anyway** +- Not recommended, but you can: + - Manually download artifacts from the workflow run + - Create PR manually following the steps in "Manual Library Update" + +### License + +GmSSL is licensed under Apache-2.0, same as this project. +Redistribution is permitted under the terms of the Apache License 2.0. + +--- + +## Release to GitHub + +### Purpose + +The `release.yml` workflow builds Python packages (wheel and sdist), tests them on multiple platforms, and creates a GitHub Release with the packages as downloadable assets. + +### Trigger Methods + +**Method 1: Push a version tag (Recommended)** +```bash +# Update version in pyproject.toml first +# Then create and push a tag +git tag v2.2.3 +git push origin v2.2.3 +``` + +**Method 2: Manual trigger** +1. Go to **Actions** tab in GitHub +2. Select **Release to GitHub** workflow +3. Click **Run workflow** +4. Select branch (usually `main`) +5. Click **Run workflow** button + +### What Happens + +1. **Build Stage**: Creates Python packages + - Builds universal wheel (`gmssl_python-{version}-py3-none-any.whl`) + - Builds source distribution (`gmssl_python-{version}.tar.gz`) + - Verifies package includes bundled libraries + - Validates package metadata with `twine check` + +2. **Test Stage**: Tests packages on multiple platforms + - **Platforms**: Linux, macOS, Windows + - **Python versions**: 3.8, 3.12 + - Installs wheel and runs full test suite + - Verifies library loading and version info + +3. **Release Stage**: Creates GitHub Release (only if all tests pass) + - Creates release with version tag + - Generates release notes automatically + - Uploads wheel and sdist as release assets + - **Public release** - users can download packages directly + +### Version Management + +**Important**: The version in `pyproject.toml` must match the git tag. + +```toml +# pyproject.toml +[project] +version = "2.2.3" # Must match tag v2.2.3 +``` + +The workflow will verify version consistency and fail if they don't match. + +### Release Notes + +Release notes are auto-generated and include: +- Installation instructions +- What's included in the package +- Package file descriptions +- Link to documentation + +You can customize the release notes after creation if needed. + +### After Release + +Users can install the package in three ways: + +**1. From PyPI (if you publish there)** +```bash +pip install gmssl-python==2.2.3 +``` + +**2. From GitHub Release** +```bash +# Download wheel from release page, then: +pip install gmssl_python-2.2.3-py3-none-any.whl +``` + +**3. From source** +```bash +pip install https://github.com/GmSSL/GmSSL-Python/archive/refs/tags/v2.2.3.tar.gz +``` + +### Publishing to PyPI + +This workflow only creates GitHub Releases. To publish to PyPI: + +```bash +# Download packages from GitHub Release or build locally +python -m build + +# Upload to PyPI +twine upload dist/* +``` + +Or create a separate workflow for PyPI publishing (recommended). + +### Troubleshooting + +**Q: Workflow fails with "Version mismatch"** +- Make sure `pyproject.toml` version matches the git tag +- Example: Tag `v2.2.3` requires `version = "2.2.3"` in pyproject.toml + +**Q: Tests fail on specific platform** +- Check the Actions tab for detailed test output +- Common issues: missing libraries, import errors +- Fix the issue and re-run the workflow or push a new tag + +**Q: Want to delete a release** +- Go to Releases page +- Click on the release +- Click "Delete this release" +- Optionally delete the tag: `git push --delete origin v2.2.3` + +**Q: Want to update a release** +- You can edit release notes and re-upload files manually +- Or delete the release and tag, then re-run the workflow + +### Best Practices + +1. **Test before releasing** + - Run tests locally: `pytest tests/ -v` + - Build and test wheel locally: `python -m build && pip install dist/*.whl` + +2. **Version bumping** + - Update version in `pyproject.toml` + - Update CHANGELOG or release notes + - Commit changes before tagging + +3. **Semantic versioning** + - Use semantic versioning: `MAJOR.MINOR.PATCH` + - Example: `2.2.3` → `2.2.4` (patch), `2.3.0` (minor), `3.0.0` (major) + +4. **Tag naming** + - Always prefix with `v`: `v2.2.3` + - Use annotated tags: `git tag -a v2.2.3 -m "Release version 2.2.3"` diff --git a/.github/workflows/build-gmssl-libs.yml b/.github/workflows/build-gmssl-libs.yml new file mode 100644 index 0000000..2a1976f --- /dev/null +++ b/.github/workflows/build-gmssl-libs.yml @@ -0,0 +1,591 @@ +name: Build GmSSL Libraries + +# This workflow builds GmSSL dynamic libraries for multiple platforms, +# tests them automatically, and creates a PR to update the bundled libraries. +# +# Workflow stages: +# 1. Build: Compile libraries for Linux (x86_64, aarch64), macOS (universal), Windows (x86_64) +# 2. Test: Run full test suite on multiple platforms and Python versions +# 3. PR: Create pull request with updated libraries (only if all tests pass) +# +# Trigger manually when you need to update bundled libraries + +on: + workflow_dispatch: + inputs: + gmssl_version: + description: 'GmSSL version to build (e.g., 3.1.1, master, or commit hash)' + required: true + default: 'master' + gmssl_repo: + description: 'GmSSL repository URL' + required: true + default: 'https://github.com/guanzhi/GmSSL.git' + build_platforms: + description: 'Platforms to build (comma-separated: linux-x86_64,linux-aarch64,macos,windows or "all")' + required: true + default: 'all' + test_platforms: + description: 'Platforms to test (comma-separated: ubuntu-latest,ubuntu-22.04,macos-latest,windows-latest or "all")' + required: true + default: 'all' + +jobs: + build-linux-x86_64: + name: Build GmSSL for Linux (x86_64) + runs-on: ubuntu-latest + if: ${{ github.event.inputs.build_platforms == 'all' || contains(github.event.inputs.build_platforms, 'linux-x86_64') }} + + steps: + - name: Checkout GmSSL-Python + uses: actions/checkout@v4 + + - name: Clone GmSSL + run: | + git clone --depth 1 --branch ${{ github.event.inputs.gmssl_version }} \ + ${{ github.event.inputs.gmssl_repo }} GmSSL + + - name: Build GmSSL in x86_64 manylinux container + run: | + # Use manylinux2014 (GLIBC 2.17) for maximum compatibility + # This ensures the library works on Ubuntu 14.04+, Debian 8+, RHEL 7+ + docker run --rm --platform linux/amd64 \ + -v $PWD/GmSSL:/workspace \ + -v $PWD/artifacts:/artifacts \ + quay.io/pypa/manylinux2014_x86_64 \ + bash -c " + yum install -y cmake3 gcc gcc-c++ make file binutils && \ + ln -sf /usr/bin/cmake3 /usr/bin/cmake && \ + cd /workspace && \ + rm -rf build && mkdir build && cd build && \ + cmake .. -DCMAKE_BUILD_TYPE=Release && \ + make -j\$(nproc) && \ + mkdir -p /artifacts/linux-x86_64 && \ + cp bin/libgmssl.so.3 /artifacts/linux-x86_64/ && \ + echo '=== Library info ===' && \ + file /artifacts/linux-x86_64/libgmssl.so.3 && \ + ldd /artifacts/linux-x86_64/libgmssl.so.3 || true && \ + ls -lh /artifacts/linux-x86_64/libgmssl.so.3 && \ + echo '=== GLIBC version requirement ===' && \ + objdump -T /artifacts/linux-x86_64/libgmssl.so.3 | grep GLIBC | awk '{print \$5}' | sort -Vu && \ + echo '=== SM3 PBKDF2 symbol check ===' && \ + if ! nm -D /artifacts/linux-x86_64/libgmssl.so.3 | grep -q sm3_pbkdf2; then \ + echo 'ERROR: sm3_pbkdf2 symbol not found!'; \ + exit 1; \ + fi && \ + echo 'sm3_pbkdf2 symbol found' && \ + echo '=== SM9 symbols check ===' && \ + nm -D /artifacts/linux-x86_64/libgmssl.so.3 | grep -i sm9_sign | head -5 || echo 'No SM9 symbols found' + " + + - name: Verify library + run: | + file artifacts/linux-x86_64/libgmssl.so.3 + ls -lh artifacts/linux-x86_64/libgmssl.so.3 + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gmssl-linux-x86_64 + path: artifacts/linux-x86_64/libgmssl.so.3 + retention-days: 7 + + build-linux-aarch64: + name: Build GmSSL for Linux (aarch64) + runs-on: ubuntu-latest + if: ${{ github.event.inputs.build_platforms == 'all' || contains(github.event.inputs.build_platforms, 'linux-aarch64') }} + + steps: + - name: Checkout GmSSL-Python + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Clone GmSSL + run: | + git clone --depth 1 --branch ${{ github.event.inputs.gmssl_version }} \ + ${{ github.event.inputs.gmssl_repo }} GmSSL + + - name: Build GmSSL in ARM64 manylinux container + run: | + # Use manylinux2014 (GLIBC 2.17) for maximum compatibility + docker run --rm --platform linux/arm64 \ + -v $PWD/GmSSL:/workspace \ + -v $PWD/artifacts:/artifacts \ + quay.io/pypa/manylinux2014_aarch64 \ + bash -c " + yum install -y gcc gcc-c++ make file binutils && \ + cmake --version && \ + cd /workspace && \ + rm -rf build && mkdir build && cd build && \ + cmake .. -DCMAKE_BUILD_TYPE=Release && \ + make -j\$(nproc) && \ + mkdir -p /artifacts/linux-aarch64 && \ + cp bin/libgmssl.so.3 /artifacts/linux-aarch64/ && \ + echo '=== Library info ===' && \ + file /artifacts/linux-aarch64/libgmssl.so.3 && \ + ldd /artifacts/linux-aarch64/libgmssl.so.3 || true && \ + ls -lh /artifacts/linux-aarch64/libgmssl.so.3 && \ + echo '=== GLIBC version requirement ===' && \ + objdump -T /artifacts/linux-aarch64/libgmssl.so.3 | grep GLIBC | awk '{print \$5}' | sort -Vu && \ + echo '=== SM3 PBKDF2 symbol check ===' && \ + if ! nm -D /artifacts/linux-aarch64/libgmssl.so.3 | grep -q sm3_pbkdf2; then \ + echo 'ERROR: sm3_pbkdf2 symbol not found!'; \ + exit 1; \ + fi && \ + echo 'sm3_pbkdf2 symbol found' && \ + echo '=== SM9 symbols check ===' && \ + nm -D /artifacts/linux-aarch64/libgmssl.so.3 | grep -i sm9_sign | head -5 || echo 'No SM9 symbols found' + " + + - name: Verify library + run: | + file artifacts/linux-aarch64/libgmssl.so.3 + ls -lh artifacts/linux-aarch64/libgmssl.so.3 + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gmssl-linux-aarch64 + path: artifacts/linux-aarch64/libgmssl.so.3 + retention-days: 7 + + build-macos-arm64: + name: Build GmSSL for macOS (arm64) + runs-on: macos-latest + if: ${{ github.event.inputs.build_platforms == 'all' || contains(github.event.inputs.build_platforms, 'macos') }} + + steps: + - name: Checkout GmSSL-Python + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + brew install cmake + + - name: Clone GmSSL + run: | + git clone --depth 1 --branch ${{ github.event.inputs.gmssl_version }} \ + ${{ github.event.inputs.gmssl_repo }} GmSSL + + - name: Build GmSSL + run: | + cd GmSSL + mkdir build && cd build + cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_OSX_ARCHITECTURES=arm64 \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 + make -j$(sysctl -n hw.ncpu) + + - name: Copy library + run: | + mkdir -p artifacts/macos-arm64 + cp GmSSL/build/bin/libgmssl.3.dylib artifacts/macos-arm64/ + + # Verify library (NO STRIP - debugging CI issues) + echo "=== Library info ===" + file artifacts/macos-arm64/libgmssl.3.dylib + otool -L artifacts/macos-arm64/libgmssl.3.dylib + ls -lh artifacts/macos-arm64/libgmssl.3.dylib + + echo "=== SM9 symbols check ===" + nm -g artifacts/macos-arm64/libgmssl.3.dylib | grep -i sm9_sign | head -5 || echo "No SM9 symbols found" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gmssl-macos-arm64 + path: artifacts/macos-arm64/libgmssl.3.dylib + retention-days: 7 + + build-macos-x86_64: + name: Build GmSSL for macOS (x86_64) + runs-on: macos-13 # Intel-based runner + if: ${{ github.event.inputs.build_platforms == 'all' || contains(github.event.inputs.build_platforms, 'macos') }} + + steps: + - name: Checkout GmSSL-Python + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + brew install cmake + + - name: Clone GmSSL + run: | + git clone --depth 1 --branch ${{ github.event.inputs.gmssl_version }} \ + ${{ github.event.inputs.gmssl_repo }} GmSSL + + - name: Build GmSSL + run: | + cd GmSSL + mkdir build && cd build + cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_OSX_ARCHITECTURES=x86_64 \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 + make -j$(sysctl -n hw.ncpu) + + - name: Copy library + run: | + mkdir -p artifacts/macos-x86_64 + cp GmSSL/build/bin/libgmssl.3.dylib artifacts/macos-x86_64/ + + # Verify library (NO STRIP - debugging CI issues) + echo "=== Library info ===" + file artifacts/macos-x86_64/libgmssl.3.dylib + otool -L artifacts/macos-x86_64/libgmssl.3.dylib + ls -lh artifacts/macos-x86_64/libgmssl.3.dylib + + echo "=== SM9 symbols check ===" + nm -g artifacts/macos-x86_64/libgmssl.3.dylib | grep -i sm9_sign | head -5 || echo "No SM9 symbols found" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gmssl-macos-x86_64 + path: artifacts/macos-x86_64/libgmssl.3.dylib + retention-days: 7 + + build-windows: + name: Build GmSSL for Windows (x86_64) + runs-on: windows-latest + if: ${{ github.event.inputs.build_platforms == 'all' || contains(github.event.inputs.build_platforms, 'windows') }} + + steps: + - name: Checkout GmSSL-Python + uses: actions/checkout@v4 + + - name: Setup MSVC + uses: microsoft/setup-msbuild@v2 + + - name: Clone GmSSL + run: | + git clone --depth 1 --branch ${{ github.event.inputs.gmssl_version }} ` + ${{ github.event.inputs.gmssl_repo }} GmSSL + + - name: Build GmSSL + run: | + cd GmSSL + mkdir build + cd build + cmake .. -A x64 + cmake --build . --config Release + + - name: Copy library + run: | + mkdir -p artifacts/windows-x86_64 + cp GmSSL/build/bin/Release/gmssl.dll artifacts/windows-x86_64/ + + # Verify library exists + ls -l artifacts/windows-x86_64/gmssl.dll + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gmssl-windows-x86_64 + path: artifacts/windows-x86_64/gmssl.dll + retention-days: 7 + + create-macos-universal: + name: Create macOS Universal Binary + needs: [build-macos-arm64, build-macos-x86_64] + runs-on: macos-latest + if: ${{ always() && (github.event.inputs.build_platforms == 'all' || contains(github.event.inputs.build_platforms, 'macos')) && !cancelled() && !contains(needs.*.result, 'failure') }} + + steps: + - name: Download arm64 library + uses: actions/download-artifact@v4 + with: + name: gmssl-macos-arm64 + path: macos-arm64 + + - name: Download x86_64 library + uses: actions/download-artifact@v4 + with: + name: gmssl-macos-x86_64 + path: macos-x86_64 + + - name: Create universal binary + run: | + mkdir -p artifacts/macos-universal + lipo -create \ + macos-arm64/libgmssl.3.dylib \ + macos-x86_64/libgmssl.3.dylib \ + -output artifacts/macos-universal/libgmssl.3.dylib + + # Verify + file artifacts/macos-universal/libgmssl.3.dylib + lipo -info artifacts/macos-universal/libgmssl.3.dylib + + - name: Upload universal binary + uses: actions/upload-artifact@v4 + with: + name: gmssl-macos-universal + path: artifacts/macos-universal/libgmssl.3.dylib + retention-days: 7 + + test-libraries: + name: Test libraries on ${{ matrix.os }} with Python ${{ matrix.python-version }} + needs: [build-linux-x86_64, build-linux-aarch64, create-macos-universal, build-windows] + runs-on: ${{ matrix.os }} + if: ${{ always() && !cancelled() && !contains(needs.*.result, 'failure') }} + + strategy: + fail-fast: false # Continue testing other combinations even if one fails + matrix: + os: [ubuntu-latest, ubuntu-22.04, macos-latest, windows-latest] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + exclude: + # Only test all Python versions on ubuntu-latest to save CI time + # ubuntu-22.04 tests Python 3.8 and 3.12 (LTS compatibility check) + - os: ubuntu-22.04 + python-version: '3.9' + - os: ubuntu-22.04 + python-version: '3.10' + - os: ubuntu-22.04 + python-version: '3.11' + # macOS and Windows only test latest Python + - os: macos-latest + python-version: '3.8' + - os: macos-latest + python-version: '3.9' + - os: macos-latest + python-version: '3.10' + - os: macos-latest + python-version: '3.11' + - os: windows-latest + python-version: '3.8' + - os: windows-latest + python-version: '3.9' + - os: windows-latest + python-version: '3.10' + - os: windows-latest + python-version: '3.11' + + steps: + - name: Check if platform should be tested + id: check_platform + run: | + if [ "${{ github.event.inputs.test_platforms }}" = "all" ] || echo "${{ github.event.inputs.test_platforms }}" | grep -q "${{ matrix.os }}"; then + echo "should_run=true" >> $GITHUB_OUTPUT + else + echo "should_run=false" >> $GITHUB_OUTPUT + echo "Skipping ${{ matrix.os }} - not in test_platforms" + fi + shell: bash + + - name: Checkout GmSSL-Python + if: steps.check_platform.outputs.should_run == 'true' + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + if: steps.check_platform.outputs.should_run == 'true' + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Download Linux x86_64 library + if: steps.check_platform.outputs.should_run == 'true' && matrix.os == 'ubuntu-latest' + uses: actions/download-artifact@v4 + with: + name: gmssl-linux-x86_64 + path: src/gmssl/_libs + + - name: Rename Linux x86_64 library + if: steps.check_platform.outputs.should_run == 'true' && matrix.os == 'ubuntu-latest' + shell: bash + run: | + mv src/gmssl/_libs/libgmssl.so.3 src/gmssl/_libs/libgmssl.so.3.x86_64 + + - name: Download Linux x86_64 library (Ubuntu 22.04) + if: steps.check_platform.outputs.should_run == 'true' && matrix.os == 'ubuntu-22.04' + uses: actions/download-artifact@v4 + with: + name: gmssl-linux-x86_64 + path: src/gmssl/_libs + + - name: Rename Linux x86_64 library (Ubuntu 22.04) + if: steps.check_platform.outputs.should_run == 'true' && matrix.os == 'ubuntu-22.04' + shell: bash + run: | + mv src/gmssl/_libs/libgmssl.so.3 src/gmssl/_libs/libgmssl.so.3.x86_64 + + - name: Download macOS universal library + if: steps.check_platform.outputs.should_run == 'true' && matrix.os == 'macos-latest' + uses: actions/download-artifact@v4 + with: + name: gmssl-macos-universal + path: src/gmssl/_libs + + - name: Download Windows library + if: steps.check_platform.outputs.should_run == 'true' && matrix.os == 'windows-latest' + uses: actions/download-artifact@v4 + with: + name: gmssl-windows-x86_64 + path: src/gmssl/_libs + + - name: Verify library files + if: steps.check_platform.outputs.should_run == 'true' + shell: bash + run: | + echo "=== Library files in src/gmssl/_libs/ ===" + ls -lh src/gmssl/_libs/ || true + + # Verify library architecture and symbols + if [ "${{ matrix.os }}" = "ubuntu-latest" ] || [ "${{ matrix.os }}" = "ubuntu-22.04" ]; then + echo "=== Linux library info ===" + file src/gmssl/_libs/libgmssl.so.3.x86_64 + ldd src/gmssl/_libs/libgmssl.so.3.x86_64 || true + echo "=== GLIBC requirement ===" + objdump -T src/gmssl/_libs/libgmssl.so.3.x86_64 | grep GLIBC | awk '{print $5}' | sort -Vu + echo "=== SM3 PBKDF2 symbol check ===" + nm -D src/gmssl/_libs/libgmssl.so.3.x86_64 | grep sm3_pbkdf2 || echo "WARNING: sm3_pbkdf2 not found" + echo "=== SM9 symbols check ===" + nm -D src/gmssl/_libs/libgmssl.so.3.x86_64 | grep -i sm9_sign | head -5 || echo "No SM9 symbols found" + elif [ "${{ matrix.os }}" = "macos-latest" ]; then + echo "=== macOS library info ===" + file src/gmssl/_libs/libgmssl.3.dylib + lipo -info src/gmssl/_libs/libgmssl.3.dylib + otool -L src/gmssl/_libs/libgmssl.3.dylib + echo "=== SM9 symbols check ===" + nm -g src/gmssl/_libs/libgmssl.3.dylib | grep -i sm9_sign | head -5 || echo "No SM9 symbols found" + elif [ "${{ matrix.os }}" = "windows-latest" ]; then + echo "=== Windows library info ===" + file src/gmssl/_libs/gmssl.dll || ls -l src/gmssl/_libs/gmssl.dll + fi + + - name: Install dependencies + if: steps.check_platform.outputs.should_run == 'true' + run: | + python -m pip install --upgrade pip + pip install pytest + + - name: Install package in development mode + if: steps.check_platform.outputs.should_run == 'true' + run: pip install -e . + + - name: Verify installation + if: steps.check_platform.outputs.should_run == 'true' + shell: bash + run: | + echo "=== System info ===" + if [ "${{ matrix.os }}" = "ubuntu-latest" ] || [ "${{ matrix.os }}" = "ubuntu-22.04" ]; then + echo "OS: $(cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '\"')" + echo "GLIBC: $(ldd --version | head -1)" + fi + + echo "" + echo "=== Python and library info ===" + python --version + python -c "import sys; print(f'Python: {sys.version}')" + python -c "from gmssl import GMSSL_LIBRARY_VERSION, GMSSL_PYTHON_VERSION; print(f'GmSSL Library: {GMSSL_LIBRARY_VERSION}'); print(f'Python Binding: {GMSSL_PYTHON_VERSION}')" + + echo "" + echo "=== Loaded library path ===" + python -c "import gmssl._lib as lib; import ctypes.util; print(f'System library: {ctypes.util.find_library(\"gmssl\")}'); print(f'Using library: {lib.gmssl._name}')" + + - name: Run tests + if: steps.check_platform.outputs.should_run == 'true' + run: pytest tests/ -v -s --tb=short + + - name: Run SM9 tests separately with debug + if: steps.check_platform.outputs.should_run == 'true' && always() + shell: bash + run: | + echo "=== Running SM9 tests with verbose output ===" + pytest tests/test_gmssl.py::test_sm9_enc_decrypt tests/test_gmssl.py::test_sm9_sign_verify -v -s --tb=long || echo "SM9 tests failed" + + create-pr: + name: Create PR with updated libraries + needs: [test-libraries] + runs-on: ubuntu-latest + # Only create PR when all platforms are built and tested + if: ${{ github.event.inputs.build_platforms == 'all' && github.event.inputs.test_platforms == 'all' }} + + steps: + - name: Checkout GmSSL-Python + uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: downloaded-libs + + - name: Organize libraries + run: | + mkdir -p src/gmssl/_libs + + # Linux x86_64 + cp downloaded-libs/gmssl-linux-x86_64/libgmssl.so.3 src/gmssl/_libs/libgmssl.so.3.x86_64 + + # Linux aarch64 + cp downloaded-libs/gmssl-linux-aarch64/libgmssl.so.3 src/gmssl/_libs/libgmssl.so.3.aarch64 + + # macOS universal binary (already created by create-macos-universal job) + cp downloaded-libs/gmssl-macos-universal/libgmssl.3.dylib src/gmssl/_libs/ + + # Windows + cp downloaded-libs/gmssl-windows-x86_64/gmssl.dll src/gmssl/_libs/ + + # List files with architecture info + echo "=== Bundled Libraries ===" + ls -lh src/gmssl/_libs/ + echo "" + echo "=== Architecture Verification ===" + file src/gmssl/_libs/libgmssl.so.3.x86_64 || true + file src/gmssl/_libs/libgmssl.so.3.aarch64 || true + file src/gmssl/_libs/libgmssl.3.dylib || true + file src/gmssl/_libs/gmssl.dll || true + + # Clean up temporary directory (should not be committed) + rm -rf downloaded-libs + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + commit-message: | + chore: update bundled GmSSL libraries to ${{ github.event.inputs.gmssl_version }} + + - Linux x86_64: libgmssl.so.3.x86_64 + - Linux aarch64: libgmssl.so.3.aarch64 + - macOS universal: libgmssl.3.dylib (arm64 + x86_64) + - Windows x86_64: gmssl.dll + + Built from: ${{ github.event.inputs.gmssl_repo }} + Version: ${{ github.event.inputs.gmssl_version }} + branch: update-gmssl-libs-${{ github.event.inputs.gmssl_version }} + delete-branch: true + title: 'chore: update bundled GmSSL libraries to ${{ github.event.inputs.gmssl_version }}' + body: | + ## Update Bundled GmSSL Libraries + + This PR updates the bundled GmSSL dynamic libraries to version **${{ github.event.inputs.gmssl_version }}**. + + ### Changes + - ✅ Linux x86_64: `libgmssl.so.3.x86_64` + - ✅ Linux aarch64: `libgmssl.so.3.aarch64` + - ✅ macOS universal: `libgmssl.3.dylib` (arm64 + x86_64) + - ✅ Windows x86_64: `gmssl.dll` + + ### Build Information + - **Source**: ${{ github.event.inputs.gmssl_repo }} + - **Version**: ${{ github.event.inputs.gmssl_version }} + - **Built by**: GitHub Actions workflow + + ### Testing Checklist + - [ ] All tests pass on Linux x86_64 + - [ ] All tests pass on Linux aarch64 + - [ ] All tests pass on macOS (arm64) + - [ ] All tests pass on macOS (x86_64) + - [ ] All tests pass on Windows + - [ ] Wheel package includes all libraries + - [ ] Library loading priority works correctly (system > bundled) + + ### License + GmSSL is licensed under Apache-2.0, same as this project. + Redistribution is permitted under the terms of the Apache License 2.0. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..03afd7f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,139 @@ +name: CI + +# Trigger on push/pull requests to main branch and manual dispatch +on: + push: + branches: + - main + - master + pull_request: + branches: + - main + - master + workflow_dispatch: + inputs: + python-version: + description: 'Python version to test (leave empty for all versions)' + required: false + type: string + +jobs: + lint: + name: Lint and Format Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install dependencies + run: | + uv pip install --system ruff + + - name: Run ruff lint + run: | + echo "=== Running ruff lint ===" + ruff check src/ tests/ + + - name: Run ruff format check + run: | + echo "=== Running ruff format check ===" + ruff format --check src/ tests/ + + test: + name: Test on Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install dependencies + run: | + uv pip install --system pytest pytest-cov + + - name: Install package in development mode + run: | + uv pip install --system -e . + + - name: Verify installation + run: | + python -c "from gmssl import GMSSL_LIBRARY_VERSION, GMSSL_PYTHON_VERSION; print(f'GmSSL Library: {GMSSL_LIBRARY_VERSION}'); print(f'Python Binding: {GMSSL_PYTHON_VERSION}')" + + - name: Run tests with coverage + run: | + pytest tests/ -v --cov=src/gmssl --cov-report=xml --cov-report=term + + - name: Upload coverage to Codecov + if: matrix.python-version == '3.12' + uses: codecov/codecov-action@v5 + with: + files: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + build: + name: Build Package + needs: [lint, test] + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Verify package + run: | + echo "=== Built packages ===" + ls -lh dist/ + echo "" + echo "=== Package metadata ===" + twine check dist/* + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: python-packages + path: dist/* + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..47f8242 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,210 @@ +name: Release to GitHub + +# This workflow builds Python packages (wheel and sdist), tests them, +# and creates a GitHub Release with the packages as attachments. +# +# Trigger: +# - Automatically when pushing a version tag (e.g., v2.2.3) +# - Manually from Actions tab + +on: + push: + tags: + - 'v*' # Trigger on version tags like v2.2.3 + workflow_dispatch: # Allow manual trigger + +jobs: + build-package: + name: Build Python package + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Extract version + id: get_version + run: | + if [ "${{ github.ref_type }}" = "tag" ]; then + # Extract version from tag (v2.2.3 -> 2.2.3) + VERSION=${GITHUB_REF#refs/tags/v} + else + # Extract version from pyproject.toml + VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Version: $VERSION" + + - name: Verify version consistency + run: | + TOML_VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') + WORKFLOW_VERSION="${{ steps.get_version.outputs.version }}" + + if [ "${{ github.ref_type }}" = "tag" ]; then + if [ "$TOML_VERSION" != "$WORKFLOW_VERSION" ]; then + echo "Error: Tag version ($WORKFLOW_VERSION) doesn't match pyproject.toml version ($TOML_VERSION)" + exit 1 + fi + fi + + echo "Version verified: $TOML_VERSION" + + - name: Build wheel and sdist + run: python -m build + + - name: Verify package contents + run: | + echo "=== Built packages ===" + ls -lh dist/ + + echo "" + echo "=== Wheel contents (libraries) ===" + unzip -l dist/gmssl_python-*.whl | grep -E "(\.dylib|\.so|\.dll)" || echo "No libraries found in wheel!" + + echo "" + echo "=== Package metadata ===" + twine check dist/* + + - name: Upload packages as artifacts + uses: actions/upload-artifact@v4 + with: + name: python-packages + path: dist/* + retention-days: 7 + + test-package: + name: Test package on ${{ matrix.os }} + needs: build-package + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.8', '3.12'] + + steps: + - name: Checkout code (for tests) + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Download packages + uses: actions/download-artifact@v4 + with: + name: python-packages + path: dist + + - name: Install package from wheel + shell: bash + run: | + pip install pytest + pip install dist/gmssl_python-*.whl + + - name: Verify installation + shell: bash + run: | + python -c "from gmssl import GMSSL_LIBRARY_VERSION, GMSSL_PYTHON_VERSION; print(f'GmSSL Library: {GMSSL_LIBRARY_VERSION}'); print(f'Python Binding: {GMSSL_PYTHON_VERSION}')" + + - name: Run tests + run: pytest tests/ -v + + create-release: + name: Create GitHub Release + needs: test-package + runs-on: ubuntu-latest + permissions: + contents: write # Required for creating releases + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for changelog + + - name: Extract version + id: get_version + run: | + if [ "${{ github.ref_type }}" = "tag" ]; then + VERSION=${GITHUB_REF#refs/tags/v} + TAG_NAME=${GITHUB_REF#refs/tags/} + else + VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') + TAG_NAME="v$VERSION" + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + + - name: Download packages + uses: actions/download-artifact@v4 + with: + name: python-packages + path: dist + + - name: Generate release notes + id: release_notes + run: | + cat > release_notes.md << 'EOF' + ## GmSSL-Python v${{ steps.get_version.outputs.version }} + + ### Installation + + ```bash + pip install gmssl-python==${{ steps.get_version.outputs.version }} + ``` + + Or download the wheel from this release and install locally: + + ```bash + pip install gmssl_python-${{ steps.get_version.outputs.version }}-py3-none-any.whl + ``` + + ### What's Included + + - ✅ Python binding for GmSSL cryptographic library + - ✅ Bundled GmSSL libraries for Linux (x86_64, aarch64), macOS (universal), Windows (x86_64) + - ✅ No need to install GmSSL separately + - ✅ Tested on Python 3.8-3.12 + + ### Package Files + + - `gmssl_python-${{ steps.get_version.outputs.version }}-py3-none-any.whl` - Universal wheel (recommended) + - `gmssl_python-${{ steps.get_version.outputs.version }}.tar.gz` - Source distribution + + ### Documentation + + See [README.md](https://github.com/${{ github.repository }}/blob/main/README.md) for usage examples. + + ### License + + Apache-2.0 + EOF + + cat release_notes.md + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.get_version.outputs.tag_name }} + name: Release ${{ steps.get_version.outputs.tag_name }} + body_path: release_notes.md + files: | + dist/*.whl + dist/*.tar.gz + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index b795e33..11c830b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ __pycache__/ # C extensions *.so +# But allow bundled GmSSL libraries +!src/gmssl/_libs/*.so* # Distribution / packaging .Python @@ -129,3 +131,5 @@ dmypy.json .pyre/ *.pem .DS_Store + +.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a9a3895 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,53 @@ +default_language_version: + python: "3.12" + +repos: + # Conventional commits enforcement + - repo: https://gitee.com/ferstar/conventional-pre-commit.git + rev: v3.6.0 + hooks: + - id: conventional-pre-commit + args: + - "--verbose" + stages: [commit-msg] + + # Basic file checks + - repo: https://gitee.com/ferstar/pre-commit-hooks.git + rev: v5.0.0 + hooks: + - id: check-ast + - id: check-case-conflict + - id: check-toml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + + # Local hooks for Python code quality + - repo: local + hooks: + # Ruff formatter + - id: ruff-format + name: ruff-format + entry: ruff format --no-cache + language: system + types: [python] + description: "Ruff code formatter" + + # Ruff linter + - id: ruff + name: ruff + description: "Ruff code checker" + entry: ruff check --force-exclude --no-cache + args: [--fix, --exit-non-zero-on-fix] + language: system + types: [python] + + # Run tests on pre-push + - id: pytest + name: pytest + entry: pytest -q --tb=short + language: system + stages: [pre-push] + pass_filenames: false + verbose: true diff --git a/CODE_QUALITY_REPORT.md b/CODE_QUALITY_REPORT.md new file mode 100644 index 0000000..f5ab0bb --- /dev/null +++ b/CODE_QUALITY_REPORT.md @@ -0,0 +1,250 @@ +# 代码质量检查报告 + +生成时间:2025-10-18 +检查范围:`src/gmssl/` 目录下所有 Python 文件 + +## 📊 总体评估 + +| 指标 | 状态 | 说明 | +|------|------|------| +| Ruff 检查 | ✅ 通过 | 所有检查通过 | +| 函数长度 | ✅ 良好 | 仅1个函数>50行 | +| 代码重复 | ⚠️ 需改进 | 发现多处重复模式 | +| 错误处理 | ⚠️ 需改进 | 不一致的错误处理 | +| 类型注解 | ❌ 缺失 | 所有函数缺少类型注解 | +| 文档字符串 | ✅ 良好 | 大部分函数有文档 | + +## 🔍 发现的问题 + +### 1. 重复代码模式(高优先级) + +#### 问题 1.1: PEM 工具函数重复 +**位置**: `src/gmssl/_pem_utils.py` (行 442-542) + +**问题描述**: +四个函数有几乎相同的结构,只是参数和调用的函数名不同: +- `pem_export_encrypted_key` +- `pem_import_encrypted_key` +- `pem_export_public_key` +- `pem_import_public_key` + +**重复代码量**: ~100 行 + +**建议方案**: +```python +def _call_platform_specific_pem_function( + func_name: str, + key, + path: str, + *args, + file_mode: str = "rb" +): + """ + Generic wrapper for platform-specific PEM operations. + + Args: + func_name: Base function name (e.g., "sm2_public_key_info_to_pem") + key: Key object + path: File path + *args: Additional arguments (e.g., password) + file_mode: File mode for non-Windows platforms + """ + if sys.platform == "win32": + windows_func = globals()[f"{func_name}_windows"] + windows_func(key, path, *args) + else: + with open_file(path, file_mode) as fp: + gmssl_func = getattr(gmssl, func_name) + if gmssl_func(byref(key), *args, fp) != 1: + raise NativeError(f"{func_name} failed") +``` + +**影响**: 可减少约 80 行重复代码 + +--- + +#### 问题 1.2: 错误处理模式不一致 +**统计**: +- `"libgmssl inner error"` 出现 56 次 +- `if ... != 1:` 模式出现 39 次 + +**问题描述**: +代码库中已有 `raise_on_error` 辅助函数,但很多地方没有使用: + +```python +# 当前代码(重复模式) +if gmssl.sm4_cbc_encrypt_init(byref(self), key, iv) != 1: + raise NativeError("libgmssl inner error") + +# 应该使用 +raise_on_error( + gmssl.sm4_cbc_encrypt_init(byref(self), key, iv), + "sm4_cbc_encrypt_init" +) +``` + +**建议**: +1. 统一使用 `raise_on_error` 函数 +2. 提供更具体的错误消息(包含函数名) + +**影响**: 可提高错误消息的一致性和可调试性 + +--- + +#### 问题 1.3: 验证逻辑重复 +**统计**: 19 处类似的长度/大小验证 + +**问题描述**: +```python +# 在多个类中重复 +if len(key) != SM4_KEY_SIZE: + raise ValueError("Invalid key length") + +if len(iv) != SM4_BLOCK_SIZE: + raise ValueError("Invalid IV size") +``` + +**建议方案**: +```python +def validate_length(data: bytes, expected: int, name: str) -> None: + """Validate data length matches expected size.""" + if len(data) != expected: + raise ValueError( + f"Invalid {name} length: expected {expected}, got {len(data)}" + ) + +# 使用 +validate_length(key, SM4_KEY_SIZE, "key") +validate_length(iv, SM4_BLOCK_SIZE, "IV") +``` + +**影响**: 提高验证逻辑的一致性和错误消息质量 + +--- + +### 2. 缺少类型注解(中优先级) + +**问题描述**: +所有函数都缺少类型注解,这在现代 Python 开发中是不好的实践。 + +**示例**: +```python +# 当前 +def rand_bytes(size): + """Generate random bytes.""" + ... + +# 建议 +def rand_bytes(size: int) -> bytes: + """Generate random bytes.""" + ... +``` + +**建议**: +1. 为所有公共 API 添加类型注解 +2. 为内部函数添加类型注解(可选但推荐) +3. 使用 `mypy` 进行类型检查 + +**影响**: +- 提高代码可维护性 +- 更好的 IDE 支持 +- 减少类型相关的 bug + +--- + +### 3. 魔法字符串和常量(低优先级) + +**问题描述**: +一些字符串在代码中重复出现: + +```python +# 文件模式 +"rb", "wb" # 多处使用 + +# 错误消息 +"libgmssl inner error" # 56 次 +"Invalid key length" # 多次 +``` + +**建议**: +```python +# 在 _constants.py 中添加 +FILE_MODE_READ_BINARY = "rb" +FILE_MODE_WRITE_BINARY = "wb" +ERROR_MSG_NATIVE = "Native library error" +``` + +**影响**: 提高代码可维护性 + +--- + +## 📈 改进建议优先级 + +### 🔴 高优先级(建议立即处理) + +1. **重构 PEM 工具函数** (问题 1.1) + - 影响:减少 ~80 行重复代码 + - 难度:中等 + - 预计时间:1-2 小时 + +2. **统一错误处理** (问题 1.2) + - 影响:提高代码一致性和可调试性 + - 难度:低 + - 预计时间:2-3 小时 + +### 🟡 中优先级(建议近期处理) + +3. **提取验证逻辑** (问题 1.3) + - 影响:提高验证一致性 + - 难度:低 + - 预计时间:1 小时 + +4. **添加类型注解** (问题 2) + - 影响:提高代码质量和可维护性 + - 难度:中等 + - 预计时间:4-6 小时 + +### 🟢 低优先级(可选) + +5. **提取魔法字符串** (问题 3) + - 影响:小幅提高可维护性 + - 难度:低 + - 预计时间:30 分钟 + +--- + +## ✅ 已完成的改进 + +1. ✅ **重构库加载逻辑** (`_lib.py`) + - 消除了 ~40 行重复代码 + - 减少嵌套层次从 4 层到 2 层 + - 提取了平台检测逻辑 + - 添加了常量定义 + +--- + +## 🎯 下一步行动 + +建议按以下顺序进行改进: + +1. **第一阶段**(本周) + - [ ] 重构 PEM 工具函数 + - [ ] 统一错误处理模式 + +2. **第二阶段**(下周) + - [ ] 提取验证逻辑 + - [ ] 为公共 API 添加类型注解 + +3. **第三阶段**(可选) + - [ ] 为内部函数添加类型注解 + - [ ] 提取魔法字符串 + - [ ] 配置 mypy 类型检查 + +--- + +## 📝 备注 + +- 所有改进都应保持向后兼容 +- 每次改进后运行完整测试套件 +- 遵循项目的 Conventional Commits 规范 +- 使用 pre-commit hooks 确保代码质量 diff --git a/README.md b/README.md index bff227f..6d138a4 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,68 @@ 目前`gmssl-python`功能可以覆盖除SSL/TLS/TLCP之外的国密算法主要应用开发场景。 +## 项目结构 + +本项目采用 Python 包最佳实践的 `src/` 布局: + +``` +GmSSL-Python/ +├── src/ +│ └── gmssl/ +│ ├── __init__.py # 公共 API 导出 +│ └── _core.py # 核心实现 (ctypes bindings) +├── tests/ +│ └── test_gmssl.py # 测试套件 +└── pyproject.toml # 包配置 +``` + +**为什么使用 `src/` 布局?** +- 强制开发者测试已安装的包,而不是源代码目录 +- 避免开发环境和安装版本之间的导入混淆 +- PyPA (Python Packaging Authority) 推荐 +- 被现代 Python 项目广泛采用 (requests, pytest 等) + ## 安装 -由于`gmssl-python`以`ctypes`方式实现,因此所有密码功能都是通过调用本地安装的GmSSL动态库 (如`/usr/local/lib/libgmssl.so`)实现的,在安装和调用`gmssl-python`之前必须首先在系统上安装GmSSL,然后通过Python的包管理工具`pip`从Python代码仓库安装,或者从`gmssl-python`项目的代码仓库https://github.com/GmSSL/GmSSL-Python 下载最新的源代码,从本地安装。 +### 快速安装(推荐) + +从 v2.2.2 开始,`gmssl-python` 已经包含了 GmSSL 动态库,可以直接通过 `pip` 安装使用: + +```bash +pip install gmssl-python +``` + +**支持的平台:** +- ✅ **Linux x86_64** (GLIBC 2.17+) - Ubuntu 14.04+, Debian 8+, RHEL 7+ +- ✅ **Linux aarch64** (GLIBC 2.17+) - ARM64 服务器和开发板 +- ✅ **macOS** (11.0+) - Intel 和 Apple Silicon 通用二进制 +- ✅ **Windows x86_64** - Windows 10/11 -### 安装GmSSL +安装后即可直接使用,无需额外安装 GmSSL 库。 + +**系统要求:** +- Linux: GLIBC 2.17 或更高版本(2012 年发布,几乎所有现代发行版都满足) +- macOS: macOS 11.0 (Big Sur) 或更高版本 +- Windows: Windows 10 或更高版本 + +### 库加载优先级 + +`gmssl-python` 使用智能库加载策略(遵循 "Never break userspace" 原则): + +1. **系统库优先**:如果系统已安装 GmSSL(如 `/usr/local/lib/libgmssl.so`),优先使用系统库 +2. **包内库回退**:如果系统没有 GmSSL,使用包内打包的库 +3. **版本检查**:如果系统库版本 < 3.1.1,自动回退到包内库(如果可用) + +这确保了: +- ✅ 已有系统 GmSSL 的用户继续使用其版本(向后兼容) +- ✅ 新用户无需手动安装 GmSSL(开箱即用) +- ✅ 开发者可以通过安装系统 GmSSL 覆盖包内库(灵活性) + +### 手动安装 GmSSL(可选) + +如果你希望使用系统级 GmSSL 库(例如需要最新版本或自定义编译),可以按以下步骤手动安装: + +#### 安装GmSSL 首先在https://github.com/guanzhi/GmSSL 项目上下载最新的GmSSL代码[GmSSL-master.zip](https://github.com/guanzhi/GmSSL/archive/refs/heads/master.zip),编译并安装。GmSSL代码是C语言编写的,需要安装GCC、CMake来编译,在Ubuntu/Debian系统上可以执行 @@ -60,17 +117,22 @@ $ pip show gmssl-python 从代码仓库中安装的`gmssl-python`通常不是最新版本,可以下载最新的GmSSL-Python代码 [GmSSL-Python-main.zip](https://github.com/GmSSL/GmSSL-Python/archive/refs/heads/main.zip),本地安装。 -解压缩并进入源代码目录`GmSSL-Python-main`。由于最新代码可能还处于开发过程中,在安装前必须进行测试确保全部功能正确,`gmssl-python`中提供了测试,执行如下命令 +解压缩并进入源代码目录`GmSSL-Python-main`。由于最新代码可能还处于开发过程中,在安装前必须进行测试确保全部功能正确,`gmssl-python`中提供了基于 pytest 的测试,执行如下命令 运行测试 ```bash -$ python -m unittest -v -................ ----------------------------------------------------------------------- -Ran 16 tests in 1.407s +$ pytest tests/ -v +================================================= test session starts ================================================= +platform darwin -- Python 3.12.9, pytest-8.4.2, pluggy-1.6.0 +collected 19 items + +tests/test_gmssl.py::TestVersion::test_library_version_num PASSED [ 5%] +tests/test_gmssl.py::TestVersion::test_library_version_string PASSED [ 10%] +... +tests/test_gmssl.py::TestSM2Certificate::test_sm2_certificate_parsing PASSED [100%] -OK +================================================= 19 passed in 1.04s ================================================== ``` 上面的输出表明测试通过。 @@ -122,7 +184,76 @@ echo -n abc | gmssl sm3 可以看到输出相同的SM3哈希值 +## 开发指南 +### 运行测试 + +项目包含 **117 个测试**,覆盖所有主要功能、错误处理、边界条件、性能和线程安全测试。 + +```bash +# 运行所有测试(推荐使用 uv) +uv run pytest tests/ -v + +# 或使用 pytest 直接运行 +pytest tests/ -v + +# 运行特定测试文件 +uv run pytest tests/test_errors.py -v # 错误处理测试 +uv run pytest tests/test_edge_cases.py -v # 边界条件测试 +uv run pytest tests/test_additional_methods.py -v # 补充方法测试 +uv run pytest tests/test_pygmssl_missing.py -v # pygmssl缺失测试 +uv run pytest tests/test_thread_safety.py -v # 线程安全测试 +``` + +**测试覆盖**: +- ✅ 功能测试: 17 个 +- ✅ 错误处理测试: 34 个 +- ✅ 边界条件测试: 28 个 +- ✅ 补充方法测试: 15 个 +- ✅ pygmssl缺失测试: 13 个(性能、压力、边界测试) +- ✅ 线程安全测试: 10 个(多线程并发测试) + +### 线程安全说明 + +⚠️ **重要**: `Sm4Gcm` 类由于底层 GmSSL 库实现限制,**不是线程安全的**。 + +如果需要在多线程环境中使用 SM4-GCM,必须使用锁保护: + +```python +import threading +from gmssl import Sm4Gcm, DO_ENCRYPT + +lock = threading.Lock() + +def encrypt_with_gcm(key, iv, aad, plaintext): + with lock: # 必须使用锁保护 + sm4_gcm = Sm4Gcm(key, iv, aad, 16, DO_ENCRYPT) + ciphertext = sm4_gcm.update(plaintext) + ciphertext += sm4_gcm.finish() + return ciphertext +``` + +其他所有密码算法(SM2, SM3, SM4-CBC/CTR, SM9, ZUC 等)都是线程安全的,可以在多线程环境中直接使用。 + +### 修改代码 + +1. 编辑 `src/gmssl/_core.py` 中的实现代码 +2. 如需更新公共 API,修改 `src/gmssl/__init__.py` +3. 运行测试验证:`uv run pytest tests/ -v` +4. 格式化代码:`ruff format src/ tests/` +5. 检查代码:`ruff check src/ tests/` + +### 发布到 PyPI + +参考 https://packaging.python.org/distributing/ + +1. 更新版本号: + - `src/gmssl/_core.py` 中的 `GMSSL_PYTHON_VERSION = "x.y.z"` + - `pyproject.toml` 中的 `version = "x.y.z"` +2. 构建包:`python3 -m build` +3. 发布到 PyPI:`python3 -m twine upload dist/*` + +**注意:** 版本号必须在两个文件中保持同步。 @@ -246,7 +377,7 @@ HMAC-SM3是基于SM3密码杂凑算法的消息认证码(MAC)算法,消息认 模块`gmssl`中包含如下Sm3Hmac的常量: -* `SM3_HMAC_MIN_KEY_SIZE` +* `SM3_HMAC_MIN_KEY_SIZE` * `SM3_HMAC_MAX_KEY_SIZE` * `SM3_HMAC_SIZE` HMAC-SM3密钥长度,与SM3哈希值的长度相等 @@ -258,9 +389,9 @@ gmssl.Sm3Hmac(key) 对象Sm3Hmac的方法: -* `Sm3Hmac.update(data : bytes)` -* `Sm3Hmac.generate_mac() -> bytes` -* `Sm3Hmac.reset()` +* `Sm3Hmac.update(data : bytes)` +* `Sm3Hmac.generate_mac() -> bytes` +* `Sm3Hmac.reset()` HMAC-SM3算法可以看作是带密钥的SM3算法,因此在生成`Sm3Hmac`对象时需要传入一个密钥`key`作为输入参数。虽然HMAC-SM3在算法和实现上对密钥长度没有限制,但是出于安全性、效率等方面的考虑,HMAC-SM3算法的密钥长度建议采用32字节(等同于SM3哈希值的长度),不应少于16字节,采用比32字节更长的密钥长度会增加计算开销而不会增加安全性。 @@ -286,7 +417,7 @@ HMAC-SM3算法可以看作是带密钥的SM3算法,因此在生成`Sm3Hmac`对 模块`gmssl`中包含如下Sm3Pbkdf2的常量 -* `SM3_PBKDF2_MIN_ITER` +* `SM3_PBKDF2_MIN_ITER` * `SM3_PBKDF2_MAX_ITER` * `SM3_PBKDF2_MAX_SALT_SIZE` * `SM3_PBKDF2_DEFAULT_SALT_SIZE` @@ -322,7 +453,7 @@ SM4算法是分组密码算法,其密钥长度为128比特(16字节),分 模块`gmssl`中包含如下SM4的常量 -* `SM4_KEY_SIZE` +* `SM4_KEY_SIZE` * `SM4_BLOCK_SIZE` `SM4`类实现了基本的SM4分组密码算法,类`SM4`的对象是由构造函数生成的。 @@ -333,7 +464,7 @@ gmssl.Sm4(key, encrypt) 对象SM4的方法: -* `Sm4.encrypt(block : int) -> bytes` +* `Sm4.encrypt(block : int) -> bytes` `Sm4`对象在创建时需要提供`SM4_KEY_SIZE`字节长度的密钥,以及一个布尔值`DO_ENCRYPT`表示是用于加密还是解密。方法`encrypt`根据创建时的选择进行加密或解密,每次调用`encrypt`只处理一个分组,即读入`SM4_BLOCK_SIZE`长度的输入。 @@ -357,7 +488,7 @@ CBC模式是应用最广泛的分组密码加密模式之一,虽然目前不 模块`gmssl`中包含如下Sm4Cbc的常量: -* `SM4_CBC_IV_SIZE` +* `SM4_CBC_IV_SIZE` `Sm4Cbc`类实现了基本的SM4-CBC分组密码算法,类`Sm4Cbc`的对象是由构造函数生成的。 @@ -397,7 +528,7 @@ CTR加密模式可以加密任意长度的消息,和CBC模式不同,并不 模块`gmssl`中包含如下Sm4Ctr的常量: -* `SM4_CTR_IV_SIZE` +* `SM4_CTR_IV_SIZE` `Sm4Ctr`类实现了基本的SM4-CBC分组密码算法,类`Sm4Ctr`的对象是由构造函数生成的。 @@ -546,7 +677,7 @@ gmssl.Sm2Key() ```python >>> sm2 = Sm2Key() >>> sm2.generate_key() ->>> +>>> >>> sm2.export_encrypted_private_key_info_pem('sm2.pem', 'password') >>> private_key = Sm2Key() >>> private_key.import_encrypted_private_key_info_pem('sm2.pem', 'password') @@ -908,10 +1039,25 @@ verify.update(message.encode('utf-8')) ret = verify.verify(sig, master_pub, signer_id) ``` +## 许可证 +### GmSSL-Python +本项目采用 Apache License 2.0 许可证。详见 [LICENSE](LICENSE) 文件。 +### 打包的 GmSSL 库 +从 v2.2.2 开始,`gmssl-python` 包含了预编译的 GmSSL 动态库(位于 `src/gmssl/_libs/`)。 +**GmSSL 库信息:** +- **项目**: [GmSSL](https://github.com/guanzhi/GmSSL) +- **版本**: 3.1.1 +- **许可证**: Apache License 2.0 +- **版权**: Copyright 2014-2023 The GmSSL Project +根据 Apache License 2.0 的条款,我们被允许重新分发 GmSSL 库的二进制文件。GmSSL 库与本项目采用相同的许可证,因此用户可以自由使用、修改和分发。 +**重要说明:** +- 打包的 GmSSL 库仅用于便利性,用户可以选择使用系统安装的 GmSSL 库 +- 系统库(如果存在)将优先于打包的库被加载 +- 如需最新版本或自定义编译的 GmSSL,请参考上述"手动安装 GmSSL"部分 diff --git a/pyproject.toml b/pyproject.toml index cd544f6..c01d834 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,3 +21,92 @@ classifiers = [ [project.urls] "Homepage" = "https://gmssl.github.io/GmSSL-Python/" "Bug Tracker" = "https://github.com/GmSSL/GmSSL-Python/issues" + +# Standard Python package structure with src/ layout +[tool.setuptools.packages.find] +where = ["src"] + +# Include bundled GmSSL dynamic libraries in the package +# This allows pip users to use gmssl_python without installing GmSSL separately +[tool.setuptools.package-data] +gmssl = [ + "_libs/*.dylib", # macOS + "_libs/*.so*", # Linux (includes .so, .so.3, .so.3.1, etc.) + "_libs/*.dll", # Windows +] + +[tool.uv] +package = true +compile-bytecode = true +concurrent-downloads = 4 + +[[tool.uv.index]] +name = "tsinghua" +url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" + +[dependency-groups] +dev = [ + "pip>=24.0", + "pre-commit>=2.21.0", + "pytest>=7.4.4", + "pytest-cov>=4.1.0", + "ruff>=0.14.1", + "setuptools>=68.0.0", +] + +[tool.ruff] +line-length = 100 +target-version = "py38" + +[tool.ruff.lint] +# Enable essential rules - no bullshit, just real problems +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "W", # pycodestyle warnings + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "SIM", # flake8-simplify +] + +# Ignore rules that conflict with good taste +ignore = [ + "E501", # Line too long - let formatter handle it + "N801", # Class name should use CapWords - ignore for C struct compatibility +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +line-ending = "auto" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] + +[tool.coverage.run] +source = ["src/gmssl"] +omit = [] + +[tool.coverage.report] +# Exclude lines with platform-specific code markers +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if sys.platform == .win32.:", # Windows-specific code + "@abstractmethod", +] + +# Show missing lines in coverage report +show_missing = true + +# Fail if coverage is below this threshold (optional) + fail_under = 75 diff --git a/src/gmssl/__init__.py b/src/gmssl/__init__.py new file mode 100644 index 0000000..1f39a89 --- /dev/null +++ b/src/gmssl/__init__.py @@ -0,0 +1,175 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - Public API exports + +""" +GmSSL Python Binding + +This package provides Python bindings for the GmSSL cryptographic library. +All cryptographic operations are implemented by calling the native GmSSL +dynamic library through ctypes. + +Following Linus's philosophy: "Bad programmers worry about the code. +Good programmers worry about data structures and their relationships." + +This module exports a clean, minimal public API. Internal implementation +details are kept in separate modules. +""" + +# Version information +# Constants +from gmssl._constants import ( + # SM2 Public Key Cryptography Constants + DO_DECRYPT, + DO_ENCRYPT, + DO_SIGN, + DO_VERIFY, + SM2_DEFAULT_ID, + SM2_MAX_CIPHERTEXT_SIZE, + SM2_MAX_PLAINTEXT_SIZE, + SM2_MAX_SIGNATURE_SIZE, + SM2_MIN_CIPHERTEXT_SIZE, + SM2_MIN_PLAINTEXT_SIZE, + # SM3 Hash Constants + SM3_DIGEST_SIZE, + SM3_HMAC_MAX_KEY_SIZE, + SM3_HMAC_MIN_KEY_SIZE, + SM3_HMAC_SIZE, + SM3_PBKDF2_DEFAULT_SALT_SIZE, + SM3_PBKDF2_MAX_ITER, + SM3_PBKDF2_MAX_KEY_SIZE, + SM3_PBKDF2_MAX_SALT_SIZE, + SM3_PBKDF2_MIN_ITER, + # SM4 Block Cipher Constants + SM4_BLOCK_SIZE, + SM4_CBC_IV_SIZE, + SM4_CTR_IV_SIZE, + SM4_GCM_DEFAULT_IV_SIZE, + SM4_GCM_DEFAULT_TAG_SIZE, + SM4_GCM_MAX_IV_SIZE, + SM4_GCM_MAX_TAG_SIZE, + SM4_GCM_MIN_IV_SIZE, + SM4_KEY_SIZE, + # SM9 Identity-Based Cryptography Constants + SM9_MAX_CIPHERTEXT_SIZE, + SM9_MAX_ID_SIZE, + SM9_MAX_PLAINTEXT_SIZE, + SM9_SIGNATURE_SIZE, + # ZUC Stream Cipher Constants + ZUC_BLOCK_SIZE, + ZUC_IV_SIZE, + ZUC_KEY_SIZE, +) + +# Exceptions +from gmssl._lib import NativeError, StateError + +# Random number generator +from gmssl._random import rand_bytes + +# SM2 Public Key Cryptography +from gmssl._sm2 import Sm2Key, Sm2Signature + +# SM3 Hash +from gmssl._sm3 import Sm3, Sm3Hmac, sm3_pbkdf2 + +# SM4 Block Cipher +from gmssl._sm4 import Sm4, Sm4Cbc, Sm4Ctr, Sm4Gcm + +# SM9 Identity-Based Cryptography +from gmssl._sm9 import ( + Sm9EncKey, + Sm9EncMasterKey, + Sm9Signature, + Sm9SignKey, + Sm9SignMasterKey, +) +from gmssl._version import ( + GMSSL_LIBRARY_VERSION, + GMSSL_PYTHON_VERSION, + gmssl_library_version_num, + gmssl_library_version_str, +) + +# X.509 Certificate +from gmssl._x509 import Sm2Certificate, Validity + +# ZUC Stream Cipher +from gmssl._zuc import Zuc + +# Explicit public API declaration +__all__ = [ + # Version information + "GMSSL_LIBRARY_VERSION", + "GMSSL_PYTHON_VERSION", + "gmssl_library_version_num", + "gmssl_library_version_str", + # Exceptions + "NativeError", + "StateError", + # Random number generator + "rand_bytes", + # SM3 Hash + "SM3_DIGEST_SIZE", + "SM3_HMAC_MAX_KEY_SIZE", + "SM3_HMAC_MIN_KEY_SIZE", + "SM3_HMAC_SIZE", + "SM3_PBKDF2_DEFAULT_SALT_SIZE", + "SM3_PBKDF2_MAX_ITER", + "SM3_PBKDF2_MAX_KEY_SIZE", + "SM3_PBKDF2_MAX_SALT_SIZE", + "SM3_PBKDF2_MIN_ITER", + "Sm3", + "Sm3Hmac", + "sm3_pbkdf2", + # SM4 Block Cipher + "SM4_BLOCK_SIZE", + "SM4_CBC_IV_SIZE", + "SM4_CTR_IV_SIZE", + "SM4_GCM_DEFAULT_IV_SIZE", + "SM4_GCM_DEFAULT_TAG_SIZE", + "SM4_GCM_MAX_IV_SIZE", + "SM4_GCM_MAX_TAG_SIZE", + "SM4_GCM_MIN_IV_SIZE", + "SM4_KEY_SIZE", + "Sm4", + "Sm4Cbc", + "Sm4Ctr", + "Sm4Gcm", + # ZUC Stream Cipher + "ZUC_BLOCK_SIZE", + "ZUC_IV_SIZE", + "ZUC_KEY_SIZE", + "Zuc", + # SM2 Public Key Cryptography + "DO_DECRYPT", + "DO_ENCRYPT", + "DO_SIGN", + "DO_VERIFY", + "SM2_DEFAULT_ID", + "SM2_MAX_CIPHERTEXT_SIZE", + "SM2_MAX_PLAINTEXT_SIZE", + "SM2_MAX_SIGNATURE_SIZE", + "SM2_MIN_CIPHERTEXT_SIZE", + "SM2_MIN_PLAINTEXT_SIZE", + "Sm2Certificate", + "Sm2Key", + "Sm2Signature", + # SM9 Identity-Based Cryptography + "SM9_MAX_CIPHERTEXT_SIZE", + "SM9_MAX_ID_SIZE", + "SM9_MAX_PLAINTEXT_SIZE", + "SM9_SIGNATURE_SIZE", + "Sm9EncKey", + "Sm9EncMasterKey", + "Sm9SignKey", + "Sm9SignMasterKey", + "Sm9Signature", + # X.509 Certificate + "Validity", +] diff --git a/src/gmssl/_constants.py b/src/gmssl/_constants.py new file mode 100644 index 0000000..f260e42 --- /dev/null +++ b/src/gmssl/_constants.py @@ -0,0 +1,89 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - Constants definitions + +""" +Internal module for all GmSSL constants. +This module should not be imported directly by users. +""" + +# ============================================================================= +# SM3 Hash Constants +# ============================================================================= + +SM3_DIGEST_SIZE = 32 +SM3_HMAC_MIN_KEY_SIZE = 16 +SM3_HMAC_MAX_KEY_SIZE = 64 +SM3_HMAC_SIZE = SM3_DIGEST_SIZE +SM3_PBKDF2_MIN_ITER = 10000 # from +SM3_PBKDF2_MAX_ITER = 16777216 # 2^24 +SM3_PBKDF2_MAX_SALT_SIZE = 64 # from +SM3_PBKDF2_DEFAULT_SALT_SIZE = 8 # from +SM3_PBKDF2_MAX_KEY_SIZE = 256 # from gmssljni.c:sm3_pbkdf2():sizeof(keybuf) + +# ============================================================================= +# SM4 Block Cipher Constants +# ============================================================================= + +SM4_KEY_SIZE = 16 +SM4_BLOCK_SIZE = 16 +SM4_CBC_IV_SIZE = SM4_BLOCK_SIZE +SM4_CTR_IV_SIZE = 16 +SM4_GCM_MIN_IV_SIZE = 1 +SM4_GCM_MAX_IV_SIZE = 64 +SM4_GCM_DEFAULT_IV_SIZE = 12 +SM4_GCM_DEFAULT_TAG_SIZE = 16 +SM4_GCM_MAX_TAG_SIZE = 16 + +# ============================================================================= +# ZUC Stream Cipher Constants +# ============================================================================= + +ZUC_KEY_SIZE = 16 +ZUC_IV_SIZE = 16 +ZUC_BLOCK_SIZE = 4 # ZUC is a stream cipher with 4-byte (32-bit) blocks + +# ============================================================================= +# SM2 Public Key Cryptography Constants +# ============================================================================= + +SM2_DEFAULT_ID = "1234567812345678" +SM2_MAX_SIGNATURE_SIZE = 72 +SM2_MIN_PLAINTEXT_SIZE = 1 +SM2_MAX_PLAINTEXT_SIZE = 255 +SM2_MIN_CIPHERTEXT_SIZE = 45 +SM2_MAX_CIPHERTEXT_SIZE = 366 + +# ============================================================================= +# Encryption/Decryption and Sign/Verify Mode Constants +# ============================================================================= + +DO_ENCRYPT = True +DO_DECRYPT = False +DO_SIGN = True +DO_VERIFY = False + +# ============================================================================= +# SM9 Identity-Based Cryptography Constants +# ============================================================================= + +SM9_MAX_ID_SIZE = 63 +SM9_MAX_PLAINTEXT_SIZE = 255 +SM9_MAX_CIPHERTEXT_SIZE = 367 +SM9_SIGNATURE_SIZE = 104 + +# ============================================================================= +# Internal Constants (not exported in __init__.py) +# ============================================================================= + +_SM3_STATE_WORDS = 8 +_SM3_BLOCK_SIZE = 64 +_SM4_NUM_ROUNDS = 32 +_ASN1_TAG_IA5String = 22 +_ASN1_TAG_SEQUENCE = 0x30 +_ASN1_TAG_SET = 0x31 diff --git a/src/gmssl/_file_utils.py b/src/gmssl/_file_utils.py new file mode 100644 index 0000000..a2f0192 --- /dev/null +++ b/src/gmssl/_file_utils.py @@ -0,0 +1,59 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - File operation utilities + +""" +Internal module for safe file operations. +This module should not be imported directly by users. +""" + +from contextlib import contextmanager +from ctypes import c_void_p + +from gmssl._lib import libc + + +@contextmanager +def open_file(path, mode): + """ + Context manager for safe file operations with automatic cleanup. + + Ensures file descriptors are always closed, even if an exception occurs. + + Args: + path: File path (str or bytes) + mode: File mode (e.g., "rb", "wb") + + Yields: + c_void_p: File pointer for use with libc functions + + Raises: + OSError: If file cannot be opened + + Example: + with open_file("key.pem", "wb") as fp: + gmssl.sm2_private_key_info_encrypt_to_pem(key, passwd, fp) + """ + if isinstance(path, str): + path = path.encode("utf-8") + if isinstance(mode, str): + mode = mode.encode("utf-8") + + libc.fopen.restype = c_void_p + fp = libc.fopen(path, mode) + + if not fp: + raise OSError(f"Cannot open file: {path.decode('utf-8')}") + + try: + yield c_void_p(fp) + finally: + # Flush buffer before closing to ensure all data is written + # This is especially important on macOS where buffering behavior differs + libc.fflush(c_void_p(fp)) + libc.fclose(c_void_p(fp)) diff --git a/src/gmssl/_lib.py b/src/gmssl/_lib.py new file mode 100644 index 0000000..4b13dda --- /dev/null +++ b/src/gmssl/_lib.py @@ -0,0 +1,324 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - Library loading and exceptions + +""" +Internal module for GmSSL library loading and exception definitions. +This module should not be imported directly by users. +""" + +import os +import platform +import sys +from ctypes import cdll +from ctypes.util import find_library + +# ============================================================================= +# Library Loading +# ============================================================================= + +# Minimum required GmSSL version (3.1.1) +REQUIRED_VERSION = 30101 +GMSSL_REPO_URL = "https://github.com/guanzhi/GmSSL" + + +def _get_platform_library_name(): + """ + Get platform-specific library name for bundled GmSSL library. + + Returns: + str: Library filename for current platform + + Raises: + ValueError: If platform/architecture is not supported + """ + if sys.platform == "darwin": + return "libgmssl.3.dylib" + + if sys.platform == "win32": + return "gmssl.dll" + + # Linux and other Unix-like systems - detect architecture + machine = platform.machine().lower() + if machine in ("aarch64", "arm64"): + return "libgmssl.so.3.aarch64" + + if machine in ("x86_64", "amd64"): + return "libgmssl.so.3.x86_64" + + # Unsupported architecture + raise ValueError( + f"Unsupported Linux architecture: {machine}\n" + f"Bundled GmSSL libraries are only available for:\n" + f" - x86_64 (amd64)\n" + f" - aarch64 (arm64)\n" + f"Please install GmSSL manually: {GMSSL_REPO_URL}" + ) + + +def _get_bundled_library_path(): + """ + Get full path to bundled GmSSL library. + + Returns: + str or None: Path to bundled library if exists, None otherwise + """ + try: + lib_name = _get_platform_library_name() + except ValueError: + return None + + lib_dir = os.path.join(os.path.dirname(__file__), "_libs") + lib_path = os.path.join(lib_dir, lib_name) + + return lib_path if os.path.exists(lib_path) else None + + +def _check_library_version(lib): + """ + Check if loaded library meets version requirement. + + Args: + lib: Loaded CDLL library instance + + Returns: + tuple: (is_valid, version_number) + """ + version = lib.gmssl_version_num() + return version >= REQUIRED_VERSION, version + + +def _load_gmssl_library(): + """ + Load GmSSL library with version check and smart fallback. + + Priority: + 1. System library (if version >= 3.1.1) + 2. Bundled library (fallback if system lib too old or missing) + + Returns: + CDLL: Loaded GmSSL library instance + + Raises: + ValueError: If no suitable library found or version too old + """ + # Try system library first + system_lib_path = find_library("gmssl") + if system_lib_path: + lib = cdll.LoadLibrary(system_lib_path) + is_valid, version = _check_library_version(lib) + if is_valid: + return lib + # System library too old, will try bundled as fallback + + # Try bundled library + bundled_lib_path = _get_bundled_library_path() + if bundled_lib_path: + lib = cdll.LoadLibrary(bundled_lib_path) + is_valid, version = _check_library_version(lib) + if is_valid: + return lib + + # No suitable library found - provide helpful error message + if system_lib_path: + # System library exists but too old + raise ValueError( + f"GmSSL version too old: {version} < {REQUIRED_VERSION} (required)\n" + f"Loaded from: {system_lib_path}\n" + f"Please upgrade GmSSL: {GMSSL_REPO_URL}" + ) + + # No library found at all + raise ValueError( + "GmSSL library not found. Install it via:\n" + f" - System package: {GMSSL_REPO_URL}\n" + " - Or reinstall gmssl_python (should include bundled library)" + ) + + +# Load GmSSL library +gmssl = _load_gmssl_library() + +# Load C standard library for file operations +if sys.platform == "win32": + libc = cdll.LoadLibrary(find_library("msvcrt")) +else: + libc = cdll.LoadLibrary(find_library("c")) + +# ============================================================================= +# Exceptions +# ============================================================================= + + +class NativeError(Exception): + """ + GmSSL library inner error + """ + + +class StateError(Exception): + """ + Crypto state error + """ + + +# ============================================================================= +# Error Handling Utilities +# ============================================================================= + + +def raise_on_error(result, func_name): + """ + Raise NativeError if gmssl function returns error. + + Args: + result: Return value from gmssl function (1 = success, other = error) + func_name: Name of the gmssl function that was called + + Raises: + NativeError: If result != 1 + + Example: + raise_on_error(gmssl.sm2_key_generate(byref(key)), "sm2_key_generate") + """ + if result != 1: + raise NativeError(f"{func_name} failed") + + +def check_gmssl_error(func): + """ + Decorator that automatically checks gmssl function call results. + + The decorated function should return a tuple of (result, func_name) or + just call gmssl functions that return 1 on success. + + Example 1 - Return tuple: + @check_gmssl_error + def generate_key(self): + result = gmssl.sm2_key_generate(byref(self)) + return result, "sm2_key_generate" + + Example 2 - Check inline: + @check_gmssl_error + def generate_key(self): + gmssl.sm2_key_generate(byref(self)) | "sm2_key_generate" + + Note: For simple cases, use raise_on_error() directly instead. + """ + from functools import wraps + + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + # If function returns a tuple (result, func_name), check the result + if isinstance(result, tuple) and len(result) == 2: + ret_val, func_name = result + if ret_val != 1: + raise NativeError(f"{func_name} failed") + return ret_val + # Otherwise return as-is (function already handled errors) + return result + + return wrapper + + +class GmsslCall: + """ + Callable wrapper for gmssl functions with automatic error checking. + + This provides a cleaner alternative to raise_on_error() for repeated calls + to the same gmssl function. + + Example: + # Create wrapper once + sm2_sign = GmsslCall(gmssl.sm2_sign, "sm2_sign") + + # Use it multiple times + sm2_sign(byref(self), dgst, sig, byref(siglen)) + sm2_sign(byref(other), dgst2, sig2, byref(siglen2)) + + Alternative usage with auto-naming: + # Function name is extracted from the callable + encrypt = GmsslCall(gmssl.sm2_encrypt) # name = "sm2_encrypt" + encrypt(byref(self), data, len(data), outbuf, byref(outlen)) + """ + + def __init__(self, func, name=None): + """ + Initialize the gmssl function wrapper. + + Args: + func: The gmssl function to wrap + name: Optional function name for error messages. + If not provided, attempts to extract from func.__name__ + """ + self.func = func + # Try to extract name from function if not provided + if name is None and hasattr(func, "__name__"): + # Remove 'gmssl_' prefix if present + name = func.__name__.replace("gmssl_", "") + self.name = name or "gmssl_function" + + def __call__(self, *args, **kwargs): + """ + Call the wrapped gmssl function and check for errors. + + Returns: + The result from the gmssl function (typically 1 on success) + + Raises: + NativeError: If the gmssl function returns a value != 1 + """ + result = self.func(*args, **kwargs) + if result != 1: + raise NativeError(f"{self.name} failed") + return result + + +class _GmsslProxy: + """ + Proxy object for gmssl library that auto-wraps functions with error checking. + + This eliminates the need to manually call raise_on_error or GmsslCall for every + gmssl function call. Function names are automatically extracted from the attribute. + + Example: + # Instead of: + raise_on_error(gmssl.sm2_key_generate(byref(key)), "sm2_key_generate") + + # Simply write: + checked.sm2_key_generate(byref(key)) + + # The function name is automatically extracted and errors are checked + """ + + def __init__(self, lib): + """ + Initialize proxy with the gmssl library. + + Args: + lib: The loaded gmssl CDLL library instance + """ + self._lib = lib + + def __getattr__(self, name): + """ + Get gmssl function and wrap it with automatic error checking. + + Args: + name: Function name (e.g., "sm2_key_generate") + + Returns: + GmsslCall: Wrapped function with error checking + """ + func = getattr(self._lib, name) + return GmsslCall(func, name) + + +# Create a proxy instance for convenient access +checked = _GmsslProxy(gmssl) diff --git a/src/gmssl/_libs/gmssl.dll b/src/gmssl/_libs/gmssl.dll new file mode 100644 index 0000000..efec017 Binary files /dev/null and b/src/gmssl/_libs/gmssl.dll differ diff --git a/src/gmssl/_libs/libgmssl.3.dylib b/src/gmssl/_libs/libgmssl.3.dylib new file mode 100755 index 0000000..3db798e Binary files /dev/null and b/src/gmssl/_libs/libgmssl.3.dylib differ diff --git a/src/gmssl/_libs/libgmssl.so.3.aarch64 b/src/gmssl/_libs/libgmssl.so.3.aarch64 new file mode 100644 index 0000000..ae4bdb1 Binary files /dev/null and b/src/gmssl/_libs/libgmssl.so.3.aarch64 differ diff --git a/src/gmssl/_libs/libgmssl.so.3.x86_64 b/src/gmssl/_libs/libgmssl.so.3.x86_64 new file mode 100644 index 0000000..60df825 Binary files /dev/null and b/src/gmssl/_libs/libgmssl.so.3.x86_64 differ diff --git a/src/gmssl/_pem_utils.py b/src/gmssl/_pem_utils.py new file mode 100644 index 0000000..77f5608 --- /dev/null +++ b/src/gmssl/_pem_utils.py @@ -0,0 +1,495 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - PEM file utilities for Windows compatibility + +""" +Internal module for PEM file operations with Windows FILE* workaround. +This module should not be imported directly by users. + +On Windows, FILE* pointers cannot be passed across DLL boundaries due to +different C runtime versions. This module provides Python-based PEM I/O +that works around this limitation by using DER format + base64 encoding. + +For Linux/macOS, this module provides wrapper functions that delegate to +the native FILE*-based functions for best performance. +""" + +import base64 +import sys +from ctypes import POINTER, byref, c_char_p, c_size_t, c_uint8, c_void_p, create_string_buffer + +from gmssl._file_utils import open_file +from gmssl._lib import NativeError, gmssl, libc + + +def _write_pem_windows(path, name, der_data): + """ + Write PEM file on Windows using Python file I/O. + + Args: + path: File path (str) + name: PEM label (e.g., "ENCRYPTED PRIVATE KEY") + der_data: DER-encoded data (bytes) + """ + with open(path, "wb") as f: + f.write(f"-----BEGIN {name}-----\n".encode("ascii")) + # Base64 encode with 64 characters per line (PEM standard) + b64_data = base64.b64encode(der_data) + for i in range(0, len(b64_data), 64): + f.write(b64_data[i : i + 64]) + f.write(b"\n") + f.write(f"-----END {name}-----\n".encode("ascii")) + + +def _read_pem_windows(path, name): + """ + Read PEM file on Windows using Python file I/O. + + Args: + path: File path (str) + name: PEM label (e.g., "ENCRYPTED PRIVATE KEY") + + Returns: + bytes: DER-encoded data + """ + begin_marker = f"-----BEGIN {name}-----" + end_marker = f"-----END {name}-----" + + with open(path) as f: + lines = f.readlines() + + # Find begin and end markers + begin_idx = None + end_idx = None + for i, line in enumerate(lines): + line = line.strip() + if line == begin_marker: + begin_idx = i + elif line == end_marker: + end_idx = i + break + + if begin_idx is None or end_idx is None: + raise ValueError(f"Invalid PEM file: missing {name} markers") + + # Extract base64 data + b64_data = "".join(line.strip() for line in lines[begin_idx + 1 : end_idx]) + return base64.b64decode(b64_data) + + +# ============================================================================= +# SM2 Public Key PEM Operations +# ============================================================================= + + +def sm2_public_key_info_to_pem_windows(key, path): + """ + Export SM2 public key to PEM file (Windows-compatible). + """ + # Use stack buffer like the C implementation + buf = create_string_buffer(512) + p = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + outlen = c_size_t(0) + + if gmssl.sm2_public_key_info_to_der(byref(key), byref(p), byref(outlen)) != 1: + raise NativeError("sm2_public_key_info_to_der failed") + + _write_pem_windows(path, "PUBLIC KEY", buf.raw[: outlen.value]) + + +def sm2_public_key_info_from_pem_windows(key, path): + """ + Import SM2 public key from PEM file (Windows-compatible). + """ + der_data = _read_pem_windows(path, "PUBLIC KEY") + # Create a buffer and pointer like the C implementation + buf = create_string_buffer(der_data) + cp = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + der_len = c_size_t(len(der_data)) + + if gmssl.sm2_public_key_info_from_der(byref(key), byref(cp), byref(der_len)) != 1: + raise NativeError("sm2_public_key_info_from_der failed") + + +# ============================================================================= +# SM2 Private Key PEM Operations +# ============================================================================= + + +def sm2_private_key_info_encrypt_to_pem_windows(key, path, passwd): + """ + Export SM2 encrypted private key to PEM file (Windows-compatible). + + Uses DER export + Python file I/O to avoid FILE* cross-DLL issues. + """ + # Use stack buffer like the C implementation + buf = create_string_buffer(1024) + p = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + outlen = c_size_t(0) + + if gmssl.sm2_private_key_info_encrypt_to_der(byref(key), passwd, byref(p), byref(outlen)) != 1: + raise NativeError("sm2_private_key_info_encrypt_to_der failed") + + _write_pem_windows(path, "ENCRYPTED PRIVATE KEY", buf.raw[: outlen.value]) + + +def sm2_private_key_info_decrypt_from_pem_windows(key, path, passwd): + """ + Import SM2 encrypted private key from PEM file (Windows-compatible). + """ + # Read PEM file and decode to DER + der_data = _read_pem_windows(path, "ENCRYPTED PRIVATE KEY") + + # Parse DER format - create buffer and pointer like the C implementation + buf = create_string_buffer(der_data) + cp = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + der_len = c_size_t(len(der_data)) + attrs_ptr = c_void_p() + attrs_len = c_size_t() + + if ( + gmssl.sm2_private_key_info_decrypt_from_der( + byref(key), + byref(attrs_ptr), + byref(attrs_len), + passwd, + byref(cp), + byref(der_len), + ) + != 1 + ): + raise NativeError("sm2_private_key_info_decrypt_from_der failed") + + +# ============================================================================= +# SM9 Encryption Master Key PEM Operations +# ============================================================================= + + +def sm9_enc_master_public_key_to_pem_windows(mpk, path): + """ + Export SM9 encryption master public key to PEM file (Windows-compatible). + """ + # Use stack buffer like the C implementation + buf = create_string_buffer(1024) + p = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + outlen = c_size_t(0) + + if gmssl.sm9_enc_master_public_key_to_der(byref(mpk), byref(p), byref(outlen)) != 1: + raise NativeError("sm9_enc_master_public_key_to_der failed") + + _write_pem_windows(path, "SM9 ENC MASTER PUBLIC KEY", buf.raw[: outlen.value]) + + +def sm9_enc_master_public_key_from_pem_windows(mpk, path): + """ + Import SM9 encryption master public key from PEM file (Windows-compatible). + """ + der_data = _read_pem_windows(path, "SM9 ENC MASTER PUBLIC KEY") + buf = create_string_buffer(der_data) + cp = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + der_len = c_size_t(len(der_data)) + + if gmssl.sm9_enc_master_public_key_from_der(byref(mpk), byref(cp), byref(der_len)) != 1: + raise NativeError("sm9_enc_master_public_key_from_der failed") + + +def sm9_enc_master_key_info_encrypt_to_pem_windows(msk, path, passwd): + """ + Export SM9 encryption master key to PEM file (Windows-compatible). + """ + # Use stack buffer like the C implementation + buf = create_string_buffer(1024) + p = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + outlen = c_size_t(0) + + if ( + gmssl.sm9_enc_master_key_info_encrypt_to_der(byref(msk), passwd, byref(p), byref(outlen)) + != 1 + ): + raise NativeError("sm9_enc_master_key_info_encrypt_to_der failed") + + _write_pem_windows(path, "ENCRYPTED SM9 ENC MASTER KEY", buf.raw[: outlen.value]) + + +def sm9_enc_master_key_info_decrypt_from_pem_windows(msk, path, passwd): + """ + Import SM9 encryption master key from PEM file (Windows-compatible). + """ + der_data = _read_pem_windows(path, "ENCRYPTED SM9 ENC MASTER KEY") + buf = create_string_buffer(der_data) + cp = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + der_len = c_size_t(len(der_data)) + + if ( + gmssl.sm9_enc_master_key_info_decrypt_from_der( + byref(msk), passwd, byref(cp), byref(der_len) + ) + != 1 + ): + raise NativeError("sm9_enc_master_key_info_decrypt_from_der failed") + + +# ============================================================================= +# SM9 Signature Master Key PEM Operations +# ============================================================================= + + +def sm9_sign_master_public_key_to_pem_windows(mpk, path): + """ + Export SM9 signature master public key to PEM file (Windows-compatible). + """ + # Use stack buffer like the C implementation + buf = create_string_buffer(1024) + p = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + outlen = c_size_t(0) + + if gmssl.sm9_sign_master_public_key_to_der(byref(mpk), byref(p), byref(outlen)) != 1: + raise NativeError("sm9_sign_master_public_key_to_der failed") + + _write_pem_windows(path, "SM9 SIGN MASTER PUBLIC KEY", buf.raw[: outlen.value]) + + +def sm9_sign_master_public_key_from_pem_windows(mpk, path): + """ + Import SM9 signature master public key from PEM file (Windows-compatible). + """ + der_data = _read_pem_windows(path, "SM9 SIGN MASTER PUBLIC KEY") + buf = create_string_buffer(der_data) + cp = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + der_len = c_size_t(len(der_data)) + + if gmssl.sm9_sign_master_public_key_from_der(byref(mpk), byref(cp), byref(der_len)) != 1: + raise NativeError("sm9_sign_master_public_key_from_der failed") + + +def sm9_sign_master_key_info_encrypt_to_pem_windows(msk, path, passwd): + """ + Export SM9 signature master key to PEM file (Windows-compatible). + """ + # Use stack buffer like the C implementation + buf = create_string_buffer(1024) + p = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + outlen = c_size_t(0) + + if ( + gmssl.sm9_sign_master_key_info_encrypt_to_der(byref(msk), passwd, byref(p), byref(outlen)) + != 1 + ): + raise NativeError("sm9_sign_master_key_info_encrypt_to_der failed") + + _write_pem_windows(path, "ENCRYPTED SM9 SIGN MASTER KEY", buf.raw[: outlen.value]) + + +def sm9_sign_master_key_info_decrypt_from_pem_windows(msk, path, passwd): + """ + Import SM9 signature master key from PEM file (Windows-compatible). + """ + der_data = _read_pem_windows(path, "ENCRYPTED SM9 SIGN MASTER KEY") + buf = create_string_buffer(der_data) + cp = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + der_len = c_size_t(len(der_data)) + + if ( + gmssl.sm9_sign_master_key_info_decrypt_from_der( + byref(msk), passwd, byref(cp), byref(der_len) + ) + != 1 + ): + raise NativeError("sm9_sign_master_key_info_decrypt_from_der failed") + + +# ============================================================================= +# SM9 Encryption Key PEM Operations +# ============================================================================= + + +def sm9_enc_key_info_encrypt_to_pem_windows(key, path, passwd): + """ + Export SM9 encryption key to PEM file (Windows-compatible). + """ + # Use stack buffer like the C implementation + buf = create_string_buffer(1024) + p = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + outlen = c_size_t(0) + + if gmssl.sm9_enc_key_info_encrypt_to_der(byref(key), passwd, byref(p), byref(outlen)) != 1: + raise NativeError("sm9_enc_key_info_encrypt_to_der failed") + + _write_pem_windows(path, "ENCRYPTED SM9 ENC PRIVATE KEY", buf.raw[: outlen.value]) + + +def sm9_enc_key_info_decrypt_from_pem_windows(key, path, passwd): + """ + Import SM9 encryption key from PEM file (Windows-compatible). + """ + der_data = _read_pem_windows(path, "ENCRYPTED SM9 ENC PRIVATE KEY") + buf = create_string_buffer(der_data) + cp = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + der_len = c_size_t(len(der_data)) + + if gmssl.sm9_enc_key_info_decrypt_from_der(byref(key), passwd, byref(cp), byref(der_len)) != 1: + raise NativeError("sm9_enc_key_info_decrypt_from_der failed") + + +# ============================================================================= +# SM9 Signature Key PEM Operations +# ============================================================================= + + +def sm9_sign_key_info_encrypt_to_pem_windows(key, path, passwd): + """ + Export SM9 signature key to PEM file (Windows-compatible). + """ + # Use stack buffer like the C implementation + buf = create_string_buffer(1024) + p = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + outlen = c_size_t(0) + + if gmssl.sm9_sign_key_info_encrypt_to_der(byref(key), passwd, byref(p), byref(outlen)) != 1: + raise NativeError("sm9_sign_key_info_encrypt_to_der failed") + + _write_pem_windows(path, "ENCRYPTED SM9 SIGN PRIVATE KEY", buf.raw[: outlen.value]) + + +def sm9_sign_key_info_decrypt_from_pem_windows(key, path, passwd): + """ + Import SM9 signature key from PEM file (Windows-compatible). + """ + der_data = _read_pem_windows(path, "ENCRYPTED SM9 SIGN PRIVATE KEY") + buf = create_string_buffer(der_data) + cp = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + der_len = c_size_t(len(der_data)) + + if gmssl.sm9_sign_key_info_decrypt_from_der(byref(key), passwd, byref(cp), byref(der_len)) != 1: + raise NativeError("sm9_sign_key_info_decrypt_from_der failed") + + +# ============================================================================= +# X.509 Certificate PEM Operations +# ============================================================================= + + +def x509_cert_to_pem_windows(cert, certlen, path): + """ + Export X.509 certificate to PEM file (Windows-compatible). + """ + _write_pem_windows(path, "CERTIFICATE", bytes(cert[:certlen])) + + +def x509_cert_from_pem_windows(path): + """ + Import X.509 certificate from PEM file (Windows-compatible). + + Returns: + tuple: (cert_data, cert_len) - Certificate DER data and length + """ + der_data = _read_pem_windows(path, "CERTIFICATE") + buf = create_string_buffer(der_data) + cp = POINTER(c_uint8)(c_uint8.from_buffer(buf)) + der_len = c_size_t(len(der_data)) + + cert_ptr = c_void_p() + cert_len = c_size_t() + + if gmssl.x509_cert_from_der(byref(cert_ptr), byref(cert_len), byref(cp), byref(der_len)) != 1: + raise NativeError("x509_cert_from_der failed") + + # Copy certificate data + cert_data = create_string_buffer(cert_len.value) + libc.memcpy(cert_data, cert_ptr, cert_len.value) + + return cert_data, cert_len.value + + +# ============================================================================= +# Cross-Platform Wrapper Functions +# ============================================================================= + + +def _call_platform_pem_function(func_name, key, path, file_mode, extra_args=()): + """ + Generic cross-platform wrapper for PEM operations. + + Automatically selects Windows-compatible or FILE*-based implementation. + Windows function name is derived by appending '_windows' to func_name. + + Args: + func_name: Base function name (e.g., "sm2_public_key_info_to_pem") + key: Key object (SM2Key, Sm9EncMasterKey, etc.) + path: File path (str) + file_mode: File mode for non-Windows platforms ("rb" or "wb") + extra_args: Additional arguments tuple (e.g., (c_char_p(passwd),)) + + Raises: + NativeError: If the operation fails + """ + if sys.platform == "win32": + # Windows: Use Python-based implementation + windows_func = globals()[f"{func_name}_windows"] + windows_func(key, path, *extra_args) + else: + # Linux/macOS: Use FILE* for best performance + with open_file(path, file_mode) as fp: + gmssl_func = getattr(gmssl, func_name) + if gmssl_func(byref(key), *extra_args, fp) != 1: + raise NativeError(f"{func_name} failed") + + +def pem_export_encrypted_key(key, path, passwd, export_func_name): + """ + Cross-platform wrapper for exporting encrypted keys to PEM. + + Args: + key: Key object (SM2Key, Sm9EncMasterKey, etc.) + path: File path (str) + passwd: Password (bytes) + export_func_name: Name of the gmssl export function + (e.g., "sm2_private_key_info_encrypt_to_pem") + """ + _call_platform_pem_function(export_func_name, key, path, "wb", extra_args=(c_char_p(passwd),)) + + +def pem_import_encrypted_key(key, path, passwd, import_func_name): + """ + Cross-platform wrapper for importing encrypted keys from PEM. + + Args: + key: Key object (SM2Key, Sm9EncMasterKey, etc.) + path: File path (str) + passwd: Password (bytes) + import_func_name: Name of the gmssl import function + (e.g., "sm2_private_key_info_decrypt_from_pem") + """ + _call_platform_pem_function(import_func_name, key, path, "rb", extra_args=(c_char_p(passwd),)) + + +def pem_export_public_key(key, path, export_func_name): + """ + Cross-platform wrapper for exporting public keys to PEM. + + Args: + key: Key object (SM2Key, etc.) + path: File path (str) + export_func_name: Name of the gmssl export function + (e.g., "sm2_public_key_info_to_pem") + """ + _call_platform_pem_function(export_func_name, key, path, "wb") + + +def pem_import_public_key(key, path, import_func_name): + """ + Cross-platform wrapper for importing public keys from PEM. + + Args: + key: Key object (SM2Key, etc.) + path: File path (str) + import_func_name: Name of the gmssl import function + (e.g., "sm2_public_key_info_from_pem") + """ + _call_platform_pem_function(import_func_name, key, path, "rb") diff --git a/src/gmssl/_random.py b/src/gmssl/_random.py new file mode 100644 index 0000000..d902d0c --- /dev/null +++ b/src/gmssl/_random.py @@ -0,0 +1,27 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - Random number generator + +""" +Internal module for random number generation. +This module should not be imported directly by users. +""" + +from ctypes import c_size_t, create_string_buffer + +from gmssl._lib import gmssl + +# ============================================================================= +# Random Number Generator +# ============================================================================= + + +def rand_bytes(size): + buf = create_string_buffer(size) + gmssl.rand_bytes(buf, c_size_t(size)) + return buf.raw diff --git a/src/gmssl/_sm2.py b/src/gmssl/_sm2.py new file mode 100644 index 0000000..a8e1bf6 --- /dev/null +++ b/src/gmssl/_sm2.py @@ -0,0 +1,204 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - SM2 public key cryptography and signature + +""" +Internal module for SM2 elliptic curve cryptography. +This module should not be imported directly by users. +""" + +from ctypes import Structure, byref, c_char_p, c_size_t, c_uint, c_uint8, create_string_buffer + +from gmssl._constants import ( + DO_SIGN, + DO_VERIFY, + SM2_DEFAULT_ID, + SM2_MAX_CIPHERTEXT_SIZE, + SM2_MAX_PLAINTEXT_SIZE, + SM2_MAX_SIGNATURE_SIZE, + SM3_DIGEST_SIZE, +) +from gmssl._lib import StateError, checked, gmssl + +# Import cross-platform PEM wrappers +from gmssl._pem_utils import ( + pem_export_encrypted_key, + pem_export_public_key, + pem_import_encrypted_key, + pem_import_public_key, +) +from gmssl._sm3 import Sm3 + +# ============================================================================= +# SM2 Public Key Cryptography +# ============================================================================= + + +class Sm2Z256Point(Structure): + """SM2_Z256_POINT structure using Jacobian coordinates (X, Y, Z).""" + + _fields_ = [ + ("X", c_uint8 * 32), # sm2_z256_t + ("Y", c_uint8 * 32), # sm2_z256_t + ("Z", c_uint8 * 32), # sm2_z256_t + ] + + +class Sm2Key(Structure): + _fields_ = [("public_key", Sm2Z256Point), ("private_key", c_uint8 * 32)] + + def __init__(self): + self._has_public_key = False + self._has_private_key = False + + def generate_key(self): + checked.sm2_key_generate(byref(self)) + self._has_public_key = True + self._has_private_key = True + + def has_private_key(self): + return self._has_private_key + + def has_public_key(self): + return self._has_public_key + + def compute_z(self, signer_id=SM2_DEFAULT_ID): + if not self._has_public_key: + raise TypeError("has no public key") + signer_id = signer_id.encode("utf-8") + z = create_string_buffer(SM3_DIGEST_SIZE) + gmssl.sm2_compute_z(z, byref(self), c_char_p(signer_id), c_size_t(len(signer_id))) + return z.raw + + def export_encrypted_private_key_info_pem(self, path, passwd): + if not self._has_private_key: + raise TypeError("has no private key") + passwd = passwd.encode("utf-8") + pem_export_encrypted_key(self, path, passwd, "sm2_private_key_info_encrypt_to_pem") + + def import_encrypted_private_key_info_pem(self, path, passwd): + passwd = passwd.encode("utf-8") + pem_import_encrypted_key(self, path, passwd, "sm2_private_key_info_decrypt_from_pem") + self._has_public_key = True + self._has_private_key = True + + def export_public_key_info_pem(self, path): + if not self._has_public_key: + raise TypeError("has no public key") + pem_export_public_key(self, path, "sm2_public_key_info_to_pem") + + def import_public_key_info_pem(self, path): + pem_import_public_key(self, path, "sm2_public_key_info_from_pem") + self._has_public_key = True + self._has_private_key = False + + def sign(self, dgst): + if not self._has_private_key: + raise TypeError("has no private key") + if len(dgst) != SM3_DIGEST_SIZE: + raise ValueError("Invalid SM3 digest size") + sig = create_string_buffer(SM2_MAX_SIGNATURE_SIZE) + siglen = c_size_t() + checked.sm2_sign(byref(self), dgst, sig, byref(siglen)) + return sig[: siglen.value] + + def verify(self, dgst, signature): + if not self._has_public_key: + raise TypeError("has no public key") + if len(dgst) != SM3_DIGEST_SIZE: + raise ValueError("Invalid SM3 digest size") + return gmssl.sm2_verify(byref(self), dgst, signature, c_size_t(len(signature))) == 1 + + def encrypt(self, data): + if not self._has_public_key: + raise TypeError("has no public key") + if len(data) > SM2_MAX_PLAINTEXT_SIZE: + raise ValueError("Plaintext too long") + outbuf = create_string_buffer(SM2_MAX_CIPHERTEXT_SIZE) + outlen = c_size_t() + checked.sm2_encrypt(byref(self), data, c_size_t(len(data)), outbuf, byref(outlen)) + return outbuf[: outlen.value] + + def decrypt(self, ciphertext): + if not self._has_private_key: + raise TypeError("has no private key") + outbuf = create_string_buffer(SM2_MAX_PLAINTEXT_SIZE) + outlen = c_size_t() + checked.sm2_decrypt( + byref(self), + ciphertext, + c_size_t(len(ciphertext)), + outbuf, + byref(outlen), + ) + return outbuf[: outlen.value] + + +# ============================================================================= +# SM2 Signature +# ============================================================================= + + +class Sm2SignPreComp(Structure): + _fields_ = [ + ("k", c_uint8 * 32), # sm2_z256_t = uint64_t[4] = 32 bytes + ("x1_modn", c_uint8 * 32), + ] + + +class Sm2Signature(Structure): + _fields_ = [ + ("sm3_ctx", Sm3), + ("saved_sm3_ctx", Sm3), + ("key", Sm2Key), + ("fast_sign_private", c_uint8 * 32), # sm2_z256_t + ("pre_comp", Sm2SignPreComp * 32), # SM2_SIGN_PRE_COMP_COUNT = 32 + ("num_pre_comp", c_uint), # unsigned int + ("public_point_table", Sm2Z256Point * 16), + ] + + def __init__(self, sm2_key, signer_id=SM2_DEFAULT_ID, sign=DO_SIGN): + signer_id = signer_id.encode("utf-8") + if sign == DO_SIGN: + if not sm2_key.has_private_key(): + raise TypeError("SM2 key has no private key") + checked.sm2_sign_init( + byref(self), + byref(sm2_key), + c_char_p(signer_id), + c_size_t(len(signer_id)), + ) + else: + if not sm2_key.has_public_key(): + raise TypeError("SM2 key has no public key") + checked.sm2_verify_init( + byref(self), + byref(sm2_key), + c_char_p(signer_id), + c_size_t(len(signer_id)), + ) + self._sign = sign + + def update(self, data): + if self._sign == DO_SIGN: + checked.sm2_sign_update(byref(self), data, c_size_t(len(data))) + else: + checked.sm2_verify_update(byref(self), data, c_size_t(len(data))) + + def sign(self): + if self._sign != DO_SIGN: + raise StateError("not sign state") + sig = create_string_buffer(SM2_MAX_SIGNATURE_SIZE) + siglen = c_size_t() + checked.sm2_sign_finish(byref(self), sig, byref(siglen)) + return sig[: siglen.value] + + def verify(self, signature): + if self._sign != DO_VERIFY: + raise StateError("not verify state") + return gmssl.sm2_verify_finish(byref(self), signature, c_size_t(len(signature))) == 1 diff --git a/src/gmssl/_sm3.py b/src/gmssl/_sm3.py new file mode 100644 index 0000000..30af460 --- /dev/null +++ b/src/gmssl/_sm3.py @@ -0,0 +1,123 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - SM3 hash, HMAC, and PBKDF2 + +""" +Internal module for SM3 cryptographic hash functions. +This module should not be imported directly by users. +""" + +from ctypes import ( + Structure, + byref, + c_char_p, + c_size_t, + c_uint8, + c_uint32, + c_uint64, + create_string_buffer, +) + +from gmssl._constants import ( + _SM3_BLOCK_SIZE, + _SM3_STATE_WORDS, + SM3_DIGEST_SIZE, + SM3_HMAC_MAX_KEY_SIZE, + SM3_HMAC_MIN_KEY_SIZE, + SM3_HMAC_SIZE, + SM3_PBKDF2_MAX_ITER, + SM3_PBKDF2_MAX_KEY_SIZE, + SM3_PBKDF2_MAX_SALT_SIZE, + SM3_PBKDF2_MIN_ITER, +) +from gmssl._lib import checked, gmssl + +# ============================================================================= +# SM3 Hash +# ============================================================================= + + +class Sm3(Structure): + _fields_ = [ + ("dgst", c_uint32 * _SM3_STATE_WORDS), + ("nblocks", c_uint64), + ("block", c_uint8 * _SM3_BLOCK_SIZE), + ("num", c_size_t), + ] + + def __init__(self): + gmssl.sm3_init(byref(self)) + + def reset(self): + gmssl.sm3_init(byref(self)) + + def update(self, data): + gmssl.sm3_update(byref(self), data, c_size_t(len(data))) + + def digest(self): + dgst = create_string_buffer(SM3_DIGEST_SIZE) + gmssl.sm3_finish(byref(self), dgst) + return dgst.raw + + +# ============================================================================= +# SM3 HMAC +# ============================================================================= + + +class Sm3Hmac(Structure): + _fields_ = [("sm3_ctx", Sm3), ("key", c_uint8 * _SM3_BLOCK_SIZE)] + + def __init__(self, key): + if len(key) < SM3_HMAC_MIN_KEY_SIZE or len(key) > SM3_HMAC_MAX_KEY_SIZE: + raise ValueError("Invalid SM3 HMAC key length") + gmssl.sm3_hmac_init(byref(self), key, c_size_t(len(key))) + + def reset(self, key): + if len(key) < SM3_HMAC_MIN_KEY_SIZE or len(key) > SM3_HMAC_MAX_KEY_SIZE: + raise ValueError("Invalid SM3 HMAC key length") + gmssl.sm3_hmac_init(byref(self), key, c_size_t(len(key))) + + def update(self, data): + gmssl.sm3_hmac_update(byref(self), data, c_size_t(len(data))) + + def generate_mac(self): + hmac = create_string_buffer(SM3_HMAC_SIZE) + gmssl.sm3_hmac_finish(byref(self), hmac) + return hmac.raw + + +# ============================================================================= +# SM3 PBKDF2 +# ============================================================================= + + +def sm3_pbkdf2(passwd, salt, iterator, keylen): + if len(salt) > SM3_PBKDF2_MAX_SALT_SIZE: + raise ValueError("Invalid salt length") + + if iterator < SM3_PBKDF2_MIN_ITER or iterator > SM3_PBKDF2_MAX_ITER: + raise ValueError("Invalid iterator value") + + if keylen > SM3_PBKDF2_MAX_KEY_SIZE: + raise ValueError("Invalid key length") + + passwd = passwd.encode("utf-8") + key = create_string_buffer(keylen) + + checked.sm3_pbkdf2( + c_char_p(passwd), + c_size_t(len(passwd)), + salt, + c_size_t(len(salt)), + c_size_t(iterator), + c_size_t(keylen), + key, + ) + + return key.raw diff --git a/src/gmssl/_sm4.py b/src/gmssl/_sm4.py new file mode 100644 index 0000000..320722b --- /dev/null +++ b/src/gmssl/_sm4.py @@ -0,0 +1,276 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - SM4 block cipher and modes + +""" +Internal module for SM4 block cipher and its modes of operation. +This module should not be imported directly by users. +""" + +from ctypes import Structure, byref, c_size_t, c_uint8, c_uint32, c_uint64, create_string_buffer + +from gmssl._constants import ( + _SM4_NUM_ROUNDS, + DO_ENCRYPT, + SM4_BLOCK_SIZE, + SM4_GCM_DEFAULT_TAG_SIZE, + SM4_GCM_MAX_IV_SIZE, + SM4_GCM_MAX_TAG_SIZE, + SM4_GCM_MIN_IV_SIZE, + SM4_KEY_SIZE, +) +from gmssl._lib import checked, gmssl + +# ============================================================================= +# SM4 Block Cipher +# ============================================================================= + + +class Sm4(Structure): + _fields_ = [("rk", c_uint32 * _SM4_NUM_ROUNDS)] + + def __init__(self, key, encrypt): + if len(key) != SM4_KEY_SIZE: + raise ValueError("Invalid key length") + if encrypt: + gmssl.sm4_set_encrypt_key(byref(self), key) + else: + gmssl.sm4_set_decrypt_key(byref(self), key) + self._encrypt = encrypt + + def encrypt(self, block): + """ + Encrypt/decrypt a single block (16 bytes). + + Note: This method works for both encryption and decryption modes + depending on how the instance was initialized. For better code + clarity, use decrypt() method when working in decryption mode. + + Args: + block: Input block (16 bytes) + + Returns: + bytes: Output block (16 bytes) + """ + if len(block) != SM4_BLOCK_SIZE: + raise ValueError("Invalid block size") + outbuf = create_string_buffer(SM4_BLOCK_SIZE) + gmssl.sm4_encrypt(byref(self), block, outbuf) + return outbuf.raw + + def decrypt(self, block): + """ + Decrypt a single block (16 bytes). + + This is an alias for encrypt() that provides better code clarity + when working in decryption mode. The actual operation is determined + by the mode specified during initialization. + + Args: + block: Input block (16 bytes) + + Returns: + bytes: Output block (16 bytes) + + Raises: + ValueError: If instance was initialized in encryption mode + """ + if self._encrypt: + raise ValueError("Cannot call decrypt() on encryption mode instance") + return self.encrypt(block) + + +# ============================================================================= +# SM4-CBC Mode +# ============================================================================= + + +class Sm4Cbc(Structure): + _fields_ = [ + ("sm4_key", Sm4), + ("iv", c_uint8 * SM4_BLOCK_SIZE), + ("block", c_uint8 * SM4_BLOCK_SIZE), + ("block_nbytes", c_size_t), + ] + + def __init__(self, key, iv, encrypt): + if len(key) != SM4_KEY_SIZE: + raise ValueError("Invalid key length") + if len(iv) != SM4_BLOCK_SIZE: + raise ValueError("Invalid IV size") + if encrypt == DO_ENCRYPT: + checked.sm4_cbc_encrypt_init(byref(self), key, iv) + else: + checked.sm4_cbc_decrypt_init(byref(self), key, iv) + self._encrypt = encrypt + + def update(self, data): + outbuf = create_string_buffer(len(data) + SM4_BLOCK_SIZE) + outlen = c_size_t() + if self._encrypt == DO_ENCRYPT: + checked.sm4_cbc_encrypt_update( + byref(self), data, c_size_t(len(data)), outbuf, byref(outlen) + ) + else: + checked.sm4_cbc_decrypt_update( + byref(self), data, c_size_t(len(data)), outbuf, byref(outlen) + ) + return outbuf[0 : outlen.value] + + def finish(self): + outbuf = create_string_buffer(SM4_BLOCK_SIZE) + outlen = c_size_t() + if self._encrypt == DO_ENCRYPT: + checked.sm4_cbc_encrypt_finish(byref(self), outbuf, byref(outlen)) + else: + checked.sm4_cbc_decrypt_finish(byref(self), outbuf, byref(outlen)) + return outbuf[: outlen.value] + + +# ============================================================================= +# SM4-CTR Mode +# ============================================================================= + + +class Sm4Ctr(Structure): + _fields_ = [ + ("sm4_key", Sm4), + ("ctr", c_uint8 * SM4_BLOCK_SIZE), + ("block", c_uint8 * SM4_BLOCK_SIZE), + ("block_nbytes", c_size_t), + ] + + def __init__(self, key, ctr): + if len(key) != SM4_KEY_SIZE: + raise ValueError("Invalid key length") + if len(ctr) != SM4_BLOCK_SIZE: + raise ValueError("Invalid CTR size") + checked.sm4_ctr_encrypt_init(byref(self), key, ctr) + + def update(self, data): + outbuf = create_string_buffer(len(data) + SM4_BLOCK_SIZE) + outlen = c_size_t() + checked.sm4_ctr_encrypt_update( + byref(self), data, c_size_t(len(data)), outbuf, byref(outlen) + ) + return outbuf[0 : outlen.value] + + def finish(self): + outbuf = create_string_buffer(SM4_BLOCK_SIZE) + outlen = c_size_t() + checked.sm4_ctr_encrypt_finish(byref(self), outbuf, byref(outlen)) + return outbuf[: outlen.value] + + +# ============================================================================= +# GCM Mode Support Structures +# ============================================================================= + + +class gf128_t(Structure): + _fields_ = [("hi", c_uint64), ("lo", c_uint64)] + + +class Ghash(Structure): + _fields_ = [ + ("H", gf128_t), + ("X", gf128_t), + ("aadlen", c_size_t), + ("clen", c_size_t), + ("block", c_uint8 * 16), + ("num", c_size_t), + ] + + +# ============================================================================= +# SM4-GCM Mode +# ============================================================================= + + +class Sm4Gcm(Structure): + """ + SM4-GCM (Galois/Counter Mode) authenticated encryption. + + WARNING: This class is NOT thread-safe due to underlying GmSSL library + implementation. If you need to use SM4-GCM in a multi-threaded environment, + you must protect each instance with a lock (threading.Lock). + + Example: + # Single-threaded usage (safe) + sm4_gcm = Sm4Gcm(key, iv, aad, taglen, DO_ENCRYPT) + ciphertext = sm4_gcm.update(plaintext) + sm4_gcm.finish() + + # Multi-threaded usage (requires lock) + lock = threading.Lock() + with lock: + sm4_gcm = Sm4Gcm(key, iv, aad, taglen, DO_ENCRYPT) + ciphertext = sm4_gcm.update(plaintext) + sm4_gcm.finish() + """ + + _fields_ = [ + ("sm4_ctr_ctx", Sm4Ctr), + ("mac_ctx", Ghash), + ("Y", c_uint8 * 16), + ("taglen", c_size_t), + ("mac", c_uint8 * 16), + ("maclen", c_size_t), + ("encedlen", c_uint64), + ] + + def __init__(self, key, iv, aad, taglen=SM4_GCM_DEFAULT_TAG_SIZE, encrypt=True): + if len(key) != SM4_KEY_SIZE: + raise ValueError("Invalid key length") + if len(iv) < SM4_GCM_MIN_IV_SIZE or len(iv) > SM4_GCM_MAX_IV_SIZE: + raise ValueError("Invalid IV size") + if taglen < 1 or taglen > SM4_GCM_MAX_TAG_SIZE: + raise ValueError("Invalid Tag length") + if encrypt == DO_ENCRYPT: + checked.sm4_gcm_encrypt_init( + byref(self), + key, + c_size_t(len(key)), + iv, + c_size_t(len(iv)), + aad, + c_size_t(len(aad)), + c_size_t(taglen), + ) + else: + checked.sm4_gcm_decrypt_init( + byref(self), + key, + c_size_t(len(key)), + iv, + c_size_t(len(iv)), + aad, + c_size_t(len(aad)), + c_size_t(taglen), + ) + self._encrypt = encrypt + + def update(self, data): + outbuf = create_string_buffer(len(data) + SM4_BLOCK_SIZE) + outlen = c_size_t() + if self._encrypt == DO_ENCRYPT: + checked.sm4_gcm_encrypt_update( + byref(self), data, c_size_t(len(data)), outbuf, byref(outlen) + ) + else: + checked.sm4_gcm_decrypt_update( + byref(self), data, c_size_t(len(data)), outbuf, byref(outlen) + ) + return outbuf[0 : outlen.value] + + def finish(self): + outbuf = create_string_buffer(SM4_BLOCK_SIZE + SM4_GCM_MAX_TAG_SIZE) + outlen = c_size_t() + if self._encrypt == DO_ENCRYPT: + checked.sm4_gcm_encrypt_finish(byref(self), outbuf, byref(outlen)) + else: + checked.sm4_gcm_decrypt_finish(byref(self), outbuf, byref(outlen)) + return outbuf[: outlen.value] diff --git a/src/gmssl/_sm9.py b/src/gmssl/_sm9.py new file mode 100644 index 0000000..bd9333b --- /dev/null +++ b/src/gmssl/_sm9.py @@ -0,0 +1,362 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - SM9 identity-based cryptography + +""" +Internal module for SM9 identity-based cryptography. +This module should not be imported directly by users. +""" + +from ctypes import ( + Structure, + byref, + c_char_p, + c_size_t, + c_uint64, + create_string_buffer, +) + +from gmssl._constants import ( + DO_SIGN, + DO_VERIFY, + SM9_MAX_CIPHERTEXT_SIZE, + SM9_MAX_PLAINTEXT_SIZE, + SM9_SIGNATURE_SIZE, +) +from gmssl._lib import NativeError, StateError, gmssl + +# Import cross-platform PEM wrappers +from gmssl._pem_utils import ( + pem_export_encrypted_key, + pem_export_public_key, + pem_import_encrypted_key, + pem_import_public_key, +) +from gmssl._sm3 import Sm3 + +# ============================================================================= +# SM9 Base Types +# ============================================================================= + + +class sm9_bn_t(Structure): + _fields_ = [("d", c_uint64 * 8)] + + +class sm9_fp2_t(Structure): + _fields_ = [("d", sm9_bn_t * 2)] + + +class Sm9Point(Structure): + _fields_ = [("X", sm9_bn_t), ("Y", sm9_bn_t), ("Z", sm9_bn_t)] + + +class Sm9TwistPoint(Structure): + _fields_ = [("X", sm9_fp2_t), ("Y", sm9_fp2_t), ("Z", sm9_fp2_t)] + + +# ============================================================================= +# SM9 Encryption - User Key +# ============================================================================= + + +class Sm9EncKey(Structure): + _fields_ = [("Ppube", Sm9Point), ("de", Sm9TwistPoint)] + + def __init__(self, owner_id): + self._id = owner_id.encode("utf-8") + self._has_private_key = False + + def get_id(self): + return self._id + + def has_private_key(self): + return self._has_private_key + + def import_encrypted_private_key_info_pem(self, path, passwd): + passwd = passwd.encode("utf-8") + pem_import_encrypted_key(self, path, passwd, "sm9_enc_key_info_decrypt_from_pem") + self._has_private_key = True + + def export_encrypted_private_key_info_pem(self, path, passwd): + if not self._has_private_key: + raise TypeError("has no private key") + passwd = passwd.encode("utf-8") + pem_export_encrypted_key(self, path, passwd, "sm9_enc_key_info_encrypt_to_pem") + + def import_enc_master_public_key_pem(self, path): + pem_import_public_key(self, path, "sm9_enc_master_public_key_from_pem") + + def encrypt(self, plaintext): + if len(plaintext) > SM9_MAX_PLAINTEXT_SIZE: + raise ValueError("Invalid plaintext length") + outbuf = create_string_buffer(SM9_MAX_CIPHERTEXT_SIZE) + outlen = c_size_t() + if ( + gmssl.sm9_encrypt( + byref(self), + c_char_p(self._id), + c_size_t(len(self._id)), + plaintext, + c_size_t(len(plaintext)), + outbuf, + byref(outlen), + ) + != 1 + ): + raise NativeError("libgmssl inner error") + return outbuf[: outlen.value] + + def decrypt(self, ciphertext): + if not self._has_private_key: + raise TypeError("has no private key") + outbuf = create_string_buffer(SM9_MAX_PLAINTEXT_SIZE) + outlen = c_size_t() + if ( + gmssl.sm9_decrypt( + byref(self), + c_char_p(self._id), + c_size_t(len(self._id)), + ciphertext, + c_size_t(len(ciphertext)), + outbuf, + byref(outlen), + ) + != 1 + ): + raise NativeError("libgmssl inner error") + return outbuf[: outlen.value] + + +# ============================================================================= +# SM9 Encryption - Master Key +# ============================================================================= + + +class Sm9EncMasterKey(Structure): + _fields_ = [("Ppube", Sm9Point), ("ke", sm9_bn_t)] + + def __init__(self): + self._has_public_key = False + self._has_private_key = False + + def generate_master_key(self): + if gmssl.sm9_enc_master_key_generate(byref(self)) != 1: + raise NativeError("libgmssl inner error") + self._has_public_key = True + self._has_private_key = True + + def extract_key(self, identity): + if not self._has_private_key: + raise TypeError("has no master key") + key = Sm9EncKey(identity) + identity = identity.encode("utf-8") + if ( + gmssl.sm9_enc_master_key_extract_key( + byref(self), c_char_p(identity), c_size_t(len(identity)), byref(key) + ) + != 1 + ): + raise NativeError("libgmssl inner error") + key._has_private_key = True + return key + + def import_encrypted_master_key_info_pem(self, path, passwd): + passwd = passwd.encode("utf-8") + pem_import_encrypted_key(self, path, passwd, "sm9_enc_master_key_info_decrypt_from_pem") + self._has_public_key = True + self._has_private_key = True + + def export_encrypted_master_key_info_pem(self, path, passwd): + if not self._has_private_key: + raise TypeError("has no master key") + passwd = passwd.encode("utf-8") + pem_export_encrypted_key(self, path, passwd, "sm9_enc_master_key_info_encrypt_to_pem") + + def export_public_master_key_pem(self, path): + if not self._has_public_key: + raise TypeError("has no public master key") + pem_export_public_key(self, path, "sm9_enc_master_public_key_to_pem") + + def import_public_master_key_pem(self, path): + pem_import_public_key(self, path, "sm9_enc_master_public_key_from_pem") + self._has_public_key = True + self._has_private_key = False + + def encrypt(self, plaintext, to): + if not self._has_public_key: + raise TypeError("has no public master key") + if len(plaintext) > SM9_MAX_PLAINTEXT_SIZE: + raise ValueError("Invalid plaintext length") + to = to.encode("utf-8") + outbuf = create_string_buffer(SM9_MAX_CIPHERTEXT_SIZE) + outlen = c_size_t() + if ( + gmssl.sm9_encrypt( + byref(self), + c_char_p(to), + c_size_t(len(to)), + plaintext, + c_size_t(len(plaintext)), + outbuf, + byref(outlen), + ) + != 1 + ): + raise NativeError("libgmssl inner error") + return outbuf[: outlen.value] + + +# ============================================================================= +# SM9 Signature - User Key +# ============================================================================= + + +class Sm9SignKey(Structure): + _fields_ = [("Ppubs", Sm9TwistPoint), ("ds", Sm9Point)] + + def __init__(self, owner_id): + self._id = owner_id.encode("utf-8") + self._has_public_key = False + self._has_private_key = False + + def get_id(self): + return self._id + + def has_private_key(self): + return self._has_private_key + + def has_public_key(self): + return self._has_public_key + + def import_encrypted_private_key_info_pem(self, path, passwd): + passwd = passwd.encode("utf-8") + pem_import_encrypted_key(self, path, passwd, "sm9_sign_key_info_decrypt_from_pem") + self._has_public_key = True + self._has_private_key = True + + def export_encrypted_private_key_info_pem(self, path, passwd): + if not self._has_private_key: + raise TypeError("has no private key") + passwd = passwd.encode("utf-8") + pem_export_encrypted_key(self, path, passwd, "sm9_sign_key_info_encrypt_to_pem") + + def import_sign_master_public_key_pem(self, path): + pem_import_public_key(self, path, "sm9_sign_master_public_key_from_pem") + self._has_public_key = True + self._has_private_key = False + + +# ============================================================================= +# SM9 Signature - Master Key +# ============================================================================= + + +class Sm9SignMasterKey(Structure): + _fields_ = [("Ppubs", Sm9TwistPoint), ("ks", sm9_bn_t)] + + def __init__(self): + self._has_public_key = False + self._has_private_key = False + + def generate_master_key(self): + if gmssl.sm9_sign_master_key_generate(byref(self)) != 1: + raise NativeError("libgmssl inner error") + self._has_public_key = True + self._has_private_key = True + + def extract_key(self, identity): + if not self._has_private_key: + raise TypeError("has no master key") + key = Sm9SignKey(identity) + identity = identity.encode("utf-8") + if ( + gmssl.sm9_sign_master_key_extract_key( + byref(self), c_char_p(identity), c_size_t(len(identity)), byref(key) + ) + != 1 + ): + raise NativeError("libgmssl inner error") + key._has_public_key = True + key._has_private_key = True + return key + + def import_encrypted_master_key_info_pem(self, path, passwd): + passwd = passwd.encode("utf-8") + pem_import_encrypted_key(self, path, passwd, "sm9_sign_master_key_info_decrypt_from_pem") + self._has_public_key = True + self._has_private_key = True + + def export_encrypted_master_key_info_pem(self, path, passwd): + if not self._has_private_key: + raise TypeError("has no master key") + passwd = passwd.encode("utf-8") + pem_export_encrypted_key(self, path, passwd, "sm9_sign_master_key_info_encrypt_to_pem") + + def export_public_master_key_pem(self, path): + if not self._has_public_key: + raise TypeError("has no public master key") + pem_export_public_key(self, path, "sm9_sign_master_public_key_to_pem") + + def import_public_master_key_pem(self, path): + pem_import_public_key(self, path, "sm9_sign_master_public_key_from_pem") + self._has_public_key = True + self._has_private_key = False + + +# ============================================================================= +# SM9 Signature Context +# ============================================================================= + + +class Sm9Signature(Structure): + _fields_ = [("sm3", Sm3)] + + def __init__(self, sign=DO_SIGN): + if sign == DO_SIGN: + if gmssl.sm9_sign_init(byref(self)) != 1: + raise NativeError("libgmssl inner error") + else: + if gmssl.sm9_verify_init(byref(self)) != 1: + raise NativeError("libgmssl inner error") + self._sign = sign + + def update(self, data): + if self._sign == DO_SIGN: + if gmssl.sm9_sign_update(byref(self), data, c_size_t(len(data))) != 1: + raise NativeError("libgmssl inner error") + else: + if gmssl.sm9_verify_update(byref(self), data, c_size_t(len(data))) != 1: + raise NativeError("libgmssl inner error") + + def sign(self, sign_key): + if self._sign != DO_SIGN: + raise StateError("not sign state") + if not sign_key.has_private_key(): + raise TypeError("has no private key") + sig = create_string_buffer(SM9_SIGNATURE_SIZE) + siglen = c_size_t(SM9_SIGNATURE_SIZE) + if gmssl.sm9_sign_finish(byref(self), byref(sign_key), sig, byref(siglen)) != 1: + raise NativeError("libgmssl inner error") + return sig[: siglen.value] + + def verify(self, signature, public_master_key, signer_id): + if self._sign != DO_VERIFY: + raise StateError("not verify state") + signer_id = signer_id.encode("utf-8") + return ( + gmssl.sm9_verify_finish( + byref(self), + signature, + c_size_t(len(signature)), + byref(public_master_key), + c_char_p(signer_id), + c_size_t(len(signer_id)), + ) + == 1 + ) diff --git a/src/gmssl/_version.py b/src/gmssl/_version.py new file mode 100644 index 0000000..b34e05a --- /dev/null +++ b/src/gmssl/_version.py @@ -0,0 +1,35 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - Version information + +""" +Internal module for version information. +This module should not be imported directly by users. +""" + +from ctypes import c_char_p + +from gmssl._lib import gmssl + +# ============================================================================= +# Version Information +# ============================================================================= + +GMSSL_PYTHON_VERSION = "2.2.2" + + +def gmssl_library_version_num(): + return gmssl.gmssl_version_num() + + +def gmssl_library_version_str(): + gmssl.gmssl_version_str.restype = c_char_p + return gmssl.gmssl_version_str().decode("ascii") + + +GMSSL_LIBRARY_VERSION = gmssl_library_version_str() diff --git a/src/gmssl/_x509.py b/src/gmssl/_x509.py new file mode 100644 index 0000000..c05ebf1 --- /dev/null +++ b/src/gmssl/_x509.py @@ -0,0 +1,260 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - X.509 certificate parsing and validation + +""" +Internal module for X.509 certificate handling. +This module should not be imported directly by users. +""" + +import datetime +import sys +from ctypes import byref, c_char_p, c_int, c_size_t, c_ulong, c_void_p, create_string_buffer + +from gmssl._constants import _ASN1_TAG_SEQUENCE, _ASN1_TAG_SET, _ASN1_TAG_IA5String +from gmssl._file_utils import open_file +from gmssl._lib import NativeError, gmssl, libc +from gmssl._pem_utils import x509_cert_from_pem_windows, x509_cert_to_pem_windows +from gmssl._sm2 import Sm2Key + +# ============================================================================= +# X.509 Certificate Parsing Utilities +# ============================================================================= + + +def gmssl_parse_attr_type_and_value(name, d, dlen): + oid = c_int() + tag = c_int() + val = c_void_p() + vlen = c_size_t() + + if gmssl.x509_name_type_from_der(byref(oid), byref(d), byref(dlen)) != 1: + raise NativeError("libgmssl inner error") + gmssl.x509_name_type_name.restype = c_char_p + oid_name = gmssl.x509_name_type_name(oid).decode("ascii") + + if oid_name == "emailAddress": + if ( + gmssl.asn1_ia5_string_from_der_ex( + _ASN1_TAG_IA5String, byref(val), byref(vlen), byref(d), byref(dlen) + ) + != 1 + ): + raise NativeError("libgmssl inner error") + else: + if ( + gmssl.x509_directory_name_from_der( + byref(tag), byref(val), byref(vlen), byref(d), byref(dlen) + ) + != 1 + ): + raise NativeError("libgmssl inner error") + + if dlen.value != 0: + raise ValueError("invalid der encoding") + + value = create_string_buffer(vlen.value) + libc.memcpy(value, val, vlen) + + name[oid_name] = value.raw.decode("utf-8") + return True + + +def gmssl_parse_rdn(name, d, dlen): + v = c_void_p() + vlen = c_size_t() + + while dlen.value > 0: + if ( + gmssl.asn1_type_from_der( + _ASN1_TAG_SEQUENCE, byref(v), byref(vlen), byref(d), byref(dlen) + ) + != 1 + ): + raise NativeError("libgmssl inner error") + + if gmssl_parse_attr_type_and_value(name, v, vlen) != 1: + raise NativeError("libgmssl inner error") + + return True + + +# https://stacktuts.com/how-to-correctly-pass-pointer-to-pointer-into-dll-in-python-and-ctypes# +def gmssl_parse_name(name, d, dlen): + v = c_void_p() + vlen = c_size_t() + + while dlen.value > 0: + if ( + gmssl.asn1_nonempty_type_from_der( + c_int(_ASN1_TAG_SET), byref(v), byref(vlen), byref(d), byref(dlen) + ) + != 1 + ): + raise NativeError("libgmssl inner error") + gmssl_parse_rdn(name, v, vlen) + return True + + +class Validity: + def __init__(self, not_before, not_after): + self.not_before = datetime.datetime.fromtimestamp(not_before) + self.not_after = datetime.datetime.fromtimestamp(not_after) + + +class Sm2Certificate: + def import_pem(self, path): + if sys.platform == "win32": + # Windows: Use Python-based PEM reading to avoid FILE* issues + cert_data, cert_len = x509_cert_from_pem_windows(path) + self._cert = cert_data + else: + # Linux/macOS: Use FILE* for best performance + cert = c_void_p() + certlen = c_size_t() + if ( + gmssl.x509_cert_new_from_file(byref(cert), byref(certlen), path.encode("utf-8")) + != 1 + ): + raise NativeError("libgmssl inner error") + + self._cert = create_string_buffer(certlen.value) + libc.memcpy(self._cert, cert, certlen) + libc.free(cert) + + def get_raw(self): + return self._cert + + def export_pem(self, path): + if sys.platform == "win32": + # Windows: Use Python-based PEM writing to avoid FILE* issues + x509_cert_to_pem_windows(self._cert, len(self._cert), path) + else: + # Linux/macOS: Use FILE* for best performance + with open_file(path, "wb") as fp: + if gmssl.x509_cert_to_pem(self._cert, c_size_t(len(self._cert)), fp) != 1: + raise NativeError("libgmssl inner error") + + def get_serial_number(self): + serial_ptr = c_void_p() + serial_len = c_size_t() + + if ( + gmssl.x509_cert_get_issuer_and_serial_number( + self._cert, + c_size_t(len(self._cert)), + None, + None, + byref(serial_ptr), + byref(serial_len), + ) + != 1 + ): + raise NativeError("libgmssl inner error") + + serial = create_string_buffer(serial_len.value) + libc.memcpy(serial, serial_ptr, serial_len) + return serial.raw + + def get_issuer(self): + issuer_ptr = c_void_p() + issuer_len = c_size_t() + if ( + gmssl.x509_cert_get_issuer( + self._cert, + c_size_t(len(self._cert)), + byref(issuer_ptr), + byref(issuer_len), + ) + != 1 + ): + raise NativeError("libgmssl inner error") + issuer_raw = create_string_buffer(issuer_len.value) + libc.memcpy(issuer_raw, issuer_ptr, issuer_len) + + issuer = {"raw_data": issuer_raw.raw} + gmssl_parse_name(issuer, issuer_ptr, issuer_len) + return issuer + + def get_subject(self): + subject_ptr = c_void_p() + subject_len = c_size_t() + if ( + gmssl.x509_cert_get_subject( + self._cert, + c_size_t(len(self._cert)), + byref(subject_ptr), + byref(subject_len), + ) + != 1 + ): + raise NativeError("libgmssl inner error") + subject_raw = create_string_buffer(subject_len.value) + libc.memcpy(subject_raw, subject_ptr, subject_len) + + subject = {"raw_data": subject_raw.raw} + gmssl_parse_name(subject, subject_ptr, subject_len) + return subject + + def get_subject_public_key(self): + public_key = Sm2Key() + gmssl.x509_cert_get_subject_public_key( + self._cert, c_size_t(len(self._cert)), byref(public_key) + ) + public_key._has_private_key = False + public_key._has_public_key = True + return public_key + + def get_validity(self): + not_before = c_ulong() + not_after = c_ulong() + if ( + gmssl.x509_cert_get_details( + self._cert, + c_size_t(len(self._cert)), + None, + None, + None, + None, + None, + None, + byref(not_before), + byref(not_after), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ) + != 1 + ): + raise NativeError("libgmssl inner error") + return Validity(not_before.value, not_after.value) + + def verify_by_ca_certificate(self, cacert, sm2_id): + cacert_raw = cacert.get_raw() + sm2_id = sm2_id.encode("utf-8") + + return ( + gmssl.x509_cert_verify_by_ca_cert( + self._cert, + c_size_t(len(self._cert)), + cacert_raw, + c_size_t(len(cacert_raw)), + c_char_p(sm2_id), + c_size_t(len(sm2_id)), + ) + == 1 + ) diff --git a/src/gmssl/_zuc.py b/src/gmssl/_zuc.py new file mode 100644 index 0000000..62d27fa --- /dev/null +++ b/src/gmssl/_zuc.py @@ -0,0 +1,53 @@ +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# GmSSL-Python - ZUC stream cipher + +""" +Internal module for ZUC stream cipher. +This module should not be imported directly by users. +""" + +from ctypes import Structure, byref, c_size_t, c_uint8, c_uint32, create_string_buffer + +from gmssl._constants import ZUC_BLOCK_SIZE, ZUC_IV_SIZE, ZUC_KEY_SIZE +from gmssl._lib import checked + +# ============================================================================= +# ZUC Stream Cipher +# ============================================================================= + + +class ZucState(Structure): + _fields_ = [("LFSR", c_uint32 * 16), ("R1", c_uint32), ("R2", c_uint32)] + + +class Zuc(Structure): + _fields_ = [ + ("zuc_state", ZucState), + ("block", c_uint8 * 4), + ("block_nbytes", c_size_t), + ] + + def __init__(self, key, iv): + if len(key) != ZUC_KEY_SIZE: + raise ValueError("Invalid key length") + if len(iv) != ZUC_IV_SIZE: + raise ValueError("Invalid IV size") + checked.zuc_encrypt_init(byref(self), key, iv) + + def update(self, data): + outbuf = create_string_buffer(len(data) + ZUC_BLOCK_SIZE) + outlen = c_size_t() + checked.zuc_encrypt_update(byref(self), data, c_size_t(len(data)), outbuf, byref(outlen)) + return outbuf[0 : outlen.value] + + def finish(self): + outbuf = create_string_buffer(ZUC_BLOCK_SIZE) + outlen = c_size_t() + checked.zuc_encrypt_finish(byref(self), outbuf, byref(outlen)) + return outbuf[: outlen.value] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..c7661b8 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# GmSSL Python binding tests diff --git a/tests/test_additional_methods.py b/tests/test_additional_methods.py new file mode 100644 index 0000000..b6ed67a --- /dev/null +++ b/tests/test_additional_methods.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python +# +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 + +""" +Additional method tests for GmSSL Python binding. + +Tests for methods that were not covered in the main test suite, +including import/export operations, get_id methods, and other utilities. +""" + +import tempfile +from pathlib import Path + +import pytest + +from gmssl import ( + DO_SIGN, + DO_VERIFY, + SM9_MAX_PLAINTEXT_SIZE, + Sm2Certificate, + Sm9EncKey, + Sm9EncMasterKey, + Sm9Signature, + Sm9SignKey, + Sm9SignMasterKey, +) + +# ============================================================================= +# SM9 Encryption Key Tests +# ============================================================================= + + +def test_sm9_enc_key_get_id(): + """SM9 EncKey should return correct ID.""" + key = Sm9EncKey("Alice") + assert key.get_id() == b"Alice" + + +def test_sm9_enc_key_import_export(): + """SM9 EncKey should support import/export of encrypted private key.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + enc_msk_pem = tmpdir / "enc_msk.pem" + enc_key_pem = tmpdir / "enc_key.pem" + + # Generate master key and extract user key + master_key = Sm9EncMasterKey() + master_key.generate_master_key() + master_key.export_encrypted_master_key_info_pem(str(enc_msk_pem), "password") + + key = master_key.extract_key("Alice") + + # Export user key + key.export_encrypted_private_key_info_pem(str(enc_key_pem), "userpass") + + # Import user key + key2 = Sm9EncKey("Alice") + key2.import_encrypted_private_key_info_pem(str(enc_key_pem), "userpass") + + # Test decryption with imported key + ciphertext = master_key.encrypt(b"test message", "Alice") + plaintext = key2.decrypt(ciphertext) + assert plaintext == b"test message" + + +def test_sm9_enc_key_has_private_key(): + """SM9 EncKey should track private key status.""" + # New key without private key + key = Sm9EncKey("Alice") + assert not key.has_private_key() + + # Extract key from master key + master_key = Sm9EncMasterKey() + master_key.generate_master_key() + key = master_key.extract_key("Alice") + assert key.has_private_key() + + +# ============================================================================= +# SM9 Signature Key Tests +# ============================================================================= + + +def test_sm9_sign_key_get_id(): + """SM9 SignKey should return correct ID.""" + key = Sm9SignKey("Bob") + assert key.get_id() == b"Bob" + + +def test_sm9_sign_key_import_export(): + """SM9 SignKey should support import/export of encrypted private key.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + sign_msk_pem = tmpdir / "sign_msk.pem" + sign_key_pem = tmpdir / "sign_key.pem" + + # Generate master key and extract user key + master_key = Sm9SignMasterKey() + master_key.generate_master_key() + master_key.export_encrypted_master_key_info_pem(str(sign_msk_pem), "password") + + key = master_key.extract_key("Bob") + + # Export user key + key.export_encrypted_private_key_info_pem(str(sign_key_pem), "userpass") + + # Import user key + key2 = Sm9SignKey("Bob") + key2.import_encrypted_private_key_info_pem(str(sign_key_pem), "userpass") + + # Test signing with imported key + sign = Sm9Signature(DO_SIGN) + sign.update(b"message") + sig = sign.sign(key2) + + # Verify signature + verify = Sm9Signature(DO_VERIFY) + verify.update(b"message") + assert verify.verify(sig, master_key, "Bob") + + +def test_sm9_sign_key_has_private_key(): + """SM9 SignKey should track private key status.""" + # New key without private key + key = Sm9SignKey("Bob") + assert not key.has_private_key() + + # Extract key from master key + master_key = Sm9SignMasterKey() + master_key.generate_master_key() + key = master_key.extract_key("Bob") + assert key.has_private_key() + + +def test_sm9_sign_key_has_public_key(): + """SM9 SignKey should track public key status.""" + # New key without public key + key = Sm9SignKey("Bob") + assert not key.has_public_key() + + # Extract key from master key + master_key = Sm9SignMasterKey() + master_key.generate_master_key() + key = master_key.extract_key("Bob") + assert key.has_public_key() + + +# ============================================================================= +# SM9 User Key - Master Public Key Import Tests +# ============================================================================= + + +def test_sm9_enc_key_import_master_public_key(): + """SM9 EncKey should support importing master public key.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + enc_mpk_pem = tmpdir / "enc_mpk.pem" + + # Generate and export master public key + master_key = Sm9EncMasterKey() + master_key.generate_master_key() + master_key.export_public_master_key_pem(str(enc_mpk_pem)) + + # Import master public key into user key + user_key = Sm9EncKey("Alice") + user_key.import_enc_master_public_key_pem(str(enc_mpk_pem)) + + # Should be able to encrypt with the imported public key + plaintext = b"test message" + ciphertext = user_key.encrypt(plaintext) + assert len(ciphertext) > 0 + + +def test_sm9_sign_key_import_master_public_key(): + """SM9 SignKey should support importing master public key.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + sign_mpk_pem = tmpdir / "sign_mpk.pem" + + # Generate and export master public key + master_key = Sm9SignMasterKey() + master_key.generate_master_key() + master_key.export_public_master_key_pem(str(sign_mpk_pem)) + + # Import master public key into user key + user_key = Sm9SignKey("Bob") + user_key.import_sign_master_public_key_pem(str(sign_mpk_pem)) + + # Should have public key but no private key + assert user_key.has_public_key() + assert not user_key.has_private_key() + + +# ============================================================================= +# SM9 EncKey Direct Encryption Test +# ============================================================================= + + +def test_sm9_enc_key_encrypt_with_master_public_key(): + """SM9 EncKey can encrypt after importing master public key.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + enc_mpk_pem = tmpdir / "enc_mpk.pem" + enc_msk_pem = tmpdir / "enc_msk.pem" + + # Generate master key + master_key = Sm9EncMasterKey() + master_key.generate_master_key() + master_key.export_public_master_key_pem(str(enc_mpk_pem)) + master_key.export_encrypted_master_key_info_pem(str(enc_msk_pem), "password") + + # Create user key with master public key + alice_key = Sm9EncKey("Alice") + alice_key.import_enc_master_public_key_pem(str(enc_mpk_pem)) + + # Encrypt using user key (which has master public key) + plaintext = b"secret message" + ciphertext = alice_key.encrypt(plaintext) + + # Decrypt using extracted private key + master = Sm9EncMasterKey() + master.import_encrypted_master_key_info_pem(str(enc_msk_pem), "password") + alice_private = master.extract_key("Alice") + decrypted = alice_private.decrypt(ciphertext) + + assert decrypted == plaintext + + +def test_sm9_enc_key_encrypt_plaintext_too_long(): + """SM9 EncKey encrypt should reject plaintext that is too long.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + enc_mpk_pem = tmpdir / "enc_mpk.pem" + + master_key = Sm9EncMasterKey() + master_key.generate_master_key() + master_key.export_public_master_key_pem(str(enc_mpk_pem)) + + alice_key = Sm9EncKey("Alice") + alice_key.import_enc_master_public_key_pem(str(enc_mpk_pem)) + + # Try to encrypt plaintext that's too long + with pytest.raises(ValueError, match="Invalid plaintext length"): + alice_key.encrypt(b"x" * (SM9_MAX_PLAINTEXT_SIZE + 1)) + + +# ============================================================================= +# SM2 Certificate Tests +# ============================================================================= + + +def test_sm2_certificate_export_pem(): + """SM2 Certificate should support export to PEM.""" + cert_txt = """\ +-----BEGIN CERTIFICATE----- +MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQG +EwJDTjEOMAwGA1UECgwFTlJDQUMxDzANBgNVBAMMBlJPT1RDQTAeFw0xMjA3MTQw +MzExNTlaFw00MjA3MDcwMzExNTlaMC4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQKDAVO +UkNBQzEPMA0GA1UEAwwGUk9PVENBMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE +MPCca6pmgcchsTf2UnBeL9rtp4nw+itk1Kzrmbnqo05lUwkwlWK+4OIrtFdAqnRT +V7Q9v1htkv42TsIutzd126NdMFswHwYDVR0jBBgwFoAUTDKxl9kzG8SmBcHG5Yti +W/CXdlgwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFEwysZfZ +MxvEpgXBxuWLYlvwl3ZYMAwGCCqBHM9VAYN1BQADSAAwRQIgG1bSLeOXp3oB8H7b +53W+CKOPl2PknmWEq/lMhtn25HkCIQDaHDgWxWFtnCrBjH16/W3Ezn7/U/Vjo5xI +pDoiVhsLwg== +-----END CERTIFICATE-----""" + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + import_pem = tmpdir / "import.pem" + export_pem = tmpdir / "export.pem" + + # Write certificate to file + import_pem.write_text(cert_txt) + + # Import certificate + cert = Sm2Certificate() + cert.import_pem(str(import_pem)) + + # Export certificate + cert.export_pem(str(export_pem)) + + # Verify exported file exists and has content + assert export_pem.exists() + exported_content = export_pem.read_text() + assert "BEGIN CERTIFICATE" in exported_content + assert "END CERTIFICATE" in exported_content + + +def test_sm2_certificate_get_raw(): + """SM2 Certificate should return raw DER data.""" + cert_txt = """\ +-----BEGIN CERTIFICATE----- +MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQG +EwJDTjEOMAwGA1UECgwFTlJDQUMxDzANBgNVBAMMBlJPT1RDQTAeFw0xMjA3MTQw +MzExNTlaFw00MjA3MDcwMzExNTlaMC4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQKDAVO +UkNBQzEPMA0GA1UEAwwGUk9PVENBMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE +MPCca6pmgcchsTf2UnBeL9rtp4nw+itk1Kzrmbnqo05lUwkwlWK+4OIrtFdAqnRT +V7Q9v1htkv42TsIutzd126NdMFswHwYDVR0jBBgwFoAUTDKxl9kzG8SmBcHG5Yti +W/CXdlgwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFEwysZfZ +MxvEpgXBxuWLYlvwl3ZYMAwGCCqBHM9VAYN1BQADSAAwRQIgG1bSLeOXp3oB8H7b +53W+CKOPl2PknmWEq/lMhtn25HkCIQDaHDgWxWFtnCrBjH16/W3Ezn7/U/Vjo5xI +pDoiVhsLwg== +-----END CERTIFICATE-----""" + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + cert_pem = tmpdir / "cert.pem" + cert_pem.write_text(cert_txt) + + # Import certificate + cert = Sm2Certificate() + cert.import_pem(str(cert_pem)) + + # Get raw DER data + raw_data = cert.get_raw() + + # Raw data should be a ctypes array (not bytes) and non-empty + # The _cert field is a ctypes.c_char_Array + assert hasattr(raw_data, "__len__") + assert len(raw_data) > 0 + + +# ============================================================================= +# SM9 Master Key Status Tests +# ============================================================================= + + +def test_sm9_enc_master_key_has_keys(): + """SM9 EncMasterKey should track key status.""" + master_key = Sm9EncMasterKey() + + # Initially no keys + assert not master_key._has_public_key + assert not master_key._has_private_key + + # After generation + master_key.generate_master_key() + assert master_key._has_public_key + assert master_key._has_private_key + + +def test_sm9_sign_master_key_has_keys(): + """SM9 SignMasterKey should track key status.""" + master_key = Sm9SignMasterKey() + + # Initially no keys + assert not master_key._has_public_key + assert not master_key._has_private_key + + # After generation + master_key.generate_master_key() + assert master_key._has_public_key + assert master_key._has_private_key + + +def test_sm9_enc_master_key_import_public_only(): + """SM9 EncMasterKey should support importing public key only.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + enc_mpk_pem = tmpdir / "enc_mpk.pem" + + # Generate and export public master key + master_key = Sm9EncMasterKey() + master_key.generate_master_key() + master_key.export_public_master_key_pem(str(enc_mpk_pem)) + + # Import public key only + master_pub = Sm9EncMasterKey() + master_pub.import_public_master_key_pem(str(enc_mpk_pem)) + + # Should have public key but not private key + assert master_pub._has_public_key + assert not master_pub._has_private_key + + # Should be able to encrypt + ciphertext = master_pub.encrypt(b"test", "Alice") + assert len(ciphertext) > 0 + + +def test_sm9_sign_master_key_import_public_only(): + """SM9 SignMasterKey should support importing public key only.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + sign_mpk_pem = tmpdir / "sign_mpk.pem" + + # Generate and export public master key + master_key = Sm9SignMasterKey() + master_key.generate_master_key() + master_key.export_public_master_key_pem(str(sign_mpk_pem)) + + # Import public key only + master_pub = Sm9SignMasterKey() + master_pub.import_public_master_key_pem(str(sign_mpk_pem)) + + # Should have public key but not private key + assert master_pub._has_public_key + assert not master_pub._has_private_key + + +# ============================================================================= +# Integration Tests +# ============================================================================= + + +def test_sm9_enc_full_workflow_with_key_export(): + """Test complete SM9 encryption workflow with key export/import.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + enc_msk_pem = tmpdir / "enc_msk.pem" + enc_mpk_pem = tmpdir / "enc_mpk.pem" + enc_key_pem = tmpdir / "enc_key.pem" + + # 1. Generate master key + master_key = Sm9EncMasterKey() + master_key.generate_master_key() + master_key.export_encrypted_master_key_info_pem(str(enc_msk_pem), "masterpass") + master_key.export_public_master_key_pem(str(enc_mpk_pem)) + + # 2. Extract and export user key + user_key = master_key.extract_key("Alice") + user_key.export_encrypted_private_key_info_pem(str(enc_key_pem), "userpass") + + # 3. Encrypt with public master key + master_pub = Sm9EncMasterKey() + master_pub.import_public_master_key_pem(str(enc_mpk_pem)) + plaintext = b"Secret message for Alice" + ciphertext = master_pub.encrypt(plaintext, "Alice") + + # 4. Decrypt with imported user key + user_key2 = Sm9EncKey("Alice") + user_key2.import_encrypted_private_key_info_pem(str(enc_key_pem), "userpass") + decrypted = user_key2.decrypt(ciphertext) + + assert decrypted == plaintext + + +def test_sm9_sign_full_workflow_with_key_export(): + """Test complete SM9 signature workflow with key export/import.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + sign_msk_pem = tmpdir / "sign_msk.pem" + sign_mpk_pem = tmpdir / "sign_mpk.pem" + sign_key_pem = tmpdir / "sign_key.pem" + + # 1. Generate master key + master_key = Sm9SignMasterKey() + master_key.generate_master_key() + master_key.export_encrypted_master_key_info_pem(str(sign_msk_pem), "masterpass") + master_key.export_public_master_key_pem(str(sign_mpk_pem)) + + # 2. Extract and export user key + user_key = master_key.extract_key("Bob") + user_key.export_encrypted_private_key_info_pem(str(sign_key_pem), "userpass") + + # 3. Sign with imported user key + user_key2 = Sm9SignKey("Bob") + user_key2.import_encrypted_private_key_info_pem(str(sign_key_pem), "userpass") + + sign = Sm9Signature(DO_SIGN) + sign.update(b"Message to sign") + sig = sign.sign(user_key2) + + # 4. Verify with imported public master key + master_pub = Sm9SignMasterKey() + master_pub.import_public_master_key_pem(str(sign_mpk_pem)) + + verify = Sm9Signature(DO_VERIFY) + verify.update(b"Message to sign") + assert verify.verify(sig, master_pub, "Bob") diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py new file mode 100644 index 0000000..fdbc6e7 --- /dev/null +++ b/tests/test_edge_cases.py @@ -0,0 +1,467 @@ +#!/usr/bin/env python +# +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 + +""" +Edge case and boundary condition tests for GmSSL Python binding. + +Tests boundary values, empty data, maximum sizes, and other edge cases +to ensure robust handling of unusual inputs. +""" + +from gmssl import ( + DO_DECRYPT, + DO_ENCRYPT, + DO_SIGN, + DO_VERIFY, + SM2_DEFAULT_ID, + SM2_MAX_PLAINTEXT_SIZE, + SM2_MIN_PLAINTEXT_SIZE, + SM3_HMAC_MAX_KEY_SIZE, + SM3_HMAC_MIN_KEY_SIZE, + SM3_PBKDF2_MAX_KEY_SIZE, + SM3_PBKDF2_MAX_SALT_SIZE, + SM3_PBKDF2_MIN_ITER, + SM4_GCM_MAX_IV_SIZE, + SM4_GCM_MAX_TAG_SIZE, + SM4_GCM_MIN_IV_SIZE, + SM9_MAX_PLAINTEXT_SIZE, + Sm2Key, + Sm2Signature, + Sm3, + Sm3Hmac, + Sm4, + Sm4Cbc, + Sm4Ctr, + Sm4Gcm, + Sm9EncMasterKey, + Sm9Signature, + Sm9SignMasterKey, + Zuc, + rand_bytes, + sm3_pbkdf2, +) + +# ============================================================================= +# SM3 Edge Cases +# ============================================================================= + + +def test_sm3_empty_data(): + """SM3 should handle empty data.""" + sm3 = Sm3() + sm3.update(b"") + dgst = sm3.digest() + # SM3 of empty string has a known value + assert len(dgst) == 32 + assert dgst.hex() == "1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b" + + +def test_sm3_multiple_updates(): + """SM3 should handle multiple update calls.""" + sm3_single = Sm3() + sm3_single.update(b"abcdefghijklmnopqrstuvwxyz") + dgst_single = sm3_single.digest() + + sm3_multi = Sm3() + sm3_multi.update(b"abc") + sm3_multi.update(b"def") + sm3_multi.update(b"ghi") + sm3_multi.update(b"jklmnopqrstuvwxyz") + dgst_multi = sm3_multi.digest() + + assert dgst_single == dgst_multi + + +def test_sm3_reset_multiple_times(): + """SM3 reset should work multiple times.""" + sm3 = Sm3() + + for _ in range(5): + sm3.update(b"test") + dgst = sm3.digest() + assert len(dgst) == 32 + sm3.reset() + + +def test_sm3_hmac_min_key_size(): + """SM3-HMAC should accept minimum key size.""" + key = b"x" * SM3_HMAC_MIN_KEY_SIZE + sm3_hmac = Sm3Hmac(key) + sm3_hmac.update(b"test") + mac = sm3_hmac.generate_mac() + assert len(mac) == 32 + + +def test_sm3_hmac_max_key_size(): + """SM3-HMAC should accept maximum key size.""" + key = b"x" * SM3_HMAC_MAX_KEY_SIZE + sm3_hmac = Sm3Hmac(key) + sm3_hmac.update(b"test") + mac = sm3_hmac.generate_mac() + assert len(mac) == 32 + + +def test_sm3_hmac_reset(): + """SM3-HMAC reset should allow key change.""" + key1 = b"1234567812345678" + key2 = b"8765432187654321" + + sm3_hmac = Sm3Hmac(key1) + sm3_hmac.update(b"abc") + mac1 = sm3_hmac.generate_mac() + + sm3_hmac.reset(key2) + sm3_hmac.update(b"abc") + mac2 = sm3_hmac.generate_mac() + + # Different keys should produce different MACs + assert mac1 != mac2 + + +def test_sm3_hmac_empty_data(): + """SM3-HMAC should handle empty data.""" + key = b"1234567812345678" + sm3_hmac = Sm3Hmac(key) + sm3_hmac.update(b"") + mac = sm3_hmac.generate_mac() + assert len(mac) == 32 + + +def test_sm3_pbkdf2_min_iterator(): + """SM3-PBKDF2 should accept minimum iterator count.""" + key = sm3_pbkdf2("password", b"salt", SM3_PBKDF2_MIN_ITER, 32) + assert len(key) == 32 + + +def test_sm3_pbkdf2_max_salt_size(): + """SM3-PBKDF2 should accept maximum salt size.""" + salt = b"x" * SM3_PBKDF2_MAX_SALT_SIZE + key = sm3_pbkdf2("password", salt, SM3_PBKDF2_MIN_ITER, 32) + assert len(key) == 32 + + +def test_sm3_pbkdf2_max_key_size(): + """SM3-PBKDF2 should accept maximum key size.""" + key = sm3_pbkdf2("password", b"salt", SM3_PBKDF2_MIN_ITER, SM3_PBKDF2_MAX_KEY_SIZE) + assert len(key) == SM3_PBKDF2_MAX_KEY_SIZE + + +def test_sm3_pbkdf2_empty_password(): + """SM3-PBKDF2 should handle empty password.""" + key = sm3_pbkdf2("", b"salt", SM3_PBKDF2_MIN_ITER, 32) + assert len(key) == 32 + + +# ============================================================================= +# SM4 Edge Cases +# ============================================================================= + + +def test_sm4_decrypt_method(): + """SM4 decrypt method should provide clearer API for decryption.""" + key = b"1234567812345678" + plaintext = b"block of message" + + # Encrypt + sm4_enc = Sm4(key, DO_ENCRYPT) + ciphertext = sm4_enc.encrypt(plaintext) + + # Decrypt using the decrypt method (clearer than using encrypt method) + sm4_dec = Sm4(key, DO_DECRYPT) + decrypted = sm4_dec.decrypt(ciphertext) + + assert decrypted == plaintext + + +def test_sm4_cbc_empty_data(): + """SM4-CBC should handle empty data.""" + key = b"1234567812345678" + iv = b"1234567812345678" + + sm4_cbc = Sm4Cbc(key, iv, DO_ENCRYPT) + ciphertext = sm4_cbc.update(b"") + ciphertext += sm4_cbc.finish() + + # Empty plaintext should produce padding block + assert len(ciphertext) == 16 + + +def test_sm4_cbc_multiple_updates(): + """SM4-CBC should handle multiple update calls.""" + key = b"1234567812345678" + iv = b"1234567812345678" + plaintext = b"This is a longer message that will be split" + + # Single update + sm4_single = Sm4Cbc(key, iv, DO_ENCRYPT) + cipher_single = sm4_single.update(plaintext) + cipher_single += sm4_single.finish() + + # Multiple updates + sm4_multi = Sm4Cbc(key, iv, DO_ENCRYPT) + cipher_multi = sm4_multi.update(plaintext[:10]) + cipher_multi += sm4_multi.update(plaintext[10:20]) + cipher_multi += sm4_multi.update(plaintext[20:]) + cipher_multi += sm4_multi.finish() + + assert cipher_single == cipher_multi + + +def test_sm4_ctr_empty_data(): + """SM4-CTR should handle empty data.""" + key = b"1234567812345678" + iv = b"1234567812345678" + + sm4_ctr = Sm4Ctr(key, iv) + ciphertext = sm4_ctr.update(b"") + ciphertext += sm4_ctr.finish() + + assert len(ciphertext) == 0 + + +def test_sm4_gcm_min_iv_size(): + """SM4-GCM should accept minimum IV size.""" + key = b"1234567812345678" + iv = b"x" * SM4_GCM_MIN_IV_SIZE + aad = b"aad" + + sm4_gcm = Sm4Gcm(key, iv, aad, 16, DO_ENCRYPT) + ciphertext = sm4_gcm.update(b"plaintext") + ciphertext += sm4_gcm.finish() + + assert len(ciphertext) > 0 + + +def test_sm4_gcm_max_iv_size(): + """SM4-GCM should accept maximum IV size.""" + key = b"1234567812345678" + iv = b"x" * SM4_GCM_MAX_IV_SIZE + aad = b"aad" + + sm4_gcm = Sm4Gcm(key, iv, aad, 16, DO_ENCRYPT) + ciphertext = sm4_gcm.update(b"plaintext") + ciphertext += sm4_gcm.finish() + + assert len(ciphertext) > 0 + + +def test_sm4_gcm_max_tag_size(): + """SM4-GCM should accept maximum tag size.""" + key = b"1234567812345678" + iv = b"0123456789ab" + aad = b"aad" + + sm4_gcm = Sm4Gcm(key, iv, aad, SM4_GCM_MAX_TAG_SIZE, DO_ENCRYPT) + ciphertext = sm4_gcm.update(b"plaintext") + ciphertext += sm4_gcm.finish() + + assert len(ciphertext) > 0 + + +def test_sm4_gcm_empty_aad(): + """SM4-GCM should handle empty AAD.""" + key = b"1234567812345678" + iv = b"0123456789ab" + aad = b"" + + sm4_gcm = Sm4Gcm(key, iv, aad, 16, DO_ENCRYPT) + ciphertext = sm4_gcm.update(b"plaintext") + ciphertext += sm4_gcm.finish() + + sm4_gcm_dec = Sm4Gcm(key, iv, aad, 16, DO_DECRYPT) + decrypted = sm4_gcm_dec.update(ciphertext) + decrypted += sm4_gcm_dec.finish() + + assert decrypted == b"plaintext" + + +# ============================================================================= +# ZUC Edge Cases +# ============================================================================= + + +def test_zuc_empty_data(): + """ZUC should handle empty data.""" + key = b"1234567812345678" + iv = b"1234567812345678" + + zuc = Zuc(key, iv) + ciphertext = zuc.update(b"") + ciphertext += zuc.finish() + + assert len(ciphertext) == 0 + + +def test_zuc_multiple_updates(): + """ZUC should handle multiple update calls.""" + key = b"1234567812345678" + iv = b"1234567812345678" + plaintext = b"This is a test message" + + # Single update + zuc_single = Zuc(key, iv) + cipher_single = zuc_single.update(plaintext) + cipher_single += zuc_single.finish() + + # Multiple updates + zuc_multi = Zuc(key, iv) + cipher_multi = zuc_multi.update(plaintext[:5]) + cipher_multi += zuc_multi.update(plaintext[5:10]) + cipher_multi += zuc_multi.update(plaintext[10:]) + cipher_multi += zuc_multi.finish() + + assert cipher_single == cipher_multi + + +# ============================================================================= +# SM2 Edge Cases +# ============================================================================= + + +def test_sm2_encrypt_min_plaintext(): + """SM2 should handle minimum plaintext size.""" + sm2 = Sm2Key() + sm2.generate_key() + + plaintext = b"x" * SM2_MIN_PLAINTEXT_SIZE + ciphertext = sm2.encrypt(plaintext) + decrypted = sm2.decrypt(ciphertext) + + assert decrypted == plaintext + + +def test_sm2_encrypt_max_plaintext(): + """SM2 should handle maximum plaintext size.""" + sm2 = Sm2Key() + sm2.generate_key() + + plaintext = b"x" * SM2_MAX_PLAINTEXT_SIZE + ciphertext = sm2.encrypt(plaintext) + decrypted = sm2.decrypt(ciphertext) + + assert decrypted == plaintext + + +def test_sm2_signature_multiple_updates(): + """SM2 Signature should handle multiple update calls.""" + sm2 = Sm2Key() + sm2.generate_key() + + message = b"This is a long message to be signed" + + # Single update + sign_single = Sm2Signature(sm2, SM2_DEFAULT_ID, DO_SIGN) + sign_single.update(message) + sig_single = sign_single.sign() + + # Multiple updates + sign_multi = Sm2Signature(sm2, SM2_DEFAULT_ID, DO_SIGN) + sign_multi.update(message[:10]) + sign_multi.update(message[10:20]) + sign_multi.update(message[20:]) + sig_multi = sign_multi.sign() + + # Both signatures should be valid + verify = Sm2Signature(sm2, SM2_DEFAULT_ID, DO_VERIFY) + verify.update(message) + assert verify.verify(sig_single) + + verify2 = Sm2Signature(sm2, SM2_DEFAULT_ID, DO_VERIFY) + verify2.update(message) + assert verify2.verify(sig_multi) + + +def test_sm2_custom_id(): + """SM2 should work with custom signer ID.""" + sm2 = Sm2Key() + sm2.generate_key() + + custom_id = "custom_user@example.com" + + # Compute Z with custom ID + z = sm2.compute_z(custom_id) + assert len(z) == 32 + + # Sign and verify with custom ID + sign = Sm2Signature(sm2, custom_id, DO_SIGN) + sign.update(b"message") + sig = sign.sign() + + verify = Sm2Signature(sm2, custom_id, DO_VERIFY) + verify.update(b"message") + assert verify.verify(sig) + + +# ============================================================================= +# SM9 Edge Cases +# ============================================================================= + + +def test_sm9_encrypt_max_plaintext(): + """SM9 should handle maximum plaintext size.""" + master_key = Sm9EncMasterKey() + master_key.generate_master_key() + + plaintext = b"x" * SM9_MAX_PLAINTEXT_SIZE + ciphertext = master_key.encrypt(plaintext, "Alice") + + key = master_key.extract_key("Alice") + decrypted = key.decrypt(ciphertext) + + assert decrypted == plaintext + + +def test_sm9_sign_multiple_updates(): + """SM9 Signature should handle multiple update calls.""" + master_key = Sm9SignMasterKey() + master_key.generate_master_key() + + key = master_key.extract_key("Alice") + message = b"This is a long message to be signed" + + # Single update + sign_single = Sm9Signature(DO_SIGN) + sign_single.update(message) + sig_single = sign_single.sign(key) + + # Multiple updates + sign_multi = Sm9Signature(DO_SIGN) + sign_multi.update(message[:10]) + sign_multi.update(message[10:20]) + sign_multi.update(message[20:]) + sig_multi = sign_multi.sign(key) + + # Both signatures should be valid + verify = Sm9Signature(DO_VERIFY) + verify.update(message) + assert verify.verify(sig_single, master_key, "Alice") + + verify2 = Sm9Signature(DO_VERIFY) + verify2.update(message) + assert verify2.verify(sig_multi, master_key, "Alice") + + +# ============================================================================= +# Random Number Edge Cases +# ============================================================================= + + +def test_rand_bytes_various_sizes(): + """rand_bytes should work with various sizes.""" + for size in [1, 16, 32, 64, 128, 256, 1024]: + data = rand_bytes(size) + assert len(data) == size + + +def test_rand_bytes_uniqueness(): + """rand_bytes should generate unique values.""" + # Generate multiple random values and check they're different + values = [rand_bytes(32) for _ in range(10)] + # All values should be unique + assert len(set(values)) == len(values) diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 0000000..d833379 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python +# +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 + +""" +Error handling and exception tests for GmSSL Python binding. + +Tests various error conditions and exception handling to ensure +robust error reporting and proper validation. +""" + +import pytest + +from gmssl import ( + DO_DECRYPT, + DO_ENCRYPT, + DO_SIGN, + DO_VERIFY, + SM2_DEFAULT_ID, + SM2_MAX_PLAINTEXT_SIZE, + Sm2Key, + Sm2Signature, + Sm3Hmac, + Sm4, + Sm4Cbc, + Sm4Ctr, + Sm4Gcm, + StateError, + Zuc, + sm3_pbkdf2, +) + +# ============================================================================= +# SM3 Error Tests +# ============================================================================= + + +def test_sm3_hmac_invalid_key_too_short(): + """SM3-HMAC should reject keys that are too short.""" + with pytest.raises(ValueError, match="Invalid SM3 HMAC key length"): + Sm3Hmac(b"short") + + +def test_sm3_hmac_invalid_key_too_long(): + """SM3-HMAC should reject keys that are too long.""" + # SM3_HMAC_MAX_KEY_SIZE is 64 + with pytest.raises(ValueError, match="Invalid SM3 HMAC key length"): + Sm3Hmac(b"x" * 65) + + +def test_sm3_pbkdf2_invalid_salt_too_long(): + """SM3-PBKDF2 should reject salt that is too long.""" + # SM3_PBKDF2_MAX_SALT_SIZE is 64 + with pytest.raises(ValueError, match="Invalid salt length"): + sm3_pbkdf2("password", b"x" * 65, 10000, 32) + + +def test_sm3_pbkdf2_invalid_iterator_too_small(): + """SM3-PBKDF2 should reject iterator count that is too small.""" + # SM3_PBKDF2_MIN_ITER is 1 + with pytest.raises(ValueError, match="Invalid iterator value"): + sm3_pbkdf2("password", b"salt", 0, 32) + + +def test_sm3_pbkdf2_invalid_iterator_too_large(): + """SM3-PBKDF2 should reject iterator count that is too large.""" + # SM3_PBKDF2_MAX_ITER is 16777216 + with pytest.raises(ValueError, match="Invalid iterator value"): + sm3_pbkdf2("password", b"salt", 16777217, 32) + + +def test_sm3_pbkdf2_invalid_keylen_too_large(): + """SM3-PBKDF2 should reject key length that is too large.""" + # SM3_PBKDF2_MAX_KEY_SIZE is 256 + with pytest.raises(ValueError, match="Invalid key length"): + sm3_pbkdf2("password", b"salt", 10000, 257) + + +# ============================================================================= +# SM4 Error Tests +# ============================================================================= + + +def test_sm4_invalid_key_length(): + """SM4 should reject invalid key length.""" + with pytest.raises(ValueError, match="Invalid key length"): + Sm4(b"short_key", DO_ENCRYPT) + + +def test_sm4_invalid_block_size(): + """SM4 encrypt should reject invalid block size.""" + sm4 = Sm4(b"1234567812345678", DO_ENCRYPT) + with pytest.raises(ValueError, match="Invalid block size"): + sm4.encrypt(b"short") + + +def test_sm4_decrypt_on_encrypt_mode(): + """SM4 decrypt should fail when called on encryption mode instance.""" + sm4 = Sm4(b"1234567812345678", DO_ENCRYPT) + with pytest.raises(ValueError, match="Cannot call decrypt\\(\\) on encryption mode instance"): + sm4.decrypt(b"1234567812345678") + + +def test_sm4_decrypt_invalid_block_size(): + """SM4 decrypt should reject invalid block size.""" + sm4 = Sm4(b"1234567812345678", DO_DECRYPT) + with pytest.raises(ValueError, match="Invalid block size"): + sm4.decrypt(b"short") + + +def test_sm4_cbc_invalid_key_length(): + """SM4-CBC should reject invalid key length.""" + with pytest.raises(ValueError, match="Invalid key length"): + Sm4Cbc(b"short", b"1234567812345678", DO_ENCRYPT) + + +def test_sm4_cbc_invalid_iv_size(): + """SM4-CBC should reject invalid IV size.""" + with pytest.raises(ValueError, match="Invalid IV size"): + Sm4Cbc(b"1234567812345678", b"short", DO_ENCRYPT) + + +def test_sm4_ctr_invalid_key_length(): + """SM4-CTR should reject invalid key length.""" + with pytest.raises(ValueError, match="Invalid key length"): + Sm4Ctr(b"short", b"1234567812345678") + + +def test_sm4_ctr_invalid_ctr_size(): + """SM4-CTR should reject invalid CTR size.""" + with pytest.raises(ValueError, match="Invalid CTR size"): + Sm4Ctr(b"1234567812345678", b"short") + + +def test_sm4_gcm_invalid_key_length(): + """SM4-GCM should reject invalid key length.""" + with pytest.raises(ValueError, match="Invalid key length"): + Sm4Gcm(b"short", b"0123456789ab", b"aad", 16, DO_ENCRYPT) + + +def test_sm4_gcm_invalid_iv_too_short(): + """SM4-GCM should reject IV that is too short.""" + # SM4_GCM_MIN_IV_SIZE is 1 + with pytest.raises(ValueError, match="Invalid IV size"): + Sm4Gcm(b"1234567812345678", b"", b"aad", 16, DO_ENCRYPT) + + +def test_sm4_gcm_invalid_iv_too_long(): + """SM4-GCM should reject IV that is too long.""" + # SM4_GCM_MAX_IV_SIZE is 64 + with pytest.raises(ValueError, match="Invalid IV size"): + Sm4Gcm(b"1234567812345678", b"x" * 65, b"aad", 16, DO_ENCRYPT) + + +def test_sm4_gcm_invalid_tag_length(): + """SM4-GCM should reject invalid tag length.""" + with pytest.raises(ValueError, match="Invalid Tag length"): + Sm4Gcm(b"1234567812345678", b"0123456789ab", b"aad", 0, DO_ENCRYPT) + + +# ============================================================================= +# ZUC Error Tests +# ============================================================================= + + +def test_zuc_invalid_key_length(): + """ZUC should reject invalid key length.""" + with pytest.raises(ValueError, match="Invalid key length"): + Zuc(b"short", b"1234567812345678") + + +def test_zuc_invalid_iv_size(): + """ZUC should reject invalid IV size.""" + with pytest.raises(ValueError, match="Invalid IV size"): + Zuc(b"1234567812345678", b"short") + + +# ============================================================================= +# SM2 Error Tests +# ============================================================================= + + +def test_sm2_sign_without_private_key(): + """SM2 sign should fail without private key.""" + sm2 = Sm2Key() + # Key not generated, no private key + with pytest.raises(TypeError, match="has no private key"): + sm2.sign(b"0" * 32) + + +def test_sm2_encrypt_without_public_key(): + """SM2 encrypt should fail without public key.""" + sm2 = Sm2Key() + # Key not generated, no public key + with pytest.raises(TypeError, match="has no public key"): + sm2.encrypt(b"plaintext") + + +def test_sm2_verify_without_public_key(): + """SM2 verify should fail without public key.""" + sm2 = Sm2Key() + # Key not generated, no public key + with pytest.raises(TypeError, match="has no public key"): + sm2.verify(b"0" * 32, b"signature") + + +def test_sm2_decrypt_without_private_key(): + """SM2 decrypt should fail without private key.""" + sm2 = Sm2Key() + # Key not generated, no private key + with pytest.raises(TypeError, match="has no private key"): + sm2.decrypt(b"ciphertext") + + +def test_sm2_compute_z_without_public_key(): + """SM2 compute_z should fail without public key.""" + sm2 = Sm2Key() + # Key not generated, no public key + with pytest.raises(TypeError, match="has no public key"): + sm2.compute_z(SM2_DEFAULT_ID) + + +def test_sm2_sign_invalid_digest_size(): + """SM2 sign should reject invalid digest size.""" + sm2 = Sm2Key() + sm2.generate_key() + with pytest.raises(ValueError, match="Invalid SM3 digest size"): + sm2.sign(b"invalid_digest") + + +def test_sm2_verify_invalid_digest_size(): + """SM2 verify should reject invalid digest size.""" + sm2 = Sm2Key() + sm2.generate_key() + with pytest.raises(ValueError, match="Invalid SM3 digest size"): + sm2.verify(b"invalid_digest", b"signature") + + +def test_sm2_encrypt_plaintext_too_long(): + """SM2 encrypt should reject plaintext that is too long.""" + sm2 = Sm2Key() + sm2.generate_key() + with pytest.raises(ValueError, match="Plaintext too long"): + sm2.encrypt(b"x" * (SM2_MAX_PLAINTEXT_SIZE + 1)) + + +def test_sm2_export_private_key_without_private_key(): + """SM2 export private key should fail without private key.""" + sm2 = Sm2Key() + # Key not generated, no private key + with pytest.raises(TypeError, match="has no private key"): + sm2.export_encrypted_private_key_info_pem("/tmp/test.pem", "password") + + +def test_sm2_export_public_key_without_public_key(): + """SM2 export public key should fail without public key.""" + sm2 = Sm2Key() + # Key not generated, no public key + with pytest.raises(TypeError, match="has no public key"): + sm2.export_public_key_info_pem("/tmp/test.pem") + + +# ============================================================================= +# SM2 Signature State Error Tests +# ============================================================================= + + +def test_sm2_signature_sign_in_verify_state(): + """SM2 Signature sign should fail in verify state.""" + sm2 = Sm2Key() + sm2.generate_key() + + verify = Sm2Signature(sm2, SM2_DEFAULT_ID, DO_VERIFY) + verify.update(b"message") + + with pytest.raises(StateError, match="not sign state"): + verify.sign() + + +def test_sm2_signature_verify_in_sign_state(): + """SM2 Signature verify should fail in sign state.""" + sm2 = Sm2Key() + sm2.generate_key() + + sign = Sm2Signature(sm2, SM2_DEFAULT_ID, DO_SIGN) + sign.update(b"message") + + with pytest.raises(StateError, match="not verify state"): + sign.verify(b"signature") + + +def test_sm2_signature_init_without_private_key(): + """SM2 Signature init for signing should fail without private key.""" + sm2 = Sm2Key() + # Key not generated, no private key + with pytest.raises(TypeError, match="SM2 key has no private key"): + Sm2Signature(sm2, SM2_DEFAULT_ID, DO_SIGN) + + +def test_sm2_signature_init_without_public_key(): + """SM2 Signature init for verifying should fail without public key.""" + sm2 = Sm2Key() + # Key not generated, no public key + with pytest.raises(TypeError, match="SM2 key has no public key"): + Sm2Signature(sm2, SM2_DEFAULT_ID, DO_VERIFY) diff --git a/tests/test_gmssl.py b/tests/test_gmssl.py new file mode 100644 index 0000000..247dbdd --- /dev/null +++ b/tests/test_gmssl.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python +# +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 + +""" +GmSSL Python binding tests. + +Following Linus's philosophy: "Talk is cheap. Show me the code." +Simple functions, no unnecessary abstractions. +""" + +import tempfile +from pathlib import Path + +from gmssl import ( + DO_DECRYPT, + DO_ENCRYPT, + DO_SIGN, + DO_VERIFY, + GMSSL_LIBRARY_VERSION, + GMSSL_PYTHON_VERSION, + SM2_DEFAULT_ID, + Sm2Certificate, + Sm2Key, + Sm2Signature, + Sm3, + Sm3Hmac, + Sm4, + Sm4Cbc, + Sm4Ctr, + Sm4Gcm, + Sm9EncMasterKey, + Sm9Signature, + Sm9SignMasterKey, + Zuc, + gmssl_library_version_num, + rand_bytes, + sm3_pbkdf2, +) + + +# Version tests +def test_library_version_num(): + """Library version number should be positive.""" + assert gmssl_library_version_num() > 0 + + +def test_library_version_string(): + """Library version string should not be empty.""" + assert len(GMSSL_LIBRARY_VERSION) > 0 + + +def test_python_version_string(): + """Python binding version string should not be empty.""" + assert len(GMSSL_PYTHON_VERSION) > 0 + + +# Random number generation +def test_rand_bytes(): + """rand_bytes should generate correct length.""" + keylen = 20 + key = rand_bytes(keylen) + assert len(key) == keylen + + +# SM3 hash tests +def test_sm3_hash(): + """SM3 hash of 'abc' should match known value.""" + dgst_hex = "66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0" + sm3 = Sm3() + sm3.update(b"abc") + dgst = sm3.digest() + assert dgst == bytes.fromhex(dgst_hex) + + +def test_sm3_reset(): + """SM3 reset should allow reuse.""" + sm3 = Sm3() + sm3.update(b"abc") + dgst1 = sm3.digest() + + sm3.reset() + for _ in range(16): + sm3.update(b"abcd") + dgst2 = sm3.digest() + + # Different inputs should produce different hashes + assert dgst1 != dgst2 + + +# SM3-HMAC test +def test_sm3_hmac(): + """SM3-HMAC should match known value.""" + key = b"1234567812345678" + mac_hex = "0a69401a75c5d471f5166465eec89e6a65198ae885c1fdc061556254d91c1080" + sm3_hmac = Sm3Hmac(key) + sm3_hmac.update(b"abc") + mac = sm3_hmac.generate_mac() + assert mac == bytes.fromhex(mac_hex) + + +# SM3-PBKDF2 test +def test_sm3_pbkdf2(): + """SM3-PBKDF2 should derive correct key.""" + passwd = "password" + salt = b"12345678" + iterator = 10000 + keylen = 32 + keyhex = "ac5b4a93a130252181434970fa9d8e6f1083badecafc4409aaf0097c813e9fc6" + key = sm3_pbkdf2(passwd, salt, iterator, keylen) + assert key == bytes.fromhex(keyhex) + + +# SM4 block cipher test +def test_sm4_encrypt_decrypt(): + """SM4 encryption and decryption should be reversible.""" + key = b"1234567812345678" + plaintext = b"block of message" + ciphertext_hex = "dd99d30fd7baf5af2930335d2554ddb7" + + # Encrypt + sm4_enc = Sm4(key, DO_ENCRYPT) + ciphertext = sm4_enc.encrypt(plaintext) + assert ciphertext == bytes.fromhex(ciphertext_hex) + + # Decrypt + sm4_dec = Sm4(key, DO_DECRYPT) + decrypted = sm4_dec.decrypt(ciphertext) + assert decrypted == plaintext + + +# SM4-CBC test +def test_sm4_cbc_encrypt_decrypt(): + """SM4-CBC encryption and decryption should be reversible.""" + key = b"1234567812345678" + iv = b"1234567812345678" + plaintext = b"abc" + ciphertext_hex = "532b22f9a096e7e5b8d84a620f0f7078" + + # Encrypt + sm4_cbc = Sm4Cbc(key, iv, DO_ENCRYPT) + ciphertext = sm4_cbc.update(plaintext) + ciphertext += sm4_cbc.finish() + assert ciphertext == bytes.fromhex(ciphertext_hex) + + # Decrypt + sm4_cbc = Sm4Cbc(key, iv, DO_DECRYPT) + decrypted = sm4_cbc.update(ciphertext) + decrypted += sm4_cbc.finish() + assert decrypted == plaintext + + +# SM4-CTR test +def test_sm4_ctr_encrypt_decrypt(): + """SM4-CTR encryption and decryption should be reversible.""" + key = b"1234567812345678" + iv = b"1234567812345678" + plaintext = b"abc" + ciphertext_hex = "890106" + + # Encrypt + sm4_ctr = Sm4Ctr(key, iv) + ciphertext = sm4_ctr.update(plaintext) + ciphertext += sm4_ctr.finish() + assert ciphertext == bytes.fromhex(ciphertext_hex) + + # Decrypt + sm4_ctr = Sm4Ctr(key, iv) + decrypted = sm4_ctr.update(ciphertext) + decrypted += sm4_ctr.finish() + assert decrypted == plaintext + + +# SM4-GCM test +def test_sm4_gcm_encrypt_decrypt(): + """SM4-GCM encryption and decryption should be reversible.""" + key = b"1234567812345678" + iv = b"0123456789ab" + aad = b"Additional Authenticated Data" + taglen = 16 + plaintext = b"abc" + ciphertext_hex = "7d8bd8fdc7ea3b04c15fb61863f2292c15eeaa" + + # Encrypt + sm4_gcm = Sm4Gcm(key, iv, aad, taglen, DO_ENCRYPT) + ciphertext = sm4_gcm.update(plaintext) + ciphertext += sm4_gcm.finish() + assert ciphertext == bytes.fromhex(ciphertext_hex) + + # Decrypt + sm4_gcm = Sm4Gcm(key, iv, aad, taglen, DO_DECRYPT) + decrypted = sm4_gcm.update(ciphertext) + decrypted += sm4_gcm.finish() + assert decrypted == plaintext + + +# ZUC test +def test_zuc_encrypt_decrypt(): + """ZUC encryption and decryption should be reversible.""" + key = b"1234567812345678" + iv = b"1234567812345678" + plaintext = b"abc" + ciphertext_hex = "3d144b" + + # Encrypt + zuc = Zuc(key, iv) + ciphertext = zuc.update(plaintext) + ciphertext += zuc.finish() + assert ciphertext == bytes.fromhex(ciphertext_hex) + + # Decrypt + zuc = Zuc(key, iv) + decrypted = zuc.update(ciphertext) + decrypted += zuc.finish() + assert decrypted == plaintext + + +# SM2 key tests +def test_sm2_key_generation_and_export(): + """SM2 key generation, export, and import should work.""" + dgst = bytes.fromhex("66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0") + plaintext = b"abc" + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + sm2_pem = tmpdir / "sm2.pem" + sm2pub_pem = tmpdir / "sm2pub.pem" + + # Generate key + sm2 = Sm2Key() + sm2.generate_key() + sm2.export_encrypted_private_key_info_pem(str(sm2_pem), "password") + sm2.export_public_key_info_pem(str(sm2pub_pem)) + + # Import private key + sm2pri = Sm2Key() + sm2pri.import_encrypted_private_key_info_pem(str(sm2_pem), "password") + + # Import public key + sm2pub = Sm2Key() + sm2pub.import_public_key_info_pem(str(sm2pub_pem)) + + # Test signature + sig = sm2pri.sign(dgst) + verify_ret = sm2pub.verify(dgst, sig) + assert verify_ret + + # Test encryption + ciphertext = sm2pub.encrypt(plaintext) + decrypted = sm2pri.decrypt(ciphertext) + assert decrypted == plaintext + + +def test_sm2_compute_z(): + """SM2 compute_z should match known value.""" + pem_txt = """\ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE+XVF76aof3ZtBVUwXobDQwQn+Sb2 +ethykPiYkXDLFdLnTrqr0b9QuA63DPdyrxJS3LZZwp9qzaMSyStai8+nrQ== +-----END PUBLIC KEY-----""" + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + pub_pem = tmpdir / "pub.pem" + pub_pem.write_text(pem_txt) + + z_hex = "4e469c92c425960603a315491bb2181c2f25939172775e223e1759b413cfc8ba" + sm2pub = Sm2Key() + sm2pub.import_public_key_info_pem(str(pub_pem)) + z = sm2pub.compute_z(SM2_DEFAULT_ID) + assert z == bytes.fromhex(z_hex) + + +# SM2 signature test +def test_sm2_signature_context(): + """SM2 signature context should work correctly.""" + sm2 = Sm2Key() + sm2.generate_key() + + # Sign + sign = Sm2Signature(sm2, SM2_DEFAULT_ID, DO_SIGN) + sign.update(b"abc") + sig = sign.sign() + + # Verify + verify = Sm2Signature(sm2, SM2_DEFAULT_ID, DO_VERIFY) + verify.update(b"abc") + verify_ret = verify.verify(sig) + assert verify_ret + + +# SM9 encryption test +def test_sm9_enc_decrypt(): + """SM9 encryption and decryption should work.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + enc_msk_pem = tmpdir / "enc_msk.pem" + enc_mpk_pem = tmpdir / "enc_mpk.pem" + + # Generate master key + master_key = Sm9EncMasterKey() + master_key.generate_master_key() + master_key.export_encrypted_master_key_info_pem(str(enc_msk_pem), "password") + master_key.export_public_master_key_pem(str(enc_mpk_pem)) + + # Encrypt with public master key + master_pub = Sm9EncMasterKey() + master_pub.import_public_master_key_pem(str(enc_mpk_pem)) + ciphertext = master_pub.encrypt(b"plaintext", "Alice") + + # Decrypt with extracted user key + master = Sm9EncMasterKey() + master.import_encrypted_master_key_info_pem(str(enc_msk_pem), "password") + key = master.extract_key("Alice") + plaintext = key.decrypt(ciphertext) + assert plaintext == b"plaintext" + + +# SM9 signature test +def test_sm9_sign_verify(): + """SM9 signature and verification should work.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + sign_msk_pem = tmpdir / "sign_msk.pem" + sign_mpk_pem = tmpdir / "sign_mpk.pem" + + # Generate master key + master_key = Sm9SignMasterKey() + master_key.generate_master_key() + master_key.export_encrypted_master_key_info_pem(str(sign_msk_pem), "password") + master_key.export_public_master_key_pem(str(sign_mpk_pem)) + + # Sign with extracted user key + master = Sm9SignMasterKey() + master.import_encrypted_master_key_info_pem(str(sign_msk_pem), "password") + key = master.extract_key("Alice") + sign = Sm9Signature(DO_SIGN) + sign.update(b"message") + sig = sign.sign(key) + + # Verify with public master key + master_pub = Sm9SignMasterKey() + master_pub.import_public_master_key_pem(str(sign_mpk_pem)) + verify = Sm9Signature(DO_VERIFY) + verify.update(b"message") + ret = verify.verify(sig, master_pub, "Alice") + assert ret + + +# SM2 certificate test +def test_sm2_certificate_parsing(): + """SM2 certificate parsing and verification should work.""" + cert_txt = """\ +-----BEGIN CERTIFICATE----- +MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQG +EwJDTjEOMAwGA1UECgwFTlJDQUMxDzANBgNVBAMMBlJPT1RDQTAeFw0xMjA3MTQw +MzExNTlaFw00MjA3MDcwMzExNTlaMC4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQKDAVO +UkNBQzEPMA0GA1UEAwwGUk9PVENBMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE +MPCca6pmgcchsTf2UnBeL9rtp4nw+itk1Kzrmbnqo05lUwkwlWK+4OIrtFdAqnRT +V7Q9v1htkv42TsIutzd126NdMFswHwYDVR0jBBgwFoAUTDKxl9kzG8SmBcHG5Yti +W/CXdlgwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFEwysZfZ +MxvEpgXBxuWLYlvwl3ZYMAwGCCqBHM9VAYN1BQADSAAwRQIgG1bSLeOXp3oB8H7b +53W+CKOPl2PknmWEq/lMhtn25HkCIQDaHDgWxWFtnCrBjH16/W3Ezn7/U/Vjo5xI +pDoiVhsLwg== +-----END CERTIFICATE-----""" + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + rootca_pem = tmpdir / "ROOTCA.pem" + public_key_pem = tmpdir / "public_key.pem" + + rootca_pem.write_text(cert_txt) + + cert = Sm2Certificate() + cert.import_pem(str(rootca_pem)) + + # Test serial number + serial = cert.get_serial_number() + assert len(serial) > 0 + + # Test validity + validity = cert.get_validity() + assert validity.not_before < validity.not_after + + # Test issuer and subject + issuer = cert.get_issuer() + assert len(issuer) > 1 + subject = cert.get_subject() + assert len(subject) > 1 + + # Test public key export/import + public_key = cert.get_subject_public_key() + public_key.export_public_key_info_pem(str(public_key_pem)) + public_key.import_public_key_info_pem(str(public_key_pem)) + + # Test certificate verification + ret = cert.verify_by_ca_certificate(cert, SM2_DEFAULT_ID) + assert ret diff --git a/tests/test_pygmssl_missing.py b/tests/test_pygmssl_missing.py new file mode 100644 index 0000000..0246316 --- /dev/null +++ b/tests/test_pygmssl_missing.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python +# +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 + +""" +Tests for features found in pygmssl but missing in our test suite. + +This file adds tests for specific scenarios from the pygmssl project +that were not covered in our existing tests. +""" + +import os +import tempfile +from pathlib import Path + +import pytest + +from gmssl import NativeError, Sm2Key, Sm3, Sm4, Sm4Cbc + +# ============================================================================= +# SM2 Tests +# ============================================================================= + + +def test_sm2_wrong_password_import(): + """ + Test that importing an encrypted PEM with wrong password fails. + + Corresponds to pygmssl test_102_error_import_private_pem. + """ + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + pem_path = tmpdir / "test_key.pem" + + # Generate key and export with password + sm2 = Sm2Key() + sm2.generate_key() + correct_password = "test-123-456" + sm2.export_encrypted_private_key_info_pem(str(pem_path), correct_password) + + # Try to import with wrong password + sm2_wrong = Sm2Key() + with pytest.raises(NativeError, match="failed"): + sm2_wrong.import_encrypted_private_key_info_pem(str(pem_path), "wrong-password") + + +# ============================================================================= +# SM4 Tests +# ============================================================================= + + +def test_sm4_cbc_bulk_encrypt(): + """ + Test SM4-CBC encryption/decryption with large data. + + Corresponds to pygmssl test_002_cbc_bulk_encrypt. + Tests performance and correctness with ~1MB of data. + """ + key = os.urandom(16) + iv = os.urandom(16) + + # Create ~1MB of random data (512 bytes * 2099 = ~1MB) + bulk_data = os.urandom(512) * 2099 + + # Encrypt + sm4_enc = Sm4Cbc(key, iv, True) + ciphertext = sm4_enc.update(bulk_data) + ciphertext += sm4_enc.finish() + + # Decrypt + sm4_dec = Sm4Cbc(key, iv, False) + plaintext = sm4_dec.update(ciphertext) + plaintext += sm4_dec.finish() + + # Verify + assert plaintext == bulk_data + + +def test_sm4_cbc_bulk_encrypt_multiple_chunks(): + """ + Test SM4-CBC with large data processed in multiple chunks. + + This tests the streaming capability with large data. + """ + key = os.urandom(16) + iv = os.urandom(16) + + # Create large data + chunk_size = 512 + num_chunks = 2099 + chunks = [os.urandom(chunk_size) for _ in range(num_chunks)] + bulk_data = b"".join(chunks) + + # Encrypt in chunks + sm4_enc = Sm4Cbc(key, iv, True) + ciphertext_chunks = [] + for chunk in chunks: + ciphertext_chunks.append(sm4_enc.update(chunk)) + ciphertext_chunks.append(sm4_enc.finish()) + ciphertext = b"".join(ciphertext_chunks) + + # Decrypt all at once (simpler and more reliable) + sm4_dec = Sm4Cbc(key, iv, False) + plaintext = sm4_dec.update(ciphertext) + plaintext += sm4_dec.finish() + + # Verify + assert plaintext == bulk_data + + +def test_sm4_ecb_bulk_encrypt(): + """ + Test SM4-ECB encryption/decryption with large data. + + Similar to CBC bulk test but for ECB mode. + Note: SM4 ECB mode only works with single blocks (16 bytes). + For bulk data, we need to use CBC/CTR/GCM modes. + """ + key = os.urandom(16) + + # Create data that's exactly one block (16 bytes) + # SM4 ECB mode in this implementation only handles single blocks + bulk_data = os.urandom(16) + + # Encrypt + sm4_enc = Sm4(key, True) + ciphertext = sm4_enc.encrypt(bulk_data) + + # Decrypt (use encrypt method with decrypt key) + sm4_dec = Sm4(key, False) + plaintext = sm4_dec.encrypt(ciphertext) # encrypt method works for both + + # Verify + assert plaintext == bulk_data + + +# ============================================================================= +# Performance Tests +# ============================================================================= + + +def test_sm2_sign_performance(): + """ + Test SM2 signing performance with multiple iterations. + + This ensures signing remains fast even after many operations. + """ + sm2 = Sm2Key() + sm2.generate_key() + + data = b"hello, world" + digest = Sm3() + digest.update(data) + dgst = digest.digest() + + # Sign 100 times + signatures = [] + for _ in range(100): + sig = sm2.sign(dgst) + signatures.append(sig) + + # Verify all signatures are valid + for sig in signatures: + assert sm2.verify(dgst, sig) + + +def test_sm3_hash_performance(): + """ + Test SM3 hashing performance with large data. + + This ensures hashing remains fast with large inputs. + """ + # Create 10MB of data + large_data = os.urandom(1024 * 1024 * 10) + + # Hash it + sm3 = Sm3() + sm3.update(large_data) + digest = sm3.digest() + + # Verify digest is correct length + assert len(digest) == 32 + + +def test_sm3_hash_performance_streaming(): + """ + Test SM3 hashing performance with streaming data. + + This tests the update() method with many small chunks. + """ + # Create 1MB of data in 1KB chunks + chunk_size = 1024 + num_chunks = 1024 + + sm3 = Sm3() + for _ in range(num_chunks): + chunk = os.urandom(chunk_size) + sm3.update(chunk) + + digest = sm3.digest() + assert len(digest) == 32 + + +# ============================================================================= +# Stress Tests +# ============================================================================= + + +def test_sm2_key_generation_stress(): + """ + Test SM2 key generation multiple times to ensure stability. + + This ensures the random number generator and key generation + remain stable over many iterations. + """ + keys = [] + for _ in range(50): + sm2 = Sm2Key() + sm2.generate_key() + keys.append(sm2) + + # Verify all keys are different by checking private keys + # (public_key is a Structure field, not a method) + private_keys = [bytes(sm2.private_key) for sm2 in keys] + assert len(set(private_keys)) == 50 # All unique + + +def test_sm4_cbc_stress(): + """ + Test SM4-CBC with many encrypt/decrypt cycles. + + This ensures the cipher remains stable over many operations. + """ + key = os.urandom(16) + iv = os.urandom(16) + data = b"hello, world" * 100 + + # Encrypt and decrypt 100 times + for _ in range(100): + sm4_enc = Sm4Cbc(key, iv, True) + ciphertext = sm4_enc.update(data) + ciphertext += sm4_enc.finish() + + sm4_dec = Sm4Cbc(key, iv, False) + plaintext = sm4_dec.update(ciphertext) + plaintext += sm4_dec.finish() + + assert plaintext == data + + +# ============================================================================= +# Edge Cases from pygmssl +# ============================================================================= + + +def test_sm2_empty_signature_verify(): + """ + Test SM2 verification with empty signature. + + This should fail gracefully. + """ + sm2 = Sm2Key() + sm2.generate_key() + + data = b"hello, world" + digest = Sm3() + digest.update(data) + dgst = digest.digest() + + # Empty signature should fail verification + assert not sm2.verify(dgst, b"") + + +def test_sm2_corrupted_signature_verify(): + """ + Test SM2 verification with corrupted signature. + + This should fail verification. + """ + sm2 = Sm2Key() + sm2.generate_key() + + data = b"hello, world" + digest = Sm3() + digest.update(data) + dgst = digest.digest() + + # Create valid signature + sig = sm2.sign(dgst) + + # Corrupt the signature + corrupted_sig = bytearray(sig) + corrupted_sig[0] ^= 0xFF # Flip bits in first byte + + # Corrupted signature should fail verification + assert not sm2.verify(dgst, bytes(corrupted_sig)) + + +def test_sm3_hash_consistency(): + """ + Test that SM3 hash is consistent across multiple instances. + + The same input should always produce the same hash. + """ + data = b"hello, world" + + # Hash with multiple instances + hashes = [] + for _ in range(10): + sm3 = Sm3() + sm3.update(data) + hashes.append(sm3.digest()) + + # All hashes should be identical + assert len(set(hashes)) == 1 + + +def test_sm4_cbc_padding_edge_cases(): + """ + Test SM4-CBC padding with various data sizes. + + This ensures padding works correctly for all data sizes. + """ + key = os.urandom(16) + iv = os.urandom(16) + + # Test with data sizes from 0 to 32 bytes + for size in range(33): + data = os.urandom(size) + + # Encrypt + sm4_enc = Sm4Cbc(key, iv, True) + ciphertext = sm4_enc.update(data) + ciphertext += sm4_enc.finish() + + # Decrypt + sm4_dec = Sm4Cbc(key, iv, False) + plaintext = sm4_dec.update(ciphertext) + plaintext += sm4_dec.finish() + + # Verify + assert plaintext == data, f"Failed for size {size}" diff --git a/tests/test_thread_safety.py b/tests/test_thread_safety.py new file mode 100644 index 0000000..5efaa46 --- /dev/null +++ b/tests/test_thread_safety.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python +# +# Copyright 2023 The GmSSL Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may +# not use this file except in compliance with the License. +# +# http://www.apache.org/licenses/LICENSE-2.0 + +""" +Thread safety tests for GmSSL Python binding. + +Tests concurrent operations to ensure thread safety of cryptographic operations. +""" + +import os +from concurrent.futures import ThreadPoolExecutor, as_completed + +from gmssl import ( + Sm2Key, + Sm3, + Sm3Hmac, + Sm4Cbc, + Sm9EncMasterKey, + Zuc, + rand_bytes, +) + +# ============================================================================= +# Random Number Generation Thread Safety +# ============================================================================= + + +def test_rand_bytes_thread_safety(): + """ + Test that rand_bytes is thread-safe. + + Multiple threads should be able to generate random bytes concurrently + without conflicts or duplicate values. + """ + num_threads = 10 + bytes_per_thread = 100 + + def generate_random(): + return [rand_bytes(32) for _ in range(bytes_per_thread)] + + # Generate random bytes in multiple threads + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(generate_random) for _ in range(num_threads)] + results = [future.result() for future in as_completed(futures)] + + # Flatten results + all_random = [rb for thread_results in results for rb in thread_results] + + # Verify all values are unique (extremely high probability) + assert len(set(all_random)) == len(all_random) + + +# ============================================================================= +# SM3 Hash Thread Safety +# ============================================================================= + + +def test_sm3_hash_thread_safety(): + """ + Test that SM3 hashing is thread-safe. + + Multiple threads should be able to hash data concurrently. + """ + num_threads = 20 + data = b"hello, world" + + def hash_data(): + sm3 = Sm3() + sm3.update(data) + return sm3.digest() + + # Hash in multiple threads + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(hash_data) for _ in range(num_threads)] + results = [future.result() for future in as_completed(futures)] + + # All results should be identical + assert len(set(results)) == 1 + + +def test_sm3_hmac_thread_safety(): + """ + Test that SM3-HMAC is thread-safe. + + Multiple threads should be able to generate MACs concurrently. + """ + num_threads = 20 + key = b"test_key_1234567" + data = b"hello, world" + + def generate_mac(): + sm3_hmac = Sm3Hmac(key) + sm3_hmac.update(data) + return sm3_hmac.generate_mac() + + # Generate MACs in multiple threads + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(generate_mac) for _ in range(num_threads)] + results = [future.result() for future in as_completed(futures)] + + # All results should be identical + assert len(set(results)) == 1 + + +# ============================================================================= +# SM4 Cipher Thread Safety +# ============================================================================= + + +def test_sm4_cbc_thread_safety(): + """ + Test that SM4-CBC is thread-safe. + + Multiple threads should be able to encrypt/decrypt concurrently. + """ + num_threads = 20 + key = os.urandom(16) + iv = os.urandom(16) + plaintext = b"hello, world" * 10 + + def encrypt_decrypt(): + # Encrypt + sm4_enc = Sm4Cbc(key, iv, True) + ciphertext = sm4_enc.update(plaintext) + ciphertext += sm4_enc.finish() + + # Decrypt + sm4_dec = Sm4Cbc(key, iv, False) + decrypted = sm4_dec.update(ciphertext) + decrypted += sm4_dec.finish() + + return decrypted + + # Encrypt/decrypt in multiple threads + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(encrypt_decrypt) for _ in range(num_threads)] + results = [future.result() for future in as_completed(futures)] + + # All results should be identical to plaintext + assert all(result == plaintext for result in results) + + +# ============================================================================= +# ZUC Stream Cipher Thread Safety +# ============================================================================= + + +def test_zuc_thread_safety(): + """ + Test that ZUC stream cipher is thread-safe. + + Multiple threads should be able to encrypt/decrypt concurrently. + """ + num_threads = 20 + key = os.urandom(16) + iv = os.urandom(16) + plaintext = b"hello, world" * 10 + + def encrypt_decrypt(): + # Encrypt + zuc_enc = Zuc(key, iv) + ciphertext = zuc_enc.update(plaintext) + ciphertext += zuc_enc.finish() + + # Decrypt + zuc_dec = Zuc(key, iv) + decrypted = zuc_dec.update(ciphertext) + decrypted += zuc_dec.finish() + + return decrypted + + # Encrypt/decrypt in multiple threads + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(encrypt_decrypt) for _ in range(num_threads)] + results = [future.result() for future in as_completed(futures)] + + # All results should be identical to plaintext + assert all(result == plaintext for result in results) + + +# ============================================================================= +# SM2 Public Key Cryptography Thread Safety +# ============================================================================= + + +def test_sm2_key_generation_thread_safety(): + """ + Test that SM2 key generation is thread-safe. + + Multiple threads should be able to generate keys concurrently. + """ + num_threads = 20 + + def generate_key(): + sm2 = Sm2Key() + sm2.generate_key() + return bytes(sm2.private_key) + + # Generate keys in multiple threads + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(generate_key) for _ in range(num_threads)] + results = [future.result() for future in as_completed(futures)] + + # All keys should be unique + assert len(set(results)) == num_threads + + +def test_sm2_sign_verify_thread_safety(): + """ + Test that SM2 signing and verification is thread-safe. + + Multiple threads should be able to sign and verify concurrently. + """ + num_threads = 20 + + # Generate a shared key + sm2 = Sm2Key() + sm2.generate_key() + + data = b"hello, world" + digest = Sm3() + digest.update(data) + dgst = digest.digest() + + def sign_verify(): + # Sign + sig = sm2.sign(dgst) + + # Verify + assert sm2.verify(dgst, sig) + + return sig + + # Sign/verify in multiple threads + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(sign_verify) for _ in range(num_threads)] + results = [future.result() for future in as_completed(futures)] + + # All signatures should be valid (may be different due to randomness) + assert len(results) == num_threads + + +def test_sm2_encrypt_decrypt_thread_safety(): + """ + Test that SM2 encryption and decryption is thread-safe. + + Multiple threads should be able to encrypt and decrypt concurrently. + """ + num_threads = 20 + + # Generate a shared key + sm2 = Sm2Key() + sm2.generate_key() + + plaintext = b"hello, world" + + def encrypt_decrypt(): + # Encrypt + ciphertext = sm2.encrypt(plaintext) + + # Decrypt + decrypted = sm2.decrypt(ciphertext) + + return decrypted + + # Encrypt/decrypt in multiple threads + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(encrypt_decrypt) for _ in range(num_threads)] + results = [future.result() for future in as_completed(futures)] + + # All results should be identical to plaintext + assert all(result == plaintext for result in results) + + +# ============================================================================= +# SM9 Identity-Based Cryptography Thread Safety +# ============================================================================= + + +def test_sm9_key_generation_thread_safety(): + """ + Test that SM9 key generation is thread-safe. + + Multiple threads should be able to generate master keys concurrently. + """ + num_threads = 10 # Fewer threads as SM9 is slower + + def generate_master_key(): + master = Sm9EncMasterKey() + master.generate_master_key() + return True + + # Generate master keys in multiple threads + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(generate_master_key) for _ in range(num_threads)] + results = [future.result() for future in as_completed(futures)] + + # All operations should succeed + assert all(results) + + +# ============================================================================= +# Mixed Operations Thread Safety +# ============================================================================= + + +def test_mixed_operations_thread_safety(): + """ + Test thread safety with mixed cryptographic operations. + + Multiple threads performing different operations concurrently. + """ + num_iterations = 50 + + def sm3_operation(): + sm3 = Sm3() + sm3.update(b"test data") + return sm3.digest() + + def sm4_operation(): + key = os.urandom(16) + iv = os.urandom(16) + sm4 = Sm4Cbc(key, iv, True) + ciphertext = sm4.update(b"test data") + ciphertext += sm4.finish() + return ciphertext + + def sm2_operation(): + sm2 = Sm2Key() + sm2.generate_key() + return bytes(sm2.private_key) + + def random_operation(): + return rand_bytes(32) + + operations = [sm3_operation, sm4_operation, sm2_operation, random_operation] + + # Run mixed operations in multiple threads + with ThreadPoolExecutor(max_workers=20) as executor: + futures = [] + for _ in range(num_iterations): + for op in operations: + futures.append(executor.submit(op)) + + results = [future.result() for future in as_completed(futures)] + + # All operations should complete successfully + assert len(results) == num_iterations * len(operations) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..54532f2 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1319 @@ +version = 1 +revision = 3 +requires-python = ">=3.7" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", + "python_full_version < '3.8'", +] + +[[package]] +name = "cfgv" +version = "3.3.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c4/bf/d0d622b660d414a47dc7f0d303791a627663f554345b21250e39e7acb48b/cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6d/82/0a0ebd35bae9981dea55c06f8e6aaf44a49171ad798795c72c6f64cba4c2/cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.2.7" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/45/8b/421f30467e69ac0e414214856798d4bc32da1336df745e49e49ae5c1e2a8/coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", size = 762575, upload-time = "2023-05-29T20:08:50.273Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/01/24/be01e62a7bce89bcffe04729c540382caa5a06bee45ae42136c93e2499f5/coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8", size = 200724, upload-time = "2023-05-29T20:07:03.422Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3d/80/7060a445e1d2c9744b683dc935248613355657809d6c6b2716cdf4ca4766/coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb", size = 201024, upload-time = "2023-05-29T20:07:05.694Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b8/9d/926fce7e03dbfc653104c2d981c0fa71f0572a9ebd344d24c573bd6f7c4f/coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6", size = 229528, upload-time = "2023-05-29T20:07:07.307Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d1/3a/67f5d18f911abf96857f6f7e4df37ca840e38179e2cc9ab6c0b9c3380f19/coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2", size = 227842, upload-time = "2023-05-29T20:07:09.331Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b4/bd/1b2331e3a04f4cc9b7b332b1dd0f3a1261dfc4114f8479bebfcc2afee9e8/coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063", size = 228717, upload-time = "2023-05-29T20:07:11.38Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2b/86/3dbf9be43f8bf6a5ca28790a713e18902b2d884bc5fa9512823a81dff601/coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1", size = 234632, upload-time = "2023-05-29T20:07:13.376Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/91/e8/469ed808a782b9e8305a08bad8c6fa5f8e73e093bda6546c5aec68275bff/coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353", size = 232875, upload-time = "2023-05-29T20:07:15.093Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/8f/4fad1c2ba98104425009efd7eaa19af9a7c797e92d40cd2ec026fa1f58cb/coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495", size = 234094, upload-time = "2023-05-29T20:07:17.013Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/94/4e/d4e46a214ae857be3d7dc5de248ba43765f60daeb1ab077cb6c1536c7fba/coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818", size = 203184, upload-time = "2023-05-29T20:07:18.69Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1f/e9/d6730247d8dec2a3dddc520ebe11e2e860f0f98cee3639e23de6cf920255/coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850", size = 204096, upload-time = "2023-05-29T20:07:20.153Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c6/fa/529f55c9a1029c840bcc9109d5a15ff00478b7ff550a1ae361f8745f8ad5/coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", size = 200895, upload-time = "2023-05-29T20:07:21.963Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/67/d7/cd8fe689b5743fffac516597a1222834c42b80686b99f5b44ef43ccc2a43/coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", size = 201120, upload-time = "2023-05-29T20:07:23.765Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8c/95/16eed713202406ca0a37f8ac259bbf144c9d24f9b8097a8e6ead61da2dbb/coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3", size = 233178, upload-time = "2023-05-29T20:07:25.281Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c1/49/4d487e2ad5d54ed82ac1101e467e8994c09d6123c91b2a962145f3d262c2/coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", size = 230754, upload-time = "2023-05-29T20:07:27.044Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a7/cd/3ce94ad9d407a052dc2a74fbeb1c7947f442155b28264eb467ee78dea812/coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", size = 232558, upload-time = "2023-05-29T20:07:28.743Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8f/a8/12cc7b261f3082cc299ab61f677f7e48d93e35ca5c3c2f7241ed5525ccea/coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", size = 241509, upload-time = "2023-05-29T20:07:30.434Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/04/fa/43b55101f75a5e9115259e8be70ff9279921cb6b17f04c34a5702ff9b1f7/coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", size = 239924, upload-time = "2023-05-29T20:07:32.065Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/68/5f/d2bd0f02aa3c3e0311986e625ccf97fdc511b52f4f1a063e4f37b624772f/coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", size = 240977, upload-time = "2023-05-29T20:07:34.184Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ba/92/69c0722882643df4257ecc5437b83f4c17ba9e67f15dc6b77bad89b6982e/coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", size = 203168, upload-time = "2023-05-29T20:07:35.869Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b1/96/c12ed0dfd4ec587f3739f53eb677b9007853fd486ccb0e7d5512a27bab2e/coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", size = 204185, upload-time = "2023-05-29T20:07:37.39Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/d5/52fa1891d1802ab2e1b346d37d349cb41cdd4fd03f724ebbf94e80577687/coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", size = 201020, upload-time = "2023-05-29T20:07:38.724Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/24/df/6765898d54ea20e3197a26d26bb65b084deefadd77ce7de946b9c96dfdc5/coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", size = 233994, upload-time = "2023-05-29T20:07:40.274Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/15/81/b108a60bc758b448c151e5abceed027ed77a9523ecbc6b8a390938301841/coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", size = 231358, upload-time = "2023-05-29T20:07:41.998Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/61/90/c76b9462f39897ebd8714faf21bc985b65c4e1ea6dff428ea9dc711ed0dd/coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", size = 233316, upload-time = "2023-05-29T20:07:43.539Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/04/d6/8cba3bf346e8b1a4fb3f084df7d8cea25a6b6c56aaca1f2e53829be17e9e/coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", size = 240159, upload-time = "2023-05-29T20:07:44.982Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6e/ea/4a252dc77ca0605b23d477729d139915e753ee89e4c9507630e12ad64a80/coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", size = 238127, upload-time = "2023-05-29T20:07:46.522Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/5c/d9760ac497c41f9c4841f5972d0edf05d50cad7814e86ee7d133ec4a0ac8/coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", size = 239833, upload-time = "2023-05-29T20:07:47.992Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/69/8c/26a95b08059db1cbb01e4b0e6d40f2e9debb628c6ca86b78f625ceaf9bab/coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", size = 203463, upload-time = "2023-05-29T20:07:49.939Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b7/00/14b00a0748e9eda26e97be07a63cc911108844004687321ddcc213be956c/coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", size = 204347, upload-time = "2023-05-29T20:07:51.909Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/80/d7/67937c80b8fd4c909fdac29292bc8b35d9505312cff6bcab41c53c5b1df6/coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f", size = 200580, upload-time = "2023-05-29T20:07:54.076Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7a/05/084864fa4bbf8106f44fb72a56e67e0cd372d3bf9d893be818338c81af5d/coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb", size = 226237, upload-time = "2023-05-29T20:07:56.28Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/67/a2/6fa66a50e6e894286d79a3564f42bd54a9bd27049dc0a63b26d9924f0aa3/coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9", size = 224256, upload-time = "2023-05-29T20:07:58.189Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e2/c0/73f139794c742840b9ab88e2e17fe14a3d4668a166ff95d812ac66c0829d/coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd", size = 225550, upload-time = "2023-05-29T20:08:00.383Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/03/ec/6f30b4e0c96ce03b0e64aec46b4af2a8c49b70d1b5d0d69577add757b946/coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a", size = 232440, upload-time = "2023-05-29T20:08:02.495Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/22/c1/2f6c1b6f01a0996c9e067a9c780e1824351dbe17faae54388a4477e6d86f/coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959", size = 230897, upload-time = "2023-05-29T20:08:04.382Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8d/d6/53e999ec1bf7498ca4bc5f3b8227eb61db39068d2de5dcc359dec5601b5a/coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02", size = 232024, upload-time = "2023-05-29T20:08:06.031Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e9/40/383305500d24122dbed73e505a4d6828f8f3356d1f68ab6d32c781754b81/coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f", size = 203293, upload-time = "2023-05-29T20:08:07.598Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0e/bc/7e3a31534fabb043269f14fb64e2bb2733f85d4cf39e5bbc71357c57553a/coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0", size = 204040, upload-time = "2023-05-29T20:08:09.919Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c6/fc/be19131010930a6cf271da48202c8cc1d3f971f68c02fb2d3a78247f43dc/coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5", size = 200689, upload-time = "2023-05-29T20:08:11.594Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/28/d7/9a8de57d87f4bbc6f9a6a5ded1eaac88a89bf71369bb935dac3c0cf2893e/coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5", size = 200986, upload-time = "2023-05-29T20:08:13.228Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c8/e4/e6182e4697665fb594a7f4e4f27cb3a4dd00c2e3d35c5c706765de8c7866/coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9", size = 230648, upload-time = "2023-05-29T20:08:15.11Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/e3/f552d5871943f747165b92a924055c5d6daa164ae659a13f9018e22f3990/coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6", size = 228511, upload-time = "2023-05-29T20:08:16.877Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/44/55/49f65ccdd4dfd6d5528e966b28c37caec64170c725af32ab312889d2f857/coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e", size = 229852, upload-time = "2023-05-29T20:08:18.47Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0d/31/340428c238eb506feb96d4fb5c9ea614db1149517f22cc7ab8c6035ef6d9/coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050", size = 235578, upload-time = "2023-05-29T20:08:20.298Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/dd/ce/97c1dd6592c908425622fe7f31c017d11cf0421729b09101d4de75bcadc8/coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5", size = 234079, upload-time = "2023-05-29T20:08:22.365Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/de/a3/5a98dc9e239d0dc5f243ef5053d5b1bdcaa1dee27a691dfc12befeccf878/coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f", size = 234991, upload-time = "2023-05-29T20:08:24.974Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4a/fb/78986d3022e5ccf2d4370bc43a5fef8374f092b3c21d32499dee8e30b7b6/coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e", size = 203160, upload-time = "2023-05-29T20:08:26.701Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c3/1c/6b3c9c363fb1433c79128e0d692863deb761b1b78162494abb9e5c328bc0/coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c", size = 204085, upload-time = "2023-05-29T20:08:28.146Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/88/da/495944ebf0ad246235a6bd523810d9f81981f9b81c6059ba1f56e943abe0/coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9", size = 200725, upload-time = "2023-05-29T20:08:29.851Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ca/0c/3dfeeb1006c44b911ee0ed915350db30325d01808525ae7cc8d57643a2ce/coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2", size = 201022, upload-time = "2023-05-29T20:08:31.429Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/61/af/5964b8d7d9a5c767785644d9a5a63cacba9a9c45cc42ba06d25895ec87be/coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7", size = 229102, upload-time = "2023-05-29T20:08:32.982Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d9/1d/cd467fceb62c371f9adb1d739c92a05d4e550246daa90412e711226bd320/coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e", size = 227441, upload-time = "2023-05-29T20:08:35.044Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fe/57/e4f8ad64d84ca9e759d783a052795f62a9f9111585e46068845b1cb52c2b/coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1", size = 228265, upload-time = "2023-05-29T20:08:36.861Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/88/8b/b0d9fe727acae907fa7f1c8194ccb6fe9d02e1c3e9001ecf74c741f86110/coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9", size = 234217, upload-time = "2023-05-29T20:08:38.837Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/66/2e/c99fe1f6396d93551aa352c75410686e726cd4ea104479b9af1af22367ce/coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250", size = 232466, upload-time = "2023-05-29T20:08:40.768Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bb/e9/88747b40c8fb4a783b40222510ce6d66170217eb05d7f46462c36b4fa8cc/coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2", size = 233669, upload-time = "2023-05-29T20:08:42.944Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b1/d5/a8e276bc005e42114468d4fe03e0a9555786bc51cbfe0d20827a46c1565a/coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb", size = 203199, upload-time = "2023-05-29T20:08:44.734Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a9/0c/4a848ae663b47f1195abcb09a951751dd61f80b503303b9b9d768e0fd321/coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27", size = 204109, upload-time = "2023-05-29T20:08:46.417Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/67/fb/b3b1d7887e1ea25a9608b0776e480e4bbc303ca95a31fd585555ec4fff5a/coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d", size = 193207, upload-time = "2023-05-29T20:08:48.153Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.0.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.3.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.3.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.9.*'" }, +] + +[[package]] +name = "coverage" +version = "7.11.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/12/95/c49df0aceb5507a80b9fe5172d3d39bf23f05be40c23c8d77d556df96cec/coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31", size = 215800, upload-time = "2025-10-15T15:12:19.824Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/dc/c6/7bb46ce01ed634fff1d7bb53a54049f539971862cc388b304ff3c51b4f66/coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075", size = 216198, upload-time = "2025-10-15T15:12:22.549Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/94/b2/75d9d8fbf2900268aca5de29cd0a0fe671b0f69ef88be16767cc3c828b85/coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab", size = 242953, upload-time = "2025-10-15T15:12:24.139Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/65/ac/acaa984c18f440170525a8743eb4b6c960ace2dbad80dc22056a437fc3c6/coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0", size = 244766, upload-time = "2025-10-15T15:12:25.974Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d8/0d/938d0bff76dfa4a6b228c3fc4b3e1c0e2ad4aa6200c141fcda2bd1170227/coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785", size = 246625, upload-time = "2025-10-15T15:12:27.387Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/38/54/8f5f5e84bfa268df98f46b2cb396b1009734cfb1e5d6adb663d284893b32/coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591", size = 243568, upload-time = "2025-10-15T15:12:28.799Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/68/30/8ba337c2877fe3f2e1af0ed7ff4be0c0c4aca44d6f4007040f3ca2255e99/coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088", size = 244665, upload-time = "2025-10-15T15:12:30.297Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cc/fb/c6f1d6d9a665536b7dde2333346f0cc41dc6a60bd1ffc10cd5c33e7eb000/coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f", size = 242681, upload-time = "2025-10-15T15:12:32.326Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/be/38/1b532319af5f991fa153c20373291dc65c2bf532af7dbcffdeef745c8f79/coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866", size = 242912, upload-time = "2025-10-15T15:12:34.079Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/67/3d/f39331c60ef6050d2a861dc1b514fa78f85f792820b68e8c04196ad733d6/coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841", size = 243559, upload-time = "2025-10-15T15:12:35.809Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4b/55/cb7c9df9d0495036ce582a8a2958d50c23cd73f84a23284bc23bd4711a6f/coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf", size = 218266, upload-time = "2025-10-15T15:12:37.429Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/68/a8/b79cb275fa7bd0208767f89d57a1b5f6ba830813875738599741b97c2e04/coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969", size = 219169, upload-time = "2025-10-15T15:12:39.25Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/49/3a/ee1074c15c408ddddddb1db7dd904f6b81bc524e01f5a1c5920e13dbde23/coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847", size = 215912, upload-time = "2025-10-15T15:12:40.665Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/70/c4/9f44bebe5cb15f31608597b037d78799cc5f450044465bcd1ae8cb222fe1/coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc", size = 216310, upload-time = "2025-10-15T15:12:42.461Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/01/5e06077cfef92d8af926bdd86b84fb28bf9bc6ad27343d68be9b501d89f2/coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0", size = 246706, upload-time = "2025-10-15T15:12:44.001Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/40/b8/7a3f1f33b35cc4a6c37e759137533119560d06c0cc14753d1a803be0cd4a/coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7", size = 248634, upload-time = "2025-10-15T15:12:45.768Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7a/41/7f987eb33de386bc4c665ab0bf98d15fcf203369d6aacae74f5dd8ec489a/coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623", size = 250741, upload-time = "2025-10-15T15:12:47.222Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/23/c1/a4e0ca6a4e83069fb8216b49b30a7352061ca0cb38654bd2dc96b7b3b7da/coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287", size = 246837, upload-time = "2025-10-15T15:12:48.904Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5d/03/ced062a17f7c38b4728ff76c3acb40d8465634b20b4833cdb3cc3a74e115/coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552", size = 248429, upload-time = "2025-10-15T15:12:50.73Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/97/af/a7c6f194bb8c5a2705ae019036b8fe7f49ea818d638eedb15fdb7bed227c/coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de", size = 246490, upload-time = "2025-10-15T15:12:52.646Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ab/c3/aab4df02b04a8fde79068c3c41ad7a622b0ef2b12e1ed154da986a727c3f/coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601", size = 246208, upload-time = "2025-10-15T15:12:54.586Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/30/d8/e282ec19cd658238d60ed404f99ef2e45eed52e81b866ab1518c0d4163cf/coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e", size = 247126, upload-time = "2025-10-15T15:12:56.485Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d1/17/a635fa07fac23adb1a5451ec756216768c2767efaed2e4331710342a3399/coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c", size = 218314, upload-time = "2025-10-15T15:12:58.365Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2a/29/2ac1dfcdd4ab9a70026edc8d715ece9b4be9a1653075c658ee6f271f394d/coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9", size = 219203, upload-time = "2025-10-15T15:12:59.902Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/03/21/5ce8b3a0133179115af4c041abf2ee652395837cb896614beb8ce8ddcfd9/coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745", size = 217879, upload-time = "2025-10-15T15:13:01.35Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098, upload-time = "2025-10-15T15:13:02.916Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331, upload-time = "2025-10-15T15:13:04.403Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825, upload-time = "2025-10-15T15:13:05.92Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893", size = 250573, upload-time = "2025-10-15T15:13:07.471Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115", size = 251706, upload-time = "2025-10-15T15:13:09.4Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415", size = 248221, upload-time = "2025-10-15T15:13:10.964Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186", size = 249624, upload-time = "2025-10-15T15:13:12.5Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d", size = 247744, upload-time = "2025-10-15T15:13:14.554Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d", size = 247325, upload-time = "2025-10-15T15:13:16.438Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2", size = 249180, upload-time = "2025-10-15T15:13:17.959Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5", size = 218479, upload-time = "2025-10-15T15:13:19.608Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0", size = 219290, upload-time = "2025-10-15T15:13:21.593Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad", size = 217924, upload-time = "2025-10-15T15:13:23.39Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.3.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "filelock" +version = "3.12.2" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/00/0b/c506e9e44e4c4b6c89fcecda23dc115bf8e7ff7eb127e0cb9c114cbc9a15/filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81", size = 12441, upload-time = "2023-06-12T22:02:09.617Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/00/45/ec3407adf6f6b5bf867a4462b2b0af27597a26bd3cd6e2534cb6ab029938/filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec", size = 10923, upload-time = "2023-06-12T22:02:08.03Z" }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037, upload-time = "2024-09-17T19:02:01.779Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163, upload-time = "2024-09-17T19:02:00.268Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + +[[package]] +name = "gmssl-python" +version = "2.2.2" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pip", version = "24.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pip", version = "25.0.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pip", version = "25.2", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pre-commit", version = "2.21.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pre-commit", version = "3.5.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pre-commit", version = "4.3.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "7.4.4", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest-cov", version = "4.1.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest-cov", version = "5.0.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest-cov", version = "7.0.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "ruff" }, + { name = "setuptools", version = "68.0.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "setuptools", version = "75.3.2", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "setuptools", version = "80.9.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pip", specifier = ">=24.0" }, + { name = "pre-commit", specifier = ">=2.21.0" }, + { name = "pytest", specifier = ">=7.4.4" }, + { name = "pytest-cov", specifier = ">=4.1.0" }, + { name = "ruff", specifier = ">=0.14.1" }, + { name = "setuptools", specifier = ">=68.0.0" }, +] + +[[package]] +name = "identify" +version = "2.5.24" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c4/f8/498e13e408d25ee6ff04aa0acbf91ad8e9caae74be91720fc0e811e649b7/identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4", size = 98886, upload-time = "2023-05-03T14:45:38.863Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4f/fd/2c46fba2bc032ba4c970bb8de59d25187087d7138a0ebf7c1dcc91d94f01/identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d", size = 98826, upload-time = "2023-05-03T14:45:34.823Z" }, +] + +[[package]] +name = "identify" +version = "2.6.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097, upload-time = "2024-09-14T23:50:32.513Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972, upload-time = "2024-09-14T23:50:30.747Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "zipp", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569, upload-time = "2023-06-18T21:44:35.024Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934, upload-time = "2023-06-18T21:44:33.441Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "24.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882, upload-time = "2024-03-10T09:39:28.33Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488, upload-time = "2024-03-10T09:39:25.947Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pip" +version = "24.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/94/59/6638090c25e9bc4ce0c42817b5a234e183872a1129735a9330c472cc2056/pip-24.0.tar.gz", hash = "sha256:ea9bd1a847e8c5774a5777bb398c19e80bcd4e2aa16a4b301b718fe6f593aba2", size = 2132709, upload-time = "2024-02-03T09:53:18.999Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl", hash = "sha256:ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc", size = 2110226, upload-time = "2024-02-03T09:53:09.575Z" }, +] + +[[package]] +name = "pip" +version = "25.0.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/70/53/b309b4a497b09655cb7e07088966881a57d082f48ac3cb54ea729fd2c6cf/pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea", size = 1950850, upload-time = "2025-02-09T17:14:04.423Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c9/bc/b7db44f5f39f9d0494071bddae6880eb645970366d0a200022a1a93d57f5/pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f", size = 1841526, upload-time = "2025-02-09T17:14:01.463Z" }, +] + +[[package]] +name = "pip" +version = "25.2" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/20/16/650289cd3f43d5a2fadfd98c68bd1e1e7f2550a1a5326768cddfbcedb2c5/pip-25.2.tar.gz", hash = "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2", size = 1840021, upload-time = "2025-07-30T21:50:15.401Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b7/3f/945ef7ab14dc4f9d7f40288d2df998d1837ee0888ec3659c813487572faa/pip-25.2-py3-none-any.whl", hash = "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717", size = 1752557, upload-time = "2025-07-30T21:50:13.323Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.0.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/31/28/e40d24d2e2eb23135f8533ad33d582359c7825623b1e022f9d460def7c05/platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731", size = 19914, upload-time = "2023-11-10T16:43:08.316Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/31/16/70be3b725073035aa5fc3229321d06e22e73e3e09f6af78dcfdf16c7636c/platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", size = 17562, upload-time = "2023-11-10T16:43:06.949Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8a/42/8f2833655a29c4e9cb52ee8a2be04ceac61bcff4a680fb338cbd3d1e322d/pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3", size = 61613, upload-time = "2023-06-21T09:12:28.745Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/51/32/4a79112b8b87b21450b066e102d6608907f4c885ed7b04c3fdb085d4d6ae/pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", size = 17695, upload-time = "2023-06-21T09:12:27.397Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "2.21.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "cfgv", version = "3.3.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "identify", version = "2.5.24", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, + { name = "nodeenv", marker = "python_full_version < '3.8'" }, + { name = "pyyaml", version = "6.0.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "virtualenv", version = "20.26.6", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6b/00/1637ae945c6e10838ef5c41965f1c864e59301811bb203e979f335608e7c/pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658", size = 174966, upload-time = "2022-12-25T22:53:01.144Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a6/6b/6cfe3a8b351b54f4b6c6d2ad4286804e3367f628dce379c603d3b96635f4/pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad", size = 201938, upload-time = "2022-12-25T22:52:59.649Z" }, +] + +[[package]] +name = "pre-commit" +version = "3.5.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "cfgv", version = "3.4.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "identify", version = "2.6.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "nodeenv", marker = "python_full_version == '3.8.*'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "virtualenv", version = "20.35.3", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/04/b3/4ae08d21eb097162f5aad37f4585f8069a86402ed7f5362cc9ae097f9572/pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32", size = 177079, upload-time = "2023-10-13T15:57:48.334Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6c/75/526915fedf462e05eeb1c75ceaf7e3f9cde7b5ce6f62740fe5f7f19a0050/pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660", size = 203698, upload-time = "2023-10-13T15:57:46.378Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "cfgv", version = "3.4.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "identify", version = "2.6.15", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "nodeenv", marker = "python_full_version >= '3.9'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "virtualenv", version = "20.35.3", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.8' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.8'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, + { name = "iniconfig", version = "2.0.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "packaging", version = "24.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pluggy", version = "1.2.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "tomli", version = "2.0.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.8.*' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.8.*'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "packaging", version = "25.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "tomli", version = "2.3.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "packaging", version = "25.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pygments", marker = "python_full_version >= '3.9'" }, + { name = "tomli", version = "2.3.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "coverage", version = "7.2.7", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, extra = ["toml"], marker = "python_full_version < '3.8'" }, + { name = "pytest", version = "7.4.4", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245, upload-time = "2023-05-24T18:44:56.845Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949, upload-time = "2023-05-24T18:44:54.079Z" }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "coverage", version = "7.6.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, extra = ["toml"], marker = "python_full_version == '3.8.*'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, extra = ["toml"], marker = "python_full_version == '3.9.*'" }, + { name = "coverage", version = "7.11.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201, upload-time = "2023-07-18T00:00:23.308Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", size = 189447, upload-time = "2023-07-17T23:57:04.325Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f", size = 169264, upload-time = "2023-07-17T23:57:07.787Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", size = 677003, upload-time = "2023-07-17T23:57:13.144Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", size = 699070, upload-time = "2023-07-17T23:57:19.402Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", size = 705525, upload-time = "2023-07-17T23:57:25.272Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", size = 707514, upload-time = "2023-08-28T18:43:20.945Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", size = 130488, upload-time = "2023-07-17T23:57:28.144Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", size = 145338, upload-time = "2023-07-17T23:57:31.118Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867, upload-time = "2023-07-17T23:57:34.35Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530, upload-time = "2023-07-17T23:57:36.975Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244, upload-time = "2023-07-17T23:57:43.774Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871, upload-time = "2023-07-17T23:57:51.921Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729, upload-time = "2023-07-17T23:57:59.865Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528, upload-time = "2023-08-28T18:43:23.207Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286, upload-time = "2023-07-17T23:58:02.964Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699, upload-time = "2023-07-17T23:58:05.586Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", size = 178692, upload-time = "2023-08-28T18:43:24.924Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", size = 165622, upload-time = "2023-08-28T18:43:26.54Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", size = 696937, upload-time = "2024-01-18T20:40:22.92Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", size = 724969, upload-time = "2023-08-28T18:43:28.56Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", size = 712604, upload-time = "2023-08-28T18:43:30.206Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", size = 126098, upload-time = "2023-08-28T18:43:31.835Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", size = 138675, upload-time = "2023-08-28T18:43:33.613Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/d1/02baa09d39b1bb1ebaf0d850d106d1bdcb47c91958557f471153c49dc03b/PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", size = 189627, upload-time = "2023-07-17T23:58:40.188Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e5/31/ba812efa640a264dbefd258986a5e4e786230cb1ee4a9f54eb28ca01e14a/PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", size = 658438, upload-time = "2023-07-17T23:58:48.34Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4d/f1/08f06159739254c8947899c9fc901241614195db15ba8802ff142237664c/PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", size = 680304, upload-time = "2023-07-17T23:58:57.396Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d7/8f/db62b0df635b9008fe90aa68424e99cee05e68b398740c8a666a98455589/PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", size = 670140, upload-time = "2023-07-17T23:59:04.291Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cc/5c/fcabd17918348c7db2eeeb0575705aaf3f7ab1657f6ce29b2e31737dd5d1/PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", size = 137577, upload-time = "2023-07-17T23:59:07.267Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1e/ae/964ccb88a938f20ece5754878f182cfbd846924930d02d29d06af8d4c69e/PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", size = 153248, upload-time = "2023-07-17T23:59:10.608Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7f/5d/2779ea035ba1e533c32ed4a249b4e0448f583ba10830b21a3cddafe11a4e/PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", size = 191734, upload-time = "2023-07-17T23:59:13.869Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e1/a1/27bfac14b90adaaccf8c8289f441e9f76d94795ec1e7a8f134d9f2cb3d0b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", size = 723767, upload-time = "2023-07-17T23:59:20.686Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c1/39/47ed4d65beec9ce07267b014be85ed9c204fa373515355d3efa62d19d892/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", size = 749067, upload-time = "2023-07-17T23:59:28.747Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c8/6b/6600ac24725c7388255b2f5add93f91e58a5d7efaf4af244fdbcc11a541b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", size = 736569, upload-time = "2023-07-17T23:59:37.216Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0d/46/62ae77677e532c0af6c81ddd6f3dbc16bdcc1208b077457354442d220bfb/PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", size = 787738, upload-time = "2023-08-28T18:43:35.582Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d6/6a/439d1a6f834b9a9db16332ce16c4a96dd0e3970b65fe08cbecd1711eeb77/PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", size = 139797, upload-time = "2023-07-17T23:59:40.254Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/0f/9782fa5b10152abf033aec56a601177ead85ee03b57781f2d9fced09eefc/PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", size = 157350, upload-time = "2023-07-17T23:59:42.94Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/57/c5/5d09b66b41d549914802f482a2118d925d876dc2a35b2d127694c1345c34/PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", size = 197846, upload-time = "2023-07-17T23:59:46.424Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0e/88/21b2f16cb2123c1e9375f2c93486e35fdc86e63f02e274f0e99c589ef153/PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", size = 174396, upload-time = "2023-07-17T23:59:49.538Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ac/6c/967d91a8edf98d2b2b01d149bd9e51b8f9fb527c98d80ebb60c6b21d60c4/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", size = 731824, upload-time = "2023-07-17T23:59:58.111Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4a/4b/c71ef18ef83c82f99e6da8332910692af78ea32bd1d1d76c9787dfa36aea/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", size = 754777, upload-time = "2023-07-18T00:00:06.716Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7d/39/472f2554a0f1e825bd7c5afc11c817cd7a2f3657460f7159f691fbb37c51/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", size = 738883, upload-time = "2023-07-18T00:00:14.423Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/40/da/a175a35cf5583580e90ac3e2a3dbca90e43011593ae62ce63f79d7b28d92/PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", size = 750294, upload-time = "2023-08-28T18:43:37.153Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/24/62/7fcc372442ec8ea331da18c24b13710e010c5073ab851ef36bf9dacb283f/PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", size = 136936, upload-time = "2023-07-18T00:00:17.167Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/84/4d/82704d1ab9290b03da94e6425f5e87396b999fd7eb8e08f3a92c158402bf/PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", size = 152751, upload-time = "2023-07-18T00:00:19.939Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0d/a2/09f67a3589cb4320fb5ce90d3fd4c9752636b8b6ad8f34b54d76c5a54693/PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", size = 186824, upload-time = "2025-09-29T20:27:35.918Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/02/72/d972384252432d57f248767556ac083793292a4adf4e2d85dfe785ec2659/PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", size = 795069, upload-time = "2025-09-29T20:27:38.15Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a7/3b/6c58ac0fa7c4e1b35e48024eb03d00817438310447f93ef4431673c24138/PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", size = 862585, upload-time = "2025-09-29T20:27:39.715Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/25/a2/b725b61ac76a75583ae7104b3209f75ea44b13cfd026aa535ece22b7f22e/PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", size = 806018, upload-time = "2025-09-29T20:27:41.444Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6f/b0/b2227677b2d1036d84f5ee95eb948e7af53d59fe3e4328784e4d290607e0/PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", size = 802822, upload-time = "2025-09-29T20:27:42.885Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/99/a5/718a8ea22521e06ef19f91945766a892c5ceb1855df6adbde67d997ea7ed/PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", size = 143744, upload-time = "2025-09-29T20:27:44.487Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/76/b2/2b69cee94c9eb215216fc05778675c393e3aa541131dc910df8e52c83776/PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", size = 160082, upload-time = "2025-09-29T20:27:46.049Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9e/58/6ca66896635352812de66f71cdf9ff86b3a4f79071ca5730088c0cd0fc8d/ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69", size = 5513429, upload-time = "2025-10-16T18:05:41.766Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8d/39/9cc5ab181478d7a18adc1c1e051a84ee02bec94eb9bdfd35643d7c74ca31/ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b", size = 12445415, upload-time = "2025-10-16T18:04:48.227Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/2e/1226961855ccd697255988f5a2474890ac7c5863b080b15bd038df820818/ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224", size = 12784267, upload-time = "2025-10-16T18:04:52.515Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c1/ea/fd9e95863124ed159cd0667ec98449ae461de94acda7101f1acb6066da00/ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5", size = 11781872, upload-time = "2025-10-16T18:04:55.396Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1e/5a/e890f7338ff537dba4589a5e02c51baa63020acfb7c8cbbaea4831562c96/ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896", size = 12226558, upload-time = "2025-10-16T18:04:58.166Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a6/7a/8ab5c3377f5bf31e167b73651841217542bcc7aa1c19e83030835cc25204/ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61", size = 12187898, upload-time = "2025-10-16T18:05:01.455Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/48/8d/ba7c33aa55406955fc124e62c8259791c3d42e3075a71710fdff9375134f/ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6", size = 12939168, upload-time = "2025-10-16T18:05:04.397Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b4/c2/70783f612b50f66d083380e68cbd1696739d88e9b4f6164230375532c637/ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345", size = 14386942, upload-time = "2025-10-16T18:05:07.102Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/48/44/cd7abb9c776b66d332119d67f96acf15830d120f5b884598a36d9d3f4d83/ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf", size = 13990622, upload-time = "2025-10-16T18:05:09.882Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/eb/56/4259b696db12ac152fe472764b4f78bbdd9b477afd9bc3a6d53c01300b37/ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c", size = 13431143, upload-time = "2025-10-16T18:05:13.46Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e0/35/266a80d0eb97bd224b3265b9437bd89dde0dcf4faf299db1212e81824e7e/ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151", size = 13132844, upload-time = "2025-10-16T18:05:16.1Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/65/6e/d31ce218acc11a8d91ef208e002a31acf315061a85132f94f3df7a252b18/ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192", size = 13401241, upload-time = "2025-10-16T18:05:19.395Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/b5/dbc4221bf0b03774b3b2f0d47f39e848d30664157c15b965a14d890637d2/ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd", size = 12132476, upload-time = "2025-10-16T18:05:22.163Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/98/4b/ac99194e790ccd092d6a8b5f341f34b6e597d698e3077c032c502d75ea84/ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020", size = 12139749, upload-time = "2025-10-16T18:05:25.162Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/47/26/7df917462c3bb5004e6fdfcc505a49e90bcd8a34c54a051953118c00b53a/ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5", size = 12544758, upload-time = "2025-10-16T18:05:28.018Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/64/d0/81e7f0648e9764ad9b51dd4be5e5dac3fcfff9602428ccbae288a39c2c22/ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d", size = 13221811, upload-time = "2025-10-16T18:05:30.707Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c3/07/3c45562c67933cc35f6d5df4ca77dabbcd88fddaca0d6b8371693d29fd56/ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6", size = 12319467, upload-time = "2025-10-16T18:05:33.261Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/02/88/0ee4ca507d4aa05f67e292d2e5eb0b3e358fbcfe527554a2eda9ac422d6b/ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1", size = 13401123, upload-time = "2025-10-16T18:05:35.984Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636, upload-time = "2025-10-16T18:05:38.995Z" }, +] + +[[package]] +name = "setuptools" +version = "68.0.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/dc/98/5f896af066c128669229ff1aa81553ac14cfb3e5e74b6b44594132b8540e/setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235", size = 2194111, upload-time = "2023-06-19T15:53:05.082Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/42/be1c7bbdd83e1bfb160c94b9cafd8e25efc7400346cf7ccdbdb452c467fa/setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", size = 804037, upload-time = "2023-06-19T15:53:03.089Z" }, +] + +[[package]] +name = "setuptools" +version = "75.3.2" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5c/01/771ea46cce201dd42cff043a5eea929d1c030fb3d1c2ee2729d02ca7814c/setuptools-75.3.2.tar.gz", hash = "sha256:3c1383e1038b68556a382c1e8ded8887cd20141b0eb5708a6c8d277de49364f5", size = 1354489, upload-time = "2025-03-12T00:02:19.004Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/15/65/3f0dba35760d902849d39d38c0a72767794b1963227b69a587f8a336d08c/setuptools-75.3.2-py3-none-any.whl", hash = "sha256:90ab613b6583fc02d5369cbca13ea26ea0e182d1df2d943ee9cbe81d4c61add9", size = 1251198, upload-time = "2025-03-12T00:02:17.554Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3c/8b/0111dd7d6c1478bf83baa1cab85c686426c7a6274119aceb2bd9d35395ad/typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2", size = 72876, upload-time = "2023-07-02T14:20:55.045Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ec/6b/63cc3df74987c36fe26157ee12e09e8f9db4de771e0f3404263117e75b95/typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", size = 33232, upload-time = "2023-07-02T14:20:53.275Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.26.6" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "distlib", marker = "python_full_version < '3.8'" }, + { name = "filelock", version = "3.12.2", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, + { name = "platformdirs", version = "4.0.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482, upload-time = "2024-09-27T16:28:57.502Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862, upload-time = "2024-09-27T16:28:54.798Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.3" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "distlib", marker = "python_full_version >= '3.8'" }, + { name = "filelock", version = "3.16.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "filelock", version = "3.20.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "platformdirs", version = "4.5.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44", size = 6002907, upload-time = "2025-10-10T21:23:33.178Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a", size = 5981061, upload-time = "2025-10-10T21:23:30.433Z" }, +] + +[[package]] +name = "zipp" +version = "3.15.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454, upload-time = "2023-02-25T02:17:22.503Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758, upload-time = "2023-02-25T02:17:20.807Z" }, +]