diff --git a/.github/workflows/docker-stable.yml b/.github/workflows/docker-stable.yml index 4461498..2a4c92d 100644 --- a/.github/workflows/docker-stable.yml +++ b/.github/workflows/docker-stable.yml @@ -21,18 +21,18 @@ jobs: fi echo "Version ${{ inputs.version }} found on PyPI - proceeding with release" - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub with Organization Token + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build & Push Stable Docker uses: docker/build-push-action@v5 with: @@ -40,4 +40,5 @@ jobs: platforms: linux/amd64,linux/arm64 tags: socketdev/cli:stable build-args: | - CLI_VERSION=${{ inputs.version }} \ No newline at end of file + CLI_VERSION=${{ inputs.version }} + \ No newline at end of file diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 8c706ac..f3b142a 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -119,19 +119,19 @@ jobs: echo "success=false" >> $GITHUB_OUTPUT exit 1 - - name: Login to Docker Hub - if: steps.verify_package.outputs.success == 'true' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub with Organization Token + if: steps.verify_package.outputs.success == 'true' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build & Push Docker Preview if: steps.verify_package.outputs.success == 'true' uses: docker/build-push-action@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a5d0c6..b70d26e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,18 +68,18 @@ jobs: if: steps.version_check.outputs.pypi_exists != 'true' uses: pypa/gh-action-pypi-publish@v1.12.4 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub with Organization Token + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Verify package is installable id: verify_package env: diff --git a/.gitignore b/.gitignore index c481ee5..d6408eb 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ file_generator.py .env.local Pipfile test/ -logs \ No newline at end of file +logs +ai_testing/ +verify_find_files_lazy_loading.py diff --git a/Dockerfile b/Dockerfile index 76d3721..040036d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,5 +18,5 @@ RUN for i in $(seq 1 10); do \ sleep 30; \ done && \ if [ ! -z "$SDK_VERSION" ]; then \ - pip install --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socket-sdk-python==${SDK_VERSION}; \ + pip install --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketdev==${SDK_VERSION}; \ fi \ No newline at end of file diff --git a/Makefile b/Makefile index e1bc1ad..c0fb1b0 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -.PHONY: setup compile-deps sync-deps clean test lint init-tools local-dev first-time-setup update-deps dev-setup sync-all first-time-local-setup +.PHONY: setup sync clean test lint update-lock local-dev first-time-setup dev-setup sync-all first-time-local-setup # Environment variable for local SDK path (optional) -SOCKET_SDK_PATH ?= ../socket-sdk-python +SOCKET_SDK_PATH ?= ../socketdev # Environment variable to control local development mode USE_LOCAL_SDK ?= false @@ -16,44 +16,37 @@ first-time-local-setup: $(MAKE) clean $(MAKE) USE_LOCAL_SDK=true dev-setup -# Update dependencies after changing pyproject.toml -update-deps: compile-deps sync-deps +# Update lock file after changing pyproject.toml +update-lock: + uv lock # Setup for local development dev-setup: clean local-dev setup # Sync all dependencies after pulling changes -sync-all: sync-deps +sync-all: sync # === Implementation targets === -# Creates virtual environment and installs pip-tools -init-tools: - python -m venv .venv - . .venv/bin/activate && pip install pip-tools - # Installs dependencies needed for local development -# Currently: socket-sdk-python from test PyPI or local path -local-dev: init-tools +# Currently: socketdev from test PyPI or local path +local-dev: ifeq ($(USE_LOCAL_SDK),true) - . .venv/bin/activate && pip install -e $(SOCKET_SDK_PATH) + uv add --editable $(SOCKET_SDK_PATH) endif -# Creates/updates requirements.txt files with locked versions based on pyproject.toml -compile-deps: local-dev - . .venv/bin/activate && pip-compile --output-file=requirements.txt pyproject.toml - . .venv/bin/activate && pip-compile --extra=dev --output-file=requirements-dev.txt pyproject.toml - . .venv/bin/activate && pip-compile --extra=test --output-file=requirements-test.txt pyproject.toml - -# Creates virtual environment and installs dependencies from pyproject.toml -setup: compile-deps - . .venv/bin/activate && pip install -e ".[dev,test]" +# Creates virtual environment and installs dependencies from uv.lock +setup: update-lock + uv sync --all-extras +ifeq ($(USE_LOCAL_SDK),true) + uv add --editable $(SOCKET_SDK_PATH) +endif -# Installs exact versions from requirements.txt into your virtual environment -sync-deps: - . .venv/bin/activate && pip-sync requirements.txt requirements-dev.txt requirements-test.txt +# Installs exact versions from uv.lock into your virtual environment +sync: + uv sync --all-extras ifeq ($(USE_LOCAL_SDK),true) - . .venv/bin/activate && pip install -e $(SOCKET_SDK_PATH) + uv add --editable $(SOCKET_SDK_PATH) endif # Removes virtual environment and cache files @@ -62,8 +55,8 @@ clean: find . -type d -name "__pycache__" -exec rm -rf {} + test: - pytest + uv run pytest lint: - ruff check . - ruff format --check . \ No newline at end of file + uv run ruff check . + uv run ruff format --check . \ No newline at end of file diff --git a/README.md b/README.md index 66fd945..89ff153 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,100 @@ # Socket Security CLI -The Socket Security CLI was created to enable integrations with other tools like GitHub Actions, Gitlab, BitBucket, local use cases and more. The tool will get the head scan for the provided repo from Socket, create a new one, and then report any new alerts detected. If there are new alerts against the Socket security policy it'll exit with a non-Zero exit code. +The Socket Security CLI was created to enable integrations with other tools like GitHub Actions, GitLab, BitBucket, local use cases and more. The tool will get the head scan for the provided repo from Socket, create a new one, and then report any new alerts detected. If there are new alerts against the Socket security policy it'll exit with a non-Zero exit code. + +## Quick Start + +The CLI now features automatic detection of git repository information, making it much simpler to use in CI/CD environments. Most parameters are now optional and will be detected automatically from your git repository. + +### Minimal Usage Examples + +**GitHub Actions:** +```bash +socketcli --target-path $GITHUB_WORKSPACE --scm github --pr-number $PR_NUMBER +``` + +**GitLab CI:** +```bash +socketcli --target-path $CI_PROJECT_DIR --scm gitlab --pr-number ${CI_MERGE_REQUEST_IID:-0} +``` + +**Local Development:** +```bash +socketcli --target-path ./my-project +``` + +The CLI will automatically detect: +- Repository name from git remote +- Branch name from git +- Commit SHA and message from git +- Committer information from git +- Default branch status from git and CI environment +- Changed files from git commit history + +## CI/CD Workflow Examples + +Pre-configured workflow examples are available in the [`workflows/`](workflows/) directory: + +- **[GitHub Actions](workflows/github-actions.yml)** - Complete workflow with concurrency control and automatic PR detection +- **[GitLab CI](workflows/gitlab-ci.yml)** - Pipeline configuration with caching and environment variable handling +- **[Bitbucket Pipelines](workflows/bitbucket-pipelines.yml)** - Basic pipeline setup with optional path filtering + +These examples are production-ready and include best practices for each platform. + +## Monorepo Workspace Support + +The Socket CLI supports scanning specific workspaces within monorepo structures while preserving git context from the repository root. This is useful for organizations that maintain multiple applications or services in a single repository. + +### Key Features + +- **Multiple Sub-paths**: Specify multiple `--sub-path` options to scan different directories within your monorepo +- **Combined Workspace**: All sub-paths are scanned together as a single workspace in Socket +- **Git Context Preserved**: Repository metadata (commits, branches, etc.) comes from the main target-path +- **Workspace Naming**: Use `--workspace-name` to differentiate scans from different parts of your monorepo + +### Usage Examples + +**Scan multiple frontend and backend workspaces:** +```bash +socketcli --target-path /path/to/monorepo \ + --sub-path frontend \ + --sub-path backend \ + --sub-path services/api \ + --workspace-name main-app +``` + +**GitHub Actions for monorepo workspace:** +```bash +socketcli --target-path $GITHUB_WORKSPACE \ + --sub-path packages/web \ + --sub-path packages/mobile \ + --workspace-name mobile-web \ + --scm github \ + --pr-number $PR_NUMBER +``` + +This will: +- Scan manifest files in `./packages/web/` and `./packages/mobile/` +- Combine them into a single workspace scan +- Create a repository in Socket named like `my-repo-mobile-web` +- Preserve git context (commits, branch info) from the repository root + +### Requirements + +- Both `--sub-path` and `--workspace-name` must be specified together +- `--sub-path` can be used multiple times to include multiple directories +- All specified sub-paths must exist within the target-path ## Usage ```` shell -socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--integration {api,github,gitlab}] [--owner OWNER] [--branch BRANCH] - [--committers [COMMITTERS ...]] [--pr-number PR_NUMBER] [--commit-message COMMIT_MESSAGE] [--commit-sha COMMIT_SHA] - [--target-path TARGET_PATH] [--sbom-file SBOM_FILE] [--files FILES] [--default-branch] [--pending-head] - [--generate-license] [--enable-debug] [--enable-json] [--enable-sarif] [--disable-overview] [--disable-security-issue] - [--allow-unverified] [--ignore-commit-files] [--disable-blocking] [--scm SCM] [--timeout TIMEOUT] - [--exclude-license-details] +socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--repo-is-public] [--branch BRANCH] [--integration {api,github,gitlab,azure,bitbucket}] + [--owner OWNER] [--pr-number PR_NUMBER] [--commit-message COMMIT_MESSAGE] [--commit-sha COMMIT_SHA] [--committers [COMMITTERS ...]] + [--target-path TARGET_PATH] [--sbom-file SBOM_FILE] [--license-file-name LICENSE_FILE_NAME] [--save-submitted-files-list SAVE_SUBMITTED_FILES_LIST] + [--save-manifest-tar SAVE_MANIFEST_TAR] [--files FILES] [--sub-path SUB_PATH] [--workspace-name WORKSPACE_NAME] + [--excluded-ecosystems EXCLUDED_ECOSYSTEMS] [--default-branch] [--pending-head] [--generate-license] [--enable-debug] + [--enable-json] [--enable-sarif] [--disable-overview] [--exclude-license-details] [--allow-unverified] [--disable-security-issue] + [--ignore-commit-files] [--disable-blocking] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders] [--version] ```` If you don't want to provide the Socket API Token every time then you can use the environment variable `SOCKET_SECURITY_API_KEY` @@ -25,33 +109,39 @@ If you don't want to provide the Socket API Token every time then you can use th #### Repository | Parameter | Required | Default | Description | |:-----------------|:---------|:--------|:------------------------------------------------------------------------| -| --repo | False | | Repository name in owner/repo format | -| --integration | False | api | Integration type (api, github, gitlab) | -| --owner | False | | Name of the integration owner, defaults to the socket organization slug | -| --branch | False | "" | Branch name | -| --committers | False | | Committer(s) to filter by | +| --repo | False | *auto* | Repository name in owner/repo format (auto-detected from git remote) | | --repo-is-public | False | False | If set, flags a new repository creation as public. Defaults to false. | +| --integration | False | api | Integration type (api, github, gitlab, azure, bitbucket) | +| --owner | False | | Name of the integration owner, defaults to the socket organization slug | +| --branch | False | *auto* | Branch name (auto-detected from git) | +| --committers | False | *auto* | Committer(s) to filter by (auto-detected from git commit) | #### Pull Request and Commit -| Parameter | Required | Default | Description | -|:-----------------|:---------|:--------|:--------------------| -| --pr-number | False | "0" | Pull request number | -| --commit-message | False | | Commit message | -| --commit-sha | False | "" | Commit SHA | +| Parameter | Required | Default | Description | +|:-----------------|:---------|:--------|:-----------------------------------------------| +| --pr-number | False | "0" | Pull request number | +| --commit-message | False | *auto* | Commit message (auto-detected from git) | +| --commit-sha | False | *auto* | Commit SHA (auto-detected from git) | #### Path and File -| Parameter | Required | Default | Description | -|:-------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| --target-path | False | ./ | Target path for analysis | -| --sbom-file | False | | SBOM file path | -| --files | False | [] | Files to analyze (JSON array string) | -| --exclude-patterns | False | [] | List of patterns to exclude from analysis (JSON array string). You can get supported files form the [Supported Files API](https://docs.socket.dev/reference/getsupportedfiles) | +| Parameter | Required | Default | Description | +|:----------------------------|:---------|:----------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| --target-path | False | ./ | Target path for analysis | +| --sbom-file | False | | SBOM file path | +| --license-file-name | False | `license_output.json` | Name of the file to save the license details to if enabled | +| --save-submitted-files-list | False | | Save list of submitted file names to JSON file for debugging purposes | +| --save-manifest-tar | False | | Save all manifest files to a compressed tar.gz archive with original directory structure | +| --files | False | *auto* | Files to analyze (JSON array string). Auto-detected from git commit changes when not specified | +| --sub-path | False | | Sub-path within target-path for manifest file scanning (can be specified multiple times). All sub-paths are combined into a single workspace scan while preserving git context from target-path. Must be used with --workspace-name | +| --workspace-name | False | | Workspace name suffix to append to repository name (repo-name-workspace_name). Must be used with --sub-path | +| --excluded-ecosystems | False | [] | List of ecosystems to exclude from analysis (JSON array string). You can get supported files from the [Supported Files API](https://docs.socket.dev/reference/getsupportedfiles) | #### Branch and Scan Configuration -| Parameter | Required | Default | Description | -|:-----------------|:---------|:--------|:------------------------------------------------------------| -| --default-branch | False | False | Make this branch the default branch | -| --pending-head | False | False | If true, the new scan will be set as the branch's head scan | +| Parameter | Required | Default | Description | +|:-------------------------|:---------|:--------|:------------------------------------------------------------------------------------------------------| +| --default-branch | False | *auto* | Make this branch the default branch (auto-detected from git and CI environment when not specified) | +| --pending-head | False | *auto* | If true, the new scan will be set as the branch's head scan (automatically synced with default-branch) | +| --include-module-folders | False | False | If enabled will include manifest files from folders like node_modules | #### Output Configuration | Parameter | Required | Default | Description | @@ -62,6 +152,7 @@ If you don't want to provide the Socket API Token every time then you can use th | --enable-sarif | False | False | Enable SARIF output of results instead of table or JSON format | | --disable-overview | False | False | Disable overview output | | --exclude-license-details | False | False | Exclude license details from the diff report (boosts performance for large repos) | +| --version | False | False | Show program's version number and exit | #### Security Configuration | Parameter | Required | Default | Description | @@ -74,9 +165,9 @@ If you don't want to provide the Socket API Token every time then you can use th |:-------------------------|:---------|:--------|:----------------------------------------------------------------------| | --ignore-commit-files | False | False | Ignore commit files | | --disable-blocking | False | False | Disable blocking mode | +| --enable-diff | False | False | Enable diff mode even when using --integration api (forces diff mode without SCM integration) | | --scm | False | api | Source control management type | | --timeout | False | | Timeout in seconds for API requests | -| --include-module-folders | False | False | If enabled will include manifest files from folders like node_modules | #### Plugins @@ -111,26 +202,203 @@ Example `SOCKET_SLACK_CONFIG_JSON` value {"url": "https://REPLACE_ME_WEBHOOK"} ```` +## Automatic Git Detection + +The CLI now automatically detects repository information from your git environment, significantly simplifying usage in CI/CD pipelines: + +### Auto-Detected Information + +- **Repository name**: Extracted from git remote origin URL +- **Branch name**: Current git branch or CI environment variables +- **Commit SHA**: Latest commit hash or CI-provided commit SHA +- **Commit message**: Latest commit message +- **Committer information**: Git commit author details +- **Default branch status**: Determined from git repository and CI environment +- **Changed files**: Files modified in the current commit (for differential scanning) + +### Default Branch Detection + +The CLI uses intelligent default branch detection with the following priority: + +1. **Explicit `--default-branch` flag**: Takes highest priority when specified +2. **CI environment detection**: Uses CI platform variables (GitHub Actions, GitLab CI) +3. **Git repository analysis**: Compares current branch with repository's default branch +4. **Fallback**: Defaults to `false` if none of the above methods succeed + +Both `--default-branch` and `--pending-head` parameters are automatically synchronized to ensure consistent behavior. + +## GitLab Token Configuration + +The CLI supports GitLab integration with automatic authentication pattern detection for different token types. + +### Supported Token Types + +GitLab API supports two authentication methods, and the CLI automatically detects which one to use: + +1. **Bearer Token Authentication** (`Authorization: Bearer `) + - GitLab CI Job Tokens (`$CI_JOB_TOKEN`) + - Personal Access Tokens with `glpat-` prefix + - OAuth 2.0 tokens (long alphanumeric tokens) + +2. **Private Token Authentication** (`PRIVATE-TOKEN: `) + - Legacy personal access tokens + - Custom tokens that don't match Bearer patterns + +### Token Detection Logic + +The CLI automatically determines the authentication method using this logic: + +``` +if token == $CI_JOB_TOKEN: + use Bearer authentication +elif token starts with "glpat-": + use Bearer authentication +elif token is long (>40 chars) and alphanumeric: + use Bearer authentication +else: + use PRIVATE-TOKEN authentication +``` + +### Automatic Fallback + +If the initial authentication method fails with a 401 error, the CLI automatically retries with the alternative method: + +- **Bearer → PRIVATE-TOKEN**: If Bearer authentication fails, retry with PRIVATE-TOKEN +- **PRIVATE-TOKEN → Bearer**: If PRIVATE-TOKEN fails, retry with Bearer authentication + +This ensures maximum compatibility across different GitLab configurations and token types. + +### Environment Variables + +| Variable | Description | Example | +|:---------|:------------|:--------| +| `GITLAB_TOKEN` | GitLab API token (required for GitLab integration) | `glpat-xxxxxxxxxxxxxxxxxxxx` | +| `CI_JOB_TOKEN` | GitLab CI job token (automatically used in GitLab CI) | Automatically provided by GitLab CI | + +### Usage Examples + +**GitLab CI with job token (recommended):** +```yaml +variables: + GITLAB_TOKEN: $CI_JOB_TOKEN +``` + +**GitLab CI with personal access token:** +```yaml +variables: + GITLAB_TOKEN: $GITLAB_PERSONAL_ACCESS_TOKEN # Set in GitLab project/group variables +``` + +**Local development:** +```bash +export GITLAB_TOKEN="glpat-your-personal-access-token" +socketcli --integration gitlab --repo owner/repo --pr-number 123 +``` + +### Scan Behavior + +The CLI determines scanning behavior intelligently: + +- **Manifest files changed**: Performs differential scan with PR/MR comments when supported +- **No manifest files changed**: Creates full repository scan report without waiting for diff results +- **Force API mode**: When no supported manifest files are detected, automatically enables non-blocking mode + ## File Selection Behavior The CLI determines which files to scan based on the following logic: -1. **Git Commit Files**: By default, the CLI checks files changed in the current git commit first. If any of these files match supported manifest patterns (like package.json, requirements.txt, etc.), a scan is triggered. +1. **Git Commit Files (Default)**: The CLI automatically checks files changed in the current git commit. If any of these files match supported manifest patterns (like package.json, requirements.txt, etc.), a scan is triggered. + +2. **`--files` Parameter Override**: When specified, this parameter takes precedence over git commit detection. It accepts a JSON array of file paths to check for manifest files. + +3. **`--ignore-commit-files` Flag**: When set, git commit files are ignored completely, and the CLI will scan all manifest files in the target directory regardless of what changed. -2. **`--files` Parameter**: If no git commit exists, or no manifest files are found in the commit changes, the CLI checks files specified via the `--files` parameter. This parameter accepts a JSON array of file paths. +4. **Automatic Fallback**: If no manifest files are found in git commit changes and no `--files` are specified, the CLI automatically switches to "API mode" and performs a full repository scan. -3. **`--ignore-commit-files`**: When this flag is set, git commit files are ignored completely, and only files specified in `--files` are considered. This also forces a scan regardless of whether manifest files are present. +> **Important**: The CLI doesn't scan only the specified files - it uses them to determine whether a scan should be performed and what type of scan to run. When triggered, it searches the entire `--target-path` for all supported manifest files. -4. **No Manifest Files**: If no manifest files are found in either git commit changes or `--files` (and `--ignore-commit-files` is not set), the scan is skipped. +### Scanning Modes -> **Note**: The CLI does not scan only the specified files - it uses them to determine whether a scan should be performed. When a scan is triggered, it searches the entire `--target-path` for all supported manifest files. +- **Differential Mode**: When manifest files are detected in changes, performs a diff scan with PR/MR comment integration +- **API Mode**: When no manifest files are in changes, creates a full scan report without PR comments but still scans the entire repository +- **Force Mode**: With `--ignore-commit-files`, always performs a full scan regardless of changes +- **Forced Diff Mode**: With `--enable-diff`, forces differential mode even when using `--integration api` (without SCM integration) ### Examples -- **Commit with manifest file**: If your commit includes changes to `package.json`, a scan will be triggered automatically. -- **Commit without manifest files**: If your commit only changes non-manifest files (like `.github/workflows/socket.yaml`), no scan will be performed unless you use `--files` or `--ignore-commit-files`. -- **Using `--files`**: If you specify `--files '["package.json"]'`, the CLI will check if this file exists and is a manifest file before triggering a scan. -- **Using `--ignore-commit-files`**: This forces a scan of all manifest files in the target path, regardless of what's in your commit. +- **Commit with manifest file**: If your commit includes changes to `package.json`, a differential scan will be triggered automatically with PR comment integration. +- **Commit without manifest files**: If your commit only changes non-manifest files (like `.github/workflows/socket.yaml`), the CLI automatically switches to API mode and performs a full repository scan. +- **Using `--files`**: If you specify `--files '["package.json"]'`, the CLI will check if this file exists and is a manifest file before determining scan type. +- **Using `--ignore-commit-files`**: This forces a full scan of all manifest files in the target path, regardless of what's in your commit. +- **Using `--enable-diff`**: Forces diff mode without SCM integration - useful when you want differential scanning but are using `--integration api`. For example: `socketcli --integration api --enable-diff --target-path /path/to/repo` +- **Auto-detection**: Most CI/CD scenarios now work with just `socketcli --target-path /path/to/repo --scm github --pr-number $PR_NUM` + +## Debugging and Troubleshooting + +### Saving Submitted Files List + +The CLI provides a debugging option to save the list of files that were submitted for scanning: + +```bash +socketcli --save-submitted-files-list submitted_files.json +``` + +This will create a JSON file containing: +- Timestamp of when the scan was performed +- Total number of files submitted +- Total size of all files (in bytes and human-readable format) +- Complete list of file paths that were found and submitted for scanning + +Example output file: +```json +{ + "timestamp": "2025-01-22 10:30:45 UTC", + "total_files": 3, + "total_size_bytes": 2048, + "total_size_human": "2.00 KB", + "files": [ + "./package.json", + "./requirements.txt", + "./Pipfile" + ] +} +``` + +This feature is useful for: +- **Debugging**: Understanding which files the CLI found and submitted +- **Verification**: Confirming that expected manifest files are being detected +- **Size Analysis**: Understanding the total size of manifest files being uploaded +- **Troubleshooting**: Identifying why certain files might not be included in scans or if size limits are being hit + +> **Note**: This option works with both differential scans (when git commits are detected) and full scans (API mode). + +### Saving Manifest Files Archive + +For backup, sharing, or analysis purposes, you can save all manifest files to a compressed tar.gz archive: + +```bash +socketcli --save-manifest-tar manifest_files.tar.gz +``` + +This will create a compressed archive containing all the manifest files that were found and submitted for scanning, preserving their original directory structure relative to the scanned directory. + +Example usage with other options: +```bash +# Save both files list and archive +socketcli --save-submitted-files-list files.json --save-manifest-tar backup.tar.gz + +# Use with specific target path +socketcli --target-path ./my-project --save-manifest-tar my-project-manifests.tar.gz +``` + +The manifest archive feature is useful for: +- **Backup**: Creating portable backups of all dependency manifest files +- **Sharing**: Sending the exact files being analyzed to colleagues or support +- **Analysis**: Examining the dependency files offline or with other tools +- **Debugging**: Verifying file discovery and content issues +- **Compliance**: Maintaining records of scanned dependency files + +> **Note**: The tar.gz archive preserves the original directory structure, making it easy to extract and examine the files in their proper context. ## Development @@ -151,9 +419,9 @@ make first-time-setup 2. Local Development Setup (for SDK development): ```bash pyenv local 3.11 # Ensure correct Python version -SOCKET_SDK_PATH=~/path/to/socket-sdk-python make first-time-local-setup +SOCKET_SDK_PATH=~/path/to/socketdev make first-time-local-setup ``` -The default SDK path is `../socket-sdk-python` if not specified. +The default SDK path is `../socketdev` if not specified. #### Ongoing Development Tasks @@ -172,20 +440,24 @@ make sync-all High-level workflows: - `make first-time-setup`: Complete setup using PyPI packages - `make first-time-local-setup`: Complete setup for local SDK development -- `make update-deps`: Update requirements.txt files and sync dependencies +- `make update-lock`: Update uv.lock file after changing pyproject.toml - `make sync-all`: Sync dependencies after pulling changes - `make dev-setup`: Setup for local development (included in first-time-local-setup) Implementation targets: -- `make init-tools`: Creates virtual environment and installs pip-tools - `make local-dev`: Installs dependencies needed for local development -- `make compile-deps`: Generates requirements.txt files with locked versions -- `make setup`: Creates virtual environment and installs dependencies -- `make sync-deps`: Installs exact versions from requirements.txt +- `make setup`: Creates virtual environment and installs dependencies from uv.lock +- `make sync`: Installs exact versions from uv.lock - `make clean`: Removes virtual environment and cache files -- `make test`: Runs pytest suite -- `make lint`: Runs ruff for code formatting and linting +- `make test`: Runs pytest suite using uv run +- `make lint`: Runs ruff for code formatting and linting using uv run ### Environment Variables -- `SOCKET_SDK_PATH`: Path to local socket-sdk-python repository (default: ../socket-sdk-python) +#### Core Configuration +- `SOCKET_SECURITY_API_KEY`: Socket Security API token (alternative to --api-token parameter) +- `SOCKET_SDK_PATH`: Path to local socketdev repository (default: ../socketdev) + +#### GitLab Integration +- `GITLAB_TOKEN`: GitLab API token for GitLab integration (supports both Bearer and PRIVATE-TOKEN authentication) +- `CI_JOB_TOKEN`: GitLab CI job token (automatically provided in GitLab CI environments) diff --git a/pyproject.toml b/pyproject.toml index 91f3cd7..22811e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.1.0" +version = "2.2.11" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ @@ -16,7 +16,7 @@ dependencies = [ 'GitPython', 'packaging', 'python-dotenv', - 'socket-sdk-python>=2.1.2,<3' + 'socketdev>=3.0.6,<4.0.0' ] readme = "README.md" description = "Socket Security CLI for CI/CD" @@ -45,13 +45,14 @@ test = [ dev = [ "ruff>=0.3.0", "twine", # for building - "pip-tools>=7.4.0", # for pip-compile + "uv>=0.1.0", # for dependency management "pre-commit", "hatch" ] [project.scripts] socketcli = "socketsecurity.socketcli:cli" +socketclidev = "socketsecurity.socketcli:cli" [project.urls] Homepage = "https://socket.dev" diff --git a/requirements-dev.lock b/requirements-dev.lock deleted file mode 100644 index 099e79b..0000000 --- a/requirements-dev.lock +++ /dev/null @@ -1,73 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: ["test"] -# all-features: false -# with-sources: false -# generate-hashes: false -# universal: false - -hatchling==1.27.0 -hatch==1.14.0 -argparse==1.4.0 - # via socketsecurity -certifi==2024.12.14 - # via requests -charset-normalizer==3.4.1 - # via requests -colorama==0.4.6 - # via pytest-watch -coverage==7.6.10 - # via pytest-cov -docopt==0.6.2 - # via pytest-watch -gitdb==4.0.12 - # via gitpython -gitpython==3.1.44 - # via socketsecurity -idna==3.10 - # via requests -iniconfig==2.0.0 - # via pytest -mdutils==1.6.0 - # via socketsecurity -packaging==24.2 - # via pytest - # via socketsecurity -pluggy==1.5.0 - # via pytest -prettytable==3.12.0 - # via socketsecurity -pytest==8.3.4 - # via pytest-asyncio - # via pytest-cov - # via pytest-mock - # via pytest-watch - # via socketsecurity -pytest-asyncio==0.25.1 - # via socketsecurity -pytest-cov==6.0.0 - # via socketsecurity -pytest-mock==3.14.0 - # via socketsecurity -pytest-watch==4.2.0 - # via socketsecurity -python-dotenv==1.0.1 - # via socketsecurity -requests==2.32.3 - # via socket-sdk-python - # via socketsecurity -smmap==5.0.2 - # via gitdb -socket-sdk-python==2.0.15 - # via socketsecurity -typing-extensions==4.12.2 - # via socket-sdk-python -urllib3==2.3.0 - # via requests -watchdog==6.0.0 - # via pytest-watch -wcwidth==0.2.13 - # via prettytable diff --git a/requirements.lock b/requirements.lock deleted file mode 100644 index 6d0be66..0000000 --- a/requirements.lock +++ /dev/null @@ -1,71 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: ["test"] -# all-features: false -# with-sources: false -# generate-hashes: false -# universal: false - -argparse==1.4.0 - # via socketsecurity -certifi==2024.12.14 - # via requests -charset-normalizer==3.4.1 - # via requests -colorama==0.4.6 - # via pytest-watch -coverage==7.6.10 - # via pytest-cov -docopt==0.6.2 - # via pytest-watch -gitdb==4.0.12 - # via gitpython -gitpython==3.1.44 - # via socketsecurity -idna==3.10 - # via requests -iniconfig==2.0.0 - # via pytest -mdutils==1.6.0 - # via socketsecurity -packaging==24.2 - # via pytest - # via socketsecurity -pluggy==1.5.0 - # via pytest -prettytable==3.12.0 - # via socketsecurity -pytest==8.3.4 - # via pytest-asyncio - # via pytest-cov - # via pytest-mock - # via pytest-watch - # via socketsecurity -pytest-asyncio==0.25.1 - # via socketsecurity -pytest-cov==6.0.0 - # via socketsecurity -pytest-mock==3.14.0 - # via socketsecurity -pytest-watch==4.2.0 - # via socketsecurity -python-dotenv==1.0.1 - # via socketsecurity -requests==2.32.3 - # via socket-sdk-python - # via socketsecurity -smmap==5.0.2 - # via gitdb -socket-sdk-python==2.0.15 - # via socketsecurity -typing-extensions==4.12.2 - # via socket-sdk-python -urllib3==2.3.0 - # via requests -watchdog==6.0.0 - # via pytest-watch -wcwidth==0.2.13 - # via prettytable diff --git a/scripts/deploy-test-docker.sh b/scripts/deploy-test-docker.sh index c9526e2..9c17eb3 100755 --- a/scripts/deploy-test-docker.sh +++ b/scripts/deploy-test-docker.sh @@ -29,7 +29,7 @@ fi if [ -z "$SDK_VERSION" ]; then echo "No SDK version specified, checking TestPyPI for latest version..." - SDK_VERSION=$(get_latest_version "socket-sdk-python") + SDK_VERSION=$(get_latest_version "socketdev") echo "Latest SDK version on TestPyPI is: $SDK_VERSION" fi diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 3e2a726..9ea1adb 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,2 +1,2 @@ __author__ = 'socket.dev' -__version__ = '2.1.0' +__version__ = '2.2.11' diff --git a/socketsecurity/config.py b/socketsecurity/config.py index 19223d7..bbfb4bc 100644 --- a/socketsecurity/config.py +++ b/socketsecurity/config.py @@ -42,12 +42,13 @@ class CliConfig: enable_sarif: bool = False disable_overview: bool = False disable_security_issue: bool = False - files: str = "[]" + files: str = None ignore_commit_files: bool = False disable_blocking: bool = False integration_type: IntegrationType = "api" integration_org_slug: Optional[str] = None pending_head: bool = False + enable_diff: bool = False timeout: Optional[int] = 1200 exclude_license_details: bool = False include_module_folders: bool = False @@ -56,6 +57,11 @@ class CliConfig: version: str = __version__ jira_plugin: PluginConfig = field(default_factory=PluginConfig) slack_plugin: PluginConfig = field(default_factory=PluginConfig) + license_file_name: str = "license_output.json" + save_submitted_files_list: Optional[str] = None + save_manifest_tar: Optional[str] = None + sub_paths: List[str] = field(default_factory=list) + workspace_name: Optional[str] = None @classmethod def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': @@ -99,6 +105,11 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': 'include_module_folders': args.include_module_folders, 'repo_is_public': args.repo_is_public, "excluded_ecosystems": args.excluded_ecosystems, + 'license_file_name': args.license_file_name, + 'save_submitted_files_list': args.save_submitted_files_list, + 'save_manifest_tar': args.save_manifest_tar, + 'sub_paths': args.sub_paths or [], + 'workspace_name': args.workspace_name, 'version': __version__ } try: @@ -122,6 +133,14 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': if args.owner: config_args['integration_org_slug'] = args.owner + # Validate that sub_paths and workspace_name are used together + if args.sub_paths and not args.workspace_name: + logging.error("--sub-path requires --workspace-name to be specified") + exit(1) + if args.workspace_name and not args.sub_paths: + logging.error("--workspace-name requires --sub-path to be specified") + exit(1) + return cls(**config_args) def to_dict(self) -> dict: @@ -253,12 +272,44 @@ def create_argument_parser() -> argparse.ArgumentParser: dest="sbom_file", help=argparse.SUPPRESS ) + path_group.add_argument( + "--license-file-name", + dest="license_file_name", + default="license_output.json", + metavar="", + help="SBOM file path" + ) + path_group.add_argument( + "--save-submitted-files-list", + dest="save_submitted_files_list", + metavar="", + help="Save list of submitted file names to JSON file for debugging purposes" + ) + path_group.add_argument( + "--save-manifest-tar", + dest="save_manifest_tar", + metavar="", + help="Save all manifest files to a compressed tar.gz archive with original directory structure" + ) path_group.add_argument( "--files", metavar="", default="[]", help="Files to analyze (JSON array string)" ) + path_group.add_argument( + "--sub-path", + dest="sub_paths", + metavar="", + action="append", + help="Sub-path within target-path for manifest file scanning (can be specified multiple times). All sub-paths will be combined into a single workspace scan while preserving git context from target-path" + ) + path_group.add_argument( + "--workspace-name", + dest="workspace_name", + metavar="", + help="Workspace name suffix to append to repository name (repo-name-workspace_name)" + ) path_group.add_argument( "--excluded-ecosystems", @@ -396,6 +447,12 @@ def create_argument_parser() -> argparse.ArgumentParser: action="store_true", help=argparse.SUPPRESS ) + advanced_group.add_argument( + "--enable-diff", + dest="enable_diff", + action="store_true", + help="Enable diff mode even when using --integration api (forces diff mode without SCM integration)" + ) advanced_group.add_argument( "--scm", metavar="", diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index fd84d4e..6dd6ecb 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -1,21 +1,23 @@ import logging import os import sys +import tarfile +import tempfile import time import io +import json from dataclasses import asdict from glob import glob from io import BytesIO from pathlib import PurePath from typing import BinaryIO, Dict, List, Tuple, Set, Union -import re from socketdev import socketdev from socketdev.exceptions import APIFailure from socketdev.fullscans import FullScanParams, SocketArtifact from socketdev.org import Organization from socketdev.repos import RepositoryInfo from socketdev.settings import SecurityPolicyRule - +import copy from socketsecurity import __version__ from socketsecurity.core.classes import ( Alert, @@ -28,6 +30,7 @@ from socketsecurity.core.exceptions import APIResourceNotFound from .socket_config import SocketConfig from .utils import socket_globs +from .resource_utils import check_file_count_against_ulimit import importlib logging_std = importlib.import_module("logging") @@ -133,25 +136,40 @@ def create_sbom_output(self, diff: Diff) -> dict: @staticmethod def expand_brace_pattern(pattern: str) -> List[str]: """ - Expands brace expressions (e.g., {a,b,c}) into separate patterns. + Recursively expands brace expressions (e.g., {a,b,c}) into separate patterns, supporting nested braces. """ - brace_regex = re.compile(r"\{([^{}]+)\}") - - # Expand all brace groups - expanded_patterns = [pattern] - while any("{" in p for p in expanded_patterns): - new_patterns = [] - for pat in expanded_patterns: - match = brace_regex.search(pat) - if match: - options = match.group(1).split(",") # Extract values inside {} - prefix, suffix = pat[:match.start()], pat[match.end():] - new_patterns.extend([prefix + opt + suffix for opt in options]) - else: - new_patterns.append(pat) - expanded_patterns = new_patterns - - return expanded_patterns + def recursive_expand(pat: str) -> List[str]: + stack = [] + for i, c in enumerate(pat): + if c == '{': + stack.append(i) + elif c == '}' and stack: + start = stack.pop() + if not stack: + # Found the outermost pair + before = pat[:start] + after = pat[i+1:] + inner = pat[start+1:i] + # Split on commas not inside nested braces + options = [] + depth = 0 + last = 0 + for j, ch in enumerate(inner): + if ch == '{': + depth += 1 + elif ch == '}': + depth -= 1 + elif ch == ',' and depth == 0: + options.append(inner[last:j]) + last = j+1 + options.append(inner[last:]) + results = [] + for opt in options: + expanded = before + opt + after + results.extend(recursive_expand(expanded)) + return results + return [pat] + return recursive_expand(pattern) @staticmethod def is_excluded(file_path: str, excluded_dirs: Set[str]) -> bool: @@ -161,6 +179,114 @@ def is_excluded(file_path: str, excluded_dirs: Set[str]) -> bool: return True return False + def save_submitted_files_list(self, files: List[str], output_path: str) -> None: + """ + Save the list of submitted file names to a JSON file for debugging. + + Args: + files: List of file paths that were submitted for scanning + output_path: Path where to save the JSON file + """ + try: + # Calculate total size of all files + total_size_bytes = 0 + valid_files = [] + + for file_path in files: + try: + if os.path.exists(file_path) and os.path.isfile(file_path): + file_size = os.path.getsize(file_path) + total_size_bytes += file_size + valid_files.append(file_path) + else: + log.warning(f"File not found or not accessible: {file_path}") + valid_files.append(file_path) # Still include in list for debugging + except OSError as e: + log.warning(f"Error accessing file {file_path}: {e}") + valid_files.append(file_path) # Still include in list for debugging + + # Convert bytes to human-readable format + def format_bytes(bytes_value): + """Convert bytes to human readable format""" + for unit in ['B', 'KB', 'MB', 'GB']: + if bytes_value < 1024.0: + return f"{bytes_value:.2f} {unit}" + bytes_value /= 1024.0 + return f"{bytes_value:.2f} TB" + + file_data = { + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()), + "total_files": len(valid_files), + "total_size_bytes": total_size_bytes, + "total_size_human": format_bytes(total_size_bytes), + "files": sorted(valid_files) + } + + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(file_data, f, indent=2, ensure_ascii=False) + + log.info(f"Saved list of {len(valid_files)} submitted files ({file_data['total_size_human']}) to: {output_path}") + + except Exception as e: + log.error(f"Failed to save submitted files list to {output_path}: {e}") + + def save_manifest_tar(self, files: List[str], output_path: str, base_dir: str) -> None: + """ + Save all manifest files to a compressed tar.gz archive with original directory structure. + + Args: + files: List of file paths to include in the archive + output_path: Path where to save the tar.gz file + base_dir: Base directory to preserve relative structure + """ + try: + # Normalize base directory + base_dir = os.path.abspath(base_dir) + if not base_dir.endswith(os.sep): + base_dir += os.sep + + log.info(f"Creating manifest tar.gz file: {output_path}") + log.debug(f"Base directory: {base_dir}") + + with tarfile.open(output_path, 'w:gz') as tar: + for file_path in files: + if not os.path.exists(file_path): + log.warning(f"File not found, skipping: {file_path}") + continue + + # Calculate relative path within the base directory + abs_file_path = os.path.abspath(file_path) + if abs_file_path.startswith(base_dir): + # File is within base directory - use relative path + arcname = os.path.relpath(abs_file_path, base_dir) + else: + # File is outside base directory - use just the filename + arcname = os.path.basename(abs_file_path) + log.warning(f"File outside base dir, using basename: {file_path} -> {arcname}") + + # Normalize archive name to use forward slashes + arcname = arcname.replace(os.sep, '/') + + log.debug(f"Adding to tar: {file_path} -> {arcname}") + tar.add(file_path, arcname=arcname) + + # Get tar file size for logging + tar_size = os.path.getsize(output_path) + + def format_bytes(bytes_value): + """Convert bytes to human readable format""" + for unit in ['B', 'KB', 'MB', 'GB']: + if bytes_value < 1024.0: + return f"{bytes_value:.2f} {unit}" + bytes_value /= 1024.0 + return f"{bytes_value:.2f} TB" + + tar_size_human = format_bytes(tar_size) + log.info(f"Successfully created tar.gz with {len(files)} files ({tar_size_human}, {tar_size:,} bytes): {output_path}") + + except Exception as e: + log.error(f"Failed to save manifest tar.gz to {output_path}: {e}") + def find_files(self, path: str) -> List[str]: """ Finds supported manifest files in the given path. @@ -176,17 +302,12 @@ def find_files(self, path: str) -> List[str]: files: Set[str] = set() # Get supported patterns from the API - try: - patterns = self.get_supported_patterns() - except Exception as e: - log.error(f"Error getting supported patterns from API: {e}") - log.warning("Falling back to local patterns") - from .utils import socket_globs as fallback_patterns - patterns = fallback_patterns + patterns = self.get_supported_patterns() for ecosystem in patterns: if ecosystem in self.config.excluded_ecosystems: continue + log.debug(f'Scanning ecosystem: {ecosystem}') ecosystem_patterns = patterns[ecosystem] for file_name in ecosystem_patterns: original_pattern = ecosystem_patterns[file_name]["pattern"] @@ -209,8 +330,24 @@ def find_files(self, path: str) -> List[str]: glob_end = time.time() log.debug(f"Globbing took {glob_end - glob_start:.4f} seconds") - log.debug(f"Total files found: {len(files)}") - return sorted(files) + file_list = sorted(files) + file_count = len(file_list) + log.info(f"Total files found: {file_count}") + + # Check if the number of manifest files might exceed ulimit -n + ulimit_check = check_file_count_against_ulimit(file_count) + if ulimit_check["can_check"]: + if ulimit_check["would_exceed"]: + log.debug(f"Found {file_count} manifest files, which may exceed the file descriptor limit (ulimit -n = {ulimit_check['soft_limit']})") + log.debug(f"Available file descriptors: {ulimit_check['available_fds']} (after {ulimit_check['buffer_size']} buffer)") + log.debug(f"Recommendation: {ulimit_check['recommendation']}") + log.debug("This may cause 'Too many open files' errors during processing") + else: + log.debug(f"File count ({file_count}) is within file descriptor limit ({ulimit_check['soft_limit']})") + else: + log.debug(f"Could not check file descriptor limit: {ulimit_check.get('error', 'Unknown error')}") + + return file_list def get_supported_patterns(self) -> Dict: """ @@ -252,17 +389,34 @@ def has_manifest_files(self, files: list) -> bool: from .utils import socket_globs as fallback_patterns patterns = fallback_patterns + # Normalize all file paths for matching + norm_files = [f.replace('\\', '/').lstrip('./') for f in files] + for ecosystem in patterns: ecosystem_patterns = patterns[ecosystem] for file_name in ecosystem_patterns: pattern_str = ecosystem_patterns[file_name]["pattern"] - for file in files: - if "\\" in file: - file = file.replace("\\", "/") - if PurePath(file).match(pattern_str): - return True + # Expand brace patterns for each manifest pattern + expanded_patterns = Core.expand_brace_pattern(pattern_str) + for exp_pat in expanded_patterns: + for file in norm_files: + # Use PurePath.match for glob-like matching + if PurePath(file).match(exp_pat): + return True return False + def check_file_count_limit(self, file_count: int) -> dict: + """ + Check if the given file count would exceed the system's file descriptor limit. + + Args: + file_count: Number of files to check + + Returns: + Dictionary with check results including recommendations + """ + return check_file_count_against_ulimit(file_count) + @staticmethod def to_case_insensitive_regex(input_string: str) -> str: """ @@ -280,61 +434,39 @@ def to_case_insensitive_regex(input_string: str) -> str: return ''.join(f'[{char.lower()}{char.upper()}]' if char.isalpha() else char for char in input_string) @staticmethod - def empty_head_scan_file() -> list[tuple[str, tuple[str, Union[BinaryIO, BytesIO]]]]: - # Create an empty file for when no head full scan so that the diff endpoint can always be used - empty_file_obj = io.BytesIO(b"") - empty_filename = "initial_head_scan" - empty_full_scan_file = [(empty_filename, (empty_filename, empty_file_obj))] - return empty_full_scan_file - - @staticmethod - def load_files_for_sending(files: List[str], workspace: str) -> List[Tuple[str, Tuple[str, BinaryIO]]]: + def empty_head_scan_file() -> List[str]: """ - Prepares files for sending to the Socket API. - - Args: - files: List of file paths from find_files() - workspace: Base directory path to make paths relative to - + Creates a temporary empty file for baseline scans when no head scan exists. + Returns: - List of tuples formatted for requests multipart upload: - [(field_name, (filename, file_object)), ...] + List containing path to a temporary empty file """ - send_files = [] - if "\\" in workspace: - workspace = workspace.replace("\\", "/") - for file_path in files: - _, name = file_path.rsplit("/", 1) - - if file_path.startswith(workspace): - key = file_path[len(workspace):] - else: - key = file_path - - key = key.lstrip("/") - key = key.lstrip("./") - - f = open(file_path, 'rb') - payload = (key, (name.lstrip(workspace), f)) - send_files.append(payload) - - return send_files - - def create_full_scan(self, files: list[tuple[str, tuple[str, BytesIO]]], params: FullScanParams) -> FullScan: + # Create a temporary empty file + temp_fd, temp_path = tempfile.mkstemp(suffix='.empty', prefix='socket_baseline_') + + # Close the file descriptor since we just need the path + # The file is already created and empty + os.close(temp_fd) + + log.debug(f"Created temporary empty file for baseline scan: {temp_path}") + return [temp_path] + + def create_full_scan(self, files: List[str], params: FullScanParams, base_paths: List[str] = None) -> FullScan: """ Creates a new full scan via the Socket API. Args: - files: List of files to scan + files: List of file paths to scan params: Parameters for the full scan + base_paths: List of base paths for the scan (optional) Returns: FullScan object with scan results """ - log.debug("Creating new full scan") + log.info("Creating new full scan") create_full_start = time.time() - res = self.sdk.fullscans.post(files, params, use_types=True) + res = self.sdk.fullscans.post(files, params, use_types=True, use_lazy_loading=True, max_open_files=50, base_paths=base_paths) if not res.success: log.error(f"Error creating full scan: {res.message}, status: {res.status}") raise Exception(f"Error creating full scan: {res.message}, status: {res.status}") @@ -346,6 +478,74 @@ def create_full_scan(self, files: list[tuple[str, tuple[str, BytesIO]]], params: return full_scan + def create_full_scan_with_report_url( + self, + paths: List[str], + params: FullScanParams, + no_change: bool = False, + save_files_list_path: str = None, + save_manifest_tar_path: str = None, + base_paths: List[str] = None + ) -> Diff: + """Create a new full scan and return with html_report_url. + + Args: + paths: List of paths to look for manifest files + params: Query params for the Full Scan endpoint + no_change: If True, return empty result + save_files_list_path: Optional path to save submitted files list for debugging + save_manifest_tar_path: Optional path to save manifest files tar.gz archive + base_paths: List of base paths for the scan (optional) + + Returns: + Dict with full scan data including html_report_url + """ + log.debug(f"starting create_full_scan_with_report_url with no_change: {no_change}") + diff = Diff( + id="NO_SCAN_RAN", + report_url="", + diff_url="" + ) + if no_change: + return diff + + # Find manifest files from all paths + all_files = [] + for path in paths: + files = self.find_files(path) + all_files.extend(files) + + # Save submitted files list if requested + if save_files_list_path and all_files: + self.save_submitted_files_list(all_files, save_files_list_path) + + # Save manifest tar.gz if requested (use first path as base) + if save_manifest_tar_path and all_files and paths: + self.save_manifest_tar(all_files, save_manifest_tar_path, paths[0]) + + if not all_files: + return diff + + try: + # Create new scan + new_scan_start = time.time() + new_full_scan = self.create_full_scan(all_files, params, base_paths=base_paths) + new_scan_end = time.time() + log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}") + except APIFailure as e: + log.error(f"Failed to create full scan: {e}") + raise + + # Construct report URL + base_socket = "https://socket.dev/dashboard/org" + diff.report_url = f"{base_socket}/{self.config.org_slug}/sbom/{new_full_scan.id}" + diff.diff_url = diff.report_url + diff.id = new_full_scan.id + diff.packages = {} + + # Return result in the format expected by the user + return diff + def check_full_scans_status(self, head_full_scan_id: str, new_full_scan_id: str) -> bool: is_ready = False current_timeout = self.config.timeout @@ -457,7 +657,7 @@ def get_package_license_text(self, package: Package) -> str: license_raw = package.license data = self.sdk.licensemetadata.post([license_raw], {'includetext': 'true'}) - license_str = data.data[0].license if data and len(data) == 1 else "" + license_str = data[0].get('text') if data and len(data) == 1 else "" return license_str def get_repo_info(self, repo_slug: str, default_branch: str = "socket-default-branch") -> RepositoryInfo: @@ -533,13 +733,41 @@ def update_package_values(pkg: Package) -> Package: pkg.url += f"/{pkg.name}/overview/{pkg.version}" return pkg - def get_added_and_removed_packages(self, head_full_scan_id: str, new_full_scan_id: str) -> Tuple[Dict[str, Package], Dict[str, Package]]: + def get_license_text_via_purl(self, packages: dict[str, Package]) -> dict: + components = [] + for purl in packages: + full_purl = f"pkg:/{purl}" + components.append({"purl": full_purl}) + results = self.sdk.purl.post( + license=True, + components=components, + licenseattrib=True, + licensedetails=True + ) + purl_packages = [] + for result in results: + ecosystem = result["type"] + name = result["name"] + package_version = result["version"] + licenseDetails = result.get("licenseDetails") + licenseAttrib = result.get("licenseAttrib") + purl = f"{ecosystem}/{name}@{package_version}" + if purl not in purl_packages and purl in packages: + packages[purl].licenseAttrib = licenseAttrib + packages[purl].licenseDetails = licenseDetails + return packages + + def get_added_and_removed_packages( + self, + head_full_scan_id: str, + new_full_scan_id: str + ) -> Tuple[Dict[str, Package], Dict[str, Package], Dict[str, Package]]: """ Get packages that were added and removed between scans. Args: - head_full_scan: Previous scan (may be None if first scan) - head_full_scan_id: New scan just created + head_full_scan_id: Previous scan (maybe None if first scan) + new_full_scan_id: New scan just created Returns: Tuple of (added_packages, removed_packages) dictionaries @@ -548,7 +776,15 @@ def get_added_and_removed_packages(self, head_full_scan_id: str, new_full_scan_i log.info(f"Comparing scans - Head scan ID: {head_full_scan_id}, New scan ID: {new_full_scan_id}") diff_start = time.time() try: - diff_report = self.sdk.fullscans.stream_diff(self.config.org_slug, head_full_scan_id, new_full_scan_id, use_types=True).data + diff_report = ( + self.sdk.fullscans.stream_diff + ( + self.config.org_slug, + head_full_scan_id, + new_full_scan_id, + use_types=True + ).data + ) except APIFailure as e: log.error(f"API Error: {e}") sys.exit(1) @@ -569,20 +805,36 @@ def get_added_and_removed_packages(self, head_full_scan_id: str, new_full_scan_i added_artifacts = diff_report.artifacts.added + diff_report.artifacts.updated removed_artifacts = diff_report.artifacts.removed + diff_report.artifacts.replaced + unchanged_artifacts = diff_report.artifacts.unchanged added_packages: Dict[str, Package] = {} removed_packages: Dict[str, Package] = {} - + packages: Dict[str, Package] = {} for artifact in added_artifacts: try: pkg = Package.from_diff_artifact(asdict(artifact)) pkg = Core.update_package_values(pkg) added_packages[artifact.id] = pkg + full_purl = f"{pkg.type}/{pkg.purl}" + if full_purl not in packages: + packages[full_purl] = pkg except KeyError: log.error(f"KeyError: Could not create package from added artifact {artifact.id}") log.error(f"Artifact details - name: {artifact.name}, version: {artifact.version}") log.error("No matching packages found in new_full_scan") + for artifact in unchanged_artifacts: + try: + pkg = Package.from_diff_artifact(asdict(artifact)) + pkg = Core.update_package_values(pkg) + full_purl = f"{pkg.type}/{pkg.purl}" + if full_purl not in packages: + packages[full_purl] = pkg + except KeyError: + log.error(f"KeyError: Could not create package from unchanged artifact {artifact.id}") + log.error(f"Artifact details - name: {artifact.name}, version: {artifact.version}") + log.error("No matching packages found in new_full_scan") + for artifact in removed_artifacts: try: pkg = Package.from_diff_artifact(asdict(artifact)) @@ -595,30 +847,48 @@ def get_added_and_removed_packages(self, head_full_scan_id: str, new_full_scan_i log.error(f"Artifact details - name: {artifact.name}, version: {artifact.version}") log.error("No matching packages found in head_full_scan") - return added_packages, removed_packages + packages = self.get_license_text_via_purl(packages) + return added_packages, removed_packages, packages def create_new_diff( self, - path: str, + paths: List[str], params: FullScanParams, - no_change: bool = False + no_change: bool = False, + save_files_list_path: str = None, + save_manifest_tar_path: str = None, + base_paths: List[str] = None ) -> Diff: """Create a new diff using the Socket SDK. Args: - path: Path to look for manifest files + paths: List of paths to look for manifest files params: Query params for the Full Scan endpoint no_change: If True, return empty diff + save_files_list_path: Optional path to save submitted files list for debugging + save_manifest_tar_path: Optional path to save manifest files tar.gz archive + base_paths: List of base paths for the scan (optional) """ log.debug(f"starting create_new_diff with no_change: {no_change}") if no_change: - return Diff(id="no_diff_id") - - # Find manifest files - files = self.find_files(path) - files_for_sending = self.load_files_for_sending(files, path) - if not files: - return Diff(id="no_diff_id") + return Diff(id="NO_DIFF_RAN", diff_url="", report_url="") + + # Find manifest files from all paths + all_files = [] + for path in paths: + files = self.find_files(path) + all_files.extend(files) + + # Save submitted files list if requested + if save_files_list_path and all_files: + self.save_submitted_files_list(all_files, save_files_list_path) + + # Save manifest tar.gz if requested (use first path as base) + if save_manifest_tar_path and all_files and paths: + self.save_manifest_tar(all_files, save_manifest_tar_path, paths[0]) + + if not all_files: + return Diff(id="NO_DIFF_RAN", diff_url="", report_url="") try: # Get head scan ID @@ -626,19 +896,44 @@ def create_new_diff( except APIResourceNotFound: head_full_scan_id = None + # If no head scan exists, create an empty baseline scan if head_full_scan_id is None: - tmp_params = params + log.info("No previous scan found - creating empty baseline scan") + new_params = copy.deepcopy(params.__dict__) + new_params.pop('include_license_details') + tmp_params = FullScanParams(**new_params) + tmp_params.include_license_details = params.include_license_details tmp_params.tmp = True tmp_params.set_as_pending_head = False tmp_params.make_default_branch = False - head_full_scan = self.create_full_scan(Core.empty_head_scan_file(), params) - head_full_scan_id = head_full_scan.id + + # Create baseline scan with empty file + empty_files = Core.empty_head_scan_file() + try: + head_full_scan = self.create_full_scan(empty_files, tmp_params, base_paths=base_paths) + head_full_scan_id = head_full_scan.id + log.debug(f"Created empty baseline scan: {head_full_scan_id}") + + # Clean up the temporary empty file + for temp_file in empty_files: + try: + os.unlink(temp_file) + log.debug(f"Cleaned up temporary file: {temp_file}") + except OSError as e: + log.warning(f"Failed to clean up temporary file {temp_file}: {e}") + except Exception as e: + # Clean up temp files even if scan creation fails + for temp_file in empty_files: + try: + os.unlink(temp_file) + except OSError: + pass + raise e # Create new scan try: new_scan_start = time.time() - new_full_scan = self.create_full_scan(files_for_sending, params) - new_full_scan.sbom_artifacts = self.get_sbom_data(new_full_scan.id) + new_full_scan = self.create_full_scan(all_files, params, base_paths=base_paths) new_scan_end = time.time() log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}") except APIFailure as e: @@ -650,12 +945,18 @@ def create_new_diff( log.error(f"Stack trace:\n{traceback.format_exc()}") raise + # Handle diff generation - now we always have both scans scans_ready = self.check_full_scans_status(head_full_scan_id, new_full_scan.id) if scans_ready is False: log.error(f"Full scans did not complete within {self.config.timeout} seconds") - added_packages, removed_packages = self.get_added_and_removed_packages(head_full_scan_id, new_full_scan.id) + ( + added_packages, + removed_packages, + packages + ) = self.get_added_and_removed_packages(head_full_scan_id, new_full_scan.id) diff = self.create_diff_report(added_packages, removed_packages) + diff.packages = packages base_socket = "https://socket.dev/dashboard/org" diff.id = new_full_scan.id @@ -664,9 +965,10 @@ def create_new_diff( if not params.include_license_details: report_url += "?include_license_details=false" diff.report_url = report_url + diff.new_scan_id = new_full_scan.id if head_full_scan_id is not None: - diff.diff_url = f"{base_socket}/{self.config.org_slug}/diff/{diff.id}/{head_full_scan_id}" + diff.diff_url = f"{base_socket}/{self.config.org_slug}/diff/{head_full_scan_id}/{diff.id}" else: diff.diff_url = diff.report_url @@ -739,15 +1041,13 @@ def create_diff_report( diff.new_capabilities = Core.get_capabilities_for_added_packages(added_packages) Core.add_purl_capabilities(diff) + if not hasattr(diff, "diff_url"): + diff.diff_url = None + if not hasattr(diff, "report_url"): + diff.report_url = None return diff - def get_all_scores(self, packages: dict[str, Package]) -> dict[str, Package]: - components = [] - for package_id in packages: - package = packages[package_id] - return packages - def create_purl(self, package_id: str, packages: dict[str, Package]) -> Purl: """ Creates the extended PURL data for package identification and tracking. @@ -795,6 +1095,8 @@ def get_source_data(package: Package, packages: dict) -> list: introduced_by = [] if package.direct: manifests = "" + if not hasattr(package, "manifestFiles") or package.manifestFiles is None: + return introduced_by for manifest_data in package.manifestFiles: manifest_file = manifest_data.get("file") manifests += f"{manifest_file};" @@ -865,6 +1167,12 @@ def add_package_alerts_to_collection(self, package: Package, alerts_collection: alert = Alert(**alert_item) props = getattr(self.config.all_issues, alert.type, default_props) introduced_by = self.get_source_data(package, packages) + + # Handle special case for license policy violations + title = props.title + if alert.type == "licenseSpdxDisj" and not title: + title = "License Policy Violation" + issue_alert = Issue( pkg_type=package.type, pkg_name=package.name, @@ -875,7 +1183,7 @@ def add_package_alerts_to_collection(self, package: Package, alerts_collection: type=alert.type, severity=alert.severity, description=props.description, - title=props.title, + title=title, suggestion=props.suggestion, next_step_title=props.nextStepTitle, introduced_by=introduced_by, @@ -887,11 +1195,10 @@ def add_package_alerts_to_collection(self, package: Package, alerts_collection: action = self.config.security_policy[alert.type]['action'] setattr(issue_alert, action, True) - if issue_alert.type != 'licenseSpdxDisj': - if issue_alert.key not in alerts_collection: - alerts_collection[issue_alert.key] = [issue_alert] - else: - alerts_collection[issue_alert.key].append(issue_alert) + if issue_alert.key not in alerts_collection: + alerts_collection[issue_alert.key] = [issue_alert] + else: + alerts_collection[issue_alert.key].append(issue_alert) return alerts_collection @@ -963,7 +1270,8 @@ def get_new_alerts( if alert_key not in removed_package_alerts: new_alerts = added_package_alerts[alert_key] for alert in new_alerts: - alert_str = f"{alert.purl},{alert.manifests},{alert.type}" + # Consolidate by package and alert type, not by manifest details + alert_str = f"{alert.purl},{alert.type}" if alert.error or alert.warn: if alert_str not in consolidated_alerts: @@ -974,7 +1282,8 @@ def get_new_alerts( removed_alerts = removed_package_alerts[alert_key] for alert in new_alerts: - alert_str = f"{alert.purl},{alert.manifests},{alert.type}" + # Consolidate by package and alert type, not by manifest details + alert_str = f"{alert.purl},{alert.type}" # Only add if: # 1. Alert isn't in removed packages (or we're not ignoring readded alerts) diff --git a/socketsecurity/core/classes.py b/socketsecurity/core/classes.py index aefb0ab..e81312c 100644 --- a/socketsecurity/core/classes.py +++ b/socketsecurity/core/classes.py @@ -97,7 +97,7 @@ class AlertCounts(TypedDict): low: int @dataclass(kw_only=True) -class Package(SocketArtifactLink): +class Package(): """ Represents a package detected in a Socket Security scan. @@ -106,16 +106,23 @@ class Package(SocketArtifactLink): """ # Common properties from both artifact types - id: str + type: str name: str version: str - type: str + release: str + diffType: str + id: str + author: List[str] = field(default_factory=list) score: SocketScore alerts: List[SocketAlert] - author: List[str] = field(default_factory=list) size: Optional[int] = None license: Optional[str] = None namespace: Optional[str] = None + topLevelAncestors: Optional[List[str]] = None + direct: Optional[bool] = False + manifestFiles: Optional[List[SocketManifestReference]] = None + dependencies: Optional[List[str]] = None + artifact: Optional[SocketArtifactLink] = None # Package-specific fields license_text: str = "" @@ -125,6 +132,7 @@ class Package(SocketArtifactLink): # Artifact-specific fields licenseDetails: Optional[list] = None + licenseAttrib: Optional[List] = None @classmethod @@ -180,7 +188,7 @@ def from_diff_artifact(cls, data: dict) -> "Package": ValueError: If reference data cannot be found in DiffArtifact """ ref = None - if data["diffType"] in ["added", "updated"] and data.get("head"): + if data["diffType"] in ["added", "updated", "unchanged"] and data.get("head"): ref = data["head"][0] elif data["diffType"] in ["removed", "replaced"] and data.get("base"): ref = data["base"][0] @@ -203,7 +211,9 @@ def from_diff_artifact(cls, data: dict) -> "Package": manifestFiles=ref.get("manifestFiles", []), dependencies=ref.get("dependencies"), artifact=ref.get("artifact"), - namespace=data.get('namespace', None) + namespace=data.get('namespace', None), + release=ref.get("release", None), + diffType=ref.get("diffType", None), ) class Issue: @@ -460,14 +470,15 @@ class Diff: """ new_packages: list[Purl] - new_capabilities: Dict[str, List[str]] removed_packages: list[Purl] + packages: dict[str, Package] + new_capabilities: Dict[str, List[str]] new_alerts: list[Issue] id: str sbom: str - packages: dict[str, Package] report_url: str diff_url: str + new_scan_id: str def __init__(self, **kwargs): if kwargs: diff --git a/socketsecurity/core/git_interface.py b/socketsecurity/core/git_interface.py index 5cf5296..84eec25 100644 --- a/socketsecurity/core/git_interface.py +++ b/socketsecurity/core/git_interface.py @@ -12,40 +12,559 @@ class Git: def __init__(self, path: str): self.path = path + self.ensure_safe_directory(path) self.repo = Repo(path) assert self.repo self.head = self.repo.head + + # Always fetch all remote refs to ensure branches exist for diffing + try: + self.repo.git.fetch('--all') + log.debug("Fetched all remote refs for diffing.") + except Exception as fetch_error: + log.debug(f"Failed to fetch all remote refs: {fetch_error}") - # Use GITHUB_SHA if available, otherwise fall back to head commit + # Use CI environment SHA if available, otherwise fall back to current HEAD commit github_sha = os.getenv('GITHUB_SHA') - if github_sha: + gitlab_sha = os.getenv('CI_COMMIT_SHA') + bitbucket_sha = os.getenv('BITBUCKET_COMMIT') + ci_sha = github_sha or gitlab_sha or bitbucket_sha + + if ci_sha: try: - self.commit = self.repo.commit(github_sha) + self.commit = self.repo.commit(ci_sha) + if github_sha: + env_source = "GITHUB_SHA" + elif gitlab_sha: + env_source = "CI_COMMIT_SHA" + else: + env_source = "BITBUCKET_COMMIT" + log.debug(f"Using commit from {env_source}: {ci_sha}") except Exception as error: - log.debug(f"Failed to get commit from GITHUB_SHA: {error}") - self.commit = self.head.commit + log.debug(f"Failed to get commit from CI environment: {error}") + # Use the actual current HEAD commit, not the head reference's commit + self.commit = self.repo.commit('HEAD') + log.debug(f"Using current HEAD commit: {self.commit.hexsha}") else: - self.commit = self.head.commit + # Use the actual current HEAD commit, not the head reference's commit + self.commit = self.repo.commit('HEAD') + log.debug(f"Using current HEAD commit: {self.commit.hexsha}") + + log.debug(f"Final commit being used: {self.commit.hexsha}") + log.debug(f"Commit author: {self.commit.author.name} <{self.commit.author.email}>") + log.debug(f"Commit committer: {self.commit.committer.name} <{self.commit.committer.email}>") - self.repo_name = self.repo.remotes.origin.url.split('.git')[0].split('/')[-1] + # Extract repository name from git remote, with fallback to default try: - self.branch = self.head.reference - urllib.parse.unquote(str(self.branch)) + remote_url = self.repo.remotes.origin.url + self.repo_name = remote_url.split('.git')[0].split('/')[-1] + log.debug(f"Repository name detected from git remote: {self.repo_name}") except Exception as error: - self.branch = None - log.debug(error) + log.debug(f"Failed to get repository name from git remote: {error}") + self.repo_name = "socket-default-repo" + log.debug(f"Using default repository name: {self.repo_name}") + + # Branch detection with priority: CI Variables -> Git Properties -> Default + # Note: CLI arguments are handled in socketcli.py and take highest priority + + # First, try CI environment variables (most accurate in CI environments) + ci_branch = None + + # GitLab CI variables + gitlab_branch = os.getenv('CI_COMMIT_BRANCH') or os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME') + + # GitHub Actions variables + github_ref = os.getenv('GITHUB_REF') # e.g., 'refs/heads/main' + github_branch = None + if github_ref and github_ref.startswith('refs/heads/'): + github_branch = github_ref.replace('refs/heads/', '') + + # Bitbucket Pipelines variables + bitbucket_branch = os.getenv('BITBUCKET_BRANCH') + + # Select CI branch with priority: GitLab -> GitHub -> Bitbucket + ci_branch = gitlab_branch or github_branch or bitbucket_branch + + if ci_branch: + self.branch = ci_branch + if gitlab_branch: + env_source = "GitLab CI" + elif github_branch: + env_source = "GitHub Actions" + else: + env_source = "Bitbucket Pipelines" + log.debug(f"Branch detected from {env_source}: {self.branch}") + else: + # Try to get branch name from git properties + try: + self.branch = self.head.reference + urllib.parse.unquote(str(self.branch)) + log.debug(f"Branch detected from git reference: {self.branch}") + except Exception as error: + log.debug(f"Failed to get branch from git reference: {error}") + + # Fallback: try to detect branch from git commands (works in detached HEAD) + git_detected_branch = None + try: + # Try git name-rev first (most reliable for detached HEAD) + result = self.repo.git.name_rev('--name-only', 'HEAD') + if result and result != 'undefined': + # Clean up the result (remove any prefixes like 'remotes/origin/') + git_detected_branch = result.split('/')[-1] + log.debug(f"Branch detected from git name-rev: {git_detected_branch}") + except Exception as git_error: + log.debug(f"git name-rev failed: {git_error}") + + if not git_detected_branch: + try: + # Fallback: try git describe --all --exact-match + result = self.repo.git.describe('--all', '--exact-match', 'HEAD') + if result and result.startswith('heads/'): + git_detected_branch = result.replace('heads/', '') + log.debug(f"Branch detected from git describe: {git_detected_branch}") + except Exception as git_error: + log.debug(f"git describe failed: {git_error}") + + if git_detected_branch: + self.branch = git_detected_branch + log.debug(f"Branch detected from git commands: {self.branch}") + else: + # Final fallback: use default branch name + self.branch = "socket-default-branch" + log.debug(f"Using default branch name: {self.branch}") self.author = self.commit.author self.commit_sha = self.commit.binsha self.commit_message = self.commit.message self.committer = self.commit.committer - self.show_files = self.repo.git.show(self.commit, name_only=True, format="%n").splitlines() + # Detect changed files in PR/MR context for GitHub, GitLab, Bitbucket; fallback to git show + self.show_files = [] + detected = False + # GitHub Actions PR context + github_base_ref = os.getenv('GITHUB_BASE_REF') + github_head_ref = os.getenv('GITHUB_HEAD_REF') + github_event_name = os.getenv('GITHUB_EVENT_NAME') + github_before_sha = os.getenv('GITHUB_EVENT_BEFORE') # previous commit for push + github_sha = os.getenv('GITHUB_SHA') # current commit + if github_event_name == 'pull_request' and github_base_ref and github_head_ref: + try: + # Fetch both branches individually + self.repo.git.fetch('origin', github_base_ref) + self.repo.git.fetch('origin', github_head_ref) + # Try remote diff first + diff_range = f"origin/{github_base_ref}...origin/{github_head_ref}" + try: + diff_files = self.repo.git.diff('--name-only', diff_range) + self.show_files = diff_files.splitlines() + log.debug(f"Changed files detected via git diff (GitHub PR remote): {self.show_files}") + detected = True + except Exception as remote_error: + log.debug(f"Remote diff failed: {remote_error}") + # Try local branch diff + local_diff_range = f"{github_base_ref}...{github_head_ref}" + try: + diff_files = self.repo.git.diff('--name-only', local_diff_range) + self.show_files = diff_files.splitlines() + log.debug(f"Changed files detected via git diff (GitHub PR local): {self.show_files}") + detected = True + except Exception as local_error: + log.debug(f"Local diff failed: {local_error}") + except Exception as error: + log.debug(f"Failed to fetch branches or diff for GitHub PR: {error}") + # Commits to default branch (push events) + elif github_event_name == 'push' and github_before_sha and github_sha: + try: + diff_files = self.repo.git.diff('--name-only', f'{github_before_sha}..{github_sha}') + self.show_files = diff_files.splitlines() + log.debug(f"Changed files detected via git diff (GitHub push): {self.show_files}") + detected = True + except Exception as error: + log.debug(f"Failed to get changed files via git diff (GitHub push): {error}") + elif github_event_name == 'push': + try: + self.show_files = self.repo.git.show(self.commit, name_only=True, format="%n").splitlines() + log.debug(f"Changed files detected via git show (GitHub push fallback): {self.show_files}") + detected = True + except Exception as error: + log.debug(f"Failed to get changed files via git show (GitHub push fallback): {error}") + # GitLab CI Merge Request context + if not detected: + gitlab_target = os.getenv('CI_MERGE_REQUEST_TARGET_BRANCH_NAME') + gitlab_source = os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME') + if gitlab_target and gitlab_source: + try: + self.repo.git.fetch('origin', gitlab_target, gitlab_source) + diff_range = f"origin/{gitlab_target}...origin/{gitlab_source}" + diff_files = self.repo.git.diff('--name-only', diff_range) + self.show_files = diff_files.splitlines() + log.debug(f"Changed files detected via git diff (GitLab): {self.show_files}") + detected = True + except Exception as error: + log.debug(f"Failed to get changed files via git diff (GitLab): {error}") + # Bitbucket Pipelines PR context + if not detected: + bitbucket_pr_id = os.getenv('BITBUCKET_PR_ID') + bitbucket_source = os.getenv('BITBUCKET_BRANCH') + bitbucket_dest = os.getenv('BITBUCKET_PR_DESTINATION_BRANCH') + # BITBUCKET_BRANCH is the source branch in PR builds + if bitbucket_pr_id and bitbucket_source and bitbucket_dest: + try: + self.repo.git.fetch('origin', bitbucket_dest, bitbucket_source) + diff_range = f"origin/{bitbucket_dest}...origin/{bitbucket_source}" + diff_files = self.repo.git.diff('--name-only', diff_range) + self.show_files = diff_files.splitlines() + log.debug(f"Changed files detected via git diff (Bitbucket): {self.show_files}") + detected = True + except Exception as error: + log.debug(f"Failed to get changed files via git diff (Bitbucket): {error}") + # Fallback to git show for single commit + if not detected: + # Check if this is a merge commit first + if self._is_merge_commit(): + # For merge commits, use git diff with parent + if self._detect_merge_commit_changes(): + detected = True + else: + # Fallback to git show if merge detection fails + self.show_files = self.repo.git.show(self.commit, name_only=True, format="%n").splitlines() + log.debug(f"Changed files detected via git show (merge commit fallback): {self.show_files}") + detected = True + else: + # Regular single commit + self.show_files = self.repo.git.show(self.commit, name_only=True, format="%n").splitlines() + log.debug(f"Changed files detected via git show: {self.show_files}") + detected = True self.changed_files = [] for item in self.show_files: if item != "": - full_path = f"{self.path}/{item}" - self.changed_files.append(full_path) + # Use relative path for glob matching + self.changed_files.append(item) + + # Determine if this commit is on the default branch + # This considers both GitHub Actions detached HEAD and regular branch situations + self.is_default_branch = self._is_commit_and_branch_default() + + def _is_commit_and_branch_default(self) -> bool: + """ + Check if both the commit is on the default branch AND we're processing the default branch. + This handles GitHub Actions detached HEAD state properly. + + Returns: + True if commit is on default branch and we're processing the default branch + """ + try: + # First check if the commit is reachable from the default branch + if not self.is_commit_on_default_branch(): + log.debug("Commit is not on default branch") + return False + + # Check if we're processing the default branch via CI environment variables + github_ref = os.getenv('GITHUB_REF') # e.g., 'refs/heads/main' or 'refs/pull/123/merge' + gitlab_branch = os.getenv('CI_COMMIT_BRANCH') + gitlab_mr_branch = os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME') + gitlab_default_branch = os.getenv('CI_DEFAULT_BRANCH', '') + bitbucket_branch = os.getenv('BITBUCKET_BRANCH') + + # Handle GitHub Actions + if github_ref: + log.debug(f"GitHub ref: {github_ref}") + + # Handle pull requests - they're not on the default branch + if github_ref.startswith('refs/pull/'): + log.debug("Processing a pull request, not default branch") + return False + + # Handle regular branch pushes + if github_ref.startswith('refs/heads/'): + branch_from_ref = github_ref.replace('refs/heads/', '') + default_branch_name = self.get_default_branch_name() + is_default = branch_from_ref == default_branch_name + log.debug(f"Branch from GITHUB_REF: {branch_from_ref}, Default: {default_branch_name}, Is default: {is_default}") + return is_default + + # Handle tags or other refs - not default branch + log.debug(f"Non-branch ref: {github_ref}, not default branch") + return False + + # Handle GitLab CI + elif gitlab_branch or gitlab_mr_branch: + # If this is a merge request, use the source branch + current_branch = gitlab_mr_branch or gitlab_branch + default_branch_name = gitlab_default_branch or self.get_default_branch_name() + + # For merge requests, they're typically not considered "default branch" + if gitlab_mr_branch: + log.debug(f"Processing GitLab MR from branch: {gitlab_mr_branch}, not default branch") + return False + + is_default = current_branch == default_branch_name + log.debug(f"GitLab branch: {current_branch}, Default: {default_branch_name}, Is default: {is_default}") + return is_default + + # Handle Bitbucket Pipelines + elif bitbucket_branch: + default_branch_name = self.get_default_branch_name() + is_default = bitbucket_branch == default_branch_name + log.debug(f"Bitbucket branch: {bitbucket_branch}, Default: {default_branch_name}, Is default: {is_default}") + return is_default + else: + # Not in GitHub Actions, use local development logic + # For local development, we consider it "default branch" if: + # 1. Currently on the default branch, OR + # 2. The commit is reachable from the default branch (part of default branch history) + + is_on_default = self.is_on_default_branch() + if is_on_default: + log.debug("Currently on default branch locally") + return True + + # Even if on feature branch, if commit is on default branch, consider it default + # This handles cases where feature branch was created from or merged to default + is_commit_default = self.is_commit_on_default_branch() + log.debug(f"Not on default branch locally, but commit is on default branch: {is_commit_default}") + return is_commit_default + + except Exception as error: + log.debug(f"Error determining if commit and branch are default: {error}") + return False @property def commit_str(self) -> str: """Return commit SHA as a string""" return self.commit.hexsha + + def get_formatted_committer(self) -> str: + """ + Get the committer in the preferred order: + 1. CLI --committers (handled in socketcli.py) + 2. CI/CD SCM username (GitHub/GitLab/BitBucket environment variables) + 3. Git username (extracted from email patterns like GitHub noreply) + 4. Git email address + 5. Git author name (fallback) + + Returns: + Formatted committer string + """ + # Check for CI/CD environment usernames first + # GitHub Actions + github_actor = os.getenv('GITHUB_ACTOR') + if github_actor: + log.debug(f"Using GitHub actor as committer: {github_actor}") + return github_actor + + # GitLab CI + gitlab_user_login = os.getenv('GITLAB_USER_LOGIN') + if gitlab_user_login: + log.debug(f"Using GitLab user login as committer: {gitlab_user_login}") + return gitlab_user_login + + # Bitbucket Pipelines + bitbucket_step_triggerer_uuid = os.getenv('BITBUCKET_STEP_TRIGGERER_UUID') + if bitbucket_step_triggerer_uuid: + log.debug(f"Using Bitbucket step triggerer as committer: {bitbucket_step_triggerer_uuid}") + return bitbucket_step_triggerer_uuid + + # Fall back to commit author/committer details + # Priority 3: Try to extract git username from email patterns first + if self.author and self.author.email and self.author.email.strip(): + email = self.author.email.strip() + + # If it's a GitHub noreply email, try to extract username + if email.endswith('@users.noreply.github.com'): + # Pattern: number+username@users.noreply.github.com + email_parts = email.split('@')[0] + if '+' in email_parts: + username = email_parts.split('+')[1] + log.debug(f"Extracted GitHub username from noreply email: {username}") + return username + + # Priority 4: Use email if available + if self.author and self.author.email and self.author.email.strip(): + email = self.author.email.strip() + log.debug(f"Using commit author email as committer: {email}") + return email + + # Priority 5: Fall back to author name as last resort + if self.author and self.author.name and self.author.name.strip(): + name = self.author.name.strip() + log.debug(f"Using commit author name as fallback committer: {name}") + return name + + # Ultimate fallback + log.debug("Using fallback committer: unknown") + return "unknown" + + def _is_merge_commit(self) -> bool: + """ + Check if the current commit is a merge commit. + + Returns: + True if this is a merge commit (has multiple parents), False otherwise + """ + try: + # A merge commit has multiple parents + is_merge = len(self.commit.parents) > 1 + log.debug(f"Commit {self.commit.hexsha[:8]} has {len(self.commit.parents)} parents, is_merge: {is_merge}") + return is_merge + except Exception as error: + log.debug(f"Error checking if commit is merge commit: {error}") + return False + + def _detect_merge_commit_changes(self) -> bool: + """ + Detect changed files in a merge commit using git diff with parent. + + This method handles the case where git show --name-only doesn't work + for merge commits (expected Git behavior). + + Returns: + True if detection was successful, False otherwise + """ + try: + if not self._is_merge_commit(): + log.debug("Not a merge commit, skipping merge commit detection") + return False + + # For merge commits, we need to diff against a parent + # We'll use the first parent (typically the target branch) + if not self.commit.parents: + log.debug("Merge commit has no parents - cannot perform merge-aware diff") + return False + + parent_commit = self.commit.parents[0] + + # Verify parent commit is accessible + try: + parent_sha = parent_commit.hexsha + # Quick validation that parent exists + self.repo.commit(parent_sha) + except Exception as parent_error: + log.error(f"Cannot resolve parent commit {parent_sha}: {parent_error}") + return False + + # Use git diff to show changes from parent to merge commit + diff_range = f'{parent_sha}..{self.commit.hexsha}' + log.debug(f"Attempting merge commit diff: git diff --name-only {diff_range}") + + diff_files = self.repo.git.diff('--name-only', diff_range) + self.show_files = diff_files.splitlines() + + log.debug(f"Changed files detected via git diff (merge commit): {self.show_files}") + log.info(f"Changed file detection: method=merge-diff, source=merge-commit-fallback, files={len(self.show_files)}") + return True + + except Exception as error: + log.debug(f"Failed to detect merge commit changes: {error}") + return False + + def get_default_branch_name(self) -> str: + """ + Get the default branch name from the remote origin. + + Returns: + Default branch name (e.g., 'main', 'master') + """ + try: + # Try to get the default branch from remote HEAD + remote_head = self.repo.remotes.origin.refs.HEAD + # Extract branch name from refs/remotes/origin/HEAD -> refs/remotes/origin/main + default_branch = str(remote_head.reference).split('/')[-1] + log.debug(f"Default branch detected: {default_branch}") + return default_branch + except Exception as error: + log.debug(f"Could not determine default branch from remote: {error}") + # Fallback: check common default branch names + for branch_name in ['main', 'master']: + try: + if f'origin/{branch_name}' in [str(ref) for ref in self.repo.remotes.origin.refs]: + log.debug(f"Using fallback default branch: {branch_name}") + return branch_name + except: + continue + + # Last fallback: assume 'main' + log.debug("Using final fallback default branch: main") + return 'main' + + def is_commit_on_default_branch(self) -> bool: + """ + Check if the current commit is reachable from the default branch. + + Returns: + True if current commit is on the default branch, False otherwise + """ + try: + default_branch = self.get_default_branch_name() + + # Get the default branch's HEAD commit + try: + # Try remote branch first + default_branch_ref = self.repo.remotes.origin.refs[default_branch] + default_branch_commit = default_branch_ref.commit + except: + # Fallback to local branch + try: + default_branch_ref = self.repo.heads[default_branch] + default_branch_commit = default_branch_ref.commit + except: + log.debug(f"Could not find default branch '{default_branch}' locally or remotely") + return False + + # Check if current commit is the same as default branch HEAD + if self.commit.hexsha == default_branch_commit.hexsha: + log.debug("Current commit is the HEAD of the default branch") + return True + + # Check if current commit is an ancestor of the default branch HEAD + # This means the commit is reachable from the default branch + is_ancestor = self.repo.is_ancestor(self.commit, default_branch_commit) + log.debug(f"Current commit is ancestor of default branch: {is_ancestor}") + return is_ancestor + + except Exception as error: + log.debug(f"Error checking if commit is on default branch: {error}") + return False + + def is_on_default_branch(self) -> bool: + """ + Check if we're currently on the default branch (not just if commit is reachable). + + Returns: + True if currently on the default branch, False otherwise + """ + try: + # If we're in detached HEAD state, we're not "on" any branch + if self.repo.head.is_detached: + log.debug("In detached HEAD state, not on any branch") + return False + + current_branch_name = self.repo.active_branch.name + default_branch_name = self.get_default_branch_name() + + is_default = current_branch_name == default_branch_name + log.debug(f"Current branch: {current_branch_name}, Default branch: {default_branch_name}, Is default: {is_default}") + return is_default + + except Exception as error: + log.debug(f"Error checking if on default branch: {error}") + return False + + @staticmethod + def ensure_safe_directory(path: str) -> None: + # Ensure the repo is marked as safe for git (prevents SHA empty/dubious ownership errors) + try : + import subprocess + abs_path = os.path.abspath(path) + # Get all safe directories + result = subprocess.run([ + "git", "config", "--global", "--get-all", "safe.directory" + ], capture_output=True, text=True) + safe_dirs = result.stdout.splitlines() if result.returncode == 0 else [] + if abs_path not in safe_dirs: + subprocess.run([ + "git", "config", "--global", "--add", "safe.directory", abs_path + ], check=True) + log.debug(f"Added {abs_path} to git safe.directory config.") + else: + log.debug(f"{abs_path} already present in git safe.directory config.") + except Exception as safe_error: + log.debug(f"Failed to set safe.directory for git: {safe_error}") \ No newline at end of file diff --git a/socketsecurity/core/helper/__init__.py b/socketsecurity/core/helper/__init__.py new file mode 100644 index 0000000..f10cb6e --- /dev/null +++ b/socketsecurity/core/helper/__init__.py @@ -0,0 +1,119 @@ +import markdown +from bs4 import BeautifulSoup, NavigableString, Tag +import string + + +class Helper: + @staticmethod + def parse_gfm_section(html_content): + """ + Parse a GitHub-Flavored Markdown section containing a table and surrounding content. + Returns a dict with "before_html", "columns", "rows_html", and "after_html". + """ + html = markdown.markdown(html_content, extensions=['extra']) + soup = BeautifulSoup(html, "html.parser") + + table = soup.find('table') + if not table: + # If no table, treat entire content as before_html + return {"before_html": html, "columns": [], "rows_html": [], "after_html": ''} + + # Collect HTML before the table + before_parts = [str(elem) for elem in table.find_previous_siblings()] + before_html = ''.join(reversed(before_parts)) + + # Collect HTML after the table + after_parts = [str(elem) for elem in table.find_next_siblings()] + after_html = ''.join(after_parts) + + # Extract table headers + headers = [th.get_text(strip=True) for th in table.find_all('th')] + + # Extract table rows (skip header) + rows_html = [] + for tr in table.find_all('tr')[1:]: + cells = [str(td) for td in tr.find_all('td')] + rows_html.append(cells) + + return { + "before_html": before_html, + "columns": headers, + "rows_html": rows_html, + "after_html": after_html + } + + @staticmethod + def parse_cell(html_td): + """Convert a table cell HTML into plain text or a dict for links/images.""" + soup = BeautifulSoup(html_td, "html.parser") + a = soup.find('a') + if a: + cell = {"url": a.get('href', '')} + img = a.find('img') + if img: + cell.update({ + "img_src": img.get('src', ''), + "title": img.get('title', ''), + "link_text": a.get_text(strip=True) + }) + else: + cell["link_text"] = a.get_text(strip=True) + return cell + return soup.get_text(strip=True) + + @staticmethod + def parse_html_parts(html_fragment): + """ + Convert an HTML fragment into a list of parts. + Each part is either: + - {"text": "..."} + - {"link": "url", "text": "..."} + - {"img_src": "url", "alt": "...", "title": "..."} + """ + soup = BeautifulSoup(html_fragment, 'html.parser') + parts = [] + + def handle_element(elem): + if isinstance(elem, NavigableString): + text = str(elem).strip() + if text and not all(ch in string.punctuation for ch in text): + parts.append({"text": text}) + elif isinstance(elem, Tag): + if elem.name == 'a': + href = elem.get('href', '') + txt = elem.get_text(strip=True) + parts.append({"link": href, "text": txt}) + elif elem.name == 'img': + parts.append({ + "img_src": elem.get('src', ''), + "alt": elem.get('alt', ''), + "title": elem.get('title', '') + }) + else: + # Recurse into children for nested tags + for child in elem.children: + handle_element(child) + + for element in soup.contents: + handle_element(element) + + return parts + + @staticmethod + def section_to_json(section_result): + """ + Convert a parsed section into structured JSON. + Returns {"before": [...], "table": [...], "after": [...]}. + """ + # Build JSON rows for the table + table_rows = [] + cols = section_result.get('columns', []) + for row_html in section_result.get('rows_html', []): + cells = [Helper.parse_cell(cell_html) for cell_html in row_html] + table_rows.append(dict(zip(cols, cells))) + + return { + "before": Helper.parse_html_parts(section_result.get('before_html', '')), + "table": table_rows, + "after": Helper.parse_html_parts(section_result.get('after_html', '')) + } \ No newline at end of file diff --git a/socketsecurity/core/lazy_file_loader.py b/socketsecurity/core/lazy_file_loader.py new file mode 100644 index 0000000..9127652 --- /dev/null +++ b/socketsecurity/core/lazy_file_loader.py @@ -0,0 +1,165 @@ +""" +Lazy file loading utilities for efficient manifest file processing. +""" +import logging +from typing import List, Tuple, Union, BinaryIO +from io import BytesIO +import os + +log = logging.getLogger("socketdev") + + +class LazyFileLoader: + """ + A file-like object that only opens the actual file when needed for reading. + This prevents keeping too many file descriptors open simultaneously. + + This class implements the standard file-like interface that requests library + expects for multipart uploads, making it a drop-in replacement for regular + file objects. + """ + + def __init__(self, file_path: str, name: str): + self.file_path = file_path + self.name = name + self._file = None + self._closed = False + self._position = 0 + + def _ensure_open(self): + """Ensure the file is open and seek to the correct position.""" + if self._closed: + raise ValueError("I/O operation on closed file.") + + if self._file is None: + self._file = open(self.file_path, 'rb') + log.debug(f"Opened file for reading: {self.file_path}") + # Seek to the current position if we've been reading before + if self._position > 0: + self._file.seek(self._position) + + def read(self, size: int = -1): + """Read from the file, opening it if needed.""" + self._ensure_open() + data = self._file.read(size) + self._position = self._file.tell() + return data + + def readline(self, size: int = -1): + """Read a line from the file.""" + self._ensure_open() + data = self._file.readline(size) + self._position = self._file.tell() + return data + + def seek(self, offset: int, whence: int = 0): + """Seek to a position in the file.""" + if self._closed: + raise ValueError("I/O operation on closed file.") + + # Calculate new position for tracking + if whence == 0: # SEEK_SET + self._position = offset + elif whence == 1: # SEEK_CUR + self._position += offset + elif whence == 2: # SEEK_END + # We need to open the file to get its size + self._ensure_open() + result = self._file.seek(offset, whence) + self._position = self._file.tell() + return result + + # If file is already open, seek it too + if self._file is not None: + result = self._file.seek(self._position) + return result + + return self._position + + def tell(self): + """Return current file position.""" + if self._closed: + raise ValueError("I/O operation on closed file.") + + if self._file is not None: + self._position = self._file.tell() + + return self._position + + def close(self): + """Close the file if it was opened.""" + if self._file is not None: + self._file.close() + log.debug(f"Closed file: {self.file_path}") + self._file = None + self._closed = True + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + @property + def closed(self): + """Check if the file is closed.""" + return self._closed + + @property + def mode(self): + """Return the file mode.""" + return 'rb' + + def readable(self): + """Return whether the file is readable.""" + return not self._closed + + def writable(self): + """Return whether the file is writable.""" + return False + + def seekable(self): + """Return whether the file supports seeking.""" + return True + + +def load_files_for_sending_lazy(files: List[str], workspace: str) -> List[Tuple[str, Tuple[str, LazyFileLoader]]]: + """ + Prepares files for sending to the Socket API using lazy loading. + + This version doesn't open all files immediately, instead it creates + LazyFileLoader objects that only open files when they're actually read. + This prevents "Too many open files" errors when dealing with large numbers + of manifest files. + + Args: + files: List of file paths from find_files() + workspace: Base directory path to make paths relative to + + Returns: + List of tuples formatted for requests multipart upload: + [(field_name, (filename, lazy_file_object)), ...] + """ + send_files = [] + if "\\" in workspace: + workspace = workspace.replace("\\", "/") + + for file_path in files: + _, name = file_path.rsplit("/", 1) + + if file_path.startswith(workspace): + key = file_path[len(workspace):] + else: + key = file_path + + key = key.lstrip("/") + key = key.lstrip("./") + + # Create lazy file loader instead of opening file immediately + # Use the relative path (key) as filename instead of truncated basename + lazy_file = LazyFileLoader(file_path, key) + payload = (key, (key, lazy_file)) + send_files.append(payload) + + log.debug(f"Prepared {len(send_files)} files for lazy loading") + return send_files diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index b86b37f..6412b4d 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -1,5 +1,6 @@ import json import logging +import os import re from pathlib import Path from mdutils import MdUtils @@ -29,6 +30,92 @@ def map_severity_to_sarif(severity: str) -> str: } return severity_mapping.get(severity.lower(), "note") + @staticmethod + def get_manifest_file_url(diff: Diff, manifest_path: str, config=None) -> str: + """ + Generate proper URL for manifest file based on the repository type and diff URL. + + :param diff: Diff object containing diff_url and report_url + :param manifest_path: Path to the manifest file (can contain multiple files separated by ';') + :param config: Configuration object to determine SCM type + :return: Properly formatted URL for the manifest file + """ + if not manifest_path: + return "" + + # Handle multiple manifest files separated by ';' - use the first one + first_manifest = manifest_path.split(';')[0] if ';' in manifest_path else manifest_path + + # Clean up the manifest path - remove build agent paths and normalize + clean_path = first_manifest + + # Remove common build agent path prefixes + prefixes_to_remove = [ + 'opt/buildagent/work/', + '/opt/buildagent/work/', + 'home/runner/work/', + '/home/runner/work/', + ] + + for prefix in prefixes_to_remove: + if clean_path.startswith(prefix): + # Find the part after the build ID (usually a hash) + parts = clean_path[len(prefix):].split('/', 2) + if len(parts) >= 3: + clean_path = parts[2] # Take everything after build ID and repo name + break + + # Remove leading slashes + clean_path = clean_path.lstrip('/') + + # Determine SCM type from config or diff_url + scm_type = "api" # Default to API + if config and hasattr(config, 'scm'): + scm_type = config.scm.lower() + elif hasattr(diff, 'diff_url') and diff.diff_url: + diff_url = diff.diff_url.lower() + if 'github.com' in diff_url or 'github' in diff_url: + scm_type = "github" + elif 'gitlab' in diff_url: + scm_type = "gitlab" + elif 'bitbucket' in diff_url: + scm_type = "bitbucket" + + # Generate URL based on SCM type using config information + # NEVER use diff.diff_url for SCM URLs - those are Socket URLs for "View report" links + if scm_type == "github": + if config and hasattr(config, 'repo') and config.repo: + # Get branch from config, default to main + branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main' + # Construct GitHub URL from repo info (could be github.com or GitHub Enterprise) + github_server = os.getenv('GITHUB_SERVER_URL', 'https://github.com') + return f"{github_server}/{config.repo}/blob/{branch}/{clean_path}" + + elif scm_type == "gitlab": + if config and hasattr(config, 'repo') and config.repo: + # Get branch from config, default to main + branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main' + # Construct GitLab URL from repo info (could be gitlab.com or self-hosted GitLab) + gitlab_server = os.getenv('CI_SERVER_URL', 'https://gitlab.com') + return f"{gitlab_server}/{config.repo}/-/blob/{branch}/{clean_path}" + + elif scm_type == "bitbucket": + if config and hasattr(config, 'repo') and config.repo: + # Get branch from config, default to main + branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main' + # Construct Bitbucket URL from repo info (could be bitbucket.org or Bitbucket Server) + bitbucket_server = os.getenv('BITBUCKET_SERVER_URL', 'https://bitbucket.org') + return f"{bitbucket_server}/{config.repo}/src/{branch}/{clean_path}" + + # Fallback to Socket file view for API or unknown repository types + if hasattr(diff, 'report_url') and diff.report_url: + # Strip leading slash and URL encode for Socket dashboard + socket_path = clean_path.lstrip('/') + encoded_path = socket_path.replace('/', '%2F') + return f"{diff.report_url}?tab=files&file={encoded_path}" + + return "" + @staticmethod def find_line_in_file(packagename: str, packageversion: str, manifest_file: str) -> tuple: """ @@ -283,7 +370,7 @@ def create_security_comment_sarif(diff) -> dict: @staticmethod def create_security_comment_json(diff: Diff) -> dict: scan_failed = False - if len(diff.new_alerts) == 0: + if len(diff.new_alerts) > 0: for alert in diff.new_alerts: alert: Issue if alert.error: @@ -292,7 +379,8 @@ def create_security_comment_json(diff: Diff) -> dict: output = { "scan_failed": scan_failed, "new_alerts": [], - "full_scan_id": diff.id + "full_scan_id": diff.id, + "diff_url": diff.diff_url } for alert in diff.new_alerts: alert: Issue @@ -300,21 +388,35 @@ def create_security_comment_json(diff: Diff) -> dict: return output @staticmethod - def security_comment_template(diff: Diff) -> str: + def security_comment_template(diff: Diff, config=None) -> str: """ Generates the security comment template in the new required format. Dynamically determines placement of the alerts table if markers like `` are used. :param diff: Diff - Contains the detected vulnerabilities and warnings. + :param config: Optional configuration object to determine SCM type. :return: str - The formatted Markdown/HTML string. """ + # Group license policy violations by PURL (ecosystem/package@version) + license_groups = {} + security_alerts = [] + + for alert in diff.new_alerts: + if alert.type == "licenseSpdxDisj": + purl_key = f"{alert.pkg_type}/{alert.pkg_name}@{alert.pkg_version}" + if purl_key not in license_groups: + license_groups[purl_key] = [] + license_groups[purl_key].append(alert) + else: + security_alerts.append(alert) + # Start of the comment comment = """ > **❗️ Caution** > **Review the following alerts detected in dependencies.** > -> According to your organization’s Security Policy, you **must** resolve all **“Block”** alerts before proceeding. It’s recommended to resolve **“Warn”** alerts too. +> According to your organization's Security Policy, you **must** resolve all **"Block"** alerts before proceeding. It's recommended to resolve **"Warn"** alerts too. > Learn more about [Socket for GitHub](https://socket.dev?utm_medium=gh). @@ -329,11 +431,13 @@ def security_comment_template(diff: Diff) -> str: """ - # Loop through alerts, dynamically generating rows - for alert in diff.new_alerts: + # Loop through security alerts (non-license), dynamically generating rows + for alert in security_alerts: severity_icon = Messages.get_severity_icon(alert.severity) action = "Block" if alert.error else "Warn" details_open = "" + # Generate proper manifest URL + manifest_url = Messages.get_manifest_file_url(diff, alert.manifests, config) # Generate a table row for each alert comment += f""" @@ -346,7 +450,7 @@ def security_comment_template(diff: Diff) -> str:
{alert.pkg_name}@{alert.pkg_version} - {alert.title}

Note: {alert.description}

-

Source: Manifest File

+

Source: Manifest File

ℹ️ Read more on: This package | This alert | @@ -364,13 +468,65 @@ def security_comment_template(diff: Diff) -> str: """ - # Close table and comment - comment += """ + # Add license policy violation entries grouped by PURL + for purl_key, alerts in license_groups.items(): + action = "Block" if any(alert.error for alert in alerts) else "Warn" + first_alert = alerts[0] + + # Use orange diamond for license policy violations + license_icon = "🔶" + + # Build license findings list + license_findings = [] + for alert in alerts: + license_findings.append(alert.title) + + comment += f""" + + + {action} + {license_icon} + +

+ {first_alert.pkg_name}@{first_alert.pkg_version} has a License Policy Violation. +

License findings:

+
    +""" + for finding in license_findings: + comment += f"
  • {finding}
  • \n" + + + # Generate proper manifest URL for license violations + license_manifest_url = Messages.get_manifest_file_url(diff, first_alert.manifests, config) + + comment += f"""
+

From: Manifest File

+

ℹ️ Read more on: This package | What is a license policy violation?

+
+

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

+

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

+

Mark the package as acceptable risk: To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore {first_alert.pkg_name}@{first_alert.pkg_version}. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

+
+
+ + + + """ + + # Close table + # Use diff_url for PRs, report_url for non-PR scans + view_report_url = "" + if hasattr(diff, 'diff_url') and diff.diff_url: + view_report_url = diff.diff_url + elif hasattr(diff, 'report_url') and diff.report_url: + view_report_url = diff.report_url + + comment += f""" -[View full report](https://socket.dev/...&action=error%2Cwarn) +[View full report]({view_report_url}?action=error%2Cwarn) """ return comment @@ -464,7 +620,7 @@ def create_acceptable_risk(md: MdUtils, ignore_commands: list) -> MdUtils: return md @staticmethod - def create_security_alert_table(diff: Diff, md: MdUtils) -> (MdUtils, list, dict): + def create_security_alert_table(diff: Diff, md: MdUtils) -> tuple[MdUtils, list, dict]: """ Creates the detected issues table based on the Security Policy :param diff: Diff - Diff report with the detected issues @@ -675,7 +831,7 @@ def create_console_security_alert_table(diff: Diff) -> PrettyTable: return alert_table @staticmethod - def create_sources(alert: Issue, style="md") -> [str, str]: + def create_sources(alert: Issue, style="md") -> tuple[str, str]: sources = [] manifests = [] diff --git a/socketsecurity/core/resource_utils.py b/socketsecurity/core/resource_utils.py new file mode 100644 index 0000000..2652bbf --- /dev/null +++ b/socketsecurity/core/resource_utils.py @@ -0,0 +1,58 @@ +""" +System resource utilities for the Socket Security CLI. +""" +import resource +import logging + +log = logging.getLogger("socketdev") + + +def get_file_descriptor_limit(): + """ + Get the current file descriptor limit (equivalent to ulimit -n) + + Returns: + tuple: (soft_limit, hard_limit) or (None, None) if error + """ + try: + soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE) + return soft_limit, hard_limit + except OSError as e: + log.error(f"Error getting file descriptor limit: {e}") + return None, None + + +def check_file_count_against_ulimit(file_count, buffer_size=100): + """ + Check if the number of files would exceed the file descriptor limit + + Args: + file_count (int): Number of files to check + buffer_size (int): Safety buffer to leave for other file operations + + Returns: + dict: Information about the check + """ + soft_limit, hard_limit = get_file_descriptor_limit() + + if soft_limit is None: + return { + "can_check": False, + "error": "Could not determine file descriptor limit", + "safe_to_process": True # Assume safe if we can't check + } + + available_fds = soft_limit - buffer_size + would_exceed = file_count > available_fds + + return { + "can_check": True, + "file_count": file_count, + "soft_limit": soft_limit, + "hard_limit": hard_limit, + "available_fds": available_fds, + "would_exceed": would_exceed, + "safe_to_process": not would_exceed, + "buffer_size": buffer_size, + "recommendation": "Consider processing files in batches or increasing ulimit" if would_exceed else "Safe to process all files" + } diff --git a/socketsecurity/core/scm/client.py b/socketsecurity/core/scm/client.py index e5bbb73..1033613 100644 --- a/socketsecurity/core/scm/client.py +++ b/socketsecurity/core/scm/client.py @@ -34,8 +34,51 @@ def get_headers(self) -> Dict: class GitlabClient(ScmClient): def get_headers(self) -> Dict: - return { - 'Authorization': f"Bearer {self.token}", + """ + Determine the appropriate authentication headers for GitLab API. + Uses the same logic as GitlabConfig._get_auth_headers() + """ + return self._get_gitlab_auth_headers(self.token) + + @staticmethod + def _get_gitlab_auth_headers(token: str) -> dict: + """ + Determine the appropriate authentication headers for GitLab API. + + GitLab supports two authentication patterns: + 1. Bearer token (OAuth 2.0 tokens, personal access tokens with api scope) + 2. Private token (personal access tokens) + """ + import os + + base_headers = { 'User-Agent': 'SocketPythonScript/0.0.1', "accept": "application/json" } + + # Check if this is a GitLab CI job token + if token == os.getenv('CI_JOB_TOKEN'): + return { + **base_headers, + 'Authorization': f"Bearer {token}" + } + + # Check for personal access token pattern + if token.startswith('glpat-'): + return { + **base_headers, + 'Authorization': f"Bearer {token}" + } + + # Check for OAuth token pattern (typically longer and alphanumeric) + if len(token) > 40 and token.isalnum(): + return { + **base_headers, + 'Authorization': f"Bearer {token}" + } + + # Default to PRIVATE-TOKEN for other token types + return { + **base_headers, + 'PRIVATE-TOKEN': f"{token}" + } diff --git a/socketsecurity/core/scm/github.py b/socketsecurity/core/scm/github.py index fa40afb..1b4fa0e 100644 --- a/socketsecurity/core/scm/github.py +++ b/socketsecurity/core/scm/github.py @@ -47,7 +47,13 @@ def from_env(cls, pr_number: Optional[str] = None) -> 'GithubConfig': # Add debug logging sha = os.getenv('GITHUB_SHA', '') log.debug(f"Loading SHA from GITHUB_SHA: {sha}") - + event_action = os.getenv('EVENT_ACTION', None) + if not event_action: + event_path = os.getenv('GITHUB_EVENT_PATH') + if event_path and os.path.exists(event_path): + with open(event_path, 'r') as f: + event = json.load(f) + event_action = event.get('action') repository = os.getenv('GITHUB_REPOSITORY', '') owner = os.getenv('GITHUB_REPOSITORY_OWNER', '') if '/' in repository: @@ -74,7 +80,7 @@ def from_env(cls, pr_number: Optional[str] = None) -> 'GithubConfig': env=os.getenv('GITHUB_ENV', ''), token=token, owner=owner, - event_action=os.getenv('EVENT_ACTION'), + event_action=event_action, headers={ 'Authorization': f"Bearer {token}", 'User-Agent': 'SocketPythonScript/0.0.1', diff --git a/socketsecurity/core/scm/gitlab.py b/socketsecurity/core/scm/gitlab.py index 8431cbf..b5f46b7 100644 --- a/socketsecurity/core/scm/gitlab.py +++ b/socketsecurity/core/scm/gitlab.py @@ -42,6 +42,9 @@ def from_env(cls) -> 'GitlabConfig': mr_source_branch = os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME') default_branch = os.getenv('CI_DEFAULT_BRANCH', '') + # Determine which authentication pattern to use + headers = cls._get_auth_headers(token) + return cls( commit_sha=os.getenv('CI_COMMIT_SHA', ''), api_url=os.getenv('CI_API_V4_URL', ''), @@ -57,21 +60,122 @@ def from_env(cls) -> 'GitlabConfig': token=token, repository=project_name, is_default_branch=(mr_source_branch == default_branch if mr_source_branch else False), - headers={ - 'Authorization': f"Bearer {token}", - 'User-Agent': 'SocketPythonScript/0.0.1', - "accept": "application/json" - } + headers=headers ) + @staticmethod + def _get_auth_headers(token: str) -> dict: + """ + Determine the appropriate authentication headers for GitLab API. + + GitLab supports two authentication patterns: + 1. Bearer token (OAuth 2.0 tokens, personal access tokens with api scope) + 2. Private token (personal access tokens) + + Logic for token type determination: + - CI_JOB_TOKEN: Always use Bearer (GitLab CI job token) + - Tokens starting with 'glpat-': Personal access tokens, try Bearer first + - OAuth tokens: Use Bearer + - Other tokens: Use PRIVATE-TOKEN as fallback + """ + base_headers = { + 'User-Agent': 'SocketPythonScript/0.0.1', + "accept": "application/json" + } + + # Check if this is a GitLab CI job token + if token == os.getenv('CI_JOB_TOKEN'): + log.debug("Using Bearer authentication for GitLab CI job token") + return { + **base_headers, + 'Authorization': f"Bearer {token}" + } + + # Check for personal access token pattern + if token.startswith('glpat-'): + log.debug("Using Bearer authentication for GitLab personal access token") + return { + **base_headers, + 'Authorization': f"Bearer {token}" + } + + # Check for OAuth token pattern (typically longer and alphanumeric) + if len(token) > 40 and token.isalnum(): + log.debug("Using Bearer authentication for potential OAuth token") + return { + **base_headers, + 'Authorization': f"Bearer {token}" + } + + # Default to PRIVATE-TOKEN for other token types + log.debug("Using PRIVATE-TOKEN authentication for GitLab token") + return { + **base_headers, + 'PRIVATE-TOKEN': f"{token}" + } + class Gitlab: def __init__(self, client: CliClient, config: Optional[GitlabConfig] = None): self.config = config or GitlabConfig.from_env() self.client = client + def _request_with_fallback(self, **kwargs): + """ + Make a request with automatic fallback between Bearer and PRIVATE-TOKEN authentication. + This provides robustness when the initial token type detection is incorrect. + """ + try: + # Try the initial request with the configured headers + return self.client.request(**kwargs) + except Exception as e: + # Check if this is an authentication error (401) + if hasattr(e, 'response') and e.response and e.response.status_code == 401: + log.debug(f"Authentication failed with initial headers, trying fallback method") + + # Determine the fallback headers + original_headers = kwargs.get('headers', self.config.headers) + fallback_headers = self._get_fallback_headers(original_headers) + + if fallback_headers and fallback_headers != original_headers: + log.debug("Retrying request with fallback authentication method") + kwargs['headers'] = fallback_headers + return self.client.request(**kwargs) + + # Re-raise the original exception if it's not an auth error or fallback failed + raise + + def _get_fallback_headers(self, original_headers: dict) -> dict: + """ + Generate fallback authentication headers. + If using Bearer, fallback to PRIVATE-TOKEN and vice versa. + """ + base_headers = { + 'User-Agent': 'SocketPythonScript/0.0.1', + "accept": "application/json" + } + + # If currently using Bearer, try PRIVATE-TOKEN + if 'Authorization' in original_headers and 'Bearer' in original_headers['Authorization']: + log.debug("Falling back from Bearer to PRIVATE-TOKEN authentication") + return { + **base_headers, + 'PRIVATE-TOKEN': f"{self.config.token}" + } + + # If currently using PRIVATE-TOKEN, try Bearer + elif 'PRIVATE-TOKEN' in original_headers: + log.debug("Falling back from PRIVATE-TOKEN to Bearer authentication") + return { + **base_headers, + 'Authorization': f"Bearer {self.config.token}" + } + + # No fallback available + return None + def check_event_type(self) -> str: pipeline_source = self.config.pipeline_source.lower() - if pipeline_source in ["web", 'merge_request_event', "push"]: + if pipeline_source in ["web", 'merge_request_event', "push", "api"]: if not self.config.mr_iid: return "main" return "diff" @@ -84,7 +188,7 @@ def check_event_type(self) -> str: def post_comment(self, body: str) -> None: path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes" payload = {"body": body} - self.client.request( + self._request_with_fallback( path=path, payload=payload, method="POST", @@ -95,7 +199,7 @@ def post_comment(self, body: str) -> None: def update_comment(self, body: str, comment_id: str) -> None: path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes/{comment_id}" payload = {"body": body} - self.client.request( + self._request_with_fallback( path=path, payload=payload, method="PUT", @@ -106,7 +210,7 @@ def update_comment(self, body: str, comment_id: str) -> None: def get_comments_for_pr(self) -> dict: log.debug(f"Getting Gitlab comments for Repo {self.config.repository} for PR {self.config.mr_iid}") path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes" - response = self.client.request( + response = self._request_with_fallback( path=path, headers=self.config.headers, base_url=self.config.api_url diff --git a/socketsecurity/output.py b/socketsecurity/output.py index 2b523d5..2948bb2 100644 --- a/socketsecurity/output.py +++ b/socketsecurity/output.py @@ -34,6 +34,22 @@ def handle_output(self, diff_report: Diff) -> None: plugin_mgr = PluginManager({"jira": jira_config}) plugin_mgr.send(diff_report, config=self.config) + # Debug Slack webhook configuration when debug is enabled (always show when debug is on) + if self.config.enable_debug: + import os + slack_enabled_env = os.getenv("SOCKET_SLACK_ENABLED", "Not set") + slack_config_env = os.getenv("SOCKET_SLACK_CONFIG_JSON", "Not set") + slack_url = "Not configured" + if self.config.slack_plugin.config and self.config.slack_plugin.config.get("url"): + slack_url = self.config.slack_plugin.config.get("url") + self.logger.debug("=== Slack Webhook Debug Information ===") + self.logger.debug(f"Slack Plugin Enabled: {self.config.slack_plugin.enabled}") + self.logger.debug(f"SOCKET_SLACK_ENABLED environment variable: {slack_enabled_env}") + self.logger.debug(f"SOCKET_SLACK_CONFIG_JSON environment variable: {slack_config_env}") + self.logger.debug(f"Slack Webhook URL: {slack_url}") + self.logger.debug(f"Slack Alert Levels: {self.config.slack_plugin.levels}") + self.logger.debug("=====================================") + if self.config.slack_plugin.enabled: slack_config = { "enabled": self.config.slack_plugin.enabled, @@ -52,10 +68,11 @@ def return_exit_code(self, diff_report: Diff) -> int: if not self.report_pass(diff_report): return 1 - - if len(diff_report.new_alerts) > 0: - # 5 means warning alerts but no blocking alerts - return 5 + + # if there are only warn alerts should be returning 0. This was not intended behavior + # if len(diff_report.new_alerts) > 0: + # # 5 means warning alerts but no blocking alerts + # return 5 return 0 def output_console_comments(self, diff_report: Diff, sbom_file_name: Optional[str] = None) -> None: @@ -66,7 +83,8 @@ def output_console_comments(self, diff_report: Diff, sbom_file_name: Optional[st console_security_comment = Messages.create_console_security_alert_table(diff_report) self.logger.info("Security issues detected by Socket Security:") - self.logger.info(console_security_comment) + self.logger.info(f"Diff Url: {diff_report.diff_url}") + self.logger.info(f"\n{console_security_comment}") def output_console_json(self, diff_report: Diff, sbom_file_name: Optional[str] = None) -> None: """Outputs JSON formatted results""" diff --git a/socketsecurity/plugins/slack.py b/socketsecurity/plugins/slack.py index 0c592dc..a1d1c43 100644 --- a/socketsecurity/plugins/slack.py +++ b/socketsecurity/plugins/slack.py @@ -15,9 +15,13 @@ def get_name(): def send(self, diff, config: CliConfig): if not self.config.get("enabled", False): + if config.enable_debug: + logger.debug("Slack plugin is disabled - skipping webhook notification") return if not self.config.get("url"): logger.warning("Slack webhook URL not configured.") + if config.enable_debug: + logger.debug("Slack webhook URL is missing from configuration") return else: url = self.config.get("url") @@ -31,6 +35,12 @@ def send(self, diff, config: CliConfig): message = self.create_slack_blocks_from_diff(diff, config) logger.debug(f"Sending message to {url}") + + if config.enable_debug: + logger.debug(f"Slack webhook URL: {url}") + logger.debug(f"Number of alerts to send: {len(diff.new_alerts)}") + logger.debug(f"Message blocks count: {len(message)}") + response = requests.post( url, json={"blocks": message} @@ -38,6 +48,8 @@ def send(self, diff, config: CliConfig): if response.status_code >= 400: logger.error("Slack error %s: %s", response.status_code, response.text) + elif config.enable_debug: + logger.debug(f"Slack webhook response: {response.status_code}") @staticmethod def create_slack_blocks_from_diff(diff: Diff, config: CliConfig): diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index e6594ad..2813790 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -6,7 +6,6 @@ from git import InvalidGitRepositoryError, NoSuchPathError from socketdev import socketdev from socketdev.fullscans import FullScanParams - from socketsecurity.config import CliConfig from socketsecurity.core import Core from socketsecurity.core.classes import Diff @@ -76,19 +75,49 @@ def main_code(): log.debug("loaded client") core = Core(socket_config, sdk) log.debug("loaded core") - # Load files - files defaults to "[]" in CliConfig + # Parse files argument try: - files = json.loads(config.files) # Will always succeed with empty list by default - is_repo = True # FIXME: This is misleading - JSON parsing success doesn't indicate repo status + if isinstance(config.files, list): + # Already a list, use as-is + specified_files = config.files + elif isinstance(config.files, str): + # Handle different string formats + files_str = config.files.strip() + + # If the string is wrapped in extra quotes, strip them + if ((files_str.startswith('"') and files_str.endswith('"')) or + (files_str.startswith("'") and files_str.endswith("'"))): + # Check if the inner content looks like JSON + inner_str = files_str[1:-1] + if inner_str.startswith('[') and inner_str.endswith(']'): + files_str = inner_str + + # Try to parse as JSON + try: + specified_files = json.loads(files_str) + except json.JSONDecodeError: + # If JSON parsing fails, try replacing single quotes with double quotes + files_str = files_str.replace("'", '"') + specified_files = json.loads(files_str) + else: + # Default to empty list + specified_files = [] except Exception as error: - # Only hits this if files was manually set to invalid JSON - log.error(f"Unable to parse {config.files}") - log.error(error) + log.error(f"Unable to parse files argument: {config.files}") + log.error(f"Error details: {error}") + log.debug(f"Files type: {type(config.files)}") + log.debug(f"Files repr: {repr(config.files)}") sys.exit(3) + # Determine if files were explicitly specified + files_explicitly_specified = config.files != "[]" and len(specified_files) > 0 + # Git setup + is_repo = False + git_repo = None try: git_repo = Git(config.target_path) + is_repo = True if not config.repo: config.repo = git_repo.repo_name if not config.commit_sha: @@ -96,21 +125,52 @@ def main_code(): if not config.branch: config.branch = git_repo.branch if not config.committers: - config.committers = [git_repo.committer] + config.committers = [git_repo.get_formatted_committer()] if not config.commit_message: config.commit_message = git_repo.commit_message - if files and not config.ignore_commit_files: # files is empty by default, so this is False unless files manually specified - files = git_repo.changed_files # Only gets git's changed files if files were manually specified - is_repo = True # Redundant since already True except InvalidGitRepositoryError: - is_repo = False # Overwrites previous True - this is the REAL repo status - config.ignore_commit_files = True # Silently changes config - should log this + is_repo = False + log.debug("Not a git repository, setting ignore_commit_files=True") + config.ignore_commit_files = True except NoSuchPathError: raise Exception(f"Unable to find path {config.target_path}") if not config.repo: - log.info("Repo name needs to be set") - sys.exit(2) + base_repo_name = "socket-default-repo" + if config.workspace_name: + config.repo = f"{base_repo_name}-{config.workspace_name}" + else: + config.repo = base_repo_name + log.debug(f"Using default repository name: {config.repo}") + + if not config.branch: + config.branch = "socket-default-branch" + log.debug(f"Using default branch name: {config.branch}") + + # Calculate the scan paths - combine target_path with sub_paths if provided + scan_paths = [] + base_paths = [config.target_path] # Always use target_path as the single base path + + if config.sub_paths: + import os + for sub_path in config.sub_paths: + full_scan_path = os.path.join(config.target_path, sub_path) + log.debug(f"Using sub-path for scanning: {full_scan_path}") + # Verify the scan path exists + if not os.path.exists(full_scan_path): + raise Exception(f"Sub-path does not exist: {full_scan_path}") + scan_paths.append(full_scan_path) + else: + # Use the target path as the single scan path + scan_paths = [config.target_path] + + # Modify repository name if workspace_name is provided + if config.workspace_name and config.repo: + config.repo = f"{config.repo}-{config.workspace_name}" + log.debug(f"Modified repository name with workspace suffix: {config.repo}") + elif config.workspace_name and not config.repo: + # If no repo name was set but workspace_name is provided, we'll use it later + log.debug(f"Workspace name provided: {config.workspace_name}") scm = None if config.scm == "github": @@ -123,29 +183,66 @@ def main_code(): from socketsecurity.core.scm.gitlab import Gitlab, GitlabConfig gitlab_config = GitlabConfig.from_env() scm = Gitlab(client=client, config=gitlab_config) - if scm is not None: + # Don't override config.default_branch if it was explicitly set via --default-branch flag + # Only use SCM detection if --default-branch wasn't provided + if scm is not None and not config.default_branch: config.default_branch = scm.config.is_default_branch + # Determine files to check based on the new logic + files_to_check = [] + force_api_mode = False + + if files_explicitly_specified: + # Case 2: Files are specified - use them and don't check commit details + files_to_check = specified_files + log.debug(f"Using explicitly specified files: {files_to_check}") + elif not config.ignore_commit_files and is_repo: + # Case 1: Files not specified and --ignore-commit-files not set - try to find changed files from commit + files_to_check = git_repo.changed_files + log.debug(f"Using changed files from commit: {files_to_check}") + else: + # ignore_commit_files is set or not a repo - scan everything but force API mode if no supported files + files_to_check = [] + log.debug("No files to check from commit (ignore_commit_files=True or not a repo)") - # Combine manually specified files with git changes if applicable - files_to_check = set(json.loads(config.files)) # Start with manually specified files - - # Add git changes if this is a repo and we're not ignoring commit files - if is_repo and not config.ignore_commit_files: - files_to_check.update(git_repo.changed_files) - - # Determine if we need to scan based on manifest files - should_skip_scan = True # Default to skipping - if config.ignore_commit_files: - should_skip_scan = False # Force scan if ignoring commit files - elif files_to_check: # If we have any files to check - should_skip_scan = not core.has_manifest_files(list(files_to_check)) - log.debug(f"in elif, should_skip_scan: {should_skip_scan}") - - if should_skip_scan: - log.debug("No manifest files found in changes, skipping scan") + # Check if we have supported manifest files + has_supported_files = files_to_check and core.has_manifest_files(files_to_check) + + # If using sub_paths, we need to check if manifest files exist in the scan paths + if config.sub_paths and not files_explicitly_specified: + # Override file checking to look in the scan paths instead + import os + from pathlib import Path + + # Get manifest files from all scan paths + try: + all_scan_files = [] + for scan_path in scan_paths: + scan_files = core.find_files(scan_path) + all_scan_files.extend(scan_files) + has_supported_files = len(all_scan_files) > 0 + log.debug(f"Found {len(all_scan_files)} manifest files across {len(scan_paths)} scan paths") + except Exception as e: + log.debug(f"Error finding files in scan paths: {e}") + has_supported_files = False + + # Case 3: If no supported files or files are empty, force API mode (no PR comments) + if not has_supported_files: + force_api_mode = True + log.debug("No supported manifest files found, forcing API mode") + + # Determine scan behavior + should_skip_scan = False # Always perform scan, but behavior changes based on supported files + if config.ignore_commit_files and not files_explicitly_specified: + # Force full scan when ignoring commit files and no explicit files + should_skip_scan = False + log.debug("Forcing full scan due to ignore_commit_files") + elif not has_supported_files: + # No supported files - still scan but in API mode + should_skip_scan = False + log.debug("No supported files but will scan in API mode") else: - log.debug("Found manifest files or forced scan, proceeding") + log.debug("Found supported manifest files, proceeding with normal scan") org_slug = core.config.org_slug if config.repo_is_public: @@ -159,6 +256,21 @@ def main_code(): except (ValueError, TypeError): pr_number = 0 + # Determine if this should be treated as default branch + # Priority order: + # 1. If --default-branch flag is explicitly set to True, use that + # 2. If SCM detected it's the default branch, use that + # 3. If it's a git repo, use git_repo.is_default_branch + # 4. Otherwise, default to False + if config.default_branch: + is_default_branch = True + elif scm is not None and hasattr(scm.config, 'is_default_branch') and scm.config.is_default_branch: + is_default_branch = True + elif is_repo and git_repo.is_default_branch: + is_default_branch = True + else: + is_default_branch = False + params = FullScanParams( org_slug=org_slug, integration_type=integration_type, @@ -169,8 +281,8 @@ def main_code(): commit_hash=config.commit_sha, pull_request=pr_number, committers=config.committers, - make_default_branch=config.default_branch, - set_as_pending_head=True + make_default_branch=is_default_branch, + set_as_pending_head=is_default_branch ) params.include_license_details = not config.exclude_license_details @@ -178,6 +290,8 @@ def main_code(): # Initialize diff diff = Diff() diff.id = "NO_DIFF_RAN" + diff.diff_url = "" + diff.report_url = "" # Handle SCM-specific flows if scm is not None and scm.check_event_type() == "comment": @@ -193,13 +307,11 @@ def main_code(): log.debug("Removing comment alerts") scm.remove_comment_alerts(comments) - elif scm is not None and scm.check_event_type() != "comment": + elif scm is not None and scm.check_event_type() != "comment" and not force_api_mode: log.info("Push initiated flow") - if should_skip_scan: - log.info("No manifest files changes, skipping scan") - elif scm.check_event_type() == "diff": + if scm.check_event_type() == "diff": log.info("Starting comment logic for PR/MR event") - diff = core.create_new_diff(config.target_path, params, no_change=should_skip_scan) + diff = core.create_new_diff(scan_paths, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar, base_paths=base_paths) comments = scm.get_comments_for_pr() log.debug("Removing comment alerts") @@ -210,7 +322,7 @@ def main_code(): overview_comment = Messages.dependency_overview_template(diff) log.debug("Creating Security Issues Comment") - security_comment = Messages.security_comment_template(diff) + security_comment = Messages.security_comment_template(diff, config) new_security_comment = True new_overview_comment = True @@ -235,7 +347,7 @@ def main_code(): log.debug("Updated security comment with no new alerts") # FIXME: diff.new_packages is never populated, neither is removed_packages - if (len(diff.new_packages) == 0 and len(diff.removed_packages) == 0) or config.disable_overview: + if (len(diff.new_packages) == 0) or config.disable_overview: if not update_old_overview_comment: new_overview_comment = False log.debug("No new/removed packages or Dependency Overview comment disabled") @@ -243,7 +355,6 @@ def main_code(): log.debug("Updated overview comment with no dependencies") log.debug(f"Adding comments for {config.scm}") - scm.add_socket_comments( security_comment, overview_comment, @@ -253,35 +364,90 @@ def main_code(): ) else: log.info("Starting non-PR/MR flow") - diff = core.create_new_diff(config.target_path, params, no_change=should_skip_scan) + diff = core.create_new_diff(scan_paths, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar, base_paths=base_paths) output_handler.handle_output(diff) - else: - log.info("API Mode") - diff = core.create_new_diff(config.target_path, params, no_change=should_skip_scan) + + elif config.enable_diff and not force_api_mode: + # New logic: --enable-diff forces diff mode even with --integration api (no SCM) + log.info("Diff mode enabled without SCM integration") + diff = core.create_new_diff(scan_paths, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar, base_paths=base_paths) output_handler.handle_output(diff) + + elif config.enable_diff and force_api_mode: + # User requested diff mode but no manifest files were detected + log.warning("--enable-diff was specified but no supported manifest files were detected in the changed files. Falling back to full scan mode.") + log.info("Creating Socket Report (full scan)") + serializable_params = { + key: value if isinstance(value, (int, float, str, list, dict, bool, type(None))) else str(value) + for key, value in params.__dict__.items() + } + log.debug(f"params={serializable_params}") + diff = core.create_full_scan_with_report_url( + scan_paths, + params, + no_change=should_skip_scan, + save_files_list_path=config.save_submitted_files_list, + save_manifest_tar_path=config.save_manifest_tar, + base_paths=base_paths + ) + log.info(f"Full scan created with ID: {diff.id}") + log.info(f"Full scan report URL: {diff.report_url}") + output_handler.handle_output(diff) + + else: + if force_api_mode: + log.info("No Manifest files changed, creating Socket Report") + serializable_params = { + key: value if isinstance(value, (int, float, str, list, dict, bool, type(None))) else str(value) + for key, value in params.__dict__.items() + } + log.debug(f"params={serializable_params}") + diff = core.create_full_scan_with_report_url( + scan_paths, + params, + no_change=should_skip_scan, + save_files_list_path=config.save_submitted_files_list, + save_manifest_tar_path=config.save_manifest_tar, + base_paths=base_paths + ) + log.info(f"Full scan created with ID: {diff.id}") + log.info(f"Full scan report URL: {diff.report_url}") + else: + log.info("API Mode") + diff = core.create_new_diff( + scan_paths, params, + no_change=should_skip_scan, + save_files_list_path=config.save_submitted_files_list, + save_manifest_tar_path=config.save_manifest_tar, + base_paths=base_paths + ) + output_handler.handle_output(diff) - # Handle license generation - if diff is not None and config.generate_license: + # Handle license generation + if not should_skip_scan and diff.id != "NO_DIFF_RAN" and diff.id != "NO_SCAN_RAN" and config.generate_license: all_packages = {} - for package_id in diff.packages: - package = diff.packages[package_id] + for purl in diff.packages: + package = diff.packages[purl] output = { - "id": package_id, + "id": package.id, "name": package.name, "version": package.version, "ecosystem": package.type, "direct": package.direct, "url": package.url, "license": package.license, - "license_text": package.license_text + "licenseDetails": package.licenseDetails, + "licenseAttrib": package.licenseAttrib, + "purl": package.purl, } - all_packages[package_id] = output - license_file = f"{config.repo}" - if config.branch: - license_file += f"_{config.branch}" - license_file += ".json" - core.save_file(license_file, json.dumps(all_packages)) + all_packages[package.id] = output + core.save_file(config.license_file_name, json.dumps(all_packages)) + + # If we forced API mode due to no supported files, behave as if --disable-blocking was set + if force_api_mode and not config.disable_blocking: + log.debug("Temporarily enabling disable_blocking due to no supported manifest files") + config.disable_blocking = True sys.exit(output_handler.return_exit_code(diff)) diff --git a/tests/unit/test_cli_config.py b/tests/unit/test_cli_config.py index db7b1f5..40178d3 100644 --- a/tests/unit/test_cli_config.py +++ b/tests/unit/test_cli_config.py @@ -24,8 +24,20 @@ def test_default_values(self): @pytest.mark.parametrize("flag,attr", [ ("--enable-debug", "enable_debug"), ("--disable-blocking", "disable_blocking"), - ("--allow-unverified", "allow_unverified") + ("--allow-unverified", "allow_unverified"), + ("--enable-diff", "enable_diff") ]) def test_boolean_flags(self, flag, attr): config = CliConfig.from_args(["--api-token", "test", flag]) - assert getattr(config, attr) is True \ No newline at end of file + assert getattr(config, attr) is True + + def test_enable_diff_default_false(self): + """Test that enable_diff defaults to False""" + config = CliConfig.from_args(["--api-token", "test"]) + assert config.enable_diff is False + + def test_enable_diff_with_integration_api(self): + """Test that enable_diff can be used with integration api""" + config = CliConfig.from_args(["--api-token", "test", "--integration", "api", "--enable-diff"]) + assert config.enable_diff is True + assert config.integration_type == "api" \ No newline at end of file diff --git a/tests/unit/test_gitlab_auth.py b/tests/unit/test_gitlab_auth.py new file mode 100644 index 0000000..be34224 --- /dev/null +++ b/tests/unit/test_gitlab_auth.py @@ -0,0 +1,116 @@ +"""Tests for GitLab authentication patterns""" +import os +import pytest +from unittest.mock import patch, MagicMock + +from socketsecurity.core.scm.gitlab import GitlabConfig + + +class TestGitlabAuthHeaders: + """Test GitLab authentication header generation""" + + def test_ci_job_token_uses_bearer(self): + """CI_JOB_TOKEN should always use Bearer authentication""" + with patch.dict(os.environ, {'CI_JOB_TOKEN': 'ci-job-token-123'}): + headers = GitlabConfig._get_auth_headers('ci-job-token-123') + assert 'Authorization' in headers + assert headers['Authorization'] == 'Bearer ci-job-token-123' + assert 'PRIVATE-TOKEN' not in headers + + def test_personal_access_token_uses_bearer(self): + """Personal access tokens (glpat-*) should use Bearer authentication""" + token = 'glpat-xxxxxxxxxxxxxxxxxxxx' + headers = GitlabConfig._get_auth_headers(token) + assert 'Authorization' in headers + assert headers['Authorization'] == f'Bearer {token}' + assert 'PRIVATE-TOKEN' not in headers + + def test_oauth_token_uses_bearer(self): + """Long alphanumeric tokens (OAuth) should use Bearer authentication""" + token = 'a' * 50 # 50 character alphanumeric token + headers = GitlabConfig._get_auth_headers(token) + assert 'Authorization' in headers + assert headers['Authorization'] == f'Bearer {token}' + assert 'PRIVATE-TOKEN' not in headers + + def test_short_token_uses_private_token(self): + """Short tokens should use PRIVATE-TOKEN authentication""" + token = 'short-token-123' + headers = GitlabConfig._get_auth_headers(token) + assert 'PRIVATE-TOKEN' in headers + assert headers['PRIVATE-TOKEN'] == token + assert 'Authorization' not in headers + + def test_mixed_alphanumeric_token_uses_private_token(self): + """Tokens with non-alphanumeric characters should use PRIVATE-TOKEN""" + token = 'token-with-dashes-and_underscores' + headers = GitlabConfig._get_auth_headers(token) + assert 'PRIVATE-TOKEN' in headers + assert headers['PRIVATE-TOKEN'] == token + assert 'Authorization' not in headers + + def test_all_headers_include_base_headers(self): + """All authentication patterns should include base headers""" + test_tokens = [ + 'glpat-xxxxxxxxxxxxxxxxxxxx', # Bearer + 'short-token' # PRIVATE-TOKEN + ] + + for token in test_tokens: + headers = GitlabConfig._get_auth_headers(token) + assert headers['User-Agent'] == 'SocketPythonScript/0.0.1' + assert headers['accept'] == 'application/json' + + @patch.dict(os.environ, {'CI_JOB_TOKEN': 'ci-token-123'}) + def test_ci_job_token_detection_priority(self): + """CI_JOB_TOKEN should be detected even if token doesn't match CI_JOB_TOKEN value""" + # This tests the case where GITLAB_TOKEN != CI_JOB_TOKEN + headers = GitlabConfig._get_auth_headers('different-token') + # Should not use Bearer since token doesn't match CI_JOB_TOKEN + assert 'PRIVATE-TOKEN' in headers + assert headers['PRIVATE-TOKEN'] == 'different-token' + + +class TestGitlabConfigFromEnv: + """Test GitlabConfig.from_env() method""" + + @patch.dict(os.environ, { + 'GITLAB_TOKEN': 'glpat-test-token', + 'CI_PROJECT_NAME': 'test-project', + 'CI_API_V4_URL': 'https://gitlab.example.com/api/v4', + 'CI_COMMIT_SHA': 'abc123', + 'CI_PROJECT_DIR': '/builds/test', + 'CI_PIPELINE_SOURCE': 'merge_request_event' + }) + def test_from_env_creates_config_with_correct_headers(self): + """from_env should create config with appropriate auth headers""" + config = GitlabConfig.from_env() + + # Should use Bearer for glpat- token + assert 'Authorization' in config.headers + assert config.headers['Authorization'] == 'Bearer glpat-test-token' + assert 'PRIVATE-TOKEN' not in config.headers + assert config.token == 'glpat-test-token' + + @patch.dict(os.environ, { + 'GITLAB_TOKEN': 'custom-token', + 'CI_PROJECT_NAME': 'test-project' + }, clear=True) + def test_from_env_with_private_token(self): + """from_env should use PRIVATE-TOKEN for non-standard tokens""" + config = GitlabConfig.from_env() + + # Should use PRIVATE-TOKEN for custom token + assert 'PRIVATE-TOKEN' in config.headers + assert config.headers['PRIVATE-TOKEN'] == 'custom-token' + assert 'Authorization' not in config.headers + + def test_from_env_missing_token_exits(self): + """from_env should exit when GITLAB_TOKEN is missing""" + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(SystemExit): + GitlabConfig.from_env() + + +if __name__ == '__main__': + pytest.main([__file__]) diff --git a/tests/unit/test_gitlab_auth_fallback.py b/tests/unit/test_gitlab_auth_fallback.py new file mode 100644 index 0000000..e9e9b0c --- /dev/null +++ b/tests/unit/test_gitlab_auth_fallback.py @@ -0,0 +1,148 @@ +"""Integration test demonstrating GitLab authentication fallback""" +import os +from unittest.mock import patch, MagicMock +import pytest + +from socketsecurity.core.scm.gitlab import Gitlab, GitlabConfig +from socketsecurity.socketcli import CliClient + + +class TestGitlabAuthFallback: + """Test GitLab authentication fallback mechanism""" + + @patch.dict(os.environ, { + 'GITLAB_TOKEN': 'test-token', + 'CI_PROJECT_NAME': 'test-project', + 'CI_API_V4_URL': 'https://gitlab.example.com/api/v4', + 'CI_MERGE_REQUEST_IID': '123', + 'CI_MERGE_REQUEST_PROJECT_ID': '456' + }) + def test_fallback_from_private_token_to_bearer(self): + """Test fallback from PRIVATE-TOKEN to Bearer authentication""" + # Create a mock client that simulates auth failure then success + mock_client = MagicMock(spec=CliClient) + + # First call (with PRIVATE-TOKEN) fails with 401 + auth_error = Exception() + auth_error.response = MagicMock() + auth_error.response.status_code = 401 + + # Second call (with Bearer) succeeds + success_response = {'notes': []} + + mock_client.request.side_effect = [auth_error, success_response] + + # Create GitLab instance with mock client + gitlab = Gitlab(client=mock_client) + + # This should trigger the fallback mechanism + result = gitlab.get_comments_for_pr() + + # Verify two requests were made + assert mock_client.request.call_count == 2 + + # First call should use PRIVATE-TOKEN (default for 'test-token') + first_call_headers = mock_client.request.call_args_list[0][1]['headers'] + assert 'PRIVATE-TOKEN' in first_call_headers + assert first_call_headers['PRIVATE-TOKEN'] == 'test-token' + + # Second call should use Bearer (fallback) + second_call_headers = mock_client.request.call_args_list[1][1]['headers'] + assert 'Authorization' in second_call_headers + assert second_call_headers['Authorization'] == 'Bearer test-token' + + @patch.dict(os.environ, { + 'GITLAB_TOKEN': 'glpat-test-token', + 'CI_PROJECT_NAME': 'test-project', + 'CI_API_V4_URL': 'https://gitlab.example.com/api/v4', + 'CI_MERGE_REQUEST_IID': '123', + 'CI_MERGE_REQUEST_PROJECT_ID': '456' + }) + def test_fallback_from_bearer_to_private_token(self): + """Test fallback from Bearer to PRIVATE-TOKEN authentication""" + # Create a mock client that simulates auth failure then success + mock_client = MagicMock(spec=CliClient) + + # First call (with Bearer) fails with 401 + auth_error = Exception() + auth_error.response = MagicMock() + auth_error.response.status_code = 401 + + # Second call (with PRIVATE-TOKEN) succeeds + success_response = {'notes': []} + + mock_client.request.side_effect = [auth_error, success_response] + + # Create GitLab instance with mock client + gitlab = Gitlab(client=mock_client) + + # This should trigger the fallback mechanism + result = gitlab.get_comments_for_pr() + + # Verify two requests were made + assert mock_client.request.call_count == 2 + + # First call should use Bearer (default for 'glpat-' token) + first_call_headers = mock_client.request.call_args_list[0][1]['headers'] + assert 'Authorization' in first_call_headers + assert first_call_headers['Authorization'] == 'Bearer glpat-test-token' + + # Second call should use PRIVATE-TOKEN (fallback) + second_call_headers = mock_client.request.call_args_list[1][1]['headers'] + assert 'PRIVATE-TOKEN' in second_call_headers + assert second_call_headers['PRIVATE-TOKEN'] == 'glpat-test-token' + + @patch.dict(os.environ, { + 'GITLAB_TOKEN': 'test-token', + 'CI_PROJECT_NAME': 'test-project', + 'CI_API_V4_URL': 'https://gitlab.example.com/api/v4', + 'CI_MERGE_REQUEST_IID': '123', + 'CI_MERGE_REQUEST_PROJECT_ID': '456' + }) + def test_non_auth_error_not_retried(self): + """Test that non-authentication errors are not retried""" + # Create a mock client that simulates a non-auth error + mock_client = MagicMock(spec=CliClient) + + # Simulate a 500 error (not auth-related) + server_error = Exception() + server_error.response = MagicMock() + server_error.response.status_code = 500 + + mock_client.request.side_effect = server_error + + # Create GitLab instance with mock client + gitlab = Gitlab(client=mock_client) + + # This should NOT trigger the fallback mechanism + with pytest.raises(Exception): + gitlab.get_comments_for_pr() + + # Verify only one request was made (no retry) + assert mock_client.request.call_count == 1 + + @patch.dict(os.environ, { + 'GITLAB_TOKEN': 'test-token', + 'CI_PROJECT_NAME': 'test-project', + 'CI_API_V4_URL': 'https://gitlab.example.com/api/v4', + 'CI_MERGE_REQUEST_IID': '123', + 'CI_MERGE_REQUEST_PROJECT_ID': '456' + }) + def test_successful_first_attempt_no_fallback(self): + """Test that successful requests don't trigger fallback""" + # Create a mock client that succeeds on first try + mock_client = MagicMock(spec=CliClient) + mock_client.request.return_value = {'notes': []} + + # Create GitLab instance with mock client + gitlab = Gitlab(client=mock_client) + + # This should succeed on first try + result = gitlab.get_comments_for_pr() + + # Verify only one request was made + assert mock_client.request.call_count == 1 + + +if __name__ == '__main__': + pytest.main([__file__]) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..3375542 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1388 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662, upload-time = "2025-08-23T14:42:44.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/70/e77b0061a6c7157bfce645c6b9a715a08d4c86b3360a7b3252818080b817/coverage-7.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c6a5c3414bfc7451b879141ce772c546985163cf553f08e0f135f0699a911801", size = 216774, upload-time = "2025-08-23T14:40:26.301Z" }, + { url = "https://files.pythonhosted.org/packages/91/08/2a79de5ecf37ee40f2d898012306f11c161548753391cec763f92647837b/coverage-7.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bc8e4d99ce82f1710cc3c125adc30fd1487d3cf6c2cd4994d78d68a47b16989a", size = 217175, upload-time = "2025-08-23T14:40:29.142Z" }, + { url = "https://files.pythonhosted.org/packages/64/57/0171d69a699690149a6ba6a4eb702814448c8d617cf62dbafa7ce6bfdf63/coverage-7.10.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:02252dc1216e512a9311f596b3169fad54abcb13827a8d76d5630c798a50a754", size = 243931, upload-time = "2025-08-23T14:40:30.735Z" }, + { url = "https://files.pythonhosted.org/packages/15/06/3a67662c55656702bd398a727a7f35df598eb11104fcb34f1ecbb070291a/coverage-7.10.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73269df37883e02d460bee0cc16be90509faea1e3bd105d77360b512d5bb9c33", size = 245740, upload-time = "2025-08-23T14:40:32.302Z" }, + { url = "https://files.pythonhosted.org/packages/00/f4/f8763aabf4dc30ef0d0012522d312f0b7f9fede6246a1f27dbcc4a1e523c/coverage-7.10.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8a81b0614642f91c9effd53eec284f965577591f51f547a1cbeb32035b4c2f", size = 247600, upload-time = "2025-08-23T14:40:33.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/31/6632219a9065e1b83f77eda116fed4c76fb64908a6a9feae41816dab8237/coverage-7.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6a29f8e0adb7f8c2b95fa2d4566a1d6e6722e0a637634c6563cb1ab844427dd9", size = 245640, upload-time = "2025-08-23T14:40:35.248Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e2/3dba9b86037b81649b11d192bb1df11dde9a81013e434af3520222707bc8/coverage-7.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fcf6ab569436b4a647d4e91accba12509ad9f2554bc93d3aee23cc596e7f99c3", size = 243659, upload-time = "2025-08-23T14:40:36.815Z" }, + { url = "https://files.pythonhosted.org/packages/02/b9/57170bd9f3e333837fc24ecc88bc70fbc2eb7ccfd0876854b0c0407078c3/coverage-7.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:90dc3d6fb222b194a5de60af8d190bedeeddcbc7add317e4a3cd333ee6b7c879", size = 244537, upload-time = "2025-08-23T14:40:38.737Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1c/93ac36ef1e8b06b8d5777393a3a40cb356f9f3dab980be40a6941e443588/coverage-7.10.5-cp310-cp310-win32.whl", hash = "sha256:414a568cd545f9dc75f0686a0049393de8098414b58ea071e03395505b73d7a8", size = 219285, upload-time = "2025-08-23T14:40:40.342Z" }, + { url = "https://files.pythonhosted.org/packages/30/95/23252277e6e5fe649d6cd3ed3f35d2307e5166de4e75e66aa7f432abc46d/coverage-7.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:e551f9d03347196271935fd3c0c165f0e8c049220280c1120de0084d65e9c7ff", size = 220185, upload-time = "2025-08-23T14:40:42.026Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f2/336d34d2fc1291ca7c18eeb46f64985e6cef5a1a7ef6d9c23720c6527289/coverage-7.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c177e6ffe2ebc7c410785307758ee21258aa8e8092b44d09a2da767834f075f2", size = 216890, upload-time = "2025-08-23T14:40:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/92448b07cc1cf2b429d0ce635f59cf0c626a5d8de21358f11e92174ff2a6/coverage-7.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14d6071c51ad0f703d6440827eaa46386169b5fdced42631d5a5ac419616046f", size = 217287, upload-time = "2025-08-23T14:40:45.214Z" }, + { url = "https://files.pythonhosted.org/packages/96/ba/ad5b36537c5179c808d0ecdf6e4aa7630b311b3c12747ad624dcd43a9b6b/coverage-7.10.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:61f78c7c3bc272a410c5ae3fde7792b4ffb4acc03d35a7df73ca8978826bb7ab", size = 247683, upload-time = "2025-08-23T14:40:46.791Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/fe3bbc8d097029d284b5fb305b38bb3404895da48495f05bff025df62770/coverage-7.10.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f39071caa126f69d63f99b324fb08c7b1da2ec28cbb1fe7b5b1799926492f65c", size = 249614, upload-time = "2025-08-23T14:40:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/a1c89a8c8712799efccb32cd0a1ee88e452f0c13a006b65bb2271f1ac767/coverage-7.10.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343a023193f04d46edc46b2616cdbee68c94dd10208ecd3adc56fcc54ef2baa1", size = 251719, upload-time = "2025-08-23T14:40:49.349Z" }, + { url = "https://files.pythonhosted.org/packages/e9/be/5576b5625865aa95b5633315f8f4142b003a70c3d96e76f04487c3b5cc95/coverage-7.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:585ffe93ae5894d1ebdee69fc0b0d4b7c75d8007983692fb300ac98eed146f78", size = 249411, upload-time = "2025-08-23T14:40:50.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/e39a113d4209da0dbbc9385608cdb1b0726a4d25f78672dc51c97cfea80f/coverage-7.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0ef4e66f006ed181df29b59921bd8fc7ed7cd6a9289295cd8b2824b49b570df", size = 247466, upload-time = "2025-08-23T14:40:52.362Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/aebb2d8c9e3533ee340bea19b71c5b76605a0268aa49808e26fe96ec0a07/coverage-7.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb7b0bbf7cc1d0453b843eca7b5fa017874735bef9bfdfa4121373d2cc885ed6", size = 248104, upload-time = "2025-08-23T14:40:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/26570d6ccce8ff5de912cbfd268e7f475f00597cb58da9991fa919c5e539/coverage-7.10.5-cp311-cp311-win32.whl", hash = "sha256:1d043a8a06987cc0c98516e57c4d3fc2c1591364831e9deb59c9e1b4937e8caf", size = 219327, upload-time = "2025-08-23T14:40:55.424Z" }, + { url = "https://files.pythonhosted.org/packages/79/79/5f48525e366e518b36e66167e3b6e5db6fd54f63982500c6a5abb9d3dfbd/coverage-7.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:fefafcca09c3ac56372ef64a40f5fe17c5592fab906e0fdffd09543f3012ba50", size = 220213, upload-time = "2025-08-23T14:40:56.724Z" }, + { url = "https://files.pythonhosted.org/packages/40/3c/9058128b7b0bf333130c320b1eb1ae485623014a21ee196d68f7737f8610/coverage-7.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:7e78b767da8b5fc5b2faa69bb001edafcd6f3995b42a331c53ef9572c55ceb82", size = 218893, upload-time = "2025-08-23T14:40:58.011Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/40d75c7128f871ea0fd829d3e7e4a14460cad7c3826e3b472e6471ad05bd/coverage-7.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2d05c7e73c60a4cecc7d9b60dbfd603b4ebc0adafaef371445b47d0f805c8a9", size = 217077, upload-time = "2025-08-23T14:40:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/18/a8/f333f4cf3fb5477a7f727b4d603a2eb5c3c5611c7fe01329c2e13b23b678/coverage-7.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32ddaa3b2c509778ed5373b177eb2bf5662405493baeff52278a0b4f9415188b", size = 217310, upload-time = "2025-08-23T14:41:00.628Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2c/fbecd8381e0a07d1547922be819b4543a901402f63930313a519b937c668/coverage-7.10.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd382410039fe062097aa0292ab6335a3f1e7af7bba2ef8d27dcda484918f20c", size = 248802, upload-time = "2025-08-23T14:41:02.012Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bc/1011da599b414fb6c9c0f34086736126f9ff71f841755786a6b87601b088/coverage-7.10.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fa22800f3908df31cea6fb230f20ac49e343515d968cc3a42b30d5c3ebf9b5a", size = 251550, upload-time = "2025-08-23T14:41:03.438Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/b5c03c0c721c067d21bc697accc3642f3cef9f087dac429c918c37a37437/coverage-7.10.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f366a57ac81f5e12797136552f5b7502fa053c861a009b91b80ed51f2ce651c6", size = 252684, upload-time = "2025-08-23T14:41:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/d474bc300ebcb6a38a1047d5c465a227605d6473e49b4e0d793102312bc5/coverage-7.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1dc8f1980a272ad4a6c84cba7981792344dad33bf5869361576b7aef42733a", size = 250602, upload-time = "2025-08-23T14:41:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2d/548c8e04249cbba3aba6bd799efdd11eee3941b70253733f5d355d689559/coverage-7.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2285c04ee8676f7938b02b4936d9b9b672064daab3187c20f73a55f3d70e6b4a", size = 248724, upload-time = "2025-08-23T14:41:08.429Z" }, + { url = "https://files.pythonhosted.org/packages/e2/96/a7c3c0562266ac39dcad271d0eec8fc20ab576e3e2f64130a845ad2a557b/coverage-7.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2492e4dd9daab63f5f56286f8a04c51323d237631eb98505d87e4c4ff19ec34", size = 250158, upload-time = "2025-08-23T14:41:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/74d4be58c70c42ef0b352d597b022baf12dbe2b43e7cb1525f56a0fb1d4b/coverage-7.10.5-cp312-cp312-win32.whl", hash = "sha256:38a9109c4ee8135d5df5505384fc2f20287a47ccbe0b3f04c53c9a1989c2bbaf", size = 219493, upload-time = "2025-08-23T14:41:11.095Z" }, + { url = "https://files.pythonhosted.org/packages/4f/08/364e6012d1d4d09d1e27437382967efed971d7613f94bca9add25f0c1f2b/coverage-7.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:6b87f1ad60b30bc3c43c66afa7db6b22a3109902e28c5094957626a0143a001f", size = 220302, upload-time = "2025-08-23T14:41:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/7c8a365e1f7355c58af4fe5faf3f90cc8e587590f5854808d17ccb4e7077/coverage-7.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:672a6c1da5aea6c629819a0e1461e89d244f78d7b60c424ecf4f1f2556c041d8", size = 218936, upload-time = "2025-08-23T14:41:13.872Z" }, + { url = "https://files.pythonhosted.org/packages/9f/08/4166ecfb60ba011444f38a5a6107814b80c34c717bc7a23be0d22e92ca09/coverage-7.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef3b83594d933020f54cf65ea1f4405d1f4e41a009c46df629dd964fcb6e907c", size = 217106, upload-time = "2025-08-23T14:41:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/d7/b71022408adbf040a680b8c64bf6ead3be37b553e5844f7465643979f7ca/coverage-7.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b96bfdf7c0ea9faebce088a3ecb2382819da4fbc05c7b80040dbc428df6af44", size = 217353, upload-time = "2025-08-23T14:41:16.656Z" }, + { url = "https://files.pythonhosted.org/packages/74/68/21e0d254dbf8972bb8dd95e3fe7038f4be037ff04ba47d6d1b12b37510ba/coverage-7.10.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63df1fdaffa42d914d5c4d293e838937638bf75c794cf20bee12978fc8c4e3bc", size = 248350, upload-time = "2025-08-23T14:41:18.128Z" }, + { url = "https://files.pythonhosted.org/packages/90/65/28752c3a896566ec93e0219fc4f47ff71bd2b745f51554c93e8dcb659796/coverage-7.10.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8002dc6a049aac0e81ecec97abfb08c01ef0c1fbf962d0c98da3950ace89b869", size = 250955, upload-time = "2025-08-23T14:41:19.577Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/ca6b7967f57f6fef31da8749ea20417790bb6723593c8cd98a987be20423/coverage-7.10.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63d4bb2966d6f5f705a6b0c6784c8969c468dbc4bcf9d9ded8bff1c7e092451f", size = 252230, upload-time = "2025-08-23T14:41:20.959Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/17a411b2a2a18f8b8c952aa01c00f9284a1fbc677c68a0003b772ea89104/coverage-7.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1f672efc0731a6846b157389b6e6d5d5e9e59d1d1a23a5c66a99fd58339914d5", size = 250387, upload-time = "2025-08-23T14:41:22.644Z" }, + { url = "https://files.pythonhosted.org/packages/c7/89/97a9e271188c2fbb3db82235c33980bcbc733da7da6065afbaa1d685a169/coverage-7.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3f39cef43d08049e8afc1fde4a5da8510fc6be843f8dea350ee46e2a26b2f54c", size = 248280, upload-time = "2025-08-23T14:41:24.061Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0ad7d0137257553eb4706b4ad6180bec0a1b6a648b092c5bbda48d0e5b2c/coverage-7.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2968647e3ed5a6c019a419264386b013979ff1fb67dd11f5c9886c43d6a31fc2", size = 249894, upload-time = "2025-08-23T14:41:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/56/fb3aba936addb4c9e5ea14f5979393f1c2466b4c89d10591fd05f2d6b2aa/coverage-7.10.5-cp313-cp313-win32.whl", hash = "sha256:0d511dda38595b2b6934c2b730a1fd57a3635c6aa2a04cb74714cdfdd53846f4", size = 219536, upload-time = "2025-08-23T14:41:27.694Z" }, + { url = "https://files.pythonhosted.org/packages/fc/54/baacb8f2f74431e3b175a9a2881feaa8feb6e2f187a0e7e3046f3c7742b2/coverage-7.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:9a86281794a393513cf117177fd39c796b3f8e3759bb2764259a2abba5cce54b", size = 220330, upload-time = "2025-08-23T14:41:29.081Z" }, + { url = "https://files.pythonhosted.org/packages/64/8a/82a3788f8e31dee51d350835b23d480548ea8621f3effd7c3ba3f7e5c006/coverage-7.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:cebd8e906eb98bb09c10d1feed16096700b1198d482267f8bf0474e63a7b8d84", size = 218961, upload-time = "2025-08-23T14:41:30.511Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a1/590154e6eae07beee3b111cc1f907c30da6fc8ce0a83ef756c72f3c7c748/coverage-7.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0520dff502da5e09d0d20781df74d8189ab334a1e40d5bafe2efaa4158e2d9e7", size = 217819, upload-time = "2025-08-23T14:41:31.962Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ff/436ffa3cfc7741f0973c5c89405307fe39b78dcf201565b934e6616fc4ad/coverage-7.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9cd64aca68f503ed3f1f18c7c9174cbb797baba02ca8ab5112f9d1c0328cd4b", size = 218040, upload-time = "2025-08-23T14:41:33.472Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ca/5787fb3d7820e66273913affe8209c534ca11241eb34ee8c4fd2aaa9dd87/coverage-7.10.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0913dd1613a33b13c4f84aa6e3f4198c1a21ee28ccb4f674985c1f22109f0aae", size = 259374, upload-time = "2025-08-23T14:41:34.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/89/21af956843896adc2e64fc075eae3c1cadb97ee0a6960733e65e696f32dd/coverage-7.10.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b7181c0feeb06ed8a02da02792f42f829a7b29990fef52eff257fef0885d760", size = 261551, upload-time = "2025-08-23T14:41:36.333Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/390a69244ab837e0ac137989277879a084c786cf036c3c4a3b9637d43a89/coverage-7.10.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d42b7396b605f774d4372dd9c49bed71cbabce4ae1ccd074d155709dd8f235", size = 263776, upload-time = "2025-08-23T14:41:38.25Z" }, + { url = "https://files.pythonhosted.org/packages/00/32/cfd6ae1da0a521723349f3129b2455832fc27d3f8882c07e5b6fefdd0da2/coverage-7.10.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4fdc777e05c4940b297bf47bf7eedd56a39a61dc23ba798e4b830d585486ca5", size = 261326, upload-time = "2025-08-23T14:41:40.343Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c4/bf8d459fb4ce2201e9243ce6c015936ad283a668774430a3755f467b39d1/coverage-7.10.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:42144e8e346de44a6f1dbd0a56575dd8ab8dfa7e9007da02ea5b1c30ab33a7db", size = 259090, upload-time = "2025-08-23T14:41:42.106Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5d/a234f7409896468e5539d42234016045e4015e857488b0b5b5f3f3fa5f2b/coverage-7.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:66c644cbd7aed8fe266d5917e2c9f65458a51cfe5eeff9c05f15b335f697066e", size = 260217, upload-time = "2025-08-23T14:41:43.591Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/87560f036099f46c2ddd235be6476dd5c1d6be6bb57569a9348d43eeecea/coverage-7.10.5-cp313-cp313t-win32.whl", hash = "sha256:2d1b73023854068c44b0c554578a4e1ef1b050ed07cf8b431549e624a29a66ee", size = 220194, upload-time = "2025-08-23T14:41:45.051Z" }, + { url = "https://files.pythonhosted.org/packages/36/a8/04a482594fdd83dc677d4a6c7e2d62135fff5a1573059806b8383fad9071/coverage-7.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:54a1532c8a642d8cc0bd5a9a51f5a9dcc440294fd06e9dda55e743c5ec1a8f14", size = 221258, upload-time = "2025-08-23T14:41:46.44Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ad/7da28594ab66fe2bc720f1bc9b131e62e9b4c6e39f044d9a48d18429cc21/coverage-7.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:74d5b63fe3f5f5d372253a4ef92492c11a4305f3550631beaa432fc9df16fcff", size = 219521, upload-time = "2025-08-23T14:41:47.882Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7f/c8b6e4e664b8a95254c35a6c8dd0bf4db201ec681c169aae2f1256e05c85/coverage-7.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:68c5e0bc5f44f68053369fa0d94459c84548a77660a5f2561c5e5f1e3bed7031", size = 217090, upload-time = "2025-08-23T14:41:49.327Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/3ee14ede30a6e10a94a104d1d0522d5fb909a7c7cac2643d2a79891ff3b9/coverage-7.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cf33134ffae93865e32e1e37df043bef15a5e857d8caebc0099d225c579b0fa3", size = 217365, upload-time = "2025-08-23T14:41:50.796Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/06ac21bf87dfb7620d1f870dfa3c2cae1186ccbcdc50b8b36e27a0d52f50/coverage-7.10.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad8fa9d5193bafcf668231294241302b5e683a0518bf1e33a9a0dfb142ec3031", size = 248413, upload-time = "2025-08-23T14:41:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/21/bc/cc5bed6e985d3a14228539631573f3863be6a2587381e8bc5fdf786377a1/coverage-7.10.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:146fa1531973d38ab4b689bc764592fe6c2f913e7e80a39e7eeafd11f0ef6db2", size = 250943, upload-time = "2025-08-23T14:41:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/43/6a9fc323c2c75cd80b18d58db4a25dc8487f86dd9070f9592e43e3967363/coverage-7.10.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6013a37b8a4854c478d3219ee8bc2392dea51602dd0803a12d6f6182a0061762", size = 252301, upload-time = "2025-08-23T14:41:56.528Z" }, + { url = "https://files.pythonhosted.org/packages/69/7c/3e791b8845f4cd515275743e3775adb86273576596dc9f02dca37357b4f2/coverage-7.10.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eb90fe20db9c3d930fa2ad7a308207ab5b86bf6a76f54ab6a40be4012d88fcae", size = 250302, upload-time = "2025-08-23T14:41:58.171Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bc/5099c1e1cb0c9ac6491b281babea6ebbf999d949bf4aa8cdf4f2b53505e8/coverage-7.10.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:384b34482272e960c438703cafe63316dfbea124ac62006a455c8410bf2a2262", size = 248237, upload-time = "2025-08-23T14:41:59.703Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/d346eb750a0b2f1e77f391498b753ea906fde69cc11e4b38dca28c10c88c/coverage-7.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:467dc74bd0a1a7de2bedf8deaf6811f43602cb532bd34d81ffd6038d6d8abe99", size = 249726, upload-time = "2025-08-23T14:42:01.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/85/eebcaa0edafe427e93286b94f56ea7e1280f2c49da0a776a6f37e04481f9/coverage-7.10.5-cp314-cp314-win32.whl", hash = "sha256:556d23d4e6393ca898b2e63a5bca91e9ac2d5fb13299ec286cd69a09a7187fde", size = 219825, upload-time = "2025-08-23T14:42:03.263Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f7/6d43e037820742603f1e855feb23463979bf40bd27d0cde1f761dcc66a3e/coverage-7.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:f4446a9547681533c8fa3e3c6cf62121eeee616e6a92bd9201c6edd91beffe13", size = 220618, upload-time = "2025-08-23T14:42:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b0/ed9432e41424c51509d1da603b0393404b828906236fb87e2c8482a93468/coverage-7.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e78bd9cf65da4c303bf663de0d73bf69f81e878bf72a94e9af67137c69b9fe9", size = 219199, upload-time = "2025-08-23T14:42:06.662Z" }, + { url = "https://files.pythonhosted.org/packages/2f/54/5a7ecfa77910f22b659c820f67c16fc1e149ed132ad7117f0364679a8fa9/coverage-7.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5661bf987d91ec756a47c7e5df4fbcb949f39e32f9334ccd3f43233bbb65e508", size = 217833, upload-time = "2025-08-23T14:42:08.262Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/25672d917cc57857d40edf38f0b867fb9627115294e4f92c8fcbbc18598d/coverage-7.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a46473129244db42a720439a26984f8c6f834762fc4573616c1f37f13994b357", size = 218048, upload-time = "2025-08-23T14:42:10.247Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7c/0b2b4f1c6f71885d4d4b2b8608dcfc79057adb7da4143eb17d6260389e42/coverage-7.10.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1f64b8d3415d60f24b058b58d859e9512624bdfa57a2d1f8aff93c1ec45c429b", size = 259549, upload-time = "2025-08-23T14:42:11.811Z" }, + { url = "https://files.pythonhosted.org/packages/94/73/abb8dab1609abec7308d83c6aec547944070526578ee6c833d2da9a0ad42/coverage-7.10.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:44d43de99a9d90b20e0163f9770542357f58860a26e24dc1d924643bd6aa7cb4", size = 261715, upload-time = "2025-08-23T14:42:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d1/abf31de21ec92731445606b8d5e6fa5144653c2788758fcf1f47adb7159a/coverage-7.10.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a931a87e5ddb6b6404e65443b742cb1c14959622777f2a4efd81fba84f5d91ba", size = 263969, upload-time = "2025-08-23T14:42:15.422Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b3/ef274927f4ebede96056173b620db649cc9cb746c61ffc467946b9d0bc67/coverage-7.10.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9559b906a100029274448f4c8b8b0a127daa4dade5661dfd821b8c188058842", size = 261408, upload-time = "2025-08-23T14:42:16.971Z" }, + { url = "https://files.pythonhosted.org/packages/20/fc/83ca2812be616d69b4cdd4e0c62a7bc526d56875e68fd0f79d47c7923584/coverage-7.10.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b08801e25e3b4526ef9ced1aa29344131a8f5213c60c03c18fe4c6170ffa2874", size = 259168, upload-time = "2025-08-23T14:42:18.512Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/e0779e5716f72d5c9962e709d09815d02b3b54724e38567308304c3fc9df/coverage-7.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed9749bb8eda35f8b636fb7632f1c62f735a236a5d4edadd8bbcc5ea0542e732", size = 260317, upload-time = "2025-08-23T14:42:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fe/4247e732f2234bb5eb9984a0888a70980d681f03cbf433ba7b48f08ca5d5/coverage-7.10.5-cp314-cp314t-win32.whl", hash = "sha256:609b60d123fc2cc63ccee6d17e4676699075db72d14ac3c107cc4976d516f2df", size = 220600, upload-time = "2025-08-23T14:42:22.027Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a0/f294cff6d1034b87839987e5b6ac7385bec599c44d08e0857ac7f164ad0c/coverage-7.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0666cf3d2c1626b5a3463fd5b05f5e21f99e6aec40a3192eee4d07a15970b07f", size = 221714, upload-time = "2025-08-23T14:42:23.616Z" }, + { url = "https://files.pythonhosted.org/packages/23/18/fa1afdc60b5528d17416df440bcbd8fd12da12bfea9da5b6ae0f7a37d0f7/coverage-7.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:bc85eb2d35e760120540afddd3044a5bf69118a91a296a8b3940dfc4fdcfe1e2", size = 219735, upload-time = "2025-08-23T14:42:25.156Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "45.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949, upload-time = "2025-08-05T23:59:27.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483, upload-time = "2025-08-05T23:58:27.132Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679, upload-time = "2025-08-05T23:58:29.152Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553, upload-time = "2025-08-05T23:58:30.596Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499, upload-time = "2025-08-05T23:58:32.03Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484, upload-time = "2025-08-05T23:58:33.526Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281, upload-time = "2025-08-05T23:58:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890, upload-time = "2025-08-05T23:58:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247, upload-time = "2025-08-05T23:58:38.781Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045, upload-time = "2025-08-05T23:58:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169, upload-time = "2025-08-05T23:58:47.121Z" }, + { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273, upload-time = "2025-08-05T23:58:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211, upload-time = "2025-08-05T23:58:50.139Z" }, + { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732, upload-time = "2025-08-05T23:58:52.253Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655, upload-time = "2025-08-05T23:58:53.848Z" }, + { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956, upload-time = "2025-08-05T23:58:55.209Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859, upload-time = "2025-08-05T23:58:56.639Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254, upload-time = "2025-08-05T23:58:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815, upload-time = "2025-08-05T23:59:00.283Z" }, + { url = "https://files.pythonhosted.org/packages/ec/24/55fc238fcaa122855442604b8badb2d442367dfbd5a7ca4bb0bd346e263a/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d", size = 4141694, upload-time = "2025-08-05T23:59:06.66Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/3ea4fa6fbe51baf3903806a0241c666b04c73d2358a3ecce09ebee8b9622/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d", size = 4375010, upload-time = "2025-08-05T23:59:08.14Z" }, + { url = "https://files.pythonhosted.org/packages/50/42/ec5a892d82d2a2c29f80fc19ced4ba669bca29f032faf6989609cff1f8dc/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da", size = 4141377, upload-time = "2025-08-05T23:59:09.584Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d7/246c4c973a22b9c2931999da953a2c19cae7c66b9154c2d62ffed811225e/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db", size = 4374609, upload-time = "2025-08-05T23:59:11.923Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fe/deea71e9f310a31fe0a6bfee670955152128d309ea2d1c79e2a5ae0f0401/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", size = 4153022, upload-time = "2025-08-05T23:59:16.954Z" }, + { url = "https://files.pythonhosted.org/packages/60/45/a77452f5e49cb580feedba6606d66ae7b82c128947aa754533b3d1bd44b0/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", size = 4386802, upload-time = "2025-08-05T23:59:18.55Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b9/a2f747d2acd5e3075fdf5c145c7c3568895daaa38b3b0c960ef830db6cdc/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", size = 4152706, upload-time = "2025-08-05T23:59:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/81/ec/381b3e8d0685a3f3f304a382aa3dfce36af2d76467da0fd4bb21ddccc7b2/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", size = 4386740, upload-time = "2025-08-05T23:59:21.525Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "docopt" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } + +[[package]] +name = "docutils" +version = "0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984, upload-time = "2025-07-29T15:20:31.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709, upload-time = "2025-07-29T15:20:28.335Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hatch" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "hatchling" }, + { name = "httpx" }, + { name = "hyperlink" }, + { name = "keyring" }, + { name = "packaging" }, + { name = "pexpect" }, + { name = "platformdirs" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "tomli-w" }, + { name = "tomlkit" }, + { name = "userpath" }, + { name = "uv" }, + { name = "virtualenv" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/43/c0b37db0e857a44ce5ffdb7e8a9b8fa6425d0b74dea698fafcd9bddb50d1/hatch-1.14.1.tar.gz", hash = "sha256:ca1aff788f8596b0dd1f8f8dfe776443d2724a86b1976fabaf087406ba3d0713", size = 5188180, upload-time = "2025-04-07T04:16:04.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/40/19c0935bf9f25808541a0e3144ac459de696c5b6b6d4511a98d456c69604/hatch-1.14.1-py3-none-any.whl", hash = "sha256:39cdaa59e47ce0c5505d88a951f4324a9c5aafa17e4a877e2fde79b36ab66c21", size = 125770, upload-time = "2025-04-07T04:16:02.525Z" }, +] + +[[package]] +name = "hatchling" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/8a/cc1debe3514da292094f1c3a700e4ca25442489731ef7c0814358816bb03/hatchling-1.27.0.tar.gz", hash = "sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6", size = 54983, upload-time = "2024-12-15T17:08:11.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl", hash = "sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b", size = 75794, upload-time = "2024-12-15T17:08:10.364Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, +] + +[[package]] +name = "id" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, +] + +[[package]] +name = "identify" +version = "2.6.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload-time = "2025-08-09T19:35:00.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload-time = "2025-08-09T19:34:59.1Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mdutils" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/4b/df40d441a280f20fa173f224577fbbb2477851a73736080c89a61ff8a4dd/mdutils-1.8.0.tar.gz", hash = "sha256:091b605b4b550465a304f1c66d37647b6008b4a19e7b5154a7d39a148e903f6d", size = 23909, upload-time = "2025-07-10T19:09:41.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/6b/d4c6e92daf6123696797e9914e4a3d36e2ba24a2ff1aad00c2d6d2afb82d/mdutils-1.8.0-py3-none-any.whl", hash = "sha256:0d3cf9af4a958f3bbd184c1097985bb1d1f9489887e1f7329942ada14359c7ba", size = 21647, upload-time = "2025-07-10T19:09:40.063Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" }, +] + +[[package]] +name = "nh3" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/96cff0977357f60f06ec4368c4c7a7a26cccfe7c9fcd54f5378bf0428fd3/nh3-0.3.0.tar.gz", hash = "sha256:d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f", size = 19655, upload-time = "2025-07-17T14:43:37.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/11/340b7a551916a4b2b68c54799d710f86cf3838a4abaad8e74d35360343bb/nh3-0.3.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb", size = 1427992, upload-time = "2025-07-17T14:43:06.848Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7f/7c6b8358cf1222921747844ab0eef81129e9970b952fcb814df417159fb9/nh3-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2", size = 798194, upload-time = "2025-07-17T14:43:08.263Z" }, + { url = "https://files.pythonhosted.org/packages/63/da/c5fd472b700ba37d2df630a9e0d8cc156033551ceb8b4c49cc8a5f606b68/nh3-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95", size = 837884, upload-time = "2025-07-17T14:43:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3c/cba7b26ccc0ef150c81646478aa32f9c9535234f54845603c838a1dc955c/nh3-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d", size = 996365, upload-time = "2025-07-17T14:43:10.243Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ba/59e204d90727c25b253856e456ea61265ca810cda8ee802c35f3fadaab00/nh3-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35", size = 1071042, upload-time = "2025-07-17T14:43:11.57Z" }, + { url = "https://files.pythonhosted.org/packages/10/71/2fb1834c10fab6d9291d62c95192ea2f4c7518bd32ad6c46aab5d095cb87/nh3-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5", size = 995737, upload-time = "2025-07-17T14:43:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/33/c1/8f8ccc2492a000b6156dce68a43253fcff8b4ce70ab4216d08f90a2ac998/nh3-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9", size = 980552, upload-time = "2025-07-17T14:43:13.763Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d6/f1c6e091cbe8700401c736c2bc3980c46dca770a2cf6a3b48a175114058e/nh3-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5", size = 593618, upload-time = "2025-07-17T14:43:15.098Z" }, + { url = "https://files.pythonhosted.org/packages/23/1e/80a8c517655dd40bb13363fc4d9e66b2f13245763faab1a20f1df67165a7/nh3-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e", size = 598948, upload-time = "2025-07-17T14:43:16.064Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e0/af86d2a974c87a4ba7f19bc3b44a8eaa3da480de264138fec82fe17b340b/nh3-0.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f", size = 580479, upload-time = "2025-07-17T14:43:17.038Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e0/cf1543e798ba86d838952e8be4cb8d18e22999be2a24b112a671f1c04fd6/nh3-0.3.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a", size = 1442218, upload-time = "2025-07-17T14:43:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/5c/86/a96b1453c107b815f9ab8fac5412407c33cc5c7580a4daf57aabeb41b774/nh3-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1", size = 823791, upload-time = "2025-07-17T14:43:19.721Z" }, + { url = "https://files.pythonhosted.org/packages/97/33/11e7273b663839626f714cb68f6eb49899da5a0d9b6bc47b41fe870259c2/nh3-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392", size = 811143, upload-time = "2025-07-17T14:43:20.779Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1b/b15bd1ce201a1a610aeb44afd478d55ac018b4475920a3118ffd806e2483/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a", size = 1064661, upload-time = "2025-07-17T14:43:21.839Z" }, + { url = "https://files.pythonhosted.org/packages/8f/14/079670fb2e848c4ba2476c5a7a2d1319826053f4f0368f61fca9bb4227ae/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49", size = 997061, upload-time = "2025-07-17T14:43:23.179Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e5/ac7fc565f5d8bce7f979d1afd68e8cb415020d62fa6507133281c7d49f91/nh3-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb", size = 924761, upload-time = "2025-07-17T14:43:24.23Z" }, + { url = "https://files.pythonhosted.org/packages/39/2c/6394301428b2017a9d5644af25f487fa557d06bc8a491769accec7524d9a/nh3-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1", size = 803959, upload-time = "2025-07-17T14:43:26.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9a/344b9f9c4bd1c2413a397f38ee6a3d5db30f1a507d4976e046226f12b297/nh3-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9", size = 844073, upload-time = "2025-07-17T14:43:27.375Z" }, + { url = "https://files.pythonhosted.org/packages/66/3f/cd37f76c8ca277b02a84aa20d7bd60fbac85b4e2cbdae77cb759b22de58b/nh3-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62", size = 1000680, upload-time = "2025-07-17T14:43:28.452Z" }, + { url = "https://files.pythonhosted.org/packages/ee/db/7aa11b44bae4e7474feb1201d8dee04fabe5651c7cb51409ebda94a4ed67/nh3-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23", size = 1076613, upload-time = "2025-07-17T14:43:30.031Z" }, + { url = "https://files.pythonhosted.org/packages/97/03/03f79f7e5178eb1ad5083af84faff471e866801beb980cc72943a4397368/nh3-0.3.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450", size = 1001418, upload-time = "2025-07-17T14:43:31.429Z" }, + { url = "https://files.pythonhosted.org/packages/ce/55/1974bcc16884a397ee699cebd3914e1f59be64ab305533347ca2d983756f/nh3-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518", size = 986499, upload-time = "2025-07-17T14:43:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/76936ec021fe1f3270c03278b8af5f2079038116b5d0bfe8538ffe699d69/nh3-0.3.0-cp38-abi3-win32.whl", hash = "sha256:6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d", size = 599000, upload-time = "2025-07-17T14:43:33.852Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/324b165d904dc1672eee5f5661c0a68d4bab5b59fbb07afb6d8d19a30b45/nh3-0.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95", size = 604530, upload-time = "2025-07-17T14:43:34.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/76/3165e84e5266d146d967a6cc784ff2fbf6ddd00985a55ec006b72bc39d5d/nh3-0.3.0-cp38-abi3-win_arm64.whl", hash = "sha256:d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2", size = 585971, upload-time = "2025-07-17T14:43:35.936Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "prettytable" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/b1/85e18ac92afd08c533603e3393977b6bc1443043115a47bb094f3b98f94f/prettytable-3.16.0.tar.gz", hash = "sha256:3c64b31719d961bf69c9a7e03d0c1e477320906a98da63952bc6698d6164ff57", size = 66276, upload-time = "2025-03-24T19:39:04.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c7/5613524e606ea1688b3bdbf48aa64bafb6d0a4ac3750274c43b6158a390f/prettytable-3.16.0-py3-none-any.whl", hash = "sha256:b5eccfabb82222f5aa46b798ff02a8452cf530a352c31bddfa29be41242863aa", size = 33863, upload-time = "2025-03-24T19:39:02.359Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, +] + +[[package]] +name = "pytest-watch" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "docopt" }, + { name = "pytest" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/47/ab65fc1d682befc318c439940f81a0de1026048479f732e84fe714cd69c0/pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9", size = 16340, upload-time = "2018-05-20T19:52:16.194Z" } + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, + { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, + { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, + { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" }, + { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "socketdev" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/b7/fe90d55105df76e9ff3af025f64b2d2b515c30ac0866a9973a093f25c5ed/socketdev-3.0.5.tar.gz", hash = "sha256:58cbe8613c3c892cdbae4941cb53f065051f8e991500d9d61618b214acf4ffc2", size = 129576, upload-time = "2025-09-09T07:15:48.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/05/c3fc7d0418c2598302ad4b0baf111fa492b31a8fa14acfa394af6f55b373/socketdev-3.0.5-py3-none-any.whl", hash = "sha256:e050f50d2c6b4447107edd3368b56b053e1df62056d424cc1616e898303638ef", size = 55083, upload-time = "2025-09-09T07:15:46.52Z" }, +] + +[[package]] +name = "socketsecurity" +version = "2.2.7" +source = { editable = "." } +dependencies = [ + { name = "gitpython" }, + { name = "mdutils" }, + { name = "packaging" }, + { name = "prettytable" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "socketdev" }, +] + +[package.optional-dependencies] +dev = [ + { name = "hatch" }, + { name = "pre-commit" }, + { name = "ruff" }, + { name = "twine" }, + { name = "uv" }, +] +test = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-watch" }, +] + +[package.metadata] +requires-dist = [ + { name = "gitpython" }, + { name = "hatch", marker = "extra == 'dev'" }, + { name = "mdutils" }, + { name = "packaging" }, + { name = "pre-commit", marker = "extra == 'dev'" }, + { name = "prettytable" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=7.4.0" }, + { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.23.0" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.1.0" }, + { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.12.0" }, + { name = "pytest-watch", marker = "extra == 'test'", specifier = ">=4.2.0" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, + { name = "socketdev", specifier = ">=3.0.5,<4.0.0" }, + { name = "twine", marker = "extra == 'dev'" }, + { name = "uv", marker = "extra == 'dev'", specifier = ">=0.1.0" }, +] +provides-extras = ["test", "dev"] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "trove-classifiers" +version = "2025.8.6.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/21/707af14daa638b0df15b5d5700349e0abdd3e5140069f9ab6e0ccb922806/trove_classifiers-2025.8.6.13.tar.gz", hash = "sha256:5a0abad839d2ed810f213ab133d555d267124ddea29f1d8a50d6eca12a50ae6e", size = 16932, upload-time = "2025-08-06T13:26:26.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/44/323a87d78f04d5329092aada803af3612dd004a64b69ba8b13046601a8c9/trove_classifiers-2025.8.6.13-py3-none-any.whl", hash = "sha256:c4e7fc83012770d80b3ae95816111c32b085716374dccee0d3fbf5c235495f9f", size = 14121, upload-time = "2025-08-06T13:26:25.063Z" }, +] + +[[package]] +name = "twine" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404, upload-time = "2025-01-21T18:45:26.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791, upload-time = "2025-01-21T18:45:24.584Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "userpath" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140, upload-time = "2024-02-29T21:39:08.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065, upload-time = "2024-02-29T21:39:07.551Z" }, +] + +[[package]] +name = "uv" +version = "0.8.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/f3/4f947303fb68daa553fc44ea5f2bcdfb1d11c4390ab52b6e3829f72b6b69/uv-0.8.13.tar.gz", hash = "sha256:a4438eca3d301183c52994a6d2baff70fd1840421a83446f3cabb1d0d0b50aff", size = 3529020, upload-time = "2025-08-21T19:20:17.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/1d/98c7985f05c1dfa4a1d9bfebfdc75fd5c86633acb3bd9a65a5233a267d0e/uv-0.8.13-py3-none-linux_armv6l.whl", hash = "sha256:3b5c6e44238007ec1d25212cafe1b37a8506d425d1dd74a267cb9072a61930f9", size = 18712142, upload-time = "2025-08-21T19:19:30.882Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/2fc23b2fb14316fafafcd918dd4bf3456aecfeae95d71deaf9d7d59e9720/uv-0.8.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2945c32b8fcf23807ef1f74c390795e2b00371c53b94c015cc6e7b0cfbab9d94", size = 18752276, upload-time = "2025-08-21T19:19:34.658Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/c8d4963271e3a9ea0886be1082c96b16881d58d6d260957e76bd41f4c991/uv-0.8.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:73459fe1403b1089853071db6770450dc03e4058848f7146d88cff5f1c352743", size = 17403078, upload-time = "2025-08-21T19:19:36.882Z" }, + { url = "https://files.pythonhosted.org/packages/9b/25/3985330034df1f99b63b947f5de37059859aede74883fc14e4500a983daf/uv-0.8.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:854c4e75024a4894477bf61684b2872b83c77ca87d1bad62692bcc31200619c3", size = 18075030, upload-time = "2025-08-21T19:19:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/68/f2/5956b44e8b77e777b1c15c011e43a7bb29c4346908f569c474db67baab02/uv-0.8.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:28c8d4560c673ff5c798f2f4422281840728f46ebf1946345b65d065f8344c03", size = 18334387, upload-time = "2025-08-21T19:19:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/5feda4185ea21e7dc79f73ba1f9ebbd2ac5da22ed5c40d7c6f6a310d4738/uv-0.8.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6c508aa9c5210577008e1919b532e38356fe68712179399f00462b3e78fd845", size = 19097001, upload-time = "2025-08-21T19:19:47.363Z" }, + { url = "https://files.pythonhosted.org/packages/02/f8/1b6e258907afdb008855418f7a0aa41568f8b7389994f62b7992352a5146/uv-0.8.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3bac51ea503d97f371222f23e845fc4ab95465ac3e958c7589d6743c75445b71", size = 20501591, upload-time = "2025-08-21T19:19:49.976Z" }, + { url = "https://files.pythonhosted.org/packages/39/17/8a9f979bb6329b74320514797bfaf0a2865e25c2edf7506fde5080ad071c/uv-0.8.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a6d37547947fcae57244b4d1f3b62fba55f4a85d3e45e7284a93b6cd5bedca4", size = 20156528, upload-time = "2025-08-21T19:19:52.096Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ca/bc42b2ae9dd0eae8b5bba34020bc3893cf7370c4125164075643a1da86c8/uv-0.8.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3735a452cdc3168932d128891d7e8866b4a2d052283c6da5ccfe0b038d1cf8bd", size = 19491271, upload-time = "2025-08-21T19:19:54.302Z" }, + { url = "https://files.pythonhosted.org/packages/82/6b/81387a715dd045f7edea452fb76a5896dcfc11b8ecf0db5106f4b0f633ec/uv-0.8.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2113cd877974b68ea2af64a2f2cc23708ba97066046e78efb72ba94e5fef617a", size = 19452076, upload-time = "2025-08-21T19:19:56.608Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/eae2eff5876576b6e1d43010924446da22f4fe33140124a4f2ae936c457d/uv-0.8.13-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4c2c5e5962239ecaff6444d5bc22422a9bd2da25a80adc6ab14cb42e4461b1cf", size = 18329700, upload-time = "2025-08-21T19:19:58.784Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6e/9a88c83a1d8845c04b911433bbe61989b9d40b4feb7a51ab1ca595107bf5/uv-0.8.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:eb90089624d92d57b8582f708973db8988e09dba6bae83991dba20731d82eb6a", size = 19218874, upload-time = "2025-08-21T19:20:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/df/f5/8f818016a1704a185dd0087cacbe0797b0ca2ef859922814e26322427756/uv-0.8.13-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:cf3ce98404ddc1e11cd2c2604668f8f81219cf00bb1227b792fdf5dbb4faf31a", size = 18310085, upload-time = "2025-08-21T19:20:03.219Z" }, + { url = "https://files.pythonhosted.org/packages/64/d2/b0d16ec65efb8b5877256e14da808a0db20c4bf3e9960d807e4c0db5f71c/uv-0.8.13-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8a3739540f8b0b5258869b1671185d55daacfa4609eaffd235573ac938ec01a6", size = 18604881, upload-time = "2025-08-21T19:20:05.413Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ba/f96a34b2aec7c38d0512b10fea182eaf0d416e2825cf4ac00fde1e375019/uv-0.8.13-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:18a502328545af511039c7b7c602a0aa89eeff23b1221a1f56d99b3a3fecfddd", size = 19569585, upload-time = "2025-08-21T19:20:08.008Z" }, + { url = "https://files.pythonhosted.org/packages/6d/7d/a57198784a1dca07668007837cfd00346cde2da079697f67daaedbfb1376/uv-0.8.13-py3-none-win32.whl", hash = "sha256:d22fa55580b224779279b98e0b23cbc45e51837e1fac616d7c5d03aff668a998", size = 18661160, upload-time = "2025-08-21T19:20:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/65ba1f45c27ff975497f2db965cd2b989fdda031afa76b978ddc72a66032/uv-0.8.13-py3-none-win_amd64.whl", hash = "sha256:20862f612de38f6dea55d40467a29f3cb621b256a4b5891ae55debbbdf1db2b4", size = 20556536, upload-time = "2025-08-21T19:20:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/47/16a2eb25166861af1a139e506e5595cb92c7f7e5aae6e71feec135093394/uv-0.8.13-py3-none-win_arm64.whl", hash = "sha256:404ca19b2d860ab661e1d78633f594e994f8422af8772ad237d763fe353da2ab", size = 19028395, upload-time = "2025-08-21T19:20:15.035Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] + +[[package]] +name = "zstandard" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/1b/c20b2ef1d987627765dcd5bf1dadb8ef6564f00a87972635099bb76b7a05/zstandard-0.24.0.tar.gz", hash = "sha256:fe3198b81c00032326342d973e526803f183f97aa9e9a98e3f897ebafe21178f", size = 905681, upload-time = "2025-08-17T18:36:36.352Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/9d/d1ca1e7bff6a7938e81180322c053c080ae9e31b0e3b393434deae7a1ae5/zstandard-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:af1394c2c5febc44e0bbf0fc6428263fa928b50d1b1982ce1d870dc793a8e5f4", size = 795228, upload-time = "2025-08-17T18:21:12.444Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ba/a40ddfbbb9f0773127701a802338f215211b018f9222b9fab1e2d498f9cd/zstandard-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e941654cef13a1d53634ec30933722eda11f44f99e1d0bc62bbce3387580d50", size = 640522, upload-time = "2025-08-17T18:21:14.133Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7c/edeee3ef8d469a1345edd86f8d123a3825d60df033bcbbd16df417bdb9e7/zstandard-0.24.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:561123d05681197c0e24eb8ab3cfdaf299e2b59c293d19dad96e1610ccd8fbc6", size = 5344625, upload-time = "2025-08-17T18:21:16.067Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2c/2f76e5058435d96ab0187303d4e9663372893cdcc95d64fdb60824951162/zstandard-0.24.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0f6d9a146e07458cb41423ca2d783aefe3a3a97fe72838973c13b8f1ecc7343a", size = 5055074, upload-time = "2025-08-17T18:21:18.483Z" }, + { url = "https://files.pythonhosted.org/packages/e4/87/3962530a568d38e64f287e11b9a38936d873617120589611c49c29af94a8/zstandard-0.24.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf02f915fa7934ea5dfc8d96757729c99a8868b7c340b97704795d6413cf5fe6", size = 5401308, upload-time = "2025-08-17T18:21:20.859Z" }, + { url = "https://files.pythonhosted.org/packages/f1/69/85e65f0fb05b4475130888cf7934ff30ac14b5979527e8f1ccb6f56e21ec/zstandard-0.24.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:35f13501a8accf834457d8e40e744568287a215818778bc4d79337af2f3f0d97", size = 5448948, upload-time = "2025-08-17T18:21:23.015Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2f/1b607274bf20ea8bcd13bea3edc0a48f984c438c09d0a050b9667dadcaed/zstandard-0.24.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92be52ca4e6e604f03d5daa079caec9e04ab4cbf6972b995aaebb877d3d24e13", size = 5555870, upload-time = "2025-08-17T18:21:24.985Z" }, + { url = "https://files.pythonhosted.org/packages/a0/9a/fadd5ffded6ab113b26704658a40444865b914de072fb460b6b51aa5fa2f/zstandard-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c9c3cba57f5792532a3df3f895980d47d78eda94b0e5b800651b53e96e0b604", size = 5044917, upload-time = "2025-08-17T18:21:27.082Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/c5edc3b00e070d0b4156993bd7bef9cba58c5f2571bd0003054cbe90005c/zstandard-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dd91b0134a32dfcd8be504e8e46de44ad0045a569efc25101f2a12ccd41b5759", size = 5571834, upload-time = "2025-08-17T18:21:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/1f/7e/9e353ed08c3d7a93050bbadbebe2f5f783b13393e0e8e08e970ef3396390/zstandard-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d6975f2d903bc354916a17b91a7aaac7299603f9ecdb788145060dde6e573a16", size = 4959108, upload-time = "2025-08-17T18:21:31.228Z" }, + { url = "https://files.pythonhosted.org/packages/af/28/135dffba375ab1f4d2c569de804647eba8bd682f36d3c01b5a012c560ff2/zstandard-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7ac6e4d727521d86d20ec291a3f4e64a478e8a73eaee80af8f38ec403e77a409", size = 5265997, upload-time = "2025-08-17T18:21:33.369Z" }, + { url = "https://files.pythonhosted.org/packages/cc/7a/702e7cbc51c39ce104c198ea6d069fb6a918eb24c5709ac79fe9371f7a55/zstandard-0.24.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:87ae1684bc3c02d5c35884b3726525eda85307073dbefe68c3c779e104a59036", size = 5440015, upload-time = "2025-08-17T18:21:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/77/40/4a2d0faa2ae6f4c847c7f77ec626abed80873035891c4a4349b735a36fb4/zstandard-0.24.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7de5869e616d426b56809be7dc6dba4d37b95b90411ccd3de47f421a42d4d42c", size = 5819056, upload-time = "2025-08-17T18:21:39.661Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fc/580504a2d7c71411a8e403b83f2388ee083819a68e0e740bf974e78839f8/zstandard-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:388aad2d693707f4a0f6cc687eb457b33303d6b57ecf212c8ff4468c34426892", size = 5362621, upload-time = "2025-08-17T18:21:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/70/66/97f6b38eeda955eaa6b5e7cfc0528039bfcb9eb8338016aacf6d83d8a75e/zstandard-0.24.0-cp310-cp310-win32.whl", hash = "sha256:962ea3aecedcc944f8034812e23d7200d52c6e32765b8da396eeb8b8ffca71ce", size = 435575, upload-time = "2025-08-17T18:21:45.477Z" }, + { url = "https://files.pythonhosted.org/packages/68/a2/5814bdd22d879b10fcc5dc37366e39603767063f06ae970f2a657f76ddac/zstandard-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:869bf13f66b124b13be37dd6e08e4b728948ff9735308694e0b0479119e08ea7", size = 505115, upload-time = "2025-08-17T18:21:44.011Z" }, + { url = "https://files.pythonhosted.org/packages/01/1f/5c72806f76043c0ef9191a2b65281dacdf3b65b0828eb13bb2c987c4fb90/zstandard-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:addfc23e3bd5f4b6787b9ca95b2d09a1a67ad5a3c318daaa783ff90b2d3a366e", size = 795228, upload-time = "2025-08-17T18:21:46.978Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/3059bd5cd834666a789251d14417621b5c61233bd46e7d9023ea8bc1043a/zstandard-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b005bcee4be9c3984b355336283afe77b2defa76ed6b89332eced7b6fa68b68", size = 640520, upload-time = "2025-08-17T18:21:48.162Z" }, + { url = "https://files.pythonhosted.org/packages/57/07/f0e632bf783f915c1fdd0bf68614c4764cae9dd46ba32cbae4dd659592c3/zstandard-0.24.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:3f96a9130171e01dbb6c3d4d9925d604e2131a97f540e223b88ba45daf56d6fb", size = 5347682, upload-time = "2025-08-17T18:21:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4c/63523169fe84773a7462cd090b0989cb7c7a7f2a8b0a5fbf00009ba7d74d/zstandard-0.24.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd0d3d16e63873253bad22b413ec679cf6586e51b5772eb10733899832efec42", size = 5057650, upload-time = "2025-08-17T18:21:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/c6/16/49013f7ef80293f5cebf4c4229535a9f4c9416bbfd238560edc579815dbe/zstandard-0.24.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b7a8c30d9bf4bd5e4dcfe26900bef0fcd9749acde45cdf0b3c89e2052fda9a13", size = 5404893, upload-time = "2025-08-17T18:21:54.54Z" }, + { url = "https://files.pythonhosted.org/packages/4d/38/78e8bcb5fc32a63b055f2b99e0be49b506f2351d0180173674f516cf8a7a/zstandard-0.24.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:52cd7d9fa0a115c9446abb79b06a47171b7d916c35c10e0c3aa6f01d57561382", size = 5452389, upload-time = "2025-08-17T18:21:56.822Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/81671f05619edbacd49bd84ce6899a09fc8299be20c09ae92f6618ccb92d/zstandard-0.24.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0f6fc2ea6e07e20df48752e7700e02e1892c61f9a6bfbacaf2c5b24d5ad504b", size = 5558888, upload-time = "2025-08-17T18:21:58.68Z" }, + { url = "https://files.pythonhosted.org/packages/49/cc/e83feb2d7d22d1f88434defbaeb6e5e91f42a4f607b5d4d2d58912b69d67/zstandard-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e46eb6702691b24ddb3e31e88b4a499e31506991db3d3724a85bd1c5fc3cfe4e", size = 5048038, upload-time = "2025-08-17T18:22:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/7a5c57ff49ef8943877f85c23368c104c2aea510abb339a2dc31ad0a27c3/zstandard-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5e3b9310fd7f0d12edc75532cd9a56da6293840c84da90070d692e0bb15f186", size = 5573833, upload-time = "2025-08-17T18:22:02.402Z" }, + { url = "https://files.pythonhosted.org/packages/f9/00/64519983cd92535ba4bdd4ac26ac52db00040a52d6c4efb8d1764abcc343/zstandard-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76cdfe7f920738ea871f035568f82bad3328cbc8d98f1f6988264096b5264efd", size = 4961072, upload-time = "2025-08-17T18:22:04.384Z" }, + { url = "https://files.pythonhosted.org/packages/72/ab/3a08a43067387d22994fc87c3113636aa34ccd2914a4d2d188ce365c5d85/zstandard-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3f2fe35ec84908dddf0fbf66b35d7c2878dbe349552dd52e005c755d3493d61c", size = 5268462, upload-time = "2025-08-17T18:22:06.095Z" }, + { url = "https://files.pythonhosted.org/packages/49/cf/2abb3a1ad85aebe18c53e7eca73223f1546ddfa3bf4d2fb83fc5a064c5ca/zstandard-0.24.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:aa705beb74ab116563f4ce784fa94771f230c05d09ab5de9c397793e725bb1db", size = 5443319, upload-time = "2025-08-17T18:22:08.572Z" }, + { url = "https://files.pythonhosted.org/packages/40/42/0dd59fc2f68f1664cda11c3b26abdf987f4e57cb6b6b0f329520cd074552/zstandard-0.24.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:aadf32c389bb7f02b8ec5c243c38302b92c006da565e120dfcb7bf0378f4f848", size = 5822355, upload-time = "2025-08-17T18:22:10.537Z" }, + { url = "https://files.pythonhosted.org/packages/99/c0/ea4e640fd4f7d58d6f87a1e7aca11fb886ac24db277fbbb879336c912f63/zstandard-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e40cd0fc734aa1d4bd0e7ad102fd2a1aefa50ce9ef570005ffc2273c5442ddc3", size = 5365257, upload-time = "2025-08-17T18:22:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/27/a9/92da42a5c4e7e4003271f2e1f0efd1f37cfd565d763ad3604e9597980a1c/zstandard-0.24.0-cp311-cp311-win32.whl", hash = "sha256:cda61c46343809ecda43dc620d1333dd7433a25d0a252f2dcc7667f6331c7b61", size = 435559, upload-time = "2025-08-17T18:22:17.29Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8e/2c8e5c681ae4937c007938f954a060fa7c74f36273b289cabdb5ef0e9a7e/zstandard-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:3b95fc06489aa9388400d1aab01a83652bc040c9c087bd732eb214909d7fb0dd", size = 505070, upload-time = "2025-08-17T18:22:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/52/10/a2f27a66bec75e236b575c9f7b0d7d37004a03aa2dcde8e2decbe9ed7b4d/zstandard-0.24.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad9fd176ff6800a0cf52bcf59c71e5de4fa25bf3ba62b58800e0f84885344d34", size = 461507, upload-time = "2025-08-17T18:22:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/0bd281d9154bba7fc421a291e263911e1d69d6951aa80955b992a48289f6/zstandard-0.24.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a2bda8f2790add22773ee7a4e43c90ea05598bffc94c21c40ae0a9000b0133c3", size = 795710, upload-time = "2025-08-17T18:22:19.189Z" }, + { url = "https://files.pythonhosted.org/packages/36/26/b250a2eef515caf492e2d86732e75240cdac9d92b04383722b9753590c36/zstandard-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cc76de75300f65b8eb574d855c12518dc25a075dadb41dd18f6322bda3fe15d5", size = 640336, upload-time = "2025-08-17T18:22:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/79/bf/3ba6b522306d9bf097aac8547556b98a4f753dc807a170becaf30dcd6f01/zstandard-0.24.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d2b3b4bda1a025b10fe0269369475f420177f2cb06e0f9d32c95b4873c9f80b8", size = 5342533, upload-time = "2025-08-17T18:22:22.326Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ec/22bc75bf054e25accdf8e928bc68ab36b4466809729c554ff3a1c1c8bce6/zstandard-0.24.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b84c6c210684286e504022d11ec294d2b7922d66c823e87575d8b23eba7c81f", size = 5062837, upload-time = "2025-08-17T18:22:24.416Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/33edfc9d286e517fb5b51d9c3210e5bcfce578d02a675f994308ca587ae1/zstandard-0.24.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c59740682a686bf835a1a4d8d0ed1eefe31ac07f1c5a7ed5f2e72cf577692b00", size = 5393855, upload-time = "2025-08-17T18:22:26.786Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/59254e9b29da6215fb3a717812bf87192d89f190f23817d88cb8868c47ac/zstandard-0.24.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6324fde5cf5120fbf6541d5ff3c86011ec056e8d0f915d8e7822926a5377193a", size = 5451058, upload-time = "2025-08-17T18:22:28.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c7/31674cb2168b741bbbe71ce37dd397c9c671e73349d88ad3bca9e9fae25b/zstandard-0.24.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51a86bd963de3f36688553926a84e550d45d7f9745bd1947d79472eca27fcc75", size = 5546619, upload-time = "2025-08-17T18:22:31.115Z" }, + { url = "https://files.pythonhosted.org/packages/e6/01/1a9f22239f08c00c156f2266db857545ece66a6fc0303d45c298564bc20b/zstandard-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d82ac87017b734f2fb70ff93818c66f0ad2c3810f61040f077ed38d924e19980", size = 5046676, upload-time = "2025-08-17T18:22:33.077Z" }, + { url = "https://files.pythonhosted.org/packages/a7/91/6c0cf8fa143a4988a0361380ac2ef0d7cb98a374704b389fbc38b5891712/zstandard-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92ea7855d5bcfb386c34557516c73753435fb2d4a014e2c9343b5f5ba148b5d8", size = 5576381, upload-time = "2025-08-17T18:22:35.391Z" }, + { url = "https://files.pythonhosted.org/packages/e2/77/1526080e22e78871e786ccf3c84bf5cec9ed25110a9585507d3c551da3d6/zstandard-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3adb4b5414febf074800d264ddf69ecade8c658837a83a19e8ab820e924c9933", size = 4953403, upload-time = "2025-08-17T18:22:37.266Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d0/a3a833930bff01eab697eb8abeafb0ab068438771fa066558d96d7dafbf9/zstandard-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6374feaf347e6b83ec13cc5dcfa70076f06d8f7ecd46cc71d58fac798ff08b76", size = 5267396, upload-time = "2025-08-17T18:22:39.757Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/90a0db9a61cd4769c06374297ecfcbbf66654f74cec89392519deba64d76/zstandard-0.24.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:13fc548e214df08d896ee5f29e1f91ee35db14f733fef8eabea8dca6e451d1e2", size = 5433269, upload-time = "2025-08-17T18:22:42.131Z" }, + { url = "https://files.pythonhosted.org/packages/ce/58/fc6a71060dd67c26a9c5566e0d7c99248cbe5abfda6b3b65b8f1a28d59f7/zstandard-0.24.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0a416814608610abf5488889c74e43ffa0343ca6cf43957c6b6ec526212422da", size = 5814203, upload-time = "2025-08-17T18:22:44.017Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6a/89573d4393e3ecbfa425d9a4e391027f58d7810dec5cdb13a26e4cdeef5c/zstandard-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0d66da2649bb0af4471699aeb7a83d6f59ae30236fb9f6b5d20fb618ef6c6777", size = 5359622, upload-time = "2025-08-17T18:22:45.802Z" }, + { url = "https://files.pythonhosted.org/packages/60/ff/2cbab815d6f02a53a9d8d8703bc727d8408a2e508143ca9af6c3cca2054b/zstandard-0.24.0-cp312-cp312-win32.whl", hash = "sha256:ff19efaa33e7f136fe95f9bbcc90ab7fb60648453b03f95d1de3ab6997de0f32", size = 435968, upload-time = "2025-08-17T18:22:49.493Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/8f96b8ddb7ad12344218fbd0fd2805702dafd126ae9f8a1fb91eef7b33da/zstandard-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc05f8a875eb651d1cc62e12a4a0e6afa5cd0cc231381adb830d2e9c196ea895", size = 505195, upload-time = "2025-08-17T18:22:47.193Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4a/bfca20679da63bfc236634ef2e4b1b4254203098b0170e3511fee781351f/zstandard-0.24.0-cp312-cp312-win_arm64.whl", hash = "sha256:b04c94718f7a8ed7cdd01b162b6caa1954b3c9d486f00ecbbd300f149d2b2606", size = 461605, upload-time = "2025-08-17T18:22:48.317Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ef/db949de3bf81ed122b8ee4db6a8d147a136fe070e1015f5a60d8a3966748/zstandard-0.24.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e4ebb000c0fe24a6d0f3534b6256844d9dbf042fdf003efe5cf40690cf4e0f3e", size = 795700, upload-time = "2025-08-17T18:22:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/99/56/fc04395d6f5eabd2fe6d86c0800d198969f3038385cb918bfbe94f2b0c62/zstandard-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:498f88f5109666c19531f0243a90d2fdd2252839cd6c8cc6e9213a3446670fa8", size = 640343, upload-time = "2025-08-17T18:22:51.999Z" }, + { url = "https://files.pythonhosted.org/packages/9b/0f/0b0e0d55f2f051d5117a0d62f4f9a8741b3647440c0ee1806b7bd47ed5ae/zstandard-0.24.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0a9e95ceb180ccd12a8b3437bac7e8a8a089c9094e39522900a8917745542184", size = 5342571, upload-time = "2025-08-17T18:22:53.734Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/d74e49f04fbd62d4b5d89aeb7a29d693fc637c60238f820cd5afe6ca8180/zstandard-0.24.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bcf69e0bcddbf2adcfafc1a7e864edcc204dd8171756d3a8f3340f6f6cc87b7b", size = 5062723, upload-time = "2025-08-17T18:22:55.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/97/df14384d4d6a004388e6ed07ded02933b5c7e0833a9150c57d0abc9545b7/zstandard-0.24.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:10e284748a7e7fbe2815ca62a9d6e84497d34cfdd0143fa9e8e208efa808d7c4", size = 5393282, upload-time = "2025-08-17T18:22:57.655Z" }, + { url = "https://files.pythonhosted.org/packages/7e/09/8f5c520e59a4d41591b30b7568595eda6fd71c08701bb316d15b7ed0613a/zstandard-0.24.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1bda8a85e5b9d5e73af2e61b23609a8cc1598c1b3b2473969912979205a1ff25", size = 5450895, upload-time = "2025-08-17T18:22:59.749Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3d/02aba892327a67ead8cba160ee835cfa1fc292a9dcb763639e30c07da58b/zstandard-0.24.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b14bc92af065d0534856bf1b30fc48753163ea673da98857ea4932be62079b1", size = 5546353, upload-time = "2025-08-17T18:23:01.457Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6e/96c52afcde44da6a5313a1f6c356349792079808f12d8b69a7d1d98ef353/zstandard-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:b4f20417a4f511c656762b001ec827500cbee54d1810253c6ca2df2c0a307a5f", size = 5046404, upload-time = "2025-08-17T18:23:03.418Z" }, + { url = "https://files.pythonhosted.org/packages/da/b6/eefee6b92d341a7db7cd1b3885d42d30476a093720fb5c181e35b236d695/zstandard-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:337572a7340e1d92fd7fb5248c8300d0e91071002d92e0b8cabe8d9ae7b58159", size = 5576095, upload-time = "2025-08-17T18:23:05.331Z" }, + { url = "https://files.pythonhosted.org/packages/a3/29/743de3131f6239ba6611e17199581e6b5e0f03f268924d42468e29468ca0/zstandard-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df4be1cf6e8f0f2bbe2a3eabfff163ef592c84a40e1a20a8d7db7f27cfe08fc2", size = 4953448, upload-time = "2025-08-17T18:23:07.225Z" }, + { url = "https://files.pythonhosted.org/packages/c9/11/bd36ef49fba82e307d69d93b5abbdcdc47d6a0bcbc7ffbbfe0ef74c2fec5/zstandard-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6885ae4b33aee8835dbdb4249d3dfec09af55e705d74d9b660bfb9da51baaa8b", size = 5267388, upload-time = "2025-08-17T18:23:09.127Z" }, + { url = "https://files.pythonhosted.org/packages/c0/23/a4cfe1b871d3f1ce1f88f5c68d7e922e94be0043f3ae5ed58c11578d1e21/zstandard-0.24.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:663848a8bac4fdbba27feea2926049fdf7b55ec545d5b9aea096ef21e7f0b079", size = 5433383, upload-time = "2025-08-17T18:23:11.343Z" }, + { url = "https://files.pythonhosted.org/packages/77/26/f3fb85f00e732cca617d4b9cd1ffa6484f613ea07fad872a8bdc3a0ce753/zstandard-0.24.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:05d27c953f2e0a3ecc8edbe91d6827736acc4c04d0479672e0400ccdb23d818c", size = 5813988, upload-time = "2025-08-17T18:23:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8c/d7e3b424b73f3ce66e754595cbcb6d94ff49790c9ac37d50e40e8145cd44/zstandard-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:77b8b7b98893eaf47da03d262816f01f251c2aa059c063ed8a45c50eada123a5", size = 5359756, upload-time = "2025-08-17T18:23:15.021Z" }, + { url = "https://files.pythonhosted.org/packages/90/6c/f1f0e11f1b295138f9da7e7ae22dcd9a1bb96a9544fa3b31507e431288f5/zstandard-0.24.0-cp313-cp313-win32.whl", hash = "sha256:cf7fbb4e54136e9a03c7ed7691843c4df6d2ecc854a2541f840665f4f2bb2edd", size = 435957, upload-time = "2025-08-17T18:23:18.835Z" }, + { url = "https://files.pythonhosted.org/packages/9f/03/ab8b82ae5eb49eca4d3662705399c44442666cc1ce45f44f2d263bb1ae31/zstandard-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:d64899cc0f33a8f446f1e60bffc21fa88b99f0e8208750d9144ea717610a80ce", size = 505171, upload-time = "2025-08-17T18:23:16.44Z" }, + { url = "https://files.pythonhosted.org/packages/db/12/89a2ecdea4bc73a934a30b66a7cfac5af352beac94d46cf289e103b65c34/zstandard-0.24.0-cp313-cp313-win_arm64.whl", hash = "sha256:57be3abb4313e0dd625596376bbb607f40059d801d51c1a1da94d7477e63b255", size = 461596, upload-time = "2025-08-17T18:23:17.603Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/f3d2c4d64aacee4aab89e788783636884786b6f8334c819f09bff1aa207b/zstandard-0.24.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b7fa260dd2731afd0dfa47881c30239f422d00faee4b8b341d3e597cface1483", size = 795747, upload-time = "2025-08-17T18:23:19.968Z" }, + { url = "https://files.pythonhosted.org/packages/32/2d/9d3e5f6627e4cb5e511803788be1feee2f0c3b94594591e92b81db324253/zstandard-0.24.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e05d66239d14a04b4717998b736a25494372b1b2409339b04bf42aa4663bf251", size = 640475, upload-time = "2025-08-17T18:23:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/be/5d/48e66abf8c146d95330e5385633a8cfdd556fa8bd14856fe721590cbab2b/zstandard-0.24.0-cp314-cp314-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:622e1e04bd8a085994e02313ba06fbcf4f9ed9a488c6a77a8dbc0692abab6a38", size = 5343866, upload-time = "2025-08-17T18:23:23.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/6c/65fe7ba71220a551e082e4a52790487f1d6bb8dfc2156883e088f975ad6d/zstandard-0.24.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:55872e818598319f065e8192ebefecd6ac05f62a43f055ed71884b0a26218f41", size = 5062719, upload-time = "2025-08-17T18:23:25.192Z" }, + { url = "https://files.pythonhosted.org/packages/cb/68/15ed0a813ff91be80cc2a610ac42e0fc8d29daa737de247bbf4bab9429a1/zstandard-0.24.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bb2446a55b3a0fd8aa02aa7194bd64740015464a2daaf160d2025204e1d7c282", size = 5393090, upload-time = "2025-08-17T18:23:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/d4/89/e560427b74fa2da6a12b8f3af8ee29104fe2bb069a25e7d314c35eec7732/zstandard-0.24.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2825a3951f945fb2613ded0f517d402b1e5a68e87e0ee65f5bd224a8333a9a46", size = 5450383, upload-time = "2025-08-17T18:23:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/a3/95/0498328cbb1693885509f2fc145402b108b750a87a3af65b7250b10bd896/zstandard-0.24.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09887301001e7a81a3618156bc1759e48588de24bddfdd5b7a4364da9a8fbc20", size = 5546142, upload-time = "2025-08-17T18:23:31.281Z" }, + { url = "https://files.pythonhosted.org/packages/8a/8a/64aa15a726594df3bf5d8decfec14fe20cd788c60890f44fcfc74d98c2cc/zstandard-0.24.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:98ca91dc9602cf351497d5600aa66e6d011a38c085a8237b370433fcb53e3409", size = 4953456, upload-time = "2025-08-17T18:23:33.234Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/e94879c5cd6017af57bcba08519ed1228b1ebb15681efd949f4a00199449/zstandard-0.24.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e69f8e534b4e254f523e2f9d4732cf9c169c327ca1ce0922682aac9a5ee01155", size = 5268287, upload-time = "2025-08-17T18:23:35.145Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e5/1a3b3a93f953dbe9e77e2a19be146e9cd2af31b67b1419d6cc8e8898d409/zstandard-0.24.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:444633b487a711e34f4bccc46a0c5dfbe1aee82c1a511e58cdc16f6bd66f187c", size = 5433197, upload-time = "2025-08-17T18:23:36.969Z" }, + { url = "https://files.pythonhosted.org/packages/39/83/b6eb1e1181de994b29804e1e0d2dc677bece4177f588c71653093cb4f6d5/zstandard-0.24.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f7d3fe9e1483171e9183ffdb1fab07c5fef80a9c3840374a38ec2ab869ebae20", size = 5813161, upload-time = "2025-08-17T18:23:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d3/2fb4166561591e9d75e8e35c79182aa9456644e2f4536f29e51216d1c513/zstandard-0.24.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:27b6fa72b57824a3f7901fc9cc4ce1c1c834b28f3a43d1d4254c64c8f11149d4", size = 5359831, upload-time = "2025-08-17T18:23:41.162Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/6a9227315b774f64a67445f62152c69b4e5e49a52a3c7c4dad8520a55e20/zstandard-0.24.0-cp314-cp314-win32.whl", hash = "sha256:fdc7a52a4cdaf7293e10813fd6a3abc0c7753660db12a3b864ab1fb5a0c60c16", size = 444448, upload-time = "2025-08-17T18:23:45.151Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/67acaba311013e0798cb96d1a2685cb6edcdfc1cae378b297ea7b02c319f/zstandard-0.24.0-cp314-cp314-win_amd64.whl", hash = "sha256:656ed895b28c7e42dd5b40dfcea3217cfc166b6b7eef88c3da2f5fc62484035b", size = 516075, upload-time = "2025-08-17T18:23:42.8Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/45fd8921263cea0228b20aa31bce47cc66016b2aba1afae1c6adcc3dbb1f/zstandard-0.24.0-cp314-cp314-win_arm64.whl", hash = "sha256:0101f835da7de08375f380192ff75135527e46e3f79bef224e3c49cb640fef6a", size = 476847, upload-time = "2025-08-17T18:23:43.892Z" }, +] diff --git a/workflows/bitbucket-pipelines.yml b/workflows/bitbucket-pipelines.yml new file mode 100644 index 0000000..d129560 --- /dev/null +++ b/workflows/bitbucket-pipelines.yml @@ -0,0 +1,81 @@ +# Socket Security Bitbucket Pipelines +# This pipeline runs Socket Security scans on every commit to any branch +# The CLI automatically detects most information from the git repository + +image: socketdev/cli:latest + +definitions: + steps: + - step: &socket-scan + name: Socket Security Scan + script: + # Run Socket CLI with minimal required parameters + # The CLI automatically detects: + # - Repository name from git + # - Branch name from git + # - Commit SHA from git + # - Commit message from git + # - Committer information from git + # - Default branch status from git repository + # - Changed files from git commit + - | + socketcli \ + --target-path $BITBUCKET_CLONE_DIR \ + --scm api \ + --pr-number ${BITBUCKET_PR_ID:-0} + # Repository variables needed (set in Bitbucket repo settings) + # SOCKET_SECURITY_API_KEY: Your Socket Security API token + +pipelines: + # Run on all branches + branches: + '**': + - step: *socket-scan + + # Run on pull requests + pull-requests: + '**': + - step: *socket-scan + +# Optional: More efficient version that only runs when manifest files change +# To use this instead, replace the pipelines section above with: +# +# pipelines: +# branches: +# '**': +# - step: +# <<: *socket-scan +# condition: +# changesets: +# includePaths: +# - "package.json" +# - "package-lock.json" +# - "yarn.lock" +# - "pnpm-lock.yaml" +# - "requirements.txt" +# - "Pipfile" +# - "Pipfile.lock" +# - "pyproject.toml" +# - "poetry.lock" +# - "go.mod" +# - "go.sum" +# - "Cargo.toml" +# - "Cargo.lock" +# - "composer.json" +# - "composer.lock" +# - "Gemfile" +# - "Gemfile.lock" +# - "**/*.csproj" +# - "**/*.fsproj" +# - "**/*.vbproj" +# - "packages.config" +# - "paket.dependencies" +# - "project.json" +# +# pull-requests: +# '**': +# - step: *socket-scan + +# Note: Bitbucket Pipelines doesn't have built-in SCM integration like +# GitHub Actions or GitLab CI, so we use --scm api mode which provides +# basic scanning without PR comment functionality. diff --git a/workflows/github-actions.yml b/workflows/github-actions.yml new file mode 100644 index 0000000..422eb25 --- /dev/null +++ b/workflows/github-actions.yml @@ -0,0 +1,63 @@ +# Socket Security GitHub Actions Workflow +# This workflow runs Socket Security scans on every commit to any branch +# It automatically detects git repository information and handles different event types + +name: socket-security-workflow +run-name: Socket Security Github Action + +on: + push: + branches: ['**'] # Run on all branches, all commits + pull_request: + types: [opened, synchronize, reopened] + issue_comment: + types: [created] + +# Prevent concurrent runs for the same commit +concurrency: + group: socket-scan-${{ github.sha }} + cancel-in-progress: true + +jobs: + socket-security: + permissions: + issues: write + contents: read + pull-requests: write + runs-on: ubuntu-latest + + # Option 1: Use the official Socket CLI container (faster, more reliable) + container: socketdev/cli:latest + + steps: + - uses: actions/checkout@v4 + with: + # For PRs, fetch one additional commit for proper diff analysis + fetch-depth: 0 + + - name: Run Socket Security Scan + env: + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_SECURITY_API_KEY }} + GH_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Determine PR number based on event type + PR_NUMBER=0 + if [ "${{ github.event_name }}" == "pull_request" ]; then + PR_NUMBER=${{ github.event.pull_request.number }} + elif [ "${{ github.event_name }}" == "issue_comment" ]; then + PR_NUMBER=${{ github.event.issue.number }} + fi + + # Run Socket CLI with minimal required parameters + # The CLI automatically detects: + # - Repository name from git + # - Branch name from git + # - Commit SHA from git + # - Commit message from git + # - Committer information from git + # - Default branch status from git and GitHub environment + # - Changed files from git commit + socketcli \ + --target-path $GITHUB_WORKSPACE \ + --scm github \ + --pr-number $PR_NUMBER diff --git a/workflows/gitlab-ci.yml b/workflows/gitlab-ci.yml new file mode 100644 index 0000000..59ea864 --- /dev/null +++ b/workflows/gitlab-ci.yml @@ -0,0 +1,80 @@ +# Socket Security GitLab CI Pipeline +# This pipeline runs Socket Security scans on every commit to any branch +# The CLI automatically detects most information from the git repository + +stages: + - security-scan + +socket-security: + stage: security-scan + image: socketdev/cli:latest + + # Run on all branches and merge requests + rules: + - if: $CI_PIPELINE_SOURCE == "push" + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + + variables: + # These environment variables are automatically available in GitLab CI + # and are used by the Socket CLI's GitLab SCM integration + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + + cache: + paths: + - .cache/pip/ + + script: + # Run Socket CLI with minimal required parameters + # The CLI automatically detects: + # - Repository name from git + # - Branch name from git + # - Commit SHA from git (or CI_COMMIT_SHA) + # - Commit message from git + # - Committer information from git + # - Default branch status from GitLab CI environment variables + # - Changed files from git commit + # - Merge request number from CI_MERGE_REQUEST_IID + - | + socketcli \ + --target-path $CI_PROJECT_DIR \ + --scm gitlab \ + --pr-number ${CI_MERGE_REQUEST_IID:-0} + + # Required for GitLab integration to work properly + variables: + SOCKET_SECURITY_API_KEY: $SOCKET_SECURITY_API_KEY + # GitLab token for API access - supports both authentication patterns: + # 1. CI_JOB_TOKEN: Built-in GitLab CI token (automatically uses Bearer auth) + # 2. Personal Access Token: Custom token (auto-detects Bearer vs PRIVATE-TOKEN) + GITLAB_TOKEN: $CI_JOB_TOKEN + +# Optional: Run only when manifest files change (more efficient) +# To use this version instead, replace the rules section above with: +# +# rules: +# - if: $CI_PIPELINE_SOURCE == "push" +# changes: +# - "package.json" +# - "package-lock.json" +# - "yarn.lock" +# - "pnpm-lock.yaml" +# - "requirements.txt" +# - "Pipfile" +# - "Pipfile.lock" +# - "pyproject.toml" +# - "poetry.lock" +# - "go.mod" +# - "go.sum" +# - "Cargo.toml" +# - "Cargo.lock" +# - "composer.json" +# - "composer.lock" +# - "Gemfile" +# - "Gemfile.lock" +# - "**/*.csproj" +# - "**/*.fsproj" +# - "**/*.vbproj" +# - "packages.config" +# - "paket.dependencies" +# - "project.json" +# - if: $CI_PIPELINE_SOURCE == "merge_request_event"