diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73de6ae..196f2f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,5 +95,7 @@ jobs: run: PYTHONPATH=graybox:whitebox:triager python3 -m unittest discover -s graybox/tests -p 'test_*.py' - name: Platform unit tests run: PYTHONPATH=platform:graybox:whitebox:triager:bundle python3 -m unittest discover -s platform/tests -p 'test_*.py' + - name: Docs site builds + run: pip install -r docs/requirements.txt && python docs/build.py - name: Reporter unit tests run: PYTHONPATH=reporter:triager python3 -m unittest discover -s reporter/tests -p 'test_*.py' diff --git a/.github/workflows/dev-push.yml b/.github/workflows/dev-push.yml index aade3f4..1cf3d10 100644 --- a/.github/workflows/dev-push.yml +++ b/.github/workflows/dev-push.yml @@ -1,4 +1,4 @@ -name: "Build and Push Dev/main OWASP OFFAT Docker Images to DockerHub" +name: "Build and push dev/main OWASP OFFAT image to Docker Hub" on: push: @@ -6,50 +6,26 @@ on: - "main" - "dev" +permissions: + contents: read + jobs: - build-and-push-dev-docker-images: + build-and-push: runs-on: ubuntu-latest steps: - - name: Branch Checkout - uses: actions/checkout@v2 - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Login to Docker Hub - uses: docker/login-action@v3 + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push offat-base docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/wolfi-base-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-base:${{ github.head_ref || github.ref_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/dev/cli-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat:${{ github.head_ref || github.ref_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/dev/backend-api-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api:${{ github.head_ref || github.ref_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api-worker docker image - uses: docker/build-push-action@v3 + # One image: Rust execution engine + the Python platform (docker/Dockerfile). + - name: Build and push offat image + uses: docker/build-push-action@v6 with: - context: ./src/ - file: ./src/DockerFiles/dev/backend-api-worker-Dockerfile + context: . + file: ./docker/Dockerfile push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api-worker:${{ github.head_ref || github.ref_name }} + tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat:${{ github.ref_name }} platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index d38f93c..03e47e0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,6 +1,6 @@ name: docs -# Build the documentation site (docs/, just-the-docs) and deploy it to +# Build the documentation site (docs/build.py -> docs/_site) and deploy it to # GitHub Pages. Requires Pages "Build and deployment" source = GitHub Actions. on: push: @@ -23,22 +23,20 @@ concurrency: jobs: build: runs-on: ubuntu-latest - defaults: - run: - working-directory: docs steps: - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 + - uses: actions/setup-python@v5 with: - ruby-version: "3.3" - bundler-cache: true - working-directory: docs + python-version: "3.12" - id: pages uses: actions/configure-pages@v5 - - name: Build with Jekyll - run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" + - name: Build the docs site + run: | + pip install -r docs/requirements.txt + python docs/build.py env: - JEKYLL_ENV: production + DOCS_BASE_PATH: ${{ steps.pages.outputs.base_path }}/ + DOCS_SITE_URL: ${{ steps.pages.outputs.base_url }}/ - uses: actions/upload-pages-artifact@v3 with: path: docs/_site diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml deleted file mode 100644 index 16d3210..0000000 --- a/.github/workflows/pypi-publish.yml +++ /dev/null @@ -1,43 +0,0 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Upload OWASP OFFAT Python Package to PyPi - -on: - release: - types: [published] - -permissions: - id-token: write # This is required for requesting the JWT - contents: read # This is required for actions/checkout - - -jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: "3.x" - - name: change cwd to src directory - run: cd src - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: | - cd src - python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.13.0 - with: - packages-dir: src/dist diff --git a/.github/workflows/release-push.yml b/.github/workflows/release-push.yml index c7bd369..9136776 100644 --- a/.github/workflows/release-push.yml +++ b/.github/workflows/release-push.yml @@ -1,88 +1,30 @@ -name: "Release: Build and Push OWASP OFFAT Docker Images to DockerHub" +name: "Release: build and push OWASP OFFAT image to Docker Hub" on: release: types: [published] +permissions: + contents: read + jobs: - build-and-push-main-docker-images: + build-and-push: runs-on: ubuntu-latest steps: - - name: Branch Checkout - uses: actions/checkout@v2 - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Login to Docker Hub - uses: docker/login-action@v2 + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # Build and publish version tag image - - name: Build and push offat-base docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/wolfi-base-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-base:${{ github.event.release.tag_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/main/cli-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat:${{ github.event.release.tag_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/main/backend-api-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api:${{ github.event.release.tag_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api-worker docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/main/backend-api-worker-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api-worker:${{ github.event.release.tag_name }} - platforms: linux/amd64,linux/arm64 - - # Build and publish latest tag image - - name: Build and push offat-base docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/wolfi-base-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-base:latest - platforms: linux/amd64,linux/arm64 - - name: Build and push offat docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/main/cli-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat:latest - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/main/backend-api-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api:latest - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api-worker docker image - uses: docker/build-push-action@v3 + - name: Build and push offat image (version + latest) + uses: docker/build-push-action@v6 with: - context: ./src/ - file: ./src/DockerFiles/main/backend-api-worker-Dockerfile + context: . + file: ./docker/Dockerfile push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api-worker:latest + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/offat:${{ github.event.release.tag_name }} + ${{ secrets.DOCKERHUB_USERNAME }}/offat:latest platforms: linux/amd64,linux/arm64 diff --git a/.gitignore b/.gitignore index 1acf502..c37ce5b 100644 --- a/.gitignore +++ b/.gitignore @@ -227,3 +227,6 @@ offat-report/ # OFFAT runtime artifacts .offat-triage-cache.json + +# docs site build output +docs/_site/ diff --git a/Makefile b/Makefile index 3da7694..41b7dd5 100644 --- a/Makefile +++ b/Makefile @@ -17,8 +17,8 @@ engine-http3: ## Build the engine with the HTTP/3 + QUIC transport enabled engine-test: ## Run the engine's unit tests cd engine-rs && cargo test -platform-install: ## Install the Python platform + its libraries - pip install ./bundle ./triager ./whitebox ./graybox ./platform ./reporter +platform-install: ## Install the platform + its libraries (with the code-graph extra) + pip install ./bundle ./triager "./whitebox[graph]" ./graybox ./platform ./reporter python-check: ## Byte-compile the Python packages python3 -m compileall -q bundle/offat_bundle triager/offat_triage whitebox/offat_wb \ @@ -37,7 +37,7 @@ smoke: ## Offline end-to-end check (map -> prg -> threat-model -> test-gen) python3 -m offat_platform validate /tmp/offat-smoke.offat.json && echo "platform smoke OK" docker: ## Build the platform image (Rust engine + Python platform) - docker build -t offat-ai:latest -f docker/Dockerfile . + docker build -t offat:latest -f docker/Dockerfile . clean: ## Remove build artifacts and reports rm -rf engine-rs/target offat-report /tmp/offat-smoke*.json diff --git a/README.md b/README.md index 99df34c..1adacbe 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,86 @@ -# OWASP OFFAT +

+ OWASP OFFAT +

-OWASP OFFAT (OFFensive Api Tester) automatically tests your API for common -vulnerabilities - generating tests from an OpenAPI/Swagger spec, fuzzing inputs, -and now also mapping and testing an API straight from its source code. +

OWASP OFFAT

-**Documentation: [owasp.github.io/OFFAT](https://owasp.github.io/OFFAT/)** +

+ OFFensive Api Tester — source-aware API security testing.
+ Maps your API from code, proves source→sink paths on a code graph, executes tests with a Rust engine, + and triages the results with AI. +

-![UnDocumented petstore API endpoint HTTP method results](./assets/images/tests/offat-v0.5.0.png) - -## Demo - -[![asciicast](https://asciinema.org/a/9MSwl7UafIVT3iJn13OcvWXeF.svg)](https://asciinema.org/a/9MSwl7UafIVT3iJn13OcvWXeF) - -> Authorized use only. OFFAT sends real requests to the target you point it at. -> The platform's execution engine sends active attack traffic and requires an -> explicit `--yes`. Only test systems and code you own or are authorized to assess. +

+ Documentation · + Install · + Usage · + GitHub Action +

--- -OFFAT can be used in two ways. +> **Authorized use only.** Live runs send real requests to the target you point +> OFFAT at and require an explicit `--yes`. Only test systems and code you own or +> are authorized to assess. -## 1. Classic CLI - spec-driven black-box testing +## What it does -Generate and run tests from an OpenAPI/Swagger file or URL. - -```bash -python -m pip install offat # CLI only (offat[api] for CLI + REST API) -offat -f swagger_file.json # or: offat -f https://example.com/docs.json -offat -h # all options +``` +map -> code graph -> threat model -> test-gen -> execute (Rust) -> triage (AI) -> report ``` -Security checks: restricted HTTP methods, SQLi, BOLA, data exposure, BOPLA / -mass assignment, broken access control, basic command injection, and basic XSS / -HTML injection - a subset of the OWASP API Top 10. - -- Full usage: [owasp.github.io/OFFAT/usage](https://owasp.github.io/OFFAT/usage.html#classic-cli) -- CI: [GitHub Action](https://owasp.github.io/OFFAT/github-action.html) -- CLI details: [`src/README.md`](./src/README.md) +- **Map** the attack surface straight from source (decorator routes, routers, + OpenAPI/Swagger specs) into one canonical [Bundle](./bundle/README.md) file. +- **Prove** source-to-sink data flows on a code property graph (Python, JS/TS) and + attach every finding to the HTTP route that reaches it. +- **Execute** the test plan with a Rust engine over HTTP/1.1, HTTP/2, WebSocket and + gRPC (HTTP/3 + QUIC opt-in), with identity-aware differential checks for + BOLA / BFLA. +- **Triage** with a token-disciplined AI triager (cached, batched, model-tiered) + or a deterministic offline heuristic. +- **Report** as SARIF 2.1.0 (with code flows), Markdown, HTML, an OWASP API Top 10 + compliance report, and JUnit. -## 2. OFFAT-AI platform - source-aware testing +## Quick start -A staged pipeline that maps the attack surface from source (decorator routes -**and** OpenAPI specs), models threats, generates tests, executes them with a -Rust engine (HTTP/1.1 + HTTP/2, WebSocket, gRPC; HTTP/3 + QUIC opt-in), and -triages the findings - all over one canonical Bundle file. +```bash +git clone https://github.com/OWASP/OFFAT.git && cd OFFAT +pip install ./bundle ./triager './whitebox[graph]' ./graybox ./platform ./reporter +make engine # Rust engine, only for live runs -``` -map -> parameter relation graph -> threat model -> test-gen -> execute -> triage -> report -``` +# source-only: no traffic is sent +offat-platform run /path/to/repo -o app.offat.json --report-dir reports -```bash -# install the platform and its libraries -pip install ./bundle ./triager ./whitebox ./graybox ./platform ./reporter -# build the Rust execution engine (needed only for live runs) -make engine - -# the whole pipeline in one command (omit --url for a source-only run) -offat-platform run /path/to/repo \ - --url https://api.your-authorized-target.test \ - --identities examples/identities.example.json \ - --yes -o app.offat.json --report-dir reports --fail-on high +# live, against a target you are authorized to test +offat-platform run /path/to/repo --url https://api.your-authorized-target.test \ + --identities examples/identities.example.json --yes -o app.offat.json --fail-on high ``` -Two source-only modes send no live traffic: +Source-only modes: ```bash -offat-whitebox /path/to/repo --no-ai # SAST -offat-graybox /path/to/repo --fail-on high # SAST + endpoint reachability +offat-whitebox /path/to/repo # code-graph SAST + AI triage +offat-graybox /path/to/repo # SAST prioritized by endpoint reachability ``` -- Overview: [OFFAT-AI.md](./OFFAT-AI.md) -- How it works: [owasp.github.io/OFFAT/how-it-works](https://owasp.github.io/OFFAT/how-it-works.html) -- Source modes: [owasp.github.io/OFFAT/source-modes](https://owasp.github.io/OFFAT/source-modes.html) -- Design docs: [docs/platform.md](./docs/platform.md), [docs/architecture.md](./docs/architecture.md) +Or use the container: `docker build -t offat -f docker/Dockerfile .` + +## Repository layout -## Features +| Path | What | +|---|---| +| [`bundle/`](./bundle) | the Bundle contract (schema, IO, validation) | +| [`platform/`](./platform) | `offat-platform`: map, PRG, threat model, test-gen, triage | +| [`engine-rs/`](./engine-rs) | `offat-engine`: Rust execution engine | +| [`whitebox/`](./whitebox) | `offat-whitebox`: code-graph taint engine + SAST pipeline | +| [`graybox/`](./graybox) | `offat-graybox`: SAST fused with endpoint reachability | +| [`triager/`](./triager) | shared AI / heuristic triager | +| [`reporter/`](./reporter) | SARIF, Markdown, HTML, compliance reports | +| [`viz/`](./viz) | static Bundle visualizer | +| [`docs/`](./docs) | documentation site sources | -- Security checks from the OWASP API Top 10 -- Automated, spec-driven and source-aware testing -- User-config-based testing (YAML) -- REST API for automation and integrations -- CLI tools and a Dockerized project -- GitHub Action for CI -- Open source under the MIT License +> The classic spec-only Python CLI (`pip install offat`, `src/`) has been retired +> in favour of the platform. Its last release remains on PyPI. ## Contributing diff --git a/action.yml b/action.yml index 78f6a42..53638fb 100644 --- a/action.yml +++ b/action.yml @@ -1,56 +1,97 @@ -name: 'OWASP OFFAT CICD Scanner' -description: 'Autonomously assesses your API for prevalent vulnerabilities in CICD pipelines' +name: 'OWASP OFFAT' +description: 'Source-aware API security testing: map from code, prove source-to-sink flows, triage with AI, report as SARIF' branding: icon: shield color: blue inputs: - file: - description: 'path or url of openapi/swagger specification file' - required: true - - rate_limit: - description: 'API requests rate limit per second. default: 60' + path: + description: 'path to the source tree (and/or OpenAPI spec) to analyze' required: false - default: '60' - + default: '.' + url: + description: 'base URL of a target you are authorized to test. Empty = source-only run (no traffic sent)' + required: false + default: '' + authorized: + description: "must be 'true' for a live run against url - confirms you own or are authorized to test the target" + required: false + default: 'false' + identities: + description: 'optional identities JSON (enables BOLA / BFLA / RBAC / auth checks on live runs)' + required: false + default: '' + fail_on: + description: 'fail the job when an actionable finding is at or above this severity (critical|high|medium|low)' + required: false + default: '' + formats: + description: 'report formats (sarif,md,html,compliance,pdf or all)' + required: false + default: 'all' + upload_sarif: + description: "'true' to upload results.sarif to GitHub code scanning (needs security-events: write)" + required: false + default: 'false' artifact_retention_days: - description: 'artifact retention period in days. default: 2' + description: 'report artifact retention period in days' required: false - default: '2' + default: '7' runs: using: composite steps: - - name: Setup Python 3.12 - uses: actions/setup-python@v5.1.0 - with: - python-version: '3.12' - - - name: Install OFFAT - run: pip install -U offat - shell: bash - - - name: Run Scanner - env: - scanner_file: ${{ inputs.file }} - scanner_rate_limit: ${{ inputs.rate_limit }} + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' - run: | - mkdir -p ./offat-tmp/ - offat --file $scanner_file --rate-limit $scanner_rate_limit -of json -o ./offat-tmp/results.json --capture-failed -H 'User-Agent: github-action-offat' - shell: bash + - name: Install OFFAT + shell: bash + run: | + cd "$GITHUB_ACTION_PATH" + pip install ./bundle ./triager './whitebox[graph]' ./graybox ./platform ./reporter - - name: Upload Scan Results to Artifacts - uses: actions/upload-artifact@v4 - with: - name: offat-results.json - path: ./offat-tmp/results.json - retention-days: ${{ inputs.artifact_retention_days }} - # retention period docs: https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#artifact-and-log-retention-policy + - name: Build the execution engine (live runs only) + if: inputs.url != '' && inputs.authorized == 'true' + shell: bash + run: | + cargo build --release --manifest-path "$GITHUB_ACTION_PATH/engine-rs/Cargo.toml" + echo "OFFAT_ENGINE=$GITHUB_ACTION_PATH/engine-rs/target/release/offat-engine" >> "$GITHUB_ENV" - - name: CleanUp - run: rm -rf ./offat-tmp/ - shell: bash + - name: Run OFFAT + shell: bash + env: + OFFAT_PATH: ${{ inputs.path }} + OFFAT_URL: ${{ inputs.url }} + OFFAT_AUTHORIZED: ${{ inputs.authorized }} + OFFAT_IDENTITIES: ${{ inputs.identities }} + OFFAT_FAIL_ON: ${{ inputs.fail_on }} + OFFAT_FORMATS: ${{ inputs.formats }} + run: | + args=(run "$OFFAT_PATH" -o offat-out/app.offat.json --report-dir offat-out/reports --formats "$OFFAT_FORMATS") + if [ -n "$OFFAT_URL" ]; then + if [ "$OFFAT_AUTHORIZED" != "true" ]; then + echo "::error::url is set but authorized is not 'true'. Live runs send real traffic; only test targets you are authorized to assess." + exit 2 + fi + args+=(--url "$OFFAT_URL" --yes) + [ -n "$OFFAT_IDENTITIES" ] && args+=(--identities "$OFFAT_IDENTITIES") + fi + [ -n "$OFFAT_FAIL_ON" ] && args+=(--fail-on "$OFFAT_FAIL_ON") + offat-platform "${args[@]}" + - name: Upload reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: offat-reports + path: offat-out/ + retention-days: ${{ inputs.artifact_retention_days }} + - name: Upload SARIF to code scanning + if: always() && inputs.upload_sarif == 'true' && hashFiles('offat-out/reports/results.sarif') != '' + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: offat-out/reports/results.sarif + category: offat diff --git a/docker/Dockerfile b/docker/Dockerfile index bc3817e..9106bf0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -7,9 +7,9 @@ RUN cd engine-rs && cargo build --release && cp target/release/offat-engine /out # Stage 2 - runtime. FROM python:3.12-slim -LABEL org.opencontainers.image.title="offat-ai" \ +LABEL org.opencontainers.image.title="offat" \ org.opencontainers.image.description="AI-augmented API pentesting platform (map, threat-model, test-gen, execute, triage, report)" \ - org.opencontainers.image.source="https://github.com/dmdhrumilmistry/offat-ai" \ + org.opencontainers.image.source="https://github.com/OWASP/OFFAT" \ org.opencontainers.image.licenses="MIT" WORKDIR /app @@ -21,7 +21,7 @@ COPY graybox ./graybox COPY platform ./platform COPY reporter ./reporter COPY examples ./examples -RUN pip install --no-cache-dir ./bundle ./triager ./whitebox ./graybox ./platform ./reporter +RUN pip install --no-cache-dir ./bundle ./triager "./whitebox[graph]" ./graybox ./platform ./reporter # Non-root by default. RUN useradd -m offat diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index e9aefa5..0000000 --- a/docs/Gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source "https://rubygems.org" - -gem "jekyll", "~> 4.3.4" -gem "just-the-docs", "0.12.0" - -# Build/serve helpers used by newer Ruby and Jekyll 4. -gem "webrick", "~> 1.8" diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 0962234..0000000 --- a/docs/_config.yml +++ /dev/null @@ -1,57 +0,0 @@ -title: OWASP OFFAT -description: >- - OFFensive Api Tester - automated API security testing, from an OpenAPI spec - or straight from source. -theme: just-the-docs - -# Pages omit `layout` in their front matter, so apply the theme's layout to all -# of them - without this, Jekyll renders bare HTML with no CSS or navigation. -defaults: - - scope: - path: "" - values: - layout: default - -# Published at https://owasp.github.io/OFFAT/ via GitHub Actions (see -# .github/workflows/pages.yml). The workflow passes --baseurl, but set these -# so local `bundle exec jekyll serve` matches production. -url: https://owasp.github.io -baseurl: /OFFAT - -logo: /assets/offat.png -color_scheme: dark - -search_enabled: true -search: - heading_level: 2 - previews: 3 - button: true - -heading_anchors: true - -aux_links: - GitHub: - - https://github.com/OWASP/OFFAT - PyPI: - - https://pypi.org/project/offat/ -aux_links_new_tab: true - -nav_external_links: - - title: Report an issue - url: https://github.com/OWASP/OFFAT/issues - hide_icon: false - -footer_content: >- - OWASP OFFAT is distributed under the MIT License. Only test systems you own - or are explicitly authorized to assess. - -# These are design/internal docs with repo-relative links; they live in the -# repo for contributors but are not built as site pages. -exclude: - - Gemfile - - Gemfile.lock - - vendor - - architecture.md - - platform.md - - whitebox.md - - graybox.md diff --git a/docs/_theme/app.js b/docs/_theme/app.js new file mode 100644 index 0000000..2a0d9dd --- /dev/null +++ b/docs/_theme/app.js @@ -0,0 +1,240 @@ +/* OWASP OFFAT docs: theme, navigation drawer, search, code blocks, TOC spy. */ +(function () { + "use strict"; + var root = document.documentElement; + var body = document.body; + + /* ---------- theme ---------- */ + function effectiveTheme() { + var t = root.getAttribute("data-theme"); + if (t) return t; + return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } + document.querySelectorAll("[data-toggle-theme]").forEach(function (btn) { + btn.addEventListener("click", function () { + var next = effectiveTheme() === "dark" ? "light" : "dark"; + root.setAttribute("data-theme", next); + try { localStorage.setItem("offat-theme", next); } catch (e) { /* storage unavailable */ } + }); + }); + + /* ---------- navigation drawer (mobile) ---------- */ + var scrim = document.querySelector(".scrim"); + function setNav(open) { + body.classList.toggle("nav-open", open); + if (scrim) scrim.hidden = !open; + } + document.querySelectorAll("[data-open-nav]").forEach(function (el) { + el.addEventListener("click", function () { setNav(true); }); + }); + document.querySelectorAll("[data-close-nav]").forEach(function (el) { + el.addEventListener("click", function () { setNav(false); }); + }); + + /* ---------- clipboard ---------- */ + function copyText(text, btn) { + function done() { + var old = btn.textContent; + btn.textContent = "copied"; + setTimeout(function () { btn.textContent = old; }, 1400); + } + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(done, function () {}); + } else { + var ta = document.createElement("textarea"); + ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; + document.body.appendChild(ta); ta.select(); + try { document.execCommand("copy"); done(); } catch (e) { /* ignore */ } + document.body.removeChild(ta); + } + } + document.querySelectorAll(".cmd").forEach(function (box) { + var btn = box.querySelector(".cmd-copy"); + if (btn) btn.addEventListener("click", function () { copyText(box.getAttribute("data-copy") || "", btn); }); + }); + + /* ---------- code blocks: header with language + copy ---------- */ + document.querySelectorAll(".prose pre").forEach(function (pre) { + var host = pre.parentElement && pre.parentElement.classList.contains("hl") ? pre.parentElement : pre; + if (host.parentElement && host.parentElement.classList.contains("codeblock")) return; + var code = pre.querySelector("code"); + var lang = host.getAttribute("data-lang") || ""; + if (!lang && code) { + var m = (code.className || "").match(/language-([\w+-]+)/); + if (m) lang = m[1]; + } + var wrap = document.createElement("div"); + wrap.className = "codeblock"; + var head = document.createElement("div"); + head.className = "code-head"; + var label = document.createElement("span"); + var dots = document.createElement("span"); + dots.className = "dots"; + for (var d = 0; d < 3; d++) dots.appendChild(document.createElement("i")); + label.appendChild(dots); + label.appendChild(document.createTextNode(lang || "text")); + head.appendChild(label); + var btn = document.createElement("button"); + btn.type = "button"; btn.className = "code-copy"; btn.textContent = "copy"; + btn.addEventListener("click", function () { copyText((code || pre).innerText.replace(/\n$/, ""), btn); }); + head.appendChild(btn); + host.parentNode.insertBefore(wrap, host); + wrap.appendChild(head); + wrap.appendChild(host); + }); + + /* ---------- table of contents scroll spy ---------- */ + var tocLinks = Array.prototype.slice.call(document.querySelectorAll(".toc a[href^='#']")); + if (tocLinks.length && "IntersectionObserver" in window) { + var byId = {}; + tocLinks.forEach(function (a) { byId[decodeURIComponent(a.getAttribute("href").slice(1))] = a; }); + var visible = {}; + var obs = new IntersectionObserver(function (entries) { + entries.forEach(function (e) { visible[e.target.id] = e.isIntersecting; }); + var heads = Object.keys(byId); + var current = null; + for (var i = 0; i < heads.length; i++) { if (visible[heads[i]]) { current = heads[i]; break; } } + if (current) { + tocLinks.forEach(function (a) { a.classList.remove("active"); }); + byId[current].classList.add("active"); + } + }, { rootMargin: "-70px 0px -65% 0px" }); + Object.keys(byId).forEach(function (id) { + var el = document.getElementById(id); + if (el) obs.observe(el); + }); + } + + /* ---------- hero pulse: align with the pipeline rail ---------- */ + function placePulse() { + var flow = document.querySelector(".flow"); + if (!flow) return; + var steps = flow.querySelectorAll(".flow-steps li"); + var pulse = flow.querySelector(".flow-pulse"); + if (!steps.length || !pulse) return; + var fr = flow.getBoundingClientRect(); + var first = steps[0].getBoundingClientRect(); + var last = steps[steps.length - 1].getBoundingClientRect(); + var top = first.top + first.height / 2 - fr.top - 3; + var end = last.top + last.height / 2 - fr.top - 3; + var rail = flow.querySelector(".flow-steps").getBoundingClientRect(); + pulse.style.top = top + "px"; + pulse.style.left = (rail.left - fr.left + 9) + "px"; + pulse.style.setProperty("--travel", (end - top) + "px"); + } + placePulse(); + window.addEventListener("resize", placePulse); + if (document.fonts && document.fonts.ready) document.fonts.ready.then(placePulse); + + /* ---------- search ---------- */ + var modal = document.getElementById("search"); + var input = document.getElementById("search-input"); + var list = document.getElementById("search-results"); + var index = null; + var sel = 0; + + function loadIndex() { + if (index) return Promise.resolve(index); + return fetch("search-index.json").then(function (r) { return r.json(); }).then(function (d) { + index = d; return d; + }).catch(function () { index = []; return index; }); + } + function esc(s) { + return String(s).replace(/[&<>"']/g, function (c) { + return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; + }); + } + function highlight(text, terms) { + var out = esc(text); + terms.forEach(function (t) { + if (!t) return; + var re = new RegExp("(" + t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "ig"); + out = out.replace(re, "$1"); + }); + return out; + } + function snippet(text, term) { + var i = text.toLowerCase().indexOf(term); + if (i < 0) return text.slice(0, 140); + var s = Math.max(0, i - 50); + return (s > 0 ? "… " : "") + text.slice(s, s + 150) + "…"; + } + function search(q) { + var terms = q.toLowerCase().split(/\s+/).filter(Boolean); + if (!terms.length) return []; + var results = []; + index.forEach(function (p) { + var title = p.t.toLowerCase(), text = (p.x || "").toLowerCase(), desc = (p.d || "").toLowerCase(); + var score = 0, all = true; + terms.forEach(function (t) { + var s = 0; + if (title.indexOf(t) >= 0) s += 12; + if (desc.indexOf(t) >= 0) s += 4; + if (text.indexOf(t) >= 0) s += 1 + Math.min(4, text.split(t).length - 1) * 0.5; + if (!s) all = false; + score += s; + }); + if (all && score) results.push({ url: p.u, title: p.t, meta: p.s, snip: snippet(p.x || p.d || "", terms[0]), score: score }); + (p.h || []).forEach(function (h) { + var hn = h.n.toLowerCase(); + if (terms.every(function (t) { return hn.indexOf(t) >= 0; })) { + results.push({ url: p.u + "#" + h.i, title: h.n, meta: p.t, snip: "", score: 10 + terms.length * 3 }); + } + }); + }); + results.sort(function (a, b) { return b.score - a.score; }); + return results.slice(0, 12).map(function (r) { r.terms = terms; return r; }); + } + function render(results, q) { + sel = 0; + if (!q) { list.innerHTML = '
  • Type to search every page and heading.
  • '; return; } + if (!results.length) { list.innerHTML = '
  • No matches for “' + esc(q) + "”.
  • "; return; } + list.innerHTML = results.map(function (r, i) { + return '
  • ' + + '' + highlight(r.title, r.terms) + '' + esc(r.meta) + "" + + (r.snip ? '' + highlight(r.snip, r.terms) + "" : "") + "
  • "; + }).join(""); + } + function moveSel(d) { + var items = list.querySelectorAll("li[role='option']"); + if (!items.length) return; + items[sel].classList.remove("sel"); + sel = (sel + d + items.length) % items.length; + items[sel].classList.add("sel"); + items[sel].scrollIntoView({ block: "nearest" }); + } + function openSearch() { + if (!modal) return; + setNav(false); + modal.hidden = false; + input.value = ""; + render([], ""); + loadIndex(); + setTimeout(function () { input.focus(); }, 10); + } + function closeSearch() { if (modal) modal.hidden = true; } + + document.querySelectorAll("[data-open-search]").forEach(function (b) { b.addEventListener("click", openSearch); }); + if (modal) { + modal.addEventListener("click", function (e) { if (e.target === modal) closeSearch(); }); + input.addEventListener("input", function () { + var q = input.value.trim(); + loadIndex().then(function () { render(search(q), q); }); + }); + input.addEventListener("keydown", function (e) { + if (e.key === "ArrowDown") { e.preventDefault(); moveSel(1); } + else if (e.key === "ArrowUp") { e.preventDefault(); moveSel(-1); } + else if (e.key === "Enter") { + var a = list.querySelector("li.sel a"); + if (a) { window.location.href = a.getAttribute("href"); closeSearch(); } + } + }); + list.addEventListener("click", function (e) { if (e.target.closest("a")) closeSearch(); }); + } + document.addEventListener("keydown", function (e) { + var typing = /input|textarea|select/i.test((e.target && e.target.tagName) || ""); + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); openSearch(); } + else if (e.key === "/" && !typing && modal && modal.hidden) { e.preventDefault(); openSearch(); } + else if (e.key === "Escape") { closeSearch(); setNav(false); } + }); +})(); diff --git a/docs/_theme/favicon.svg b/docs/_theme/favicon.svg new file mode 100644 index 0000000..6162a7e --- /dev/null +++ b/docs/_theme/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/_theme/hero.html b/docs/_theme/hero.html new file mode 100644 index 0000000..dafcd03 --- /dev/null +++ b/docs/_theme/hero.html @@ -0,0 +1,84 @@ +
    +
    +
    +

    OWASP · open source · MIT

    +

    API security testing that reads your code first.

    +

    OFFAT maps your API from source, follows every input across files on a code graph, + runs its test plan with a Rust engine and triages the results with AI — or fully offline.

    + +
    + $offat-platform run ./repo -o app.offat.json + +
    +
    + +
    +
    + pipeline + app.offat.json +
    +
      +
    1. 01Maproutes, params, specs
    2. +
    3. 02Graphcalls · data flow · guards
    4. +
    5. 03Planthreat model → test plan
    6. +
    7. 04ExecuteRust · HTTP/2 · WS · gRPC
    8. +
    9. 05TriageAI or offline heuristic
    10. +
    11. 06ReportSARIF · HTML · compliance
    12. +
    + +
    +
    +
    + +
    +
    +
    91routes mapped in Juice Shop, 4,260 functions in 6.3 s
    +
    16/16planted flows found, zero safe twins flagged
    +
    4protocols in one engine: HTTP/1.1, HTTP/2, WebSocket, gRPC
    +
    0tokens required — every AI path has an offline fallback
    +
    +
    + +
    +
    +
    +

    What you get

    +

    One pipeline, one file, every stage inspectable.

    +
    + +
    +
    diff --git a/docs/_theme/layout.html b/docs/_theme/layout.html new file mode 100644 index 0000000..6c0de80 --- /dev/null +++ b/docs/_theme/layout.html @@ -0,0 +1,117 @@ + + + +{{base}} + + +{{page_title}} + + + + + + + + + + + + + + + + + +
    +
    + + + + OFFAT + OWASP + + +
    + + + + + +
    +
    +
    + +{{hero}} + +
    + + +
    +
    +{{content}} +
    + +

    Edit this page on GitHub

    +
    + + +
    + + + + + + + + diff --git a/docs/_theme/style.css b/docs/_theme/style.css new file mode 100644 index 0000000..4e37968 --- /dev/null +++ b/docs/_theme/style.css @@ -0,0 +1,580 @@ +/* OWASP OFFAT docs theme - "signal trace". + Light tokens on :root, dark via prefers-color-scheme (unless the reader chose + light) or an explicit [data-theme="dark"]. Code blocks stay dark in both. */ + +:root { + --bg: #f6f5f0; + --bg-elev: #ffffff; + --bg-sunk: #eeece4; + --ink: #12161c; + --ink-2: #333c48; + --muted: #687280; + --line: #e3e0d6; + --line-2: #d2cec1; + --accent: #4f7d00; + --accent-fill: #c4f547; + --accent-ink: #0d1300; + --accent-soft: rgba(124, 179, 0, 0.12); + --cyan: #0a7488; + --rose: #c8294a; + --amber: #a65800; + --grid: rgba(18, 22, 28, 0.07); + --glow: rgba(160, 220, 30, 0.18); + --shadow: 0 1px 2px rgba(20, 24, 30, 0.04), 0 12px 32px rgba(20, 24, 30, 0.07); + --code-bg: #0b0f15; + --code-ink: #e3e9f0; + --code-line: #1c2430; + --code-muted: #6f7c8d; + --f-display: "Unbounded", "IBM Plex Sans", system-ui, sans-serif; + --f-sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif; + --f-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; + --top-h: 60px; + --radius: 14px; + color-scheme: light; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg: #07090c; + --bg-elev: #0e1218; + --bg-sunk: #0b0e13; + --ink: #e8edf3; + --ink-2: #b8c1cd; + --muted: #8390a1; + --line: #1a212b; + --line-2: #273141; + --accent: #c6ff3d; + --accent-fill: #c6ff3d; + --accent-ink: #0b1000; + --accent-soft: rgba(198, 255, 61, 0.1); + --cyan: #3fd9f2; + --rose: #ff5a7a; + --amber: #ffb454; + --grid: rgba(255, 255, 255, 0.05); + --glow: rgba(198, 255, 61, 0.12); + --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 18px 40px rgba(0, 0, 0, 0.45); + --code-bg: #090c11; + color-scheme: dark; + } +} + +:root[data-theme="dark"] { + --bg: #07090c; + --bg-elev: #0e1218; + --bg-sunk: #0b0e13; + --ink: #e8edf3; + --ink-2: #b8c1cd; + --muted: #8390a1; + --line: #1a212b; + --line-2: #273141; + --accent: #c6ff3d; + --accent-fill: #c6ff3d; + --accent-ink: #0b1000; + --accent-soft: rgba(198, 255, 61, 0.1); + --cyan: #3fd9f2; + --rose: #ff5a7a; + --amber: #ffb454; + --grid: rgba(255, 255, 255, 0.05); + --glow: rgba(198, 255, 61, 0.12); + --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 18px 40px rgba(0, 0, 0, 0.45); + --code-bg: #090c11; + color-scheme: dark; +} + +/* ---------- base ---------- */ +*, *::before, *::after { box-sizing: border-box; } +html { -webkit-text-size-adjust: 100%; scroll-padding-top: calc(var(--top-h) + 20px); } +body { + margin: 0; + background: var(--bg); + color: var(--ink); + font: 400 16px/1.68 var(--f-sans); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + overflow-x: hidden; +} +img, svg { max-width: 100%; } +a { color: inherit; } +::selection { background: var(--accent-fill); color: var(--accent-ink); } +:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; border-radius: 6px; } +.skip { + position: absolute; left: -999px; top: 8px; z-index: 100; + background: var(--accent-fill); color: var(--accent-ink); padding: 8px 12px; border-radius: 8px; +} +.skip:focus { left: 12px; } + +.backdrop { + position: fixed; inset: 0; z-index: -1; pointer-events: none; + background: + radial-gradient(900px 520px at 85% -10%, var(--glow), transparent 70%), + radial-gradient(circle, var(--grid) 1px, transparent 1.2px) 0 0 / 22px 22px; + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 95%); + mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 95%); +} + +/* ---------- top bar ---------- */ +.topbar { + position: sticky; top: 0; z-index: 40; + height: var(--top-h); + background: color-mix(in srgb, var(--bg) 82%, transparent); + -webkit-backdrop-filter: saturate(140%) blur(14px); + backdrop-filter: saturate(140%) blur(14px); + border-bottom: 1px solid var(--line); +} +.topbar-inner { + max-width: 1440px; height: 100%; margin: 0 auto; padding: 0 24px; + display: flex; align-items: center; gap: 20px; +} +.brand { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; } +.brand-mark { width: 30px; height: 30px; flex: none; } +.bm-bg { fill: var(--ink); } +.bm-path { fill: none; stroke: var(--accent-fill); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } +.bm-node { fill: var(--accent-fill); } +.bm-node.dim { fill: var(--bg-elev); stroke: var(--accent-fill); stroke-width: 1.4; } +.bm-sink { fill: var(--rose); } +.brand-name { font: 700 17px/1 var(--f-display); letter-spacing: 0.02em; } +.brand-tag { + font: 500 10.5px/1 var(--f-mono); letter-spacing: 0.14em; color: var(--muted); + border: 1px solid var(--line-2); border-radius: 999px; padding: 4px 7px; +} +.top-links { display: flex; gap: 4px; margin-left: 12px; } +.top-links a { + text-decoration: none; color: var(--ink-2); font-size: 14.5px; font-weight: 500; + padding: 6px 10px; border-radius: 8px; +} +.top-links a:hover { color: var(--ink); background: var(--bg-sunk); } +.top-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; } +.search-btn { + display: inline-flex; align-items: center; gap: 8px; + height: 36px; padding: 0 10px 0 12px; min-width: 220px; + background: var(--bg-elev); color: var(--muted); + border: 1px solid var(--line-2); border-radius: 10px; + font: 400 14px var(--f-sans); cursor: pointer; +} +.search-btn span { flex: 1; text-align: left; } +.search-btn:hover { border-color: var(--muted); color: var(--ink-2); } +kbd { + font: 500 11px/1 var(--f-mono); color: var(--muted); + border: 1px solid var(--line-2); border-bottom-width: 2px; border-radius: 6px; padding: 3px 6px; + background: var(--bg-sunk); +} +.icon-btn { + display: inline-grid; place-items: center; width: 36px; height: 36px; flex: none; + border: 1px solid transparent; border-radius: 10px; background: transparent; + color: var(--ink-2); cursor: pointer; text-decoration: none; +} +.icon-btn:hover { background: var(--bg-sunk); color: var(--ink); border-color: var(--line); } +.menu-btn { display: none; } +.i-sun { display: none; } +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .i-sun { display: block; } + :root:not([data-theme="light"]) .i-moon { display: none; } +} +:root[data-theme="dark"] .i-sun { display: block; } +:root[data-theme="dark"] .i-moon { display: none; } + +/* ---------- buttons ---------- */ +.btn { + display: inline-flex; align-items: center; gap: 8px; + height: 44px; padding: 0 18px; border-radius: 11px; + font: 600 15px/1 var(--f-sans); text-decoration: none; cursor: pointer; + transition: transform .15s ease, box-shadow .2s ease, border-color .2s ease, background .2s ease; +} +.btn.primary { background: var(--accent-fill); color: var(--accent-ink); border: 1px solid transparent; } +.btn.primary:hover { transform: translateY(-1px); box-shadow: 0 8px 28px var(--glow), 0 0 0 4px var(--accent-soft); } +.btn.ghost { color: var(--ink); border: 1px solid var(--line-2); background: var(--bg-elev); } +.btn.ghost:hover { border-color: var(--ink-2); transform: translateY(-1px); } + +/* ---------- hero (home) ---------- */ +.hero-inner { + max-width: 1240px; margin: 0 auto; padding: 84px 24px 64px; + display: grid; grid-template-columns: minmax(0, 1.12fr) minmax(0, 0.88fr); gap: 64px; align-items: center; +} +.eyebrow { + display: inline-flex; align-items: center; gap: 10px; margin: 0 0 22px; + font: 500 12px/1 var(--f-mono); letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-2); + border: 1px solid var(--line-2); background: var(--bg-elev); border-radius: 999px; padding: 8px 12px; +} +.pulse-dot { + width: 8px; height: 8px; border-radius: 50%; background: var(--accent-fill); + box-shadow: 0 0 0 0 var(--glow); animation: ping 2.4s ease-out infinite; +} +@keyframes ping { + 0% { box-shadow: 0 0 0 0 rgba(198, 255, 61, 0.55); } + 80%, 100% { box-shadow: 0 0 0 10px rgba(198, 255, 61, 0); } +} +.hero-title { + font: 700 clamp(2.3rem, 5.2vw, 4.1rem)/1.04 var(--f-display); + letter-spacing: -0.025em; margin: 0 0 22px; text-wrap: balance; +} +.hero-title em { + font-style: normal; + background: linear-gradient(transparent 62%, var(--accent-fill) 62%, var(--accent-fill) 92%, transparent 92%); + padding: 0 0.06em; -webkit-box-decoration-break: clone; box-decoration-break: clone; + color: var(--ink); +} +:root[data-theme="dark"] .hero-title em { color: var(--accent-ink); background: var(--accent-fill); border-radius: 6px; } +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .hero-title em { color: var(--accent-ink); background: var(--accent-fill); border-radius: 6px; } +} +.hero-lead { font-size: 1.16rem; color: var(--ink-2); max-width: 36rem; margin: 0 0 30px; } +.hero-cta { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 26px; } +.cmd { + display: flex; align-items: center; gap: 10px; max-width: 30rem; + background: var(--code-bg); color: var(--code-ink); border: 1px solid var(--code-line); + border-radius: 12px; padding: 10px 10px 10px 14px; font: 13.5px/1.4 var(--f-mono); +} +.cmd code { flex: 1; overflow-x: auto; white-space: nowrap; scrollbar-width: none; } +.cmd-prompt { color: var(--code-muted); } +.cmd-copy, .code-copy { + flex: none; font: 500 11.5px/1 var(--f-mono); letter-spacing: 0.04em; + color: var(--code-muted); background: transparent; border: 1px solid var(--code-line); + border-radius: 7px; padding: 6px 9px; cursor: pointer; +} +.cmd-copy:hover, .code-copy:hover { color: #c6ff3d; border-color: #c6ff3d; } + +/* pipeline card */ +.flow { + position: relative; margin: 0; + background: var(--bg-elev); border: 1px solid var(--line); border-radius: 20px; + box-shadow: var(--shadow); padding: 18px 22px 22px; +} +.flow::before { + content: ""; position: absolute; inset: -1px; border-radius: 20px; padding: 1px; pointer-events: none; + background: linear-gradient(140deg, var(--accent-fill), transparent 35%, transparent 65%, var(--cyan)); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; mask-composite: exclude; opacity: 0.55; +} +.flow-head { + display: flex; justify-content: space-between; align-items: center; + font: 500 11.5px/1 var(--f-mono); letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); + padding-bottom: 14px; margin-bottom: 8px; border-bottom: 1px dashed var(--line-2); +} +.flow-file { text-transform: none; letter-spacing: 0; color: var(--ink-2); } +.flow-steps { list-style: none; margin: 0; padding: 0 0 0 30px; position: relative; } +.flow-steps::before { + content: ""; position: absolute; left: 9px; top: 22px; bottom: 22px; width: 2px; border-radius: 2px; + background: linear-gradient(var(--cyan), var(--accent-fill)); + opacity: 0.55; +} +.flow-steps li { + position: relative; display: grid; grid-template-columns: 30px 88px minmax(0, 1fr); align-items: baseline; + gap: 6px; padding: 12px 0; border-bottom: 1px solid var(--line); +} +.flow-steps li:last-child { border-bottom: 0; } +.flow-steps li::before { + content: ""; position: absolute; left: -26px; top: 50%; width: 12px; height: 12px; margin-top: -6px; + border-radius: 50%; background: var(--bg-elev); border: 2px solid var(--line-2); + animation: step 6s infinite; +} +.flow-steps li:nth-child(1)::before { animation-delay: 0s; } +.flow-steps li:nth-child(2)::before { animation-delay: 1s; } +.flow-steps li:nth-child(3)::before { animation-delay: 2s; } +.flow-steps li:nth-child(4)::before { animation-delay: 3s; } +.flow-steps li:nth-child(5)::before { animation-delay: 4s; } +.flow-steps li:nth-child(6)::before { animation-delay: 5s; } +@keyframes step { + 0%, 12% { border-color: var(--accent-fill); background: var(--accent-fill); box-shadow: 0 0 0 5px var(--accent-soft); } + 22%, 100% { border-color: var(--line-2); background: var(--bg-elev); box-shadow: none; } +} +.fs-idx { font: 500 12px var(--f-mono); color: var(--muted); } +.fs-name { font: 600 15px var(--f-display); letter-spacing: 0.01em; } +.fs-out { font: 13px var(--f-mono); color: var(--ink-2); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.flow-pulse { + position: absolute; left: 30px; top: 70px; width: 6px; height: 6px; margin-left: -2px; border-radius: 50%; + background: var(--accent-fill); box-shadow: 0 0 12px 3px var(--glow); + animation: travel 6s linear infinite; +} +@keyframes travel { + 0% { transform: translateY(0); opacity: 0; } + 5% { opacity: 1; } + 92% { opacity: 1; } + 100% { transform: translateY(calc(var(--travel, 250px))); opacity: 0; } +} + +/* stats */ +.stats { border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--bg-elev) 55%, transparent); } +.stats-inner { max-width: 1240px; margin: 0 auto; padding: 0 24px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); } +.stat { padding: 28px 22px; border-left: 1px solid var(--line); } +.stat:first-child { border-left: 0; padding-left: 0; } +.stat-n { display: block; font: 700 2.1rem/1.1 var(--f-display); letter-spacing: -0.02em; color: var(--accent); margin-bottom: 6px; } +.stat-l { display: block; font-size: 14px; color: var(--muted); line-height: 1.45; } + +/* features */ +.features-inner { max-width: 1240px; margin: 0 auto; padding: 80px 24px 24px; } +.sec-head { max-width: 46rem; margin-bottom: 34px; } +.sec-kicker { font: 500 12px/1 var(--f-mono); letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent); margin: 0 0 12px; } +.sec-title { font: 700 clamp(1.6rem, 3vw, 2.3rem)/1.15 var(--f-display); letter-spacing: -0.02em; margin: 0; text-wrap: balance; } +.cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } +.card { + position: relative; display: block; text-decoration: none; overflow: hidden; + background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); + padding: 24px 22px 22px; transition: transform .2s ease, border-color .2s ease, box-shadow .2s ease; +} +.card::after { + content: ""; position: absolute; left: 0; top: 0; height: 2px; width: 100%; + background: linear-gradient(90deg, var(--accent-fill), var(--cyan)); + transform: scaleX(0); transform-origin: left; transition: transform .35s ease; +} +.card:hover { transform: translateY(-3px); border-color: var(--line-2); box-shadow: var(--shadow); } +.card:hover::after { transform: scaleX(1); } +.card-idx { font: 500 12px var(--f-mono); color: var(--muted); } +.card h3 { font: 600 1.12rem/1.3 var(--f-sans); margin: 14px 0 8px; } +.card p { margin: 0; color: var(--ink-2); font-size: 15px; line-height: 1.6; } +.card code { font: 13px var(--f-mono); } + +/* ---------- docs shell ---------- */ +.shell { + max-width: 1440px; margin: 0 auto; padding: 0 24px; + display: grid; grid-template-columns: 250px minmax(0, 1fr) 220px; gap: 56px; +} +.sidebar { + position: sticky; top: var(--top-h); align-self: start; + height: calc(100vh - var(--top-h)); overflow-y: auto; padding: 30px 4px 30px 0; + scrollbar-width: thin; +} +.sidebar-head { display: none; } +.nav-group { margin-bottom: 24px; } +.nav-label { + font: 500 11px/1 var(--f-mono); letter-spacing: 0.14em; text-transform: uppercase; + color: var(--muted); margin: 0 0 10px 12px; +} +.nav-group ul { list-style: none; margin: 0; padding: 0; } +.nav-group a { + position: relative; display: block; text-decoration: none; color: var(--ink-2); + font-size: 14.5px; padding: 6px 12px; border-radius: 9px; +} +.nav-group a:hover { background: var(--bg-sunk); color: var(--ink); } +.nav-group a.active { color: var(--ink); background: var(--accent-soft); font-weight: 600; } +.nav-group a.active::before { + content: ""; position: absolute; left: 0; top: 8px; bottom: 8px; width: 3px; border-radius: 3px; + background: var(--accent-fill); +} +.sidebar-foot { border-top: 1px solid var(--line); padding: 16px 12px 0; font-size: 13.5px; } +.sidebar-foot a { color: var(--muted); text-decoration: none; } +.sidebar-foot a:hover { color: var(--ink); } + +.content { padding: 44px 0 80px; min-width: 0; } +.prose { max-width: 760px; } + +.toc { position: sticky; top: var(--top-h); align-self: start; padding: 44px 0 30px; font-size: 13.5px; max-height: calc(100vh - var(--top-h)); overflow-y: auto; } +.toc-title { font: 500 11px/1 var(--f-mono); letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin: 0 0 12px; } +.toc ul { list-style: none; margin: 0; padding: 0; border-left: 1px solid var(--line); } +.toc li a { + display: block; text-decoration: none; color: var(--muted); padding: 4px 0 4px 14px; margin-left: -1px; + border-left: 1px solid transparent; line-height: 1.4; +} +.toc li.l3 a { padding-left: 26px; font-size: 13px; } +.toc li a:hover { color: var(--ink); } +.toc li a.active { color: var(--ink); border-left-color: var(--accent-fill); } +.toc code { font-size: 12px; } + +/* home: no sidebar / toc, centered body under the hero */ +body.home .shell { display: block; max-width: 900px; } +body.home .sidebar, body.home .toc, body.home .pager, body.home .edit { display: none; } +body.home .content { padding-top: 24px; } +body.home .prose { max-width: none; } + +/* ---------- prose ---------- */ +.prose > :first-child { margin-top: 0; } +.prose h1 { font: 700 clamp(1.9rem, 3.4vw, 2.6rem)/1.15 var(--f-display); letter-spacing: -0.02em; margin: 0 0 18px; text-wrap: balance; } +.prose h1 + p { font-size: 1.12rem; color: var(--ink-2); } +.prose h2 { + font: 600 1.55rem/1.3 var(--f-sans); letter-spacing: -0.012em; + margin: 2.4em 0 0.7em; padding-top: 1.1em; border-top: 1px solid var(--line); +} +.prose h3 { font: 600 1.16rem/1.35 var(--f-sans); margin: 1.9em 0 0.5em; } +.prose h2 code, .prose h3 code { font-size: 0.85em; } +.prose p, .prose ul, .prose ol { margin: 0 0 1.05em; } +.prose li { margin: 0.3em 0; } +.prose ul li::marker { color: var(--accent); } +.prose ol li::marker { color: var(--muted); font-family: var(--f-mono); font-size: 0.9em; } +.prose strong { font-weight: 600; color: var(--ink); } +.prose a { + color: var(--ink); text-decoration: underline; text-decoration-color: var(--cyan); + text-decoration-thickness: 1.5px; text-underline-offset: 3px; +} +.prose a:hover { color: var(--cyan); } +.prose a.anchor { + text-decoration: none; color: var(--muted); margin-left: 0.4em; opacity: 0; font-weight: 400; + transition: opacity .15s; +} +.prose h2:hover a.anchor, .prose h3:hover a.anchor, .prose a.anchor:focus { opacity: 1; } +.prose hr { border: 0; border-top: 1px solid var(--line); margin: 2.4em 0; } +.prose :not(pre) > code { + font: 0.86em var(--f-mono); background: var(--bg-sunk); border: 1px solid var(--line); + border-radius: 6px; padding: 0.12em 0.38em; overflow-wrap: anywhere; +} +.prose img { border-radius: 12px; border: 1px solid var(--line); } + +/* code blocks */ +.codeblock { + margin: 0 0 1.3em; background: var(--code-bg); border: 1px solid var(--code-line); + border-radius: 12px; overflow: hidden; +} +.code-head { + display: flex; align-items: center; justify-content: space-between; gap: 10px; + padding: 8px 10px 8px 14px; border-bottom: 1px solid var(--code-line); + font: 500 11.5px/1 var(--f-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--code-muted); +} +.code-head .dots { display: inline-flex; gap: 6px; margin-right: 10px; } +.code-head .dots i { width: 9px; height: 9px; border-radius: 50%; background: var(--code-line); display: block; } +.prose .hl, .prose pre { margin: 0 0 1.3em; } +.codeblock .hl, .codeblock pre { margin: 0; } +.prose pre { + background: var(--code-bg); color: var(--code-ink); border-radius: 12px; border: 1px solid var(--code-line); + padding: 16px 18px; overflow-x: auto; font: 13.5px/1.65 var(--f-mono); tab-size: 2; +} +.codeblock pre { border: 0; border-radius: 0; } +.prose pre code { font: inherit; background: none; border: 0; padding: 0; } + +/* pygments tokens */ +.hl .c, .hl .c1, .hl .cm, .hl .ch, .hl .cs, .hl .cp { color: #6f7c8d; font-style: italic; } +.hl .k, .hl .kd, .hl .kn, .hl .kr, .hl .kc, .hl .kt, .hl .ow { color: #ff7ab2; } +.hl .s, .hl .s1, .hl .s2, .hl .sb, .hl .sd, .hl .sh, .hl .si, .hl .sx, .hl .sa, .hl .dl { color: #b9f26b; } +.hl .se { color: #ffb454; } +.hl .nf, .hl .fm, .hl .nc, .hl .nn { color: #3fd9f2; } +.hl .nb, .hl .bp { color: #9fb6ff; } +.hl .nv, .hl .vi, .hl .vg { color: #ffcf7a; } +.hl .m, .hl .mi, .hl .mf, .hl .mh, .hl .il { color: #ffb454; } +.hl .nt, .hl .na, .hl .py { color: #3fd9f2; } +.hl .o, .hl .p { color: #c3ccd6; } +.hl .gp { color: #6f7c8d; user-select: none; } +.hl .err { color: inherit; background: none; } + +/* tables */ +.table-wrap { overflow-x: auto; margin: 0 0 1.4em; border: 1px solid var(--line); border-radius: 12px; background: var(--bg-elev); } +.prose table { width: 100%; border-collapse: collapse; font-size: 14.5px; line-height: 1.5; } +.prose th { + text-align: left; font: 500 11.5px/1.3 var(--f-mono); letter-spacing: 0.08em; text-transform: uppercase; + color: var(--muted); background: var(--bg-sunk); padding: 11px 14px; border-bottom: 1px solid var(--line); +} +.prose td { padding: 10px 14px; border-top: 1px solid var(--line); vertical-align: top; } +.prose tr:first-child td { border-top: 0; } +.prose tbody tr:hover td { background: color-mix(in srgb, var(--bg-sunk) 55%, transparent); } + +/* callouts */ +.callout { + position: relative; margin: 0 0 1.4em; padding: 14px 18px 14px 18px; + border: 1px solid var(--line); border-left: 3px solid var(--cyan); border-radius: 12px; + background: color-mix(in srgb, var(--cyan) 7%, var(--bg-elev)); +} +.callout::before { + content: "Note"; display: block; margin-bottom: 4px; + font: 500 11px/1 var(--f-mono); letter-spacing: 0.14em; text-transform: uppercase; color: var(--cyan); +} +.callout.warn { border-left-color: var(--rose); background: color-mix(in srgb, var(--rose) 7%, var(--bg-elev)); } +.callout.warn::before { content: "Heads up"; color: var(--rose); } +.callout p:last-child { margin-bottom: 0; } + +/* pager + edit */ +.pager { max-width: 760px; display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 56px; } +.pn { + display: block; text-decoration: none; border: 1px solid var(--line); border-radius: 12px; + padding: 14px 16px; background: var(--bg-elev); font-weight: 600; transition: border-color .2s, transform .2s; +} +.pn span { display: block; font: 500 11px/1 var(--f-mono); letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); margin-bottom: 8px; font-weight: 500; } +.pn.next { text-align: right; grid-column: 2; } +.pn:hover { border-color: var(--accent); transform: translateY(-1px); } +.edit { max-width: 760px; margin: 22px 0 0; font-size: 13.5px; } +.edit a { color: var(--muted); } +.edit a:hover { color: var(--ink); } + +/* 404 */ +.notfound { padding: 40px 0; } +.nf-code { font: 800 clamp(4rem, 12vw, 7rem)/1 var(--f-display); color: var(--accent); margin: 0 0 8px; letter-spacing: -0.04em; } + +/* footer */ +.site-foot { border-top: 1px solid var(--line); margin-top: 40px; } +.foot-inner { + max-width: 1440px; margin: 0 auto; padding: 28px 24px 40px; + display: flex; flex-wrap: wrap; gap: 18px 40px; justify-content: space-between; align-items: flex-start; +} +.foot-brand { font: 700 14px var(--f-display); margin: 0 0 6px; letter-spacing: 0.02em; } +.foot-note, .foot-warn { margin: 0; font-size: 13.5px; color: var(--muted); max-width: 36rem; } +.foot-warn { display: inline-flex; align-items: center; gap: 10px; } +.foot-warn .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--rose); flex: none; } + +/* ---------- search ---------- */ +.search-modal { + position: fixed; inset: 0; z-index: 60; display: flex; justify-content: center; align-items: flex-start; + padding: 12vh 16px 16px; background: rgba(5, 7, 10, 0.55); + -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); +} +.search-modal[hidden] { display: none; } +.search-panel { + width: min(640px, 100%); background: var(--bg-elev); border: 1px solid var(--line-2); + border-radius: 16px; box-shadow: var(--shadow); overflow: hidden; +} +.search-field { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--line); color: var(--muted); } +.search-field input { + flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--ink); + font: 400 17px var(--f-sans); +} +.search-field input::-webkit-search-cancel-button { display: none; } +.search-results { list-style: none; margin: 0; padding: 8px; max-height: min(60vh, 460px); overflow-y: auto; } +.search-results li a { + display: block; text-decoration: none; padding: 10px 12px; border-radius: 10px; color: var(--ink); +} +.search-results li a:hover, .search-results li.sel a { background: var(--accent-soft); } +.sr-title { font-weight: 600; } +.sr-meta { font: 500 11px var(--f-mono); letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); margin-left: 8px; } +.sr-snip { display: block; color: var(--muted); font-size: 13.5px; margin-top: 2px; line-height: 1.45; } +.sr-snip mark { background: var(--accent-fill); color: var(--accent-ink); border-radius: 3px; padding: 0 2px; } +.search-empty { padding: 18px 14px; color: var(--muted); font-size: 14px; } + +.scrim { position: fixed; inset: 0; z-index: 44; background: rgba(5, 7, 10, 0.5); } +.scrim[hidden] { display: none; } + +/* ---------- responsive ---------- */ +@media (max-width: 1180px) { + .shell { grid-template-columns: 230px minmax(0, 1fr); gap: 44px; } + .toc { display: none; } +} +@media (max-width: 1000px) { + .hero-inner { grid-template-columns: minmax(0, 1fr); gap: 40px; padding-top: 56px; } + .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .stats-inner { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .stat:nth-child(3) { border-left: 0; padding-left: 0; } + .stat:nth-child(n+3) { border-top: 1px solid var(--line); } + .top-links { display: none; } +} +@media (max-width: 880px) { + .menu-btn { display: inline-grid; } + .shell { display: block; padding: 0 16px; } + .sidebar { + position: fixed; z-index: 50; top: 0; left: 0; bottom: 0; height: auto; width: min(300px, 86vw); + background: var(--bg-elev); border-right: 1px solid var(--line); padding: 0 12px 24px; + transform: translateX(-102%); transition: transform .25s ease; + } + body.nav-open .sidebar { transform: none; } + .sidebar-head { + display: flex; align-items: center; justify-content: space-between; height: var(--top-h); + margin-bottom: 8px; padding-left: 12px; border-bottom: 1px solid var(--line); + font: 600 15px var(--f-display); + } + body.home .sidebar { display: block; } + .content { padding-top: 28px; } + .topbar-inner { padding: 0 12px 0 8px; gap: 8px; } + .search-btn { min-width: 0; width: 36px; padding: 0; justify-content: center; border-color: transparent; background: transparent; } + .search-btn span, .search-btn kbd { display: none; } +} +@media (max-width: 640px) { + .hero-inner { padding: 40px 16px 44px; } + .stats-inner, .features-inner, .foot-inner { padding-left: 16px; padding-right: 16px; } + .cards { grid-template-columns: minmax(0, 1fr); } + .stats-inner { grid-template-columns: minmax(0, 1fr); } + .stat, .stat:nth-child(3) { border-left: 0; padding-left: 0; } + .stat + .stat { border-top: 1px solid var(--line); } + .flow-steps li { grid-template-columns: 26px 74px minmax(0, 1fr); } + .fs-out { font-size: 12px; } + .pager { grid-template-columns: 1fr; } + .pn.next { grid-column: 1; } + .brand-tag { display: none; } + .prose h2 { font-size: 1.35rem; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation: none !important; transition: none !important; } + .flow-pulse { display: none; } +} diff --git a/docs/ai-roadmap.md b/docs/ai-roadmap.md index 26d8655..0b7cf42 100644 --- a/docs/ai-roadmap.md +++ b/docs/ai-roadmap.md @@ -1,3 +1,10 @@ +--- +title: AI roadmap +section: Deep dives +nav_order: 5 +description: Where AI sits in OFFAT, what the code graph adds, and what comes next. +--- + # OFFAT-AI: code-graph white-box and AI roadmap Status: proposal + first implementation (`whitebox/offat_wb/cpg/`), October 2026. diff --git a/docs/architecture.md b/docs/architecture.md index 4672f8e..cc8472a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,3 +1,10 @@ +--- +title: Architecture +section: Deep dives +nav_order: 1 +description: Shared finding schema, the pipelines, the triager and extension points. +--- + # Architecture OFFAT-AI is a staged platform whose tools share one finding schema, one triager, @@ -56,22 +63,22 @@ binary was not built with is recorded as skipped so the Bundle stays complete. ### White-box pipeline (`whitebox/`, Python) ``` -recon --> hunt --> trace --> chain --> verify (triage) --> report +recon --> code graph + patterns --> chain --> verify (triage / agent) --> report ``` -External tools (`semgrep`, `syft`, `grype`/`trivy`/`osv-scanner`, `graft`) are used -when present; a dependency-free multi-language pattern hunter guarantees output -otherwise. The **trace** stage uses graft (when installed) to bump confidence on -sinks reachable from an entry point; **verify** uses the shared token-disciplined -triager and the reporter emits JUnit plus a `--fail-on` CI gate. +The [code graph](code-graph.md) proves source-to-sink flows and attaches routes; +a dependency-free pattern hunter covers secrets, crypto and misconfiguration. +External tools (`syft`, `grype`/`trivy`/`osv-scanner`, and opt-in `semgrep`) are +used when present. **verify** uses the shared token-disciplined triager and the +reporter emits SARIF code flows, JUnit and a `--fail-on` CI gate. ### Gray-box pipeline (`graybox/`, Python) ``` -recon --> map endpoints (graft) --> hunt --> reachability --> analyze (AI) --> report +recon --> map endpoints --> code graph + patterns --> reachability --> analyze (AI) --> report ``` -Fuses the source view with a graft-mapped endpoint surface and judges each finding +Fuses the source view with the mapped endpoint surface and judges each finding by reachability (which exposed endpoint reaches the sink). No live traffic. AI runs only on reachable findings - reachable-only, batched, cached and model-tiered. @@ -94,8 +101,9 @@ verdict cache plus a cheap-model screen before a strong-model verify. - **New attack vector / test case:** extend `platform.testgen` (vectors matched to params, or a new generator). -- **New SAST rule:** add a pattern to `whitebox/offat_wb/hunt.py`, or install - semgrep for its full ruleset. +- **New source / sink / sanitizer:** pass a `--specs` JSON file, or extend + `whitebox/offat_wb/cpg/specs.py`; pattern-only rules live in + `whitebox/offat_wb/hunt.py`. - **New report format:** add a writer in `reporter/offat_report/render.py`. - **New threat mapping:** extend `triager/offat_triage/taxonomy.py`. - **Different AI backend:** point `OFFAT_AI_BASE_URL` at an Anthropic-compatible diff --git a/docs/build.py b/docs/build.py new file mode 100644 index 0000000..0eaf1c5 --- /dev/null +++ b/docs/build.py @@ -0,0 +1,241 @@ +"""Build the OFFAT documentation site. + + pip install -r docs/requirements.txt + python docs/build.py # -> docs/_site/ + python -m http.server -d docs/_site # preview + +Every ``docs/*.md`` with front matter becomes ``.html``. Front matter keys: +``title``, ``section``, ``nav_order``, ``description``, optional ``nav_title`` +and ``layout: home``. Links to other pages may be written as ``page.md``; +links to anything else in the repository are rewritten to GitHub. +""" + +from __future__ import annotations + +import html +import json +import os +import re +import shutil +import sys +from datetime import date +from typing import Dict, List + +import markdown +from markdown.extensions.toc import slugify + +HERE = os.path.dirname(os.path.abspath(__file__)) +THEME = os.path.join(HERE, "_theme") +OUT = os.path.join(HERE, "_site") +REPO = "https://github.com/OWASP/OFFAT" +BRANCH = "dev" +SITE_URL = os.environ.get("DOCS_SITE_URL", "https://owasp.github.io/OFFAT/") +BASE_PATH = os.environ.get("DOCS_BASE_PATH", "/OFFAT/") +SECTIONS = ["Start", "Concepts", "Integrate", "Deep dives"] + + +def read(path: str) -> str: + with open(path, encoding="utf-8") as fh: + return fh.read() + + +def front_matter(text: str): + if not text.startswith("---\n"): + return {}, text + end = text.index("\n---", 4) + meta = {} + for line in text[4:end].splitlines(): + if ":" in line: + k, v = line.split(":", 1) + meta[k.strip()] = v.strip().strip('"') + return meta, text[end + 4:].lstrip("\n") + + +def render(body: str): + md = markdown.Markdown(extensions=[ + "fenced_code", "tables", "sane_lists", "attr_list", "md_in_html", + "toc", "codehilite", + ], extension_configs={ + "toc": {"permalink": "#", "permalink_class": "anchor", "toc_depth": "2-3", + "slugify": slugify}, + "codehilite": {"css_class": "hl", "guess_lang": False, "pygments_lang_class": True}, + }) + out = md.convert(body) + # Tag each highlighted block with its fence language (opening fences only). + fences = re.findall(r"^\s*(```+)([\w+-]*)", body, flags=re.M) + langs = [lang for i, (_, lang) in enumerate(fences) if i % 2 == 0] + blocks = out.count('
    ') + if blocks == len(langs): + it = iter(langs) + out = re.sub(r'
    ', lambda m: f'
    ', out) + return out, md.toc_tokens + + +def rewrite_links(content: str, pages: Dict[str, Dict]) -> str: + def fix(m: "re.Match[str]") -> str: + href = html.unescape(m.group(2)) + if re.match(r"^(https?:|mailto:|#|//)", href): + return m.group(0) + path, _, anchor = href.partition("#") + frag = f"#{anchor}" if anchor else "" + if path.endswith(".md") and "/" not in path and path in pages: + return f'{m.group(1)}"{pages[path]["url"]}{frag}"' + target = os.path.normpath(os.path.join("docs", path)).replace(os.sep, "/") + kind = "tree" if path.endswith("/") or "." not in os.path.basename(target) else "blob" + return f'{m.group(1)}"{REPO}/{kind}/{BRANCH}/{target}{frag}"' + return re.sub(r'(href=)"([^"]+)"', fix, content) + + +def decorate(content: str) -> str: + # Callouts: every blockquote is a note; "Authorized use only" / "Warning" are warnings. + def callout(m: "re.Match[str]") -> str: + inner = m.group(1) + kind = "warn" if re.search(r"Authorized use only|Warning|Caution", inner) else "note" + return f'
    {inner}
    ' + content = re.sub(r"
    (.*?)
    ", callout, content, flags=re.S) + # Tables scroll horizontally on narrow screens. + content = re.sub(r"", '
    ', content) + content = content.replace("
    ", "
    ") + # External links open in a new tab. + content = re.sub(r' str: + text = re.sub(r"", " ", content, flags=re.S) + text = re.sub(r"<[^>]+>", " ", text) + return re.sub(r"\s+", " ", html.unescape(text)).strip() + + +def headings(tokens: List[Dict]) -> List[Dict]: + """Flatten the h2/h3 tokens (toc_depth 2-3 makes h2 the top level).""" + out = [] + for t in tokens: + out.append(t) + out.extend(t.get("children", [])) + return out + + +def toc_html(tokens: List[Dict]) -> str: + items = [f'
  • {h["name"]}
  • ' + for h in headings(tokens)] + if not items: + return "" + return '

    On this page

      ' + "".join(items) + "
    " + + +def nav_html(ordered: List[Dict], current: str) -> str: + out = [] + for sec in SECTIONS: + group = [p for p in ordered if p["section"] == sec] + if not group: + continue + out.append(f'") + return "".join(out) + + +def fill(tpl: str, values: Dict[str, str]) -> str: + return re.sub(r"\{\{\s*(\w+)\s*\}\}", lambda m: values.get(m.group(1), ""), tpl) + + +def build() -> int: + if os.path.isdir(OUT): + shutil.rmtree(OUT) + os.makedirs(os.path.join(OUT, "assets")) + layout = read(os.path.join(THEME, "layout.html")) + hero = read(os.path.join(THEME, "hero.html")) + + pages: Dict[str, Dict] = {} + for name in sorted(os.listdir(HERE)): + if not name.endswith(".md"): + continue + meta, body = front_matter(read(os.path.join(HERE, name))) + if "title" not in meta: + continue + stem = name[:-3] + pages[name] = { + "name": name, "body": body, "meta": meta, + "url": "./" if stem == "index" else f"{stem}.html", + "file": "index.html" if stem == "index" else f"{stem}.html", + "title": meta["title"], "nav_title": meta.get("nav_title", meta["title"]), + "section": meta.get("section", "Start"), + "order": int(meta.get("nav_order", 99)), + } + ordered = sorted(pages.values(), key=lambda p: (SECTIONS.index(p["section"]) + if p["section"] in SECTIONS else 99, p["order"])) + + search = [] + for i, p in enumerate(ordered): + content, tokens = render(p["body"]) + content = decorate(rewrite_links(content, pages)) + prev_p = ordered[i - 1] if i > 0 else None + next_p = ordered[i + 1] if i + 1 < len(ordered) else None + pn = "" + if prev_p: + pn += (f'') + if next_p: + pn += (f'') + is_home = p["meta"].get("layout") == "home" + values = { + "page_title": "OWASP OFFAT" if is_home else f'{p["title"]} · OWASP OFFAT', + "description": html.escape(p["meta"].get("description", "")), + "nav": nav_html(ordered, p["name"]), + "toc": "" if is_home else toc_html(tokens), + "content": content, + "prev_next": pn, + "edit_url": f"{REPO}/edit/{BRANCH}/docs/{p['name']}", + "body_class": "home" if is_home else "doc", + "hero": hero if is_home else "", + "canonical": SITE_URL + ("" if p["file"] == "index.html" else p["file"]), + "year": str(date.today().year), + "base": "", + } + with open(os.path.join(OUT, p["file"]), "w", encoding="utf-8") as fh: + fh.write(fill(layout, values)) + search.append({ + "t": p["title"], "u": p["url"], "d": p["meta"].get("description", ""), "s": p["section"], + "h": [{"i": h["id"], "n": html.unescape(re.sub(r"<[^>]+>", "", h["name"]))} + for h in headings(tokens)], + "x": plain(content)[:6000], + }) + + # 404 page: served at arbitrary paths, so anchor every relative link to the site base. + nf = fill(layout, { + "page_title": "Not found · OWASP OFFAT", "description": "Page not found", + "nav": nav_html(ordered, ""), "toc": "", "body_class": "doc", + "content": ('

    404

    No route to this page

    ' + '

    The trace went cold. Try the search, or start from the overview.

    ' + '

    Back to the overview

    '), + "prev_next": "", "edit_url": REPO, "hero": "", "canonical": SITE_URL, + "year": str(date.today().year), "base": f'', + }) + with open(os.path.join(OUT, "404.html"), "w", encoding="utf-8") as fh: + fh.write(nf) + + with open(os.path.join(OUT, "search-index.json"), "w", encoding="utf-8") as fh: + json.dump(search, fh, separators=(",", ":")) + with open(os.path.join(OUT, "sitemap.xml"), "w", encoding="utf-8") as fh: + fh.write('\n' + '\n') + for p in ordered: + fh.write(f" {SITE_URL}{'' if p['file'] == 'index.html' else p['file']}\n") + fh.write("\n") + with open(os.path.join(OUT, ".nojekyll"), "w"): + pass + + for f in ("style.css", "app.js", "favicon.svg"): + shutil.copy(os.path.join(THEME, f), os.path.join(OUT, "assets", f)) + for f in os.listdir(os.path.join(HERE, "assets")): + shutil.copy(os.path.join(HERE, "assets", f), os.path.join(OUT, "assets", f)) + print(f"built {len(ordered)} pages -> {os.path.relpath(OUT)}") + return 0 + + +if __name__ == "__main__": + sys.exit(build()) diff --git a/docs/code-graph.md b/docs/code-graph.md new file mode 100644 index 0000000..01eeb8f --- /dev/null +++ b/docs/code-graph.md @@ -0,0 +1,90 @@ +--- +title: Code graph +section: Concepts +nav_order: 2 +description: How OFFAT proves source-to-sink data flows on a code property graph, and how AI builds on it. +--- + +# Code graph + +The white-box core of OFFAT is a code property graph with an interprocedural +taint search. Instead of matching dangerous lines, it proves that attacker +input actually *reaches* a dangerous call, and records the path. + +``` +request field / route param -> locals, helpers, other modules -> sink argument + (source) (proven path) (sink) +``` + +## What goes in the graph + +| Node | Meaning | +|---|---| +| `function` | a function or method, with its route if it is an HTTP handler | +| `param`, `var` | parameters and *versioned* locals (`x = int(x)` really cuts the flow) | +| `field` | attributes on `self`, keyed by class | +| `call`, `arg`, `return` | a call site, its arguments (sinks are marked here), return values | +| `source` | attacker-controlled input: request fields, handler params | + +Edges are `flow` (optionally carrying the classes a sanitizer neutralizes), +`call` / `ret` labelled with their call site, and `calls` for the call graph. + +## Languages and frameworks + +- **Python** (stdlib `ast`): Flask, FastAPI / Starlette, Django / DRF, Tornado, + aiohttp, AWS Lambda; `src/` layouts; nested functions; work handed to job + queues, threads and executors. +- **JavaScript / TypeScript** (tree-sitter): Express-style routers including + handler factories and wrappers, `require` / `import` / `export`, callbacks, + destructured `req`. + +## Precision + +- **Matched calls and returns.** A call-site stack keeps `f(tainted)` from + leaking into an unrelated `f("constant")`. +- **Per-class sanitizers.** `shlex.quote` stops command injection, not SQL + injection. +- **Coerced inputs.** `int`-typed params and `` converters cannot carry + an injection payload, but still count for BOLA. +- **Authorization context.** Object lookups keyed by a client id are reported as + BOLA candidates only when the query is not scoped to the caller and no + ownership check follows. + +## Classes + +SQL and NoSQL injection, command and code injection, SSTI, path traversal, +SSRF, unsafe deserialization, XXE, XSS, open redirect, LDAP injection, ReDoS, +mass assignment (API3) and BOLA candidates (API1). + +Extend the catalog without code: + +```json +{ + "sinks": [{"vclass": "sqli", "cwe": "CWE-89", "names": ["internal.dbx.raw_exec"], "args": [0]}], + "sanitizers": [{"names": ["mylib.clean_html"], "classes": ["xss"]}] +} +``` + +```bash +offat-whitebox ./repo --specs specs.json +``` + +## AI on top of the graph + +- **Graph-grounded triage.** The triager sees the proven hop-by-hop path, the + route and its authorization context instead of a code snippet. +- **Graph-tool agent** (`--agent N`). A verifier with read-only tools + (`get_function`, `callers`, `callees`, `read_lines`, `flows_into`) walks the + code around each path, tries to refute it, and submits a verdict with a proof + request. Repository content is treated as untrusted data. +- **Spec inference** (`--infer-specs`). OFFAT lists the *taint frontier*, the + unknown library calls that receive attacker data, and the model labels each + one as a sink, sanitizer or plain propagator. The result is written to + `inferred-specs.json` and fed back into the next run. + +## Output + +Every finding carries `evidence.path` (each hop's file, line and code), the +entry points and HTTP routes that reach it, and appears in SARIF as a +`codeFlow`, so GitHub code scanning renders the path. `--graph-out` exports the +whole graph as JSON and GraphML for Gephi, yEd or Neo4j. diff --git a/docs/github-action.md b/docs/github-action.md index ba0b858..6700fa2 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -1,45 +1,69 @@ --- title: GitHub Action -nav_order: 6 -description: "Run the classic OFFAT scanner in CI with the OWASP/OFFAT action." +section: Integrate +nav_order: 1 +description: Run OFFAT in CI with the OWASP/OFFAT action and publish results to code scanning. --- # GitHub Action -Run the spec-driven scanner in your CI pipeline with the `OWASP/OFFAT` action. +The `OWASP/OFFAT` action installs the platform, runs the pipeline over your +repository and uploads the reports. By default it is **source-only**: nothing is +sent over the network. -1. Add a repository secret `url` pointing at your OpenAPI/Swagger document. -2. Add a workflow at `.github/workflows/offat.yml`: +## Source-only scan -{% raw %} ```yaml -name: OWASP OFFAT Sample Workflow +name: offat +on: [push, pull_request] -on: - push: - branches: - - dev - - main +permissions: + contents: read + security-events: write # only for upload_sarif jobs: - test: + offat: runs-on: ubuntu-latest steps: - - name: Download swagger/OAS file - run: curl ${url} -o /tmp/swagger.json + - uses: actions/checkout@v4 + - uses: OWASP/OFFAT@dev + with: + path: . + fail_on: high + upload_sarif: 'true' env: - url: ${{ secrets.url }} + OFFAT_AI_API_KEY: ${{ secrets.OFFAT_AI_API_KEY }} # optional: AI triage +``` + +Findings appear in **Security → Code scanning**, with each proven source-to-sink +path rendered as a code flow. + +## Live scan of a staging deployment - - name: OWASP OFFAT CICD Scanner - uses: OWASP/OFFAT@main # or pin a version, e.g. OWASP/OFFAT@v0.17.3 +A live run needs both `url` and `authorized: 'true'`. The action refuses a URL +without the explicit confirmation, mirroring the CLI's `--yes` gate. + +```yaml + - uses: OWASP/OFFAT@dev with: - file: /tmp/swagger.json # or ${{ secrets.url }} - rate_limit: 120 - artifact_retention_days: 1 + path: . + url: https://staging.api.example.test + authorized: 'true' + identities: .github/offat-identities.json + fail_on: high ``` -{% endraw %} -> Prefer pinning the action to a specific version (`OWASP/OFFAT@v0.17.3`) rather -> than `@main`, and bump it after testing. +Only point it at environments you own or are explicitly authorized to test. + +## Inputs -Only run this against an API you own or are explicitly authorized to test. +| Input | Default | Description | +|---|---|---| +| `path` | `.` | source tree and/or OpenAPI spec to analyze | +| `url` | empty | base URL of an authorized target; empty = source-only | +| `authorized` | `false` | must be `'true'` for live runs | +| `identities` | empty | identities JSON for access-control checks | +| `fail_on` | empty | fail on findings at or above `critical`, `high`, `medium` or `low` | +| `formats` | `all` | `sarif,md,html,compliance,pdf` or `all` | +| `upload_sarif` | `false` | upload `results.sarif` to code scanning | +| `artifact_retention_days` | `7` | retention for the `offat-reports` artifact | diff --git a/docs/graybox.md b/docs/graybox.md index 10ba72f..e0aff9b 100644 --- a/docs/graybox.md +++ b/docs/graybox.md @@ -1,13 +1,19 @@ -# Gray-box (`offat-graybox`) +--- +title: Gray-box design +section: Deep dives +nav_order: 4 +description: offat-graybox - SAST fused with the endpoint attack surface and judged by reachability. +--- -Gray-box fuses the **white-box** view (source, data-flow sinks) with a -**black-box** view (the HTTP endpoint attack surface, mapped from source with -[graft](https://github.com/NanoNets/Graft)) and analyzes them together with AI. -It sends **no live traffic** - reachability is established statically from the -call graph. +# Gray-box design (`offat-graybox`) + +Gray-box fuses the **white-box** view (source, proven data flows) with a +**black-box** view (the HTTP endpoint attack surface, mapped from source) and +analyzes them together with AI. It sends **no live traffic** - reachability is +established statically from the code graph. ``` -recon -> map endpoints (graft) -> hunt (SAST) -> reachability -> analyze (AI) -> report +recon -> map endpoints -> code graph + patterns -> reachability -> analyze (AI) -> report ``` ## The idea @@ -22,7 +28,7 @@ capped at inconclusive. ## Run ```bash -pip install ./triager ./whitebox ./graybox +pip install ./triager './whitebox[graph]' ./graybox offat-graybox /path/to/repo # AI when a provider is available offat-graybox ./repo --no-ai # offline heuristic only offat-graybox ./repo --provider claude-code # drive a local agent CLI @@ -35,27 +41,25 @@ offat-graybox ./repo --fail-on high # CI gate on reachable high+ finding | `-o, --out` | output directory (default `offat-report/graybox`) | | `--no-ai` | heuristic triage only (no tokens) | | `--provider` | `auto` (default), `anthropic`, `claude-code`, `codex`, `heuristic` | -| `--no-semgrep` | skip semgrep even if installed | +| `--no-graph` | skip the code graph (falls back to graft / same-file reachability) | +| `--semgrep` | also run semgrep (opt-in) | | `--no-graft` | map endpoints natively instead of via graft | | `--no-cache` | do not read/write the AI-verdict cache | | `--fail-on` | exit non-zero if an actionable finding is at/above this severity | ## Endpoint mapping -graft builds a structural graph for free (tree-sitter, no API key, no LLM -tokens); the pipeline queries it and confirms each route with a native parser so -output is deterministic. Supported route forms: Flask/FastAPI decorators, Django +A native parser maps every route deterministically. Supported route forms: Flask/FastAPI decorators, Django `path()`, Express `app/router.verb()`, Spring `@*Mapping`, Go routers (`GET/POST/Handle/HandleFunc`), Rails `routes.rb`, **and OpenAPI/Swagger specs shipped in the repo** (YAML or JSON - covers spec-driven frameworks like -connexion, where the `operationId` becomes the handler). When graft is absent the -native parser runs alone. +connexion, where the `operationId` becomes the handler). -Reachability links a finding to an endpoint three ways, in order: the finding's -enclosing function is an endpoint handler / `operationId` (works offline and for -spec-driven apps); the graft call graph connects the sink's symbol up to a -handler; or, as a conservative fallback, the sink shares a file with a route. -Each finding records which method established reachability. +Reachability comes from the [code graph](code-graph.md): a proven flow carries +the routes that reach it, and every pattern hit is linked through its enclosing +function's callers up to the route handlers. Dead code stays unreachable instead +of being promoted because it shares a file with a route. With `--no-graph` the +older heuristics (handler-name match, graft callers, same-file) are used. ## Token discipline @@ -67,7 +71,7 @@ AI analysis is the only token-spending stage, and it is kept cheap by design: | Batched + tiered | a cheap model screens in batches; a strong model verifies only the confirmed/likely subset | | Cached | verdicts keyed on a content hash; re-runs pay only for new/changed findings | -Tune with `OFFAT_GB_SCREEN_MODEL`, `OFFAT_GB_VERIFY_MODEL`, `OFFAT_GB_BATCH_SIZE`, +Tune with `OFFAT_SCREEN_MODEL`, `OFFAT_VERIFY_MODEL`, `OFFAT_BATCH_SIZE`, or `--no-cache`. Every AI path degrades to a deterministic heuristic, so a run never breaks and never requires a key. diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 692e2a3..ccc78ca 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -1,19 +1,11 @@ --- title: How it works -nav_order: 4 -description: "The OFFAT-AI platform pipeline, the Bundle contract, and the Rust engine." +section: Concepts +nav_order: 1 +description: The pipeline, the Bundle contract and the Rust execution engine. --- # How it works -{: .no_toc } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- OFFAT-AI is a staged platform. Every stage reads and writes one canonical document - the **OFFAT Bundle** (`*.offat.json`) - so the pipeline composes @@ -34,8 +26,8 @@ cleanly and each stage is a pure transform over the file. ## The stages 1. **Map** - parse source and any OpenAPI/Swagger spec into endpoints (with - params and response fields) and a sink/source inventory; build the structural - graph with `graft` when present. + params and response fields) and a sink/source inventory, backed by the + [code graph](code-graph.md). 2. **Parameter relation graph (PRG)** - link producers (an endpoint's response fields) to consumers (another endpoint's request params) by name, resource, or type, so cases can chain across endpoints and replay a foreign id for BOLA. @@ -96,10 +88,11 @@ handshake probe and message-content injection; gRPC methods get an unauthenticated-invocation probe and an unprivileged BFLA invocation judged by the returned `grpc-status`. -**Source (white-box / gray-box):** hardcoded secrets, injection sinks, unsafe -deserialization, weak crypto / TLS-off, DOM XSS sinks, SSRF sinks, misconfig, and -vulnerable dependencies (SCA), plus anything `semgrep --config auto` finds. See -[Source modes](source-modes.html). +**Source (white-box / gray-box):** proven source-to-sink flows for injection, +SSRF, path traversal, deserialization, SSTI, XSS, open redirect, ReDoS, mass +assignment and BOLA candidates (see [Code graph](code-graph.md)), plus hardcoded +secrets, weak crypto / TLS-off, misconfiguration and vulnerable dependencies +(SCA). See [Source modes](source-modes.md). ## Threat mapping @@ -110,7 +103,4 @@ OWASP API Top 10 with ASVS notes. ## Design docs -For contributors, the full design and the shared finding schema live in the repo: - -- [Platform design](https://github.com/OWASP/OFFAT/blob/dev/docs/platform.md) -- [Architecture](https://github.com/OWASP/OFFAT/blob/dev/docs/architecture.md) +For contributors: [Platform design](platform.md) and [Architecture](architecture.md). diff --git a/docs/index.md b/docs/index.md index fb5736d..cf77098 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,73 +1,32 @@ --- -title: Home +title: OWASP OFFAT +section: Start nav_order: 1 -description: "OWASP OFFAT (OFFensive Api Tester) - automated API security testing from an OpenAPI spec or straight from source." -permalink: / +nav_title: Overview +layout: home +description: Source-aware API security testing. Map your API from code, prove source-to-sink flows on a code graph, execute with a Rust engine, triage with AI. --- -# OWASP OFFAT -{: .fs-9 } - -OFFensive Api Tester. Automatically test your API for common vulnerabilities - -from an OpenAPI/Swagger spec, or straight from your source code. -{: .fs-6 .fw-300 } - -[Get started](installation.html){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } -[View on GitHub](https://github.com/OWASP/OFFAT){: .btn .fs-5 .mb-4 .mb-md-0 } - ---- - -OWASP OFFAT generates security tests from an API description and runs them for -you - fuzzing inputs and checking common OWASP API Top 10 weaknesses - and now -also maps and tests an API straight from its source code. - -There are two ways to use it: - -## 1. Classic CLI - spec-driven black-box testing - -Point `offat` at an OpenAPI/Swagger file or URL and it generates and runs tests -automatically. +## Quick start ```bash -pip install offat -offat -f swagger_file.json -``` +git clone https://github.com/OWASP/OFFAT.git && cd OFFAT +pip install ./bundle ./triager './whitebox[graph]' ./graybox ./platform ./reporter -Checks: restricted HTTP methods, SQLi, BOLA, data exposure, BOPLA / mass -assignment, broken access control, basic command injection, and basic XSS / -HTML injection. See [Usage](usage.html#classic-cli) and the -[GitHub Action](github-action.html) for CI. - -## 2. OFFAT-AI platform - source-aware testing - -A staged pipeline that maps the attack surface from source (decorator routes -**and** OpenAPI specs), models threats, generates tests, executes them with a -Rust engine (HTTP/1.1 + HTTP/2, WebSocket, gRPC), and triages the findings - all -over one canonical Bundle file. - -```bash -pip install ./bundle ./triager ./whitebox ./graybox ./platform ./reporter -make engine -offat-platform run /path/to/repo --url https://api.example.test --yes -o app.offat.json +# source-only: maps, plans, triages and reports without sending traffic +offat-platform run /path/to/repo -o app.offat.json --report-dir reports ``` -See [Usage](usage.html#offat-ai-platform) and [How it works](how-it-works.html). Two -source-only modes - [white-box and gray-box](source-modes.html) - need no live -traffic. - ---- - -## Demo - -[![asciicast](https://asciinema.org/a/9MSwl7UafIVT3iJn13OcvWXeF.svg)](https://asciinema.org/a/9MSwl7UafIVT3iJn13OcvWXeF) - -## Authorized use only -{: .text-red-200 } +Building the [Rust engine](installation.md#build-the-execution-engine) adds +live execution against targets you are authorized to test. -OFFAT sends real requests to the target you point it at. The platform's -execution engine sends active attack traffic and requires an explicit `--yes`. -Only test systems and code you own or are explicitly authorized to assess. +## Where to go next -## License +- [Installation](installation.md) - packages, engine, container, AI backends +- [Usage](usage.md) - the pipeline, stage by stage, identities and CI gates +- [How it works](how-it-works.md) - the Bundle contract and the engine +- [Code graph](code-graph.md) - taint analysis and the AI built on it +- [GitHub Action](github-action.md) - scan every push, publish to code scanning -OWASP OFFAT is distributed under the [MIT License](https://github.com/OWASP/OFFAT/blob/dev/LICENSE.md). +> **Authorized use only.** Live runs send real requests and require an explicit +> `--yes`. Only test systems and code you own or are authorized to assess. diff --git a/docs/installation.md b/docs/installation.md index cdb54d9..e843af1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,98 +1,63 @@ --- title: Installation +section: Start nav_order: 2 -description: "How to install the OFFAT CLI and the OFFAT-AI platform." +description: Install the OFFAT platform, the code-graph extra and the Rust execution engine. --- # Installation -{: .no_toc } -## Table of contents -{: .no_toc .text-delta } +OFFAT is a set of Python packages that share one data contract (the Bundle), +plus a Rust execution engine. The Python side runs anywhere; the engine is only +needed when you send live traffic to a target. -1. TOC -{:toc} +## Prerequisites ---- - -## Classic CLI (`offat`) - -### Using pip - -```bash -# from PyPI -python3 -m pip install offat # CLI only -python3 -m pip install offat[api] # CLI + REST API - -# or the latest from the default branch -python3 -m pip install git+https://github.com/OWASP/OFFAT.git -``` - -> On Termux or Windows use `pip` instead of `pip3`. A few features are -> Linux-only and may need administrator privileges. - -### Using Docker - -```bash -# CLI -docker run --rm dmdhrumilmistry/offat - -# API (POST an OpenAPI doc to /api/v1/scan/ with its type: json|yaml) -docker compose up -d -``` +| Need | For | +|---|---| +| Python 3.10+ | every stage | +| Rust 1.88+ | building the execution engine (live runs only) | +| `syft`, `grype`, `trivy` or `osv-scanner` | optional SBOM / dependency CVEs | -### From source (Poetry) +## Install the packages ```bash -sudo apt install git python3 -y # prerequisites git clone https://github.com/OWASP/OFFAT.git -cd OFFAT/src -poetry install +cd OFFAT +pip install ./bundle ./triager './whitebox[graph]' ./graybox ./platform ./reporter ``` -See [Usage - Classic CLI](usage.html#classic-cli) next. - ---- +This puts five CLIs on your `PATH`: `offat-platform`, `offat-whitebox`, +`offat-graybox`, `offat-triage` and `offat-report`. -## OFFAT-AI platform +The `[graph]` extra installs the code-graph taint engine's dependencies +(`networkx` for the graph, `tree-sitter` for JavaScript/TypeScript). Without it +the white-box pipeline falls back to its pattern hunter and records why. -The platform is a set of Python packages plus a Rust execution engine. The -Python side runs anywhere; the engine is only needed to send live traffic. - -### Prerequisites - -- Python 3.10+ -- Rust 1.88+ (only to build the execution engine) -- Optional: `graft`, `semgrep`, and an SBOM/CVE tool (`syft`, `grype`, `trivy`, - or `osv-scanner`) for richer source analysis - each is used when present. - -### Install the Python packages +## Build the execution engine ```bash -git clone https://github.com/OWASP/OFFAT.git -cd OFFAT -pip install ./bundle ./triager ./whitebox ./graybox ./platform ./reporter +make engine # -> engine-rs/target/release/offat-engine +make engine-http3 # same, with the opt-in HTTP/3 + QUIC transport ``` -This installs the CLIs: `offat-platform`, `offat-whitebox`, `offat-graybox`, -`offat-triage`, and `offat-report`. +`offat-platform run` finds the engine on `PATH`, at `$OFFAT_ENGINE`, or in +`engine-rs/target/{release,debug}/`. Source-only runs never need it. -### Build the execution engine (Rust) +## Container ```bash -make engine # -> engine-rs/target/release/offat-engine -# or with the opt-in HTTP/3 + QUIC transport: -make engine-http3 +make docker # builds offat:latest from docker/Dockerfile +docker run --rm -v "$PWD:/src" offat:latest run /src -o /src/app.offat.json ``` -`offat-platform run` auto-finds the engine on `PATH`, at `$OFFAT_ENGINE`, or in -`engine-rs/target/{release,debug}/`. You only need the engine for live runs -(`--url`); source-only runs and the white-box / gray-box modes do not. +The image contains the engine and every Python package, and runs as a non-root +user. -### Optional: AI triage backend +## AI backends (optional) -Triage works fully offline with a deterministic heuristic. To enable AI triage, -provide one of: +Everything works offline with a deterministic heuristic. To enable AI triage, +the graph-tool agent and spec inference, provide one of: | Provider | Needs | |---|---| @@ -100,4 +65,4 @@ provide one of: | `claude-code` | the `claude` CLI, already signed in | | `codex` | the `codex` CLI, already signed in | -See [Usage - OFFAT-AI platform](usage.html#offat-ai-platform). +Next: [Usage](usage.md). diff --git a/docs/platform.md b/docs/platform.md index 3ebabd2..31cabab 100644 --- a/docs/platform.md +++ b/docs/platform.md @@ -1,3 +1,10 @@ +--- +title: Platform design +section: Deep dives +nav_order: 2 +description: The staged OFFAT platform and the Bundle contract that holds it together. +--- + # OFFAT-AI Platform (v2) - design This document describes the staged platform OFFAT-AI is evolving into, and the @@ -23,7 +30,7 @@ visualizer, and the "single file" handed between stages. ``` source ─┐ - spec ─┤ 1. MAP (graft + AI + spec) -> ASM: endpoints, params, sinks, sources, auth, protocols + spec ─┤ 1. MAP (code graph + spec) -> ASM: endpoints, params, sinks, sources, auth, protocols traffic─┘ 2. PARAM RELATION GRAPH (PRG) -> producer->consumer param edges (X output feeds Y input) 3. THREAT MODEL (STRIDE + OWASP/ASVS) -> DFD, trust boundaries, ranked threats @@ -100,7 +107,7 @@ the authorized baseline. An implicit `anon` identity drives missing-auth tests. | # | Component | Language | Reuses | |---|---|---|---| -| 1 | Mapping | Python | `offat_gb.graph_map` + graft + spec parsing + AI classification | +| 1 | Mapping | Python | `offat_gb.graph_map` + code graph (`offat_wb.cpg`) + spec parsing | | 2 | PRG builder | Python | producer->consumer name/resource/type heuristics | | 3 | Threat modeling | Python | `offat_triage.taxonomy` (OWASP API/Web + CWE) | | 4 | Test-case generation | Python | KB vectors + `offat_triage` batched/cached AI | diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..06c36d7 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +markdown==3.9 +pygments==2.19.2 diff --git a/docs/source-modes.md b/docs/source-modes.md index 3e67e58..3cce63d 100644 --- a/docs/source-modes.md +++ b/docs/source-modes.md @@ -1,77 +1,64 @@ --- title: Source modes -nav_order: 5 -description: "White-box (SAST) and gray-box (SAST + reachability) - no live traffic." +section: Concepts +nav_order: 3 +description: White-box and gray-box analysis of code you own, with no live traffic. --- # Source modes -{: .no_toc } -## Table of contents -{: .no_toc .text-delta } +Two modes analyze code you own without sending any traffic. Both share the +platform's code graph, triager and report writers. -1. TOC -{:toc} - ---- - -Two modes analyze code you own without sending any live traffic. Both reuse the -same libraries, triager, and reporters as the live platform. - -## White-box (SAST) +## White-box ```bash -offat-whitebox /path/to/your/repo --no-ai # heuristic; drop --no-ai for AI triage -offat-whitebox /path/to/your/repo --fail-on high +offat-whitebox /path/to/your/repo # AI triage when a provider is available +offat-whitebox /path/to/your/repo --no-ai # offline heuristic only +offat-whitebox /path/to/your/repo --fail-on high # CI gate ``` -Pipeline: - ``` -recon -> hunt -> trace -> chain -> verify (triage) -> report +recon -> code graph + patterns -> chain -> verify (triage / agent) -> report ``` -- **recon / hunt** - find sinks and sources with a dependency-free, - multi-language pattern hunter, plus `semgrep --config auto` when installed. -- **trace** - use `graft` (when installed) to raise confidence on sinks reachable - from an entry point. -- **verify** - the shared token-disciplined triager assigns a verdict, CVSS, and - remediation. -- **report** - findings plus JUnit and a `--fail-on` CI gate. +- **recon** - detect the stack, inventory manifests, generate an SBOM and + dependency CVEs when `syft` / `grype` / `trivy` / `osv-scanner` are installed. +- **code graph** - prove source-to-sink flows and attach routes + ([details](code-graph.md)). +- **patterns** - secrets, weak crypto, TLS-off and misconfiguration. Injection + hits the graph cannot connect to a source are kept, but demoted. +- **verify** - the shared triager assigns verdict, CVSS and remediation; + `--agent N` re-verifies the top findings with graph tools. +- **report** - JSON, JSONL, SARIF (with code flows), Markdown, HTML and JUnit. -Catches: hardcoded secrets, injection sinks, unsafe deserialization, weak crypto -/ TLS-off, DOM XSS sinks, SSRF sinks, misconfiguration, and vulnerable -dependencies (SCA). +semgrep is no longer part of the default pipeline; pass `--semgrep` to add it. -## Gray-box (SAST + reachability) +## Gray-box ```bash offat-graybox /path/to/your/repo --fail-on high -offat-graybox /path/to/your/repo --no-ai -o out/report # offline +offat-graybox /path/to/your/repo --no-ai -o out/report ``` -Pipeline: - ``` -recon -> map endpoints (graft) -> hunt -> reachability -> analyze (AI) -> report +recon -> map endpoints -> code graph + patterns -> reachability -> analyze (AI) -> report ``` -Gray-box fuses the source view with a `graft`-mapped endpoint surface and judges -each finding by **reachability** - which exposed endpoint reaches the sink. AI -runs only on reachable findings, batched, cached, and model-tiered, so it stays -token-disciplined. No live traffic is sent. +Gray-box judges every finding by **reachability**: which exposed endpoint +reaches it through the call graph. Only reachable findings are sent to a model, +batched, cached and model-tiered; unreachable ones are triaged offline at zero +token cost. -## When to use which +## Which mode | Mode | Live traffic | Best for | |---|:--:|---| -| Classic CLI | yes | A running API with an OpenAPI/Swagger spec | -| Platform `run --url` | yes | Deep, source-aware testing of an API you can reach | -| Platform `run` (no `--url`) | no | Source-only pass, SAST findings only | -| White-box | no | Fast SAST over any codebase | -| Gray-box | no | SAST prioritized by endpoint reachability | +| `offat-platform run --url` | yes | deep, source-aware testing of an API you are authorized to test | +| `offat-platform run` | no | the full pipeline as a planning and SAST pass | +| `offat-whitebox` | no | fast SAST over any codebase | +| `offat-graybox` | no | SAST prioritized by endpoint reachability | ## Deep dives -- [White-box design](https://github.com/OWASP/OFFAT/blob/dev/docs/whitebox.md) -- [Gray-box design](https://github.com/OWASP/OFFAT/blob/dev/docs/graybox.md) +[White-box design](whitebox.md) · [Gray-box design](graybox.md) diff --git a/docs/usage.md b/docs/usage.md index d5f7045..000ac6e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,111 +1,17 @@ --- title: Usage +section: Start nav_order: 3 -description: "Running the OFFAT CLI and the OFFAT-AI platform pipeline." +description: Run the full pipeline, individual stages, and the source-only modes. --- # Usage -{: .no_toc } -## Table of contents -{: .no_toc .text-delta } +> **Authorized use only.** `--url` sends real requests to the target and +> requires `--yes`. Only test systems and code you own or are explicitly +> authorized to assess. -1. TOC -{:toc} - ---- - -## Authorization -{: .text-red-200 } - -OFFAT sends real requests to the target. The platform engine sends active attack -traffic and requires `--yes`. Only test systems and code you own or are -explicitly authorized to assess. - ---- - -## Classic CLI - -Generate and run tests from an OpenAPI/Swagger file or URL. - -```bash -offat -f swagger_file.json # from a file -offat -f https://example.com/docs.json # from a URL -offat -h # all options -``` - -### Output formats - -```bash -offat -f swagger_file.json -o output.json -of json # json (default) -offat -f swagger_file.json -o output.html -of html # html -offat -f swagger_file.json -o output.yaml -of yaml # yaml -``` - -### Common options - -```bash -# only test endpoint paths matching a regex -offat -f swagger_file.json -pr '/user' - -# add request headers (auth, content type, ...) -offat -f swagger_file.json -H 'Authorization: Bearer ' -H 'Accept: application/json' - -# rate-limit requests (per second) -offat -f swagger_file.json -rl 1000 - -# route through a proxy (SSL checks off by default; add --ssl to enforce) -offat -f swagger_file.json -p http://localhost:8080 -o out.json -of json -``` - -### User-provided test data - -Drive tests with your own actors, headers, and inputs (also powers broken -access control via `unauthorized_endpoints`): - -```bash -offat -f swagger_file.json -tdc test_data_config.yaml -``` - -```yaml -actors: - - actor1: - request_headers: - - name: Authorization - value: Bearer - query: - - name: id - value: 145 - type: int - unauthorized_endpoints: # for broken access control - - "/store/order/.*" - - actor2: - request_headers: - - name: Authorization - value: Bearer -``` - -### REST API - -```bash -offat-api # or: python -m offat.api -# docs at http://localhost:8000/docs -``` - ---- - -## OFFAT-AI platform - -The platform runs a staged pipeline over one canonical -[Bundle](https://github.com/OWASP/OFFAT/tree/dev/bundle) file: - -``` -map -> parameter relation graph -> threat model -> test-gen -> execute -> triage -> report -``` - -See [How it works](how-it-works.html) for what each stage does. - -### The whole pipeline in one command +## The whole pipeline ```bash offat-platform run /path/to/your/repo \ @@ -114,71 +20,76 @@ offat-platform run /path/to/your/repo \ --yes -o app.offat.json --report-dir reports --fail-on high ``` -This writes `app.offat.json` and a full `reports/` set (SARIF, Markdown, HTML, -compliance, best-effort PDF). `--url` sends real traffic, so it requires -`--yes`. **Omit `--url` for a source-only run** (map -> test-gen -> triage -> -report with SAST findings only). +This writes `app.offat.json` (the Bundle) and a `reports/` directory with SARIF, +Markdown, HTML, an OWASP API Top 10 compliance report and a best-effort PDF. + +**Omit `--url` for a source-only run.** Mapping, test planning, triage and +reporting still happen; nothing is sent over the network. -### Or run the stages individually +## Stage by stage ```bash -# 1. map the attack surface + PRG + threat model (from source and/or a spec) +# 1. map the attack surface, parameter relation graph and threat model offat-platform map /path/to/your/repo --threat-model -o app.offat.json -# 2. generate the test plan (add --identities to enable BOLA/BFLA/RBAC/auth tests) +# 2. plan tests (identities unlock BOLA / BFLA / RBAC / auth cases) offat-platform test-gen app.offat.json --identities examples/identities.example.json -# 3. execute against an authorized target (sends real traffic; --yes required) +# 3. execute against an authorized target with the Rust engine offat-engine --bundle app.offat.json --url https://api.your-authorized-target.test \ --identities examples/identities.example.json --yes -# 4. consolidate + triage, then render reports +# 4. consolidate and triage, then render reports offat-platform triage app.offat.json --fail-on high offat-report app.offat.json -o reports --formats all ``` -### Identities (access-control and auth tests) +Every stage reads and writes the same Bundle, so you can stop, inspect, edit and +resume anywhere. `offat-platform validate app.offat.json` checks it against the +schema. -`--identities` points at a JSON file where each identity has a name, role, -request `headers` (its credentials), and optional `owns` (object ids it holds). -This unlocks **BOLA (API1)**, **broken authentication (API2)**, **BFLA and RBAC -(API5)**, and **business-logic (API6)** tests. Credentials stay in that file and -never enter the Bundle. See -[`examples/identities.example.json`](https://github.com/OWASP/OFFAT/blob/dev/examples/identities.example.json). +## Identities -### Source-only modes +`--identities` points at a JSON file. Each identity has a name, a role, request +`headers` (its credentials) and optionally `owns` (object ids it holds). With two +or more identities OFFAT plans **BOLA (API1)**, **broken authentication (API2)**, +**BFLA and RBAC (API5)** and **business-logic (API6)** cases, and the engine runs +a differential baseline for each. Credentials stay in that file and never enter +the Bundle. See +[`examples/identities.example.json`](../examples/identities.example.json). -No live traffic - analyze code you own: +## Source-only modes ```bash -offat-whitebox /path/to/your/repo --no-ai # SAST; drop --no-ai for AI triage -offat-graybox /path/to/your/repo --fail-on high # SAST + endpoint reachability +offat-whitebox /path/to/repo # code-graph SAST + AI triage +offat-whitebox /path/to/repo --agent 10 # graph-tool agent re-verifies the top 10 +offat-whitebox /path/to/repo --infer-specs # AI labels unknown sinks / sanitizers +offat-whitebox /path/to/repo --graph-out # export the code graph (JSON + GraphML) +offat-graybox /path/to/repo --fail-on high # SAST prioritized by endpoint reachability ``` -See [Source modes](source-modes.html). +See [Code graph](code-graph.md) and [Source modes](source-modes.md). -### Re-triage and visualize +## AI triage + +The triager picks a backend automatically (`auto`: API key, then Claude Code, +then Codex, then the offline heuristic), or force one with `--provider` / +`OFFAT_AI_PROVIDER`. Verdicts are cached by content hash, screened in batches by +a fast model, and only the confirmed / likely subset is re-verified by a strong +model. ```bash -offat-triage findings.json --provider claude-code # add/refresh verdicts -python -m http.server -d viz 8080 # open http://localhost:8080, import *.offat.json +offat-triage findings.json --provider claude-code ``` -### AI triage backends - -The triager picks a backend automatically, or force one with `--provider` / -`OFFAT_AI_PROVIDER`. `auto` prefers an API key, then Claude Code, then Codex, -then the offline heuristic. Verdicts are cached by content hash and screened by a -cheap model before a strong model verifies the confirmed/likely subset. - ---- - -## CI gate - -Both the classic CLI and the platform support failing a build on findings: +## Visualize ```bash -offat-platform triage app.offat.json --fail-on high # exit non-zero on high+ findings +python -m http.server -d viz 8080 # open http://localhost:8080 and import a *.offat.json ``` -For spec-driven scans in CI, see the [GitHub Action](github-action.html). +## CI gate + +`--fail-on ` exits non-zero when an actionable finding (not triaged as +a false positive) is at or above that severity. For GitHub, use the +[GitHub Action](github-action.md). diff --git a/docs/whitebox.md b/docs/whitebox.md index c7729bc..c1db7cc 100644 --- a/docs/whitebox.md +++ b/docs/whitebox.md @@ -1,8 +1,15 @@ -# White-box SAST (`offat-whitebox`) +--- +title: White-box design +section: Deep dives +nav_order: 3 +description: The offat-whitebox pipeline - recon, code graph, patterns, chain, verify, report. +--- + +# White-box design (`offat-whitebox`) A static source-code security review pipeline modeled on the [security-harness](https://github.com/dmdhrumilmistry/security-harness) -workflow: **recon → hunt → trace → chain → verify → report**. +workflow: **recon → code graph + patterns → chain → verify → report**. ## Run @@ -10,8 +17,8 @@ workflow: **recon → hunt → trace → chain → verify → report**. # From the repo (no install) PYTHONPATH=whitebox:triager python3 -m offat_wb /path/to/repo -o offat-report/whitebox -# Installed -pip install ./triager ./whitebox +# Installed (the [graph] extra enables the code-graph engine) +pip install ./triager './whitebox[graph]' offat-whitebox /path/to/repo ``` @@ -22,8 +29,14 @@ offat-whitebox /path/to/repo | `--out, -o` | `offat-report/whitebox` | Output directory | | `--no-ai` | false | Heuristic triage only | | `--provider` | auto | Triage backend: `auto`, `anthropic`, `claude-code`, `codex`, `heuristic` | -| `--no-semgrep` | false | Skip semgrep even if installed | -| `--no-graft` | false | Skip graft source-to-sink tracing | +| `--no-graph` | false | Skip the code-graph taint engine | +| `--specs` | — | Extra source / sink / sanitizer specs (JSON) | +| `--local-sources` | false | Also treat argv / env / stdin as sources (CLI tools, scripts) | +| `--graph-out` | false | Export the code graph as JSON + GraphML | +| `--agent N` | 0 | Re-verify the top N graph findings with the graph-tool AI agent | +| `--infer-specs` | false | Write the taint frontier; with a provider, let AI label missing sinks / sanitizers | +| `--semgrep` | false | Also run `semgrep --config auto` (opt-in) | +| `--no-graft` | false | Skip graft tracing (only used with `--no-graph`) | | `--no-cache` | false | Do not read/write the AI-verdict cache | | `--fail-on` | — | Exit non-zero if an actionable finding is at/above this severity (CI gate) | @@ -48,23 +61,20 @@ Detects languages by file mix, inventories dependency manifests, and — when th tools are installed — generates an SBOM with **syft** and CVE findings with **grype** (falling back to **trivy**, then **osv-scanner**). -### hunt -Two complementary hunters: - -- **Built-in pattern hunter** (always on, no dependencies): high-signal - multi-language rules for hardcoded secrets (AWS/Slack/GitHub/private keys), - dangerous sinks (`eval`/`exec`, `os.system`, `shell=True`, SQL string - building), unsafe deserialization (`pickle`, unsafe `yaml.load`, - `ObjectInputStream`), weak crypto (MD5/SHA1), disabled TLS verification, DOM - XSS sinks, SSRF sinks and misconfiguration. -- **semgrep** `--config auto` when installed; results are mapped into the shared - schema with a class inferred from the rule metadata. - -### trace -When **graft** is installed, each sink's enclosing symbol is checked against the -structural call graph: a sink reachable from an entry point (route handler, -controller, `main`, CLI, message consumer) gets a confidence bump and a -data-flow note. Without graft this stage is a no-op (recorded in the report). +### code graph +The primary engine ([overview](code-graph.md)). It builds a code property graph +of the repository (Python via `ast`, JavaScript / TypeScript via tree-sitter), +marks attacker-controlled sources and dangerous sinks, and proves source → sink +paths with an interprocedural, call/return-matched search. Every finding +carries its hop-by-hop path, entry points and HTTP route. + +### patterns +A dependency-free, multi-language hunter for classes that are not data flows: +hardcoded secrets (AWS / Slack / GitHub / private keys), weak crypto (MD5 / +SHA1), disabled TLS verification and misconfiguration. Pattern hits in +injection classes are fused with the graph: a hit on a proven line is dropped as +a duplicate, a hit with no source reaching it is kept but demoted, and every hit +gains graph reachability. `semgrep --config auto` can be added with `--semgrep`. ### chain Annotates co-located findings (same file, multiple classes) into simple attack @@ -73,11 +83,14 @@ paths — e.g. a hardcoded secret next to an outbound request. ### verify The shared triager adversarially validates each finding (refute first) and assigns verdict / CVSS / remediation, with caching and model tiering (see Token -discipline). Offline heuristic when no key is set. +discipline). It receives the proven path rather than a snippet. With +`--agent N`, a graph-tool agent re-verifies the top findings by reading callers, +callees, function bodies and backward slices. Offline heuristic when no key is +set. ### report -`report.json`, `findings.jsonl`, `results.sarif`, `report.md`, `report.html`, -`report.junit.xml`. Use `--fail-on ` to gate CI on actionable findings. +`report.json`, `findings.jsonl`, `results.sarif` (with `codeFlows`), `report.md`, +`report.html`, `report.junit.xml`. Use `--fail-on ` to gate CI on actionable findings. Every finding is mapped to a threat taxonomy - **OWASP API Security Top 10 (2023)**, **OWASP Top 10 (2021)** and a primary **CWE** (each with names) - in a @@ -86,22 +99,10 @@ findings per OWASP API Top 10 category. ## Deepening results -The pipeline runs with **no external tools**, but installing them materially -improves coverage: - -```bash -pip install semgrep -# syft / grype: https://github.com/anchore -``` - -For a deeper, agentic, multi-pass review that chains findings into full attack -scenarios, use the upstream **security-harness** Claude Code plugin; this -pipeline is its CI-runnable, automation-friendly counterpart, and both emit the -same finding/report schema. - -## Relationship to `graft` +The pipeline runs with no external tools. The `[graph]` extra enables the code +graph; `syft` and `grype` (or `trivy` / `osv-scanner`) add an SBOM and dependency +CVEs; `--specs` and `--infer-specs` extend the source / sink catalog for +in-house wrappers. -`graft` (`npx @nanonets/graft`) provides structural code mapping used by -security-harness. It requires network access and (for LLM analysis) an API key, -so OFFAT-AI treats it as an optional recon enhancer rather than a hard -dependency; the built-in recon covers language/manifest mapping without it. +For a deeper, agentic, multi-pass review, the upstream **security-harness** +Claude Code plugin emits the same finding and report schema. diff --git a/index.md b/index.md index 2aff241..c17c02b 100644 --- a/index.md +++ b/index.md @@ -5,51 +5,37 @@ title: OWASP OFFAT tags: api-security level: 2 type: code, tool -pitch: Tests your API automatically for common API vulnerabilities after generating tests from provided openapi specification file. +pitch: Source-aware API security testing - maps your API from code, proves source-to-sink flows on a code graph, executes tests with a Rust engine and triages with AI. --- -OWASP OFFAT (OFFensive Api Tester) is created to automatically test API for common vulnerabilities after generating tests from openapi specification file. It provides feature to automatically fuzz inputs and use user provided inputs during tests specified via YAML config file. +OWASP OFFAT (OFFensive Api Tester) tests APIs for common vulnerabilities from +their source code and OpenAPI specifications. It maps the attack surface, proves +source-to-sink data flows on a code property graph, generates and executes tests +with a Rust engine, and triages the results with AI or an offline heuristic. -![UnDocumented petstore API endpoint HTTP method results](./src/.images/tests/offat-v0.5.0.png) +**Documentation:** [owasp.github.io/OFFAT](https://owasp.github.io/OFFAT/) -## Demo +## How it works -[![asciicast](https://asciinema.org/a/9MSwl7UafIVT3iJn13OcvWXeF.svg)](https://asciinema.org/a/9MSwl7UafIVT3iJn13OcvWXeF) - -## Security Checks - -- Restricted HTTP Methods -- SQLi -- BOLA -- Data Exposure -- BOPLA / Mass Assignment -- Broken Access Control -- Basic Command Injection -- Basic XSS/HTML Injection test - -## Features - -- Few Security Checks from OWASP API Top 10 -- Automated Testing -- User Config Based Testing -- API for Automating tests and Integrating Tool with other platforms/tools -- CLI tool -- Dockerized Project for Easy Usage -- Open Source Tool with MIT License - -## Try Tool - -- Install Tool using pip - -```bash -python -m pip install offat ``` +map -> code graph -> threat model -> test-gen -> execute (Rust) -> triage (AI) -> report +``` + +- Attack-surface mapping from source: routes, routers, decorators and OpenAPI specs +- Interprocedural taint analysis for Python and JavaScript/TypeScript +- Identity-aware access-control testing (BOLA, BFLA, RBAC) +- Rust execution engine: HTTP/1.1, HTTP/2, WebSocket, gRPC (HTTP/3 opt-in) +- AI triage with caching and model tiering, or a fully offline heuristic +- SARIF, Markdown, HTML and OWASP API Top 10 compliance reports +- GitHub Action for CI -- Run Tool +## Try it ```bash -offat -f swagger_file.json +git clone https://github.com/OWASP/OFFAT.git && cd OFFAT +pip install ./bundle ./triager './whitebox[graph]' ./graybox ./platform ./reporter +offat-platform run /path/to/repo -o app.offat.json --report-dir reports ``` -- For more usage options read Project Repo [README.md](https://github.com/OWASP/OFFAT/blob/main/src/README.md) +Only test systems and code you own or are authorized to assess. diff --git a/src/.dockerignore b/src/.dockerignore deleted file mode 100644 index 394c7b9..0000000 --- a/src/.dockerignore +++ /dev/null @@ -1,197 +0,0 @@ -### Git ### -# Created by git for backups. To disable backups in Git: -# $ git config --global mergetool.keepBackup false -*.orig - -# Created by git when using merge tools for conflicts -*.BACKUP.* -*.BASE.* -*.LOCAL.* -*.REMOTE.* -*_BACKUP_*.txt -*_BASE_*.txt -*_LOCAL_*.txt -*_REMOTE_*.txt - -### Python ### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -### Python Patch ### -# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration -poetry.toml - -# ruff -.ruff_cache/ - -# LSP config files -pyrightconfig.json - - -## conf/spec files -conf.yaml -openapi.yaml -openapi.json -specs.yaml -specs.json -swagger.yaml -swagger.json -*.json \ No newline at end of file diff --git a/src/.env.sample b/src/.env.sample deleted file mode 100644 index 859d8f9..0000000 --- a/src/.env.sample +++ /dev/null @@ -1,3 +0,0 @@ -REDIS_HOST=localhost -REDIS_PORT=6379 -AUTH_SECRET_KEY=E8f_3pHCgScOxTvyYFa1LKeJWLa4KtPKLY.FyLzesj66nHxQ5h1qhCFMQJJ_9eUL56EL3._XbYrYY8c5.foTV_yYHKcySPzBwv8FxIA1p03RFEinbex4EYZ9YvhacAiW diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index 394c7b9..0000000 --- a/src/.gitignore +++ /dev/null @@ -1,197 +0,0 @@ -### Git ### -# Created by git for backups. To disable backups in Git: -# $ git config --global mergetool.keepBackup false -*.orig - -# Created by git when using merge tools for conflicts -*.BACKUP.* -*.BASE.* -*.LOCAL.* -*.REMOTE.* -*_BACKUP_*.txt -*_BASE_*.txt -*_LOCAL_*.txt -*_REMOTE_*.txt - -### Python ### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -### Python Patch ### -# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration -poetry.toml - -# ruff -.ruff_cache/ - -# LSP config files -pyrightconfig.json - - -## conf/spec files -conf.yaml -openapi.yaml -openapi.json -specs.yaml -specs.json -swagger.yaml -swagger.json -*.json \ No newline at end of file diff --git a/src/CODE_OF_CONDUCT.md b/src/CODE_OF_CONDUCT.md deleted file mode 100644 index b7c4f6e..0000000 --- a/src/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,128 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -contact@dmdhrumilmistry.tech. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. \ No newline at end of file diff --git a/src/DISCLAIMER.md b/src/DISCLAIMER.md deleted file mode 100644 index 5362079..0000000 --- a/src/DISCLAIMER.md +++ /dev/null @@ -1,9 +0,0 @@ -# Disclaimer - -The project is provided for legitimate and ethical purposes only. Any misuse or malicious use of the project is strictly prohibited and may result in legal action. The creators and contributors of this project are not responsible for any illegal activities or damages that may arise from the misuse of the project. - -Users are advised to use the project for ethical and legitimate purposes only, and to refrain from using it for any malicious activities, including but not limited to hacking, phishing, and unauthorized access to systems or data. - -The creators and contributors of this project disclaim any liability for any damages or losses that may arise from the misuse of the project. Users are solely responsible for their use of the project and should exercise caution and diligence when using it. - -By using the project, users agree to abide by all applicable laws and regulations, and to use the project in a manner that is consistent with ethical and legitimate practices. Any unauthorized or malicious use of the project may result in legal action and other consequences. diff --git a/src/DockerFiles/dev/backend-api-Dockerfile b/src/DockerFiles/dev/backend-api-Dockerfile deleted file mode 100644 index a48ae00..0000000 --- a/src/DockerFiles/dev/backend-api-Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM dmdhrumilmistry/offat-base:dev - -EXPOSE 8000 - -ENTRYPOINT [ "python", "-m", "offat.api" ] diff --git a/src/DockerFiles/dev/backend-api-worker-Dockerfile b/src/DockerFiles/dev/backend-api-worker-Dockerfile deleted file mode 100644 index 1c50659..0000000 --- a/src/DockerFiles/dev/backend-api-worker-Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM dmdhrumilmistry/offat-base:dev - -WORKDIR /offat - -ENTRYPOINT [ "rq", "worker", "offat_task_queue" ] diff --git a/src/DockerFiles/dev/cli-Dockerfile b/src/DockerFiles/dev/cli-Dockerfile deleted file mode 100644 index ab0e0dc..0000000 --- a/src/DockerFiles/dev/cli-Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM dmdhrumilmistry/offat-base:dev - -ENTRYPOINT [ "offat" ] diff --git a/src/DockerFiles/main/backend-api-Dockerfile b/src/DockerFiles/main/backend-api-Dockerfile deleted file mode 100644 index dbd495f..0000000 --- a/src/DockerFiles/main/backend-api-Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM dmdhrumilmistry/offat-base:latest - -EXPOSE 8000 - -ENTRYPOINT [ "python", "-m", "offat.api" ] diff --git a/src/DockerFiles/main/backend-api-worker-Dockerfile b/src/DockerFiles/main/backend-api-worker-Dockerfile deleted file mode 100644 index 0b89140..0000000 --- a/src/DockerFiles/main/backend-api-worker-Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM dmdhrumilmistry/offat-base:latest - -WORKDIR /offat - -ENTRYPOINT [ "rq", "worker", "offat_task_queue" ] diff --git a/src/DockerFiles/main/cli-Dockerfile b/src/DockerFiles/main/cli-Dockerfile deleted file mode 100644 index daa4847..0000000 --- a/src/DockerFiles/main/cli-Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM dmdhrumilmistry/offat-base:latest - -ENTRYPOINT [ "offat" ] diff --git a/src/DockerFiles/wolfi-base-Dockerfile b/src/DockerFiles/wolfi-base-Dockerfile deleted file mode 100644 index 6ba05b3..0000000 --- a/src/DockerFiles/wolfi-base-Dockerfile +++ /dev/null @@ -1,59 +0,0 @@ -############################ -# Builder Stage -############################ -# use chainguard hardened images with SBOM -FROM cgr.dev/chainguard/wolfi-base AS builder - -WORKDIR /offat - -ARG version=3.12 - -ENV LANG=C.UTF-8 -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 -ENV PATH="/offat/.venv/bin:$PATH" - - -RUN apk add python-${version} py${version}-pip && \ - chown -R nonroot:nonroot /offat - -# install poetry and copy lock file -RUN python -m pip install poetry -COPY pyproject.toml poetry.lock README.md ./ -COPY offat ./offat - -# poetry config -ENV POETRY_NO_INTERACTION=1 \ - POETRY_VIRTUALENVS_IN_PROJECT=1 \ - POETRY_VIRTUALENVS_CREATE=1 \ - POETRY_CACHE_DIR=/tmp/poetry_cache - -RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install -E api --without dev - -############################ -# runtime stage -############################ -FROM cgr.dev/chainguard/wolfi-base AS runtime - -WORKDIR /offat - -ARG version=3.12 - -ENV LANG=C.UTF-8 -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 -ENV PATH="/offat/.venv/bin:$PATH" -ENV VIRTUAL_ENV=/offat/.venv - -RUN apk add python-${version} py${version}-pip && \ - chown -R nonroot:nonroot /offat - - -# copy venv from builder image -COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} - -# copy necessary files -COPY offat ./offat -COPY README.md CODE_OF_CONDUCT.md DISCLAIMER.md pyproject.toml . - -USER nonroot \ No newline at end of file diff --git a/src/MANIFEST.in b/src/MANIFEST.in deleted file mode 100644 index b0b9074..0000000 --- a/src/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include README.md -include LICENSE -include SECURITY.md -include DISCLAIMER.md - -include offat/report/templates/report.html \ No newline at end of file diff --git a/src/Makefile b/src/Makefile deleted file mode 100644 index 4a9abc9..0000000 --- a/src/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -build-slim-local-images: - @docker build -f DockerFiles/base-Dockerfile -t dmdhrumilmistry/offat-base . - # @docker build -f DockerFiles/main/cli-Dockerfile -t dmdhrumilmistry/offat . - -build-local-image: - @docker build -f DockerFiles/wolfi-base-Dockerfile -t dmdhrumilmistry/offat-base . --no-cache --progress=plain - -scan-vulns: - @trivy image dmdhrumilmistry/offat-base --scanners vuln - -local: build-local-image scan-vulns diff --git a/src/README.md b/src/README.md deleted file mode 100644 index 8c04e72..0000000 --- a/src/README.md +++ /dev/null @@ -1,328 +0,0 @@ -# OFFAT - OFFensive Api Tester - -![OffAT Logo](/assets/images/logos/offat-3.png) - -Automatically Tests for vulnerabilities after generating tests from openapi specification file. Project is in Beta stage, so sometimes it might crash while running. - -![UnDocumented petstore API endpoint HTTP method results](/assets/images/tests/offat-v0.5.0.png) - -## Security Checks - -- [x] Restricted HTTP Methods -- [x] SQLi -- [x] BOLA (Might need few bug fixes) -- [x] Data Exposure (Detects Common Data Exposures) -- [x] BOPLA / Mass Assignment -- [x] Broken Access Control -- [x] Basic Command Injection -- [x] Basic XSS/HTML Injection test -- [x] Basic SSTI test -- [ ] Broken Authentication - -## Features - -- Few Security Checks from OWASP API Top 10 -- Automated Testing -- User Config Based Testing -- API for Automating tests and Integrating Tool with other platforms/tools -- CLI tool -- Proxy Support -- Secure Dockerized Project for Easy Usage -- Open Source Tool with MIT License -- Github Action - -## Demo - -[![asciicast](https://asciinema.org/a/9MSwl7UafIVT3iJn13OcvWXeF.svg)](https://asciinema.org/a/9MSwl7UafIVT3iJn13OcvWXeF) - -> Note: The columns for 'data_leak' and 'result' in the table represent independent aspects. It's possible for there to be a data leak in the endpoint, yet the result for that endpoint may still be marked as 'Success'. This is because the 'result' column doesn't necessarily reflect the overall test result; it may indicate success even in the presence of a data leak. - -## Github Action - -- Create github action secret `url` for your repo -- Setup github action workflow in your repo `.github/workflows/offat.yml` - -```yml -name: OWASP OFFAT Sample Workflow - -on: - push: - branches: - - dev - - main - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: "download swagger/OAS file" - run: curl ${url} -o /tmp/swagger.json - env: - url: ${{ secrets.url }} - - - name: "OWASP OFFAT CICD Scanner" - uses: OWASP/OFFAT@main # OWASP/OFFAT@v0.17.3 - with: - file: /tmp/swagger.json # or ${{ secrets.url }} - rate_limit: 120 - artifact_retention_days: 1 -``` - -> Prefer locking action to specific version `OWASP/OFFAT@v0.17.3` instead of using `OWASP/OFFAT@main` and bump OFFAT action version after testing. - -## PyPi Downloads - -| Period | Count | -| :----: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -| Weekly | [![Downloads](https://static.pepy.tech/personalized-badge/offat?period=week&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/offat) | -| Monthy | [![Downloads](https://static.pepy.tech/personalized-badge/offat?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/offat) | -| Total | [![Downloads](https://static.pepy.tech/personalized-badge/offat?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/offat) | - -## Disclaimer - -The disclaimer advises users to use the open-source project for ethical and legitimate purposes only and refrain from using it for any malicious activities. The creators and contributors of the project are not responsible for any illegal activities or damages that may arise from the misuse of the project. Users are solely responsible for their use of the project and should exercise caution and diligence when using it. Any unauthorized or malicious use of the project may result in legal action and other consequences. - -[Read More](./DISCLAIMER.md) - -## Installation - -### Using pip - -- Install main branch using pip - - ```bash - python3 -m pip install git+https://github.com/OWASP/OFFAT.git - ``` - -- Install Release from PyPi - - ```bash - python3 -m pip install offat # only cli tool - python3 -m pip install offat[api] # cli + api - ``` - -### Using Containers - -### Docker - -- Build Image - - ```bash - make local - ``` - -- CLI Tool - - ```bash - docker run --rm dmdhrumilmistry/offat - ``` - -- API - - ```bash - docker compose up -d - ``` - - > POST `openapi` documentation to `/api/v1/scan/` endpoint with its valid `type` (json/yaml); `job_id` will be returned. - -### Manual Method - -- Open terminal - -- Install git package - - ```bash - sudo apt install git python3 -y - ``` - -- Install [Poetry](https://python-poetry.org/docs/master#installing-with-the-official-installer) - -- clone the repository to your machine - - ```bash - git clone https://github.com/OWASP/OFFAT.git - ``` - -- Change directory - - ```bash - cd offat - ``` - -- install with poetry - - ```bash - # without options - poetry install - ``` - -## Start OffAT - -### API - -- Start API Server - - ```bash - python -m offat.api - - # OR - - offat-api - ``` - -- API Documentation can be found at - -### CLI Tool - -- Run offat - - ```bash - offat -f swagger_file.json # using file - offat -f https://example.com/docs.json # using url - ``` - -- To get all the commands use `help` - - ```bash - offat -h - ``` - -- Save result in `json`, `yaml` or `html` formats. - - ```bash - offat -f swagger_file.json -o output.json -of html # json - offat -f swagger_file.json -o output.html -of html # html - offat -f swagger_file.json -o output.yaml -of yaml # yaml - ``` - -> `json` format is default output format. -> `yaml` format needs to be sanitized before usage since it dumps data as python objects. -> `html` format needs more visualization. - -- Run tests only for endpoint paths matching regex pattern - - ```bash - offat -f swagger_file.json -pr '/user' - ``` - -- Add headers to requests - - ```bash - offat -f swagger_file.json -H 'Accept: application/json' -H 'Authorization: Bearer YourJWTToken' - ``` - -- Run Test with Requests Rate Limited - - ```bash - offat -f swagger_file.json -rl 1000 - ``` - - > `rl`: requests rate limit per second - -- Use along with proxy - - ```bash - # without ssl check - offat -f swagger_file.json -p http://localhost:8080 -o output.json -of json # ssl checks are disabled by default to avoid certificate installations - - # with ssl check enforced - offat -f swagger_file.json -p http://localhost:8080 -o output.json -of json --ssl - ``` - - > Make sure that proxy can handle multiple requests at the same time - -- Use user provided inputs for generating tests - - ```bash - offat -f swagger_file.json -tdc test_data_config.yaml - ``` - - `test_data_config.yaml` - - ```yaml - actors: - - actor1: - request_headers: - - name: Authorization - value: Bearer [Token1] - - name: User-Agent - value: offat-actor1 - - query: - - name: id - value: 145 - type: int - - name: country - value: uk - type: str - - name: city - value: london - type: str - - body: - - name: name - value: actorone - type: str - - name: email - value: actorone@example.com - type: str - - name: phone - value: +11233211230 - type: str - - unauthorized_endpoints: # For broken access control - - "/store/order/.*" - - - actor2: - request_headers: - - name: Authorization - value: Bearer [Token2] - - name: User-Agent - value: offat-actor2 - - query: - - name: id - value: 199 - type: int - - name: country - value: uk - type: str - - name: city - value: leeds - type: str - - body: - - name: name - value: actortwo - type: str - - name: email - value: actortwo@example.com - type: str - - name: phone - value: +41912312311 - type: str - ``` - -> If you're using Termux or windows, then use `pip` instead of `pip3`. -> Few features are only for linux os, hence they might not work on windows and require admin priviliges. - -### Open In Google Cloud Shell - -- Temporary Session - [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/OWASP/OFFAT.git&ephemeral=true&show=terminal&cloudshell_print=./DISCLAIMER.md) -- Perisitent Session - [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/OWASP/OFFAT.git&ephemeral=false&show=terminal&cloudshell_print=./DISCLAIMER.md) - -## Have any Ideas 💡 or issue - -- Create an issue -- Fork the repo, update script and create a Pull Request - -## Contributing - -Refer [CONTRIBUTIONS.md](/CONTRIBUTING.md) for contributing to the project. - -## LICENSE - -OWASP OFFAT is distributed under `MIT` License. Refer [License](/LICENSE.md) for more information. diff --git a/src/docker-compose.yml b/src/docker-compose.yml deleted file mode 100644 index ceabf43..0000000 --- a/src/docker-compose.yml +++ /dev/null @@ -1,35 +0,0 @@ -version: '3' -services: - offat-redis: - image: redis:latest - ports: - - "6379:6379" - networks: - - offat-network - - offat-api: - image: dmdhrumilmistry/offat-base:latest - command: ["python", "-m", "offat.api"] - ports: - - "8000:8000" - networks: - - offat-network - environment: - - REDIS_HOST=offat-redis - - REDIS_PORT=6379 - depends_on: - - offat-redis # Wait for offat-redis to start before starting this container - - offat-api-worker: - image: dmdhrumilmistry/offat-base:latest - command: ["rq", "worker","offat_task_queue"] - networks: - - offat-network - environment: - - REDIS_HOST=offat-redis - - REDIS_PORT=6379 - depends_on: - - offat-redis # Wait for offat-redis to start before starting this container - -networks: - offat-network: diff --git a/src/offat/__init__.py b/src/offat/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/offat/__main__.py b/src/offat/__main__.py deleted file mode 100644 index cabbc61..0000000 --- a/src/offat/__main__.py +++ /dev/null @@ -1,185 +0,0 @@ -from argparse import ArgumentParser - -from .parsers.openapi import OpenAPIv3Parser -from .parsers.swagger import SwaggerParser -from .config_data_handler import validate_config_file_data -from .tester.handler import generate_and_run_tests -from .parsers import create_parser -from .utils import get_package_version, headers_list_to_dict, read_yaml - - -def banner(): - """Prints OWASP OFFAT cli banner""" - print( - r""" - _/| |\_ - / | | \ - | \ / | - | \ / \ / | - | \ | | / | - | \ _\_/^\_/_ / | - | --\//-- | - \_ \ / _/ - \__ | __/ - \ _ / - _/ \_ - / _/|\_ \ - / | \ - / v \ - OFFAT - """ - ) - - -def start(): - """Starts cli tool""" - banner() - - parser = ArgumentParser(prog='offat') - parser.add_argument( - '-f', - '--file', - dest='fpath', - type=str, - help='path or url of openapi/swagger specification file', - required=True, - ) - parser.add_argument( - '-v', '--version', action='version', version=f"%(prog)s {get_package_version()}" - ) - parser.add_argument( - '--http-methods', - dest='http_methods', - nargs='+', - choices=['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH'], - help="Specify one or more HTTP methods to use while scanning. Allowed values: GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH", - default=['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH'], - required=False - ) - parser.add_argument( - '-rl', - '--rate-limit', - dest='rate_limit', - help='API requests rate limit per second', - type=float, - default=60, - required=False, - ) - parser.add_argument( - '-pr', - '--path-regex', - dest='path_regex_pattern', - type=str, - help='run tests for paths matching given regex pattern', - required=False, - default=None, - ) - parser.add_argument( - '-o', - '--output', - dest='output_file', - type=str, - help='path to store test results', - required=False, - default=None, - ) - parser.add_argument( - '-of', - '--format', - dest='output_format', - type=str, - choices=['json', 'yaml', 'html', 'table'], - help='Data format to save (json, yaml, html, table). Default: table', - required=False, - default='table', - ) - parser.add_argument( - '-H', - '--headers', - dest='headers', - type=str, - help='HTTP requests headers that should be sent during testing eg: User-Agent: offat', - required=False, - default=None, - action='append', - nargs='*', - ) - parser.add_argument( - '-tdc', - '--test-data-config', - dest='test_data_config', - help='YAML file containing user test data for tests', - required=False, - type=str, - ) - parser.add_argument( - '-p', - '--proxy', - dest='proxies_list', - help='Proxy server URL to route HTTP requests through (e.g. "http://proxyserver:port")', - action='append', - required=False, - type=str, - default=None, - ) - parser.add_argument( - '-s', - '--ssl-verify', - dest='ssl_verify', - required=False, - action='store_true', - help='Enable SSL Verification', - ) - parser.add_argument( - '-cf', - '--capture-failed', - dest='capture_failed', - action='store_true', - help='Captures failed requests due to any exceptions into output file', - ) - parser.add_argument( - '--server', - dest='server_url', - type=str, - default=None, - required=False, - help='server/host base url to overwrite from OAS/Swagger file', - ) - args = parser.parse_args() - - # convert req headers str to dict - headers_dict: dict = headers_list_to_dict(args.headers) - - # handle rate limiting options - # TODO: allow user to opt out of rate limit - rate_limit = args.rate_limit - - # handle test user data config file - test_data_config = args.test_data_config - if test_data_config: - test_data_config = read_yaml(args.test_data_config) - test_data_config = validate_config_file_data(test_data_config) - - # parse args and run tests - api_parser: SwaggerParser | OpenAPIv3Parser = create_parser( - args.fpath, server_url=args.server_url, - ssl_verify=args.ssl_verify - ) - - generate_and_run_tests( - api_parser=api_parser, - http_methods=args.http_methods, - regex_pattern=args.path_regex_pattern, - output_file=args.output_file, - output_file_format=args.output_format, - req_headers=headers_dict, - rate_limit=rate_limit, - test_data_config=test_data_config, - proxies=args.proxies_list, - capture_failed=args.capture_failed, - ssl_verify=args.ssl_verify, - ) - - -if __name__ == '__main__': - start() diff --git a/src/offat/api/__init__.py b/src/offat/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/offat/api/__main__.py b/src/offat/api/__main__.py deleted file mode 100644 index f222bb6..0000000 --- a/src/offat/api/__main__.py +++ /dev/null @@ -1,27 +0,0 @@ -from uvicorn import run -import importlib.resources - - -def get_offat_installation_dir(): - try: - # For non-editable installation - return importlib.resources.files("offat") - except ImportError: - # For editable installation (pip install -e .) - return importlib.resources.files(".") - - -def start(): - installation_dir = get_offat_installation_dir() - run( - app="offat.api.app:app", - host="0.0.0.0", - port=8000, - workers=2, - reload=True, - reload_dirs=[installation_dir], - ) - - -if __name__ == "__main__": - start() diff --git a/src/offat/api/app.py b/src/offat/api/app.py deleted file mode 100644 index ad923ae..0000000 --- a/src/offat/api/app.py +++ /dev/null @@ -1,97 +0,0 @@ -from os import uname, environ -from textwrap import dedent - -from fastapi import status, Request, Response -from offat.api.config import app, task_queue, task_timeout, auth_secret_key -from offat.api.jobs import scan_api -from offat.api.schema import CreateScanSchema -from offat.logger import logger - - -logger.info('Secret Key: %s', auth_secret_key) - - -if ( - uname().sysname == 'Darwin' - and environ.get('OBJC_DISABLE_INITIALIZE_FORK_SAFETY') != 'YES' -): - logger.warning( - dedent( - '''Mac Users might need to configure OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES in env - Visit StackOverFlow link for more info: - https://stackoverflow.com/questions/50168647/multiprocessing-causes-python-to-crash-and-gives-an-error-may-have-been-in-progr - ''' - ) - ) - - -@app.get('/', status_code=status.HTTP_200_OK) -async def root(): - return { - 'name': 'OFFAT API', - 'project': 'https://github.com/OWASP/offat', - 'license': 'https://github.com/OWASP/offat/blob/main/LICENSE', - } - - -@app.post('/api/v1/scan', status_code=status.HTTP_201_CREATED) -async def add_scan_task( - scan_data: CreateScanSchema, request: Request, response: Response -): - # for auth - client_ip = request.client.host - secret_key = request.headers.get('SECRET-KEY', None) - if secret_key != auth_secret_key: - # return 404 for better endpoint security - response.status_code = status.HTTP_401_UNAUTHORIZED - logger.warning('INTRUSION: %s tried to create a new scan job', client_ip) - return {'message': 'Unauthorized'} - - msg = {'msg': 'Scan Task Created', 'job_id': None} - - job = task_queue.enqueue(scan_api, scan_data, job_timeout=task_timeout) - msg['job_id'] = job.id - - logger.info('SUCCESS: %s created new scan job - %s', client_ip, job.id) - - return msg - - -@app.get('/api/v1/scan/{job_id}/result') -async def get_scan_task_result(job_id: str, request: Request, response: Response): - # for auth - client_ip = request.client.host - secret_key = request.headers.get('SECRET-KEY', None) - if secret_key != auth_secret_key: - # return 404 for better endpoint security - response.status_code = status.HTTP_401_UNAUTHORIZED - logger.warning( - 'INTRUSION: %s tried to access %s job scan results', client_ip, job_id - ) - return {'message': 'Unauthorized'} - - scan_results_job = task_queue.fetch_job(job_id=job_id) - - logger.info('SUCCESS: %s accessed %s job scan results', client_ip, job_id) - - msg = 'Task Remaining or Invalid Job Id' - results = None - response.status_code = status.HTTP_202_ACCEPTED - - if scan_results_job and scan_results_job.is_started: - msg = 'Job In Progress' - - elif scan_results_job and scan_results_job.is_finished: - msg = 'Task Completed' - results = scan_results_job.result - response.status_code = status.HTTP_200_OK - - elif scan_results_job and scan_results_job.is_failed: - msg = 'Task Failed. Try Creating Task Again.' - response.status_code = status.HTTP_200_OK - - msg = { - 'msg': msg, - 'results': results, - } - return msg diff --git a/src/offat/api/auth_utils.py b/src/offat/api/auth_utils.py deleted file mode 100644 index ced8a03..0000000 --- a/src/offat/api/auth_utils.py +++ /dev/null @@ -1,12 +0,0 @@ -import secrets -import string - - -def generate_random_secret_key_string(length=128): - # Define the characters allowed in the HTTP header - characters = string.ascii_letters + string.digits + "-_." - - # Generate a random string of the specified length - random_string = "".join(secrets.choice(characters) for _ in range(length)) - - return random_string diff --git a/src/offat/api/config.py b/src/offat/api/config.py deleted file mode 100644 index c4ebf09..0000000 --- a/src/offat/api/config.py +++ /dev/null @@ -1,27 +0,0 @@ -from fastapi import FastAPI -from redis import Redis -from rq import Queue -from dotenv import load_dotenv -from os import environ - -from .auth_utils import generate_random_secret_key_string - - -load_dotenv() - -app = FastAPI( - title="OFFAT - API", - servers=[ - { - "url": "http://localhost:8000", - } - ], -) - -auth_secret_key = environ.get("AUTH_SECRET_KEY", generate_random_secret_key_string()) -redis_con = Redis( - host=environ.get("REDIS_HOST", "localhost"), - port=int(environ.get("REDIS_PORT", 6379)), -) -task_queue = Queue(name="offat_task_queue", connection=redis_con) -task_timeout = 60 * 60 # 3600 s = 1 hour diff --git a/src/offat/api/jobs.py b/src/offat/api/jobs.py deleted file mode 100644 index bbb7af7..0000000 --- a/src/offat/api/jobs.py +++ /dev/null @@ -1,31 +0,0 @@ -from sys import exc_info -from offat.utils import is_valid_url -from offat.api.schema import CreateScanSchema -from offat.tester.handler import generate_and_run_tests -from offat.parsers import create_parser -from offat.logger import logger - - -def scan_api(body_data: CreateScanSchema, ssl_verify: bool = True): - try: - url = body_data.openapi if is_valid_url(body_data.openapi) else None - spec = None if url else body_data.openapi - - api_parser = create_parser(fpath_or_url=url, spec=spec, ssl_verify=ssl_verify) - - results = generate_and_run_tests( - api_parser=api_parser, - regex_pattern=body_data.regex_pattern, - req_headers=body_data.req_headers, - rate_limit=body_data.rate_limit, - test_data_config=body_data.test_data_config, - proxies=body_data.proxies, - capture_failed=body_data.capture_failed, - remove_unused_data=body_data.remove_unused_data, - ssl_verify=ssl_verify, - ) - return results - except Exception as e: - logger.error('Error occurred while creating a job: %s', repr(e)) - logger.error('Debug Data:', exc_info=exc_info()) - return [{'error': str(e)}] diff --git a/src/offat/api/schema.py b/src/offat/api/schema.py deleted file mode 100644 index de40d4b..0000000 --- a/src/offat/api/schema.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Optional -from pydantic import BaseModel - - -class CreateScanSchema(BaseModel): - openapi: str - regex_pattern: Optional[str] = None - req_headers: Optional[dict] = {'User-Agent': 'offat-api'} - rate_limit: Optional[int] = 60 - test_data_config: Optional[dict] = None - proxies: Optional[list[str]] = None - capture_failed: Optional[bool] = False - remove_unused_data: Optional[bool] = True diff --git a/src/offat/config_data_handler.py b/src/offat/config_data_handler.py deleted file mode 100644 index 127d891..0000000 --- a/src/offat/config_data_handler.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Module contains the functions to validate the test -configuration data and populate user data for tests. -""" -from copy import deepcopy -from .logger import logger -from .utils import update_values - - -def validate_config_file_data(test_config_data: dict): - """ - Validates the provided test configuration data. - - Args: - test_config_data (dict): The test configuration data to be validated. - - Returns: - bool or dict: Returns False if the data is invalid, otherwise returns the validated test configuration data. - - """ - if not isinstance(test_config_data, dict): - logger.warning('Invalid data format') - return False - - if test_config_data.get('error', False): - logger.warning('Error Occurred While reading file: %s', test_config_data) - return False - - if not test_config_data.get('actors'): - logger.warning('actors are required') - return False - - if not test_config_data.get('actors', [])[0].get('actor1', None): - logger.warning('actor1 is required') - return False - - logger.info('User provided data will be used for generating test cases') - return test_config_data - - -def populate_user_data(actor_data: dict, actor_name: str, tests: list[dict]): - """ - Populates user data for tests. - - Args: - actor_data (dict): The data of the actor. - actor_name (str): The name of the actor. - tests (list[dict]): The list of tests. - - Returns: - list[dict]: The updated list of tests. - """ - tests = deepcopy(tests) - headers = actor_data.get('request_headers', []) - body_params = actor_data.get('body', []) - query_params = actor_data.get('query', []) - path_params = actor_data.get('path', []) - - # create HTTP request headers - request_headers = {} - for header in headers: - request_headers[header.get('name')] = header.get('value') - - for test in tests: - test['body_params'] = update_values(deepcopy(test['body_params']), body_params) - test['query_params'] = update_values( - deepcopy(test['query_params']), query_params - ) - test['path_params'] += update_values(deepcopy(test['path_params']), path_params) - # for post test processing tests such as broken authentication - test['test_actor_name'] = actor_name - if test.get('kwargs', {}).get('headers', {}).items(): - test['kwargs']['headers'] = dict( - test['kwargs']['headers'], **request_headers - ) - else: - test['kwargs']['headers'] = request_headers - - return tests diff --git a/src/offat/http.py b/src/offat/http.py deleted file mode 100644 index 01b06bc..0000000 --- a/src/offat/http.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -module for interacting with HTTP layer -""" -from random import choice -from os import name as os_name -from urllib.parse import urlparse - -from aiohttp import ClientSession, ClientTimeout -from aiolimiter import AsyncLimiter -from tenacity import retry, stop_after_attempt, retry_if_not_exception_type - -import asyncio -import aiohttp.resolver - -aiohttp.resolver.DefaultResolver = aiohttp.resolver.AsyncResolver -if os_name == 'nt': - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - - -class Proxies: - """ - class for handling proxies - """ - - def __init__(self, proxies: list[str] | None) -> None: - self.p_list = proxies - - def validate_proxy(self, proxy_url: str | None): - """ - Validates a proxy URL based on format and attempts a basic connection. - - Args: - proxy_url: The URL of the proxy server. - - Returns: - True if the proxy URL seems valid and a basic connection can be established, False otherwise. - """ - # Check for valid URL format - # TODO: implement url parse security: https://docs.python.org/3/library/urllib.parse.html#url-parsing-security - parsed_url = urlparse(proxy_url) - if all([parsed_url.scheme, parsed_url.netloc]): - return True - - return False - - def get_random_proxy(self) -> str | None: - """ - Returns random proxy from the list - """ - if not self.p_list: - return None - return choice(self.p_list) - - -class AsyncRequests: - """ - AsyncRequests class helps to send HTTP requests with rate limiting options. - """ - - def __init__( - self, - rate_limit: float = 50, - headers: dict | None = None, - proxies: list[str] | None = [], - allow_redirects: bool = True, - timeout: float = 60, - ssl_verify: bool = True, - ) -> None: - """AsyncRequests class constructor - - Args: - rate_limit (int): number of requests per seconds - delay (float): delay between consecutive requests - headers (dict): overrides default headers while sending HTTP requests - proxy (str): proxy URL to be used while sending requests - timeout (float): total timeout parameter of aiohttp.ClientTimeout - ssl_verify (bool): enforces tls/ssl verification if True - - Returns: - None - """ - self._headers = headers - self._proxy = Proxies(proxies=proxies) - self._allow_redirects = allow_redirects - self._limiter = AsyncLimiter(max_rate=rate_limit, time_period=1) - self._timeout = ClientTimeout(total=timeout) - self._ssl_verify = ssl_verify - - @retry( - stop=stop_after_attempt(3), - retry=retry_if_not_exception_type( - ( - KeyboardInterrupt, - asyncio.exceptions.CancelledError, - ), - ), - ) - async def request(self, url: str, *args, method: str = 'GET', **kwargs) -> dict: - """Send HTTP requests asynchronously - - Args: - url (str): URL of the webpage/endpoint - method (str): HTTP methods (default: GET) supports GET, POST, - PUT, HEAD, OPTIONS, DELETE - - Returns: - dict: returns request and response data as dict - """ - async with self._limiter: - async with ClientSession( - headers=self._headers, - timeout=self._timeout, - ) as session: - req_method = getattr(session, method.lower(), session.get) - - async with req_method( - url, - allow_redirects=self._allow_redirects, - proxy=self._proxy.get_random_proxy(), - ssl=self._ssl_verify, - *args, - **kwargs, - ) as response: - resp_data = { - 'status': response.status, - 'req_url': str(response.request_info.real_url), - 'query_url': str(response.url), - 'req_method': response.request_info.method, - 'req_headers': dict(**response.request_info.headers), - 'res_redirection': str(response.history), - 'res_headers': dict(response.headers), - 'res_body': await response.text(), - } - - return resp_data diff --git a/src/offat/logger.py b/src/offat/logger.py deleted file mode 100644 index f96c7ed..0000000 --- a/src/offat/logger.py +++ /dev/null @@ -1,18 +0,0 @@ -from rich.console import Console -from rich.logging import RichHandler - -import logging - - -console = Console() - -# create logger -logging.basicConfig( - format="%(message)s", - datefmt="[%X]", - handlers=[ - RichHandler(console=console, rich_tracebacks=True, tracebacks_show_locals=True) - ], -) -logger = logging.getLogger("OWASP-OFFAT") -logger.setLevel(logging.INFO) diff --git a/src/offat/parsers/__init__.py b/src/offat/parsers/__init__.py deleted file mode 100644 index 01da380..0000000 --- a/src/offat/parsers/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -from requests import get as http_get -from yaml import safe_load as yaml_load, YAMLError -from .openapi import OpenAPIv3Parser -from .swagger import SwaggerParser -from .parser import BaseParser -from ..utils import is_valid_url -from ..logger import logger - - -def create_parser( - fpath_or_url: str, - spec: dict | None = None, - server_url: str | None = None, - ssl_verify: bool = True, -) -> SwaggerParser | OpenAPIv3Parser: - """returns parser based on doc file""" - if fpath_or_url and is_valid_url(fpath_or_url): - res = http_get(fpath_or_url, timeout=3, verify=ssl_verify) - if res.status_code != 200: - logger.error( - 'server returned status code %d offat expects 200 status code', - res.status_code, - ) - exit(-1) - - # A spec served over a url can be JSON or YAML, same as a local file. - # JSON is a subset of YAML, so safe_load handles both. - try: - spec = yaml_load(res.text) - except YAMLError: - logger.error('Failed to parse spec fetched from url as JSON/YAML') - exit(-1) - - if not isinstance(spec, dict): - logger.error('Spec fetched from url is not a valid JSON/YAML object') - exit(-1) - - fpath_or_url = None # type: ignore - - try: - parser = BaseParser(file_or_url=fpath_or_url, spec=spec, server_url=server_url) - except OSError: - logger.error('File Not Found') - exit(-1) - - if parser.is_v3: - return OpenAPIv3Parser( - file_or_url=fpath_or_url, spec=spec, server_url=server_url - ) - - return SwaggerParser(fpath_or_url=fpath_or_url, spec=spec, server_url=server_url) diff --git a/src/offat/parsers/openapi.py b/src/offat/parsers/openapi.py deleted file mode 100644 index 268d348..0000000 --- a/src/offat/parsers/openapi.py +++ /dev/null @@ -1,226 +0,0 @@ -""" -module to parse OAS v3 documentation JSON/YAML files. -""" -from .parser import BaseParser -from ..utils import parse_server_url -from ..logger import logger - - -class InvalidOpenAPIv3File(Exception): - """Exception to be raised when openAPI/OAS spec validation fails""" - - -class OpenAPIv3Parser(BaseParser): - """OpenAPI v3 Spec File Parser""" - - # while adding new method to this class, make sure same method is present in SwaggerParser class - - def __init__( - self, file_or_url: str, spec: dict | None = None, *args, **kwargs - ) -> None: - super().__init__(file_or_url=file_or_url, spec=spec, *args, **kwargs) # noqa - if not self.is_v3: - raise InvalidOpenAPIv3File('Invalid OAS v3 file') - - self.http_scheme = self._get_scheme() - - # save hosts in self.hosts - self._populate_hosts() - - # raise error if host data not found - if not (self.hosts and self.hosts[0]): - raise ValueError('Host is invalid or not found') - - # parse and set host data - host_dict = self.hosts[0] - self.http_scheme = host_dict['scheme'] - self.host = f'{host_dict["host"]}:{host_dict["port"]}' - self.api_base_path = host_dict['basepath'] - self.base_url = f"{self.http_scheme}://{self.host}" - - self.request_response_params = self._get_request_response_params() - - # security schemes - self.security_schemes = self._get_security_schemes() - - def _populate_hosts(self): - servers = self.specification.get('servers', []) - hosts = [] - if not servers: - logger.error('Invalid Server Url: Server URLs are missing in spec file') - raise InvalidOpenAPIv3File('Server URLs Not Found in spec file') - - for server in servers: - scheme, host, port, basepath = parse_server_url(url=server.get('url')) - - hosts.append( - { - 'scheme': scheme, - 'host': host, - 'port': port, - 'basepath': basepath, - } - ) - - self.hosts = hosts - - def _get_scheme(self): - servers = self.specification.get('servers', []) - schemes = [] - for server in servers: - schemes.append('https' if 'https://' in server.get('url', '') else 'http') - - scheme = 'https' if 'https' in schemes else 'http' - return scheme - - def _fetch_schema_from_spec(self, param_schema_ref: str) -> dict: - schema_spec_path = param_schema_ref.split('/')[1:] - - if len(schema_spec_path) > 3: - logger.error( - 'Schema spec $ref path should not be greater than 3 (excluding #)' - ) - return {} - - schema_data: dict = self.specification - for child_ele in schema_spec_path: - schema_data: dict = schema_data.get(child_ele, {}) - - return schema_data - - def _get_param_definition_schema(self, param: dict): - """Returns Model defined schema for the passed param""" - param_schema = param.get('schema') - - # replace schema $ref with model params - if param_schema: - param_schema_ref = param_schema.get('$ref') - if param_schema_ref: - param_schema = self._fetch_schema_from_spec(param_schema_ref) - - return param_schema - - def _get_response_definition_schema(self, responses: dict): - """returns schema of API response - - Args: - responses (dict): responses from path http method json data - - Returns: - dict: - """ - for status_code in responses.keys(): - # below line could return: ["application/json", "application/xml"] - content = responses[status_code].get('content', None) - - if content: - status_code_content_type_responses = content.keys() - for status_code_content_type in status_code_content_type_responses: - status_code_content = responses[status_code]['content'][ - status_code_content_type - ].keys() - if 'parameters' in status_code_content: - responses[status_code]['schema'] = responses[status_code][ - 'content' - ][status_code_content_type]['parameters'] - elif 'schema' in status_code_content: - responses[status_code][ - 'schema' - ] = self._get_param_definition_schema( - responses[status_code]['content'][status_code_content_type] - ) - - else: - # Fetch $ref schema directly - ref = responses[status_code].get('$ref', None) - if ref: - responses[status_code]['schema'] = self._fetch_schema_from_spec(ref) - - return responses - - def _get_request_response_params(self): - """Returns Schema of requests and response params - - Args: - None - - Returns: - list: - """ - requests = [] - paths = self.specification.get('paths', {}) - - # extract endpoints and supported params - for path in paths.keys(): - path_params = paths[path].get('parameters', []) - - for http_method in paths.get(path, {}).keys(): - # consider only http methods - if http_method not in ['get', 'put', 'post', 'delete', 'options']: - continue - - request_parameters = paths[path][http_method].get('parameters', []) - security = paths[path][http_method].get('security', []) - - # create list of parameters: Fetch object schema from OAS file - body_params = [] - - body_parameter_keys = ( - paths[path][http_method].get('requestBody', {}).get('content', {}) - ) - - for body_parameter_key in body_parameter_keys: - body_parameters_dict = paths[path][http_method]['requestBody'][ - 'content' - ][body_parameter_key] - - required = paths[path][http_method]['requestBody'].get('required') - description = paths[path][http_method]['requestBody'].get( - 'description' - ) - body_param = self._get_param_definition_schema(body_parameters_dict) - - body_params.append( - { - 'in': 'body', - 'name': body_parameter_key, - 'description': description, - 'required': required, - 'schema': body_param, - } - ) - - response_params = [] - response_params = self._get_response_definition_schema( - paths[path][http_method].get('responses', {}) - ) - - # add body param to request param - request_parameters += body_params - requests.append( - { - 'http_method': http_method, - 'path': path, - 'request_params': request_parameters, - 'response_params': response_params, - 'path_params': path_params, - 'body_params': body_params, - 'security': security, - } - ) - - return requests - - def _get_security_schemes(self): - """ - Retrieves the security schemes defined in the OpenAPI specification. - - Returns: - dict: A dictionary containing the security schemes defined in the specification. - """ - security_schemes = self.specification.get('components', {}).get( - 'securitySchemes', {} - ) - if not security_schemes: - logger.warning('Security schemes not found in the OpenAPI specification.') - return security_schemes diff --git a/src/offat/parsers/parser.py b/src/offat/parsers/parser.py deleted file mode 100644 index 9576770..0000000 --- a/src/offat/parsers/parser.py +++ /dev/null @@ -1,67 +0,0 @@ -from openapi_spec_validator import validate -from openapi_spec_validator.readers import read_from_filename -from ..logger import logger -from ..utils import parse_server_url - - -class InvalidSpecVersion(Exception): - """Exception to be raised""" - - pass - - -class BaseParser: - def __init__( - self, file_or_url: str, spec: dict | None = None, server_url: str | None = None - ) -> None: - if spec: - self.specification: dict = spec - base_uri = "" - else: - self.specification, base_uri = read_from_filename(file_or_url) - - self.is_v3 = self._get_oas_version() == 3 - - # overwrite server if present according to OAS version - if self.is_v3 and server_url: - self.specification["servers"] = [{"url": server_url}] - elif server_url: - scheme, host, port, basepath = parse_server_url(url=server_url) - basepath = "/" if basepath == "" else basepath - self.specification["host"] = f"{host}:{port}" - self.specification["schemes"] = [scheme] - self.specification["basePath"] = basepath - - try: - validate(spec=self.specification, base_uri=base_uri) - self.valid = True - except Exception as e: - logger.warning("OAS/Swagger file is invalid!") - logger.error( - "Failed to validate spec %s due to err: %s", file_or_url, repr(e) - ) - self.valid = False - - self.hosts = [] - - def _get_oas_version(self): - if self.specification.get("openapi"): - return 3 - elif self.specification.get("swagger"): - return 2 - raise InvalidSpecVersion("only openapi and swagger specs are supported for now") - - def _get_endpoints(self): - """Returns list of endpoint paths along with HTTP methods allowed""" - endpoints = [] - - for endpoint in self.specification.get("paths", {}).keys(): - methods = list(self.specification["paths"][endpoint].keys()) - if "parameters" in methods: - methods.remove("parameters") - endpoints.append((endpoint, methods)) - - return endpoints - - def _get_endpoint_details_for_fuzz_test(self): - return self.specification.get("paths") diff --git a/src/offat/parsers/swagger.py b/src/offat/parsers/swagger.py deleted file mode 100644 index 1f6b8d8..0000000 --- a/src/offat/parsers/swagger.py +++ /dev/null @@ -1,137 +0,0 @@ -""" -module to parse Swagger v2 documentation JSON/YAML files. -""" -from .parser import BaseParser -from ..logger import logger - - -class InvalidSwaggerFile(Exception): - """Exception to be raised when swagger spec validation fails""" - - -class SwaggerParser(BaseParser): - """Swagger Spec file Parser""" - - # while adding new method to this class, make sure same method is present in OpenAPIv3Parser class - - def __init__( - self, fpath_or_url: str, spec: dict | None = None, *args, **kwargs - ) -> None: - super().__init__(file_or_url=fpath_or_url, spec=spec, *args, **kwargs) # noqa - if self.is_v3: - raise InvalidSwaggerFile('Invalid OAS v3 file') - - self._populate_hosts() - self.http_scheme = self._get_scheme() - self.api_base_path = self.specification.get('basePath', '') - self.base_url = f"{self.http_scheme}://{self.host}" - self.request_response_params = self._get_request_response_params() - - # security schemes - self.security_schemes = self._get_security_schemes() - - def _populate_hosts(self): - host = self.specification.get('host') - if not host: - logger.error('Invalid Host: Host is missing') - raise InvalidSwaggerFile('Host Not Found in spec file') - hosts = [host] - self.hosts = hosts - self.host = self.hosts[0] - - def _get_scheme(self): - scheme = 'https' if 'https' in self.specification.get('schemes', []) else 'http' - return scheme - - def _get_param_definition_schema(self, param: dict): - """Returns Model defined schema for the passed param""" - param_schema = param.get('schema') - - # replace schema $ref with model params - if param_schema: - param_schema_ref = param_schema.get('$ref') - - if param_schema_ref: - model_slug = param_schema_ref.split('/')[-1] - param_schema = self.specification.get('definitions', {}).get(model_slug) - - return param_schema - - def _get_response_definition_schema(self, responses: dict): - """returns schema of API response - - Args: - responses (dict): responses from path http method json data - - Returns: - dict: - """ - for status_code in responses.keys(): - status_code_response = responses[status_code].keys() - if 'parameters' in status_code_response: - responses[status_code]['schema'] = responses[status_code]['parameters'] - elif 'schema' in status_code_response: - responses[status_code]['schema'] = self._get_param_definition_schema( - responses[status_code] - ) - else: - continue - - return responses - - def _get_request_response_params(self): - """Returns Schema of requests and response params - - Args: - None - - Returns: - list: - """ - requests = [] - paths = self.specification.get('paths', {}) - - # extract endpoints and supported params - for path in paths.keys(): - path_params = paths[path].get('parameters', []) - - for http_method in paths.get(path, {}).keys(): - # consider only http methods - if http_method not in ['get', 'put', 'post', 'delete', 'options']: - continue - - # below var contains overall params - request_parameters = paths[path][http_method].get('parameters', []) - security = paths[path][http_method].get('security', []) - response_params = self._get_response_definition_schema( - paths[path][http_method].get('responses', {}) - ) - - # create list of parameters: Fetch object schema from OAS file - for param in request_parameters: - param['schema'] = self._get_param_definition_schema(param) - - requests.append( - { - 'http_method': http_method, - 'path': path, - 'request_params': request_parameters, - 'response_params': response_params, - 'path_params': path_params, - 'security': security, - } - ) - - return requests - - def _get_security_schemes(self): - """ - Retrieves the security schemes defined in the OpenAPI specification. - - Returns: - dict: A dictionary containing the security schemes defined in the specification. - """ - security_schemes = self.specification.get('securityDefinitions', {}) - if not security_schemes: - logger.warning('Security schemes not found in the OpenAPI specification.') - return security_schemes diff --git a/src/offat/report/__init__.py b/src/offat/report/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/offat/report/generator.py b/src/offat/report/generator.py deleted file mode 100644 index 8f95bfc..0000000 --- a/src/offat/report/generator.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -Report Generator utils -""" -from copy import deepcopy -from html import escape -from json import dumps as json_dumps -from os.path import dirname, join as path_join -from os import makedirs -from rich.table import Table -from yaml import dump as yaml_dump - -from .templates.table import TestResultTable -from ..logger import logger, console -from ..report import templates - - -class ReportGenerator: - """can generate report in json,yaml,table and html formats""" - - @staticmethod - def generate_html_report(results: list[dict]): - """generates html report from OFFAT results""" - html_report_template_file_name = 'report.html' - html_report_file_path = path_join( - dirname(templates.__file__), html_report_template_file_name - ) - - with open(html_report_file_path, 'r', encoding='utf-8') as f: - report_file_content = f.read() - - # TODO: validate report data to avoid HTML injection attacks. - if not isinstance(results, list): - raise ValueError('results arg expects a list[dict].') - - # HTML escape data - escaped_results = [] - escape_keys = ['response_body'] - for result_dict in results: - escaped_result_dict = {} - for key, value in result_dict.items(): - if key in escape_keys: - escaped_value = escape(value) - escaped_result_dict[key] = escaped_value - else: - escaped_result_dict[key] = value - - escaped_results.append(escaped_result_dict) - - report_file_content = report_file_content.replace( - '{ results }', json_dumps(escaped_results) - ) - - return report_file_content - - @staticmethod - def handle_report_format( - results: list[dict], report_format: str | None - ) -> str | Table: - """returns report in specified format""" - result = None - - match report_format: - case 'html': - logger.warning('HTML output format displays only basic data.') - result = ReportGenerator.generate_html_report(results=results) - case 'yaml': - logger.warning( - 'YAML output format needs to be sanitized before using it further.' - ) - result = yaml_dump( - { - 'results': results, - } - ) - case 'json': - report_format = 'json' - result = json_dumps( - { - 'results': results, - } - ) - case _: # default: CLI table - # TODO: filter failed requests first and then create new table for failed requests - report_format = 'table' - results_table = TestResultTable().generate_result_table( - deepcopy(results) - ) - result = results_table - - logger.info('Generated %s format report.', report_format.upper()) - return result - - @staticmethod - def save_report(report_path: str | None, report_file_content: str | Table | None): - """saves/prints report to console""" - if report_path != '/' and report_path: - dir_name = dirname(report_path) - if dir_name != '' and report_path: - makedirs(dir_name, exist_ok=True) - - # print to cli if report path and file content as absent else write to file location. - if ( - report_path - and report_file_content - and not isinstance(report_file_content, Table) - ): - with open(report_path, 'w', encoding='utf-8') as f: - logger.info('Writing report to file: %s', report_path) - f.write(report_file_content) - else: - if isinstance(report_file_content, Table) and report_file_content.columns: - TestResultTable().print_table(report_file_content) - elif ( - isinstance(report_file_content, Table) - and not report_file_content.columns - ): - logger.warning('No Columns found in Table.') - else: - console.print(report_file_content) - - @staticmethod - def generate_report( - results: list[dict], - report_format: str | None, - report_path: str | None, - capture_failed: bool = False, - ): - """main function used to generate report""" - if report_path: - report_format = report_path.split('.')[-1] - - # do not store errored results if `capture_failed` is False - if not capture_failed: - results = list( - filter(lambda result: result.get('error', True) is False, results) - ) - - formatted_results = ReportGenerator.handle_report_format( - results=results, report_format=report_format - ) - ReportGenerator.save_report( - report_path=report_path, report_file_content=formatted_results - ) diff --git a/src/offat/report/summary.py b/src/offat/report/summary.py deleted file mode 100644 index a46027b..0000000 --- a/src/offat/report/summary.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -OWASP OFFAT summarizer class module -""" -from rich.table import Table, Column - - -class ResultSummarizer: - """class for summarizing results""" - - @staticmethod - def get_counts(results: list[dict], filter_errors: bool = False) -> dict[str, int]: - """ - Processes results and returns test summary of errored, succeeded, failed - and data leak endpoint results count. - - Args: - results (list): OFFAT results list of dict - filter_errors (bool): filters errored results before processing count - if True. Default value: False - - Returns: - dict: name (str) as key and its associated count (int) - """ - if filter_errors: - results = list(filter(lambda result: result.get('error', False), results)) - - error_count = 0 - data_leak_count = 0 - immune_count = 0 - vulnerable_count = 0 - for result in results: - error_count += 1 if result.get('error', False) else 0 - data_leak_count += 1 if result.get('data_leak', False) else 0 - - if result.get('vulnerable'): - vulnerable_count += 1 - else: - immune_count += 1 - - count_dict = { - 'errors': error_count, - 'data_leaks': data_leak_count, - 'immune': immune_count, - 'vulnerable': vulnerable_count, - } - - return count_dict - - @staticmethod - def generate_count_summary( - results: list[dict], - filter_errors: bool = False, - output_format: str = 'table', - table_title: str | None = None, - ) -> Table | str: - """ - Processes results and returns test summary of errored, succeeded, failed - and data leak endpoint results count. - - Args: - results (list): OFFAT results list of dict - filter_errors (bool): filters errored results before processing count - if True. Default value: False - output_format (str): expected output format (table, markdown) - - Returns: - rich.Table | str : returns summary in expected format - """ - count_summary = ResultSummarizer.get_counts( - results=results, filter_errors=filter_errors - ) - match output_format: - case 'markdown': - output = '' - if table_title: - output += f"**{table_title}**\n" - - for key, count in count_summary.items(): - output += f"{key:<15}:\t{count}\n" - - case _: # table format - output = Table( - Column(header='⚔️', overflow='fold', justify='center'), - Column(header='Endpoints Count', overflow='fold'), - title=table_title, - ) - - for key, count in count_summary.items(): - output.add_row(*[key, str(count)]) - - return output diff --git a/src/offat/report/templates/__init__.py b/src/offat/report/templates/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/offat/report/templates/report.html b/src/offat/report/templates/report.html deleted file mode 100644 index 16f2333..0000000 --- a/src/offat/report/templates/report.html +++ /dev/null @@ -1,256 +0,0 @@ - - - - - - - OWASP OFFAT - - - - - - -
    -
    -
    - -
    -
    -
    -
    Request
    -
    -
    - {{request}} -
    -
    -
    -
    -
    Response
    -
    -
    - {{response}} -
    -
    -
    -
    - - -
    -
    -
    -
    Test Name:
    -
    -
    -
    -
    Test Result:
    -
    -
    -
    -
    -
    -
    Result Details:
    -
    -
    -
    -
    Test Response Filter:
    -
    -
    -
    -
    -
    -
    Data Leak:
    -
    No Data Leak Found
    -
    -
    -
    - -
    - - - - -
    - - - - - - - - - - - - \ No newline at end of file diff --git a/src/offat/report/templates/table.py b/src/offat/report/templates/table.py deleted file mode 100644 index 3f17b62..0000000 --- a/src/offat/report/templates/table.py +++ /dev/null @@ -1,102 +0,0 @@ -from rich.table import Table, Column -from ...logger import console - - -class TestResultTable: - def __init__( - self, - table_width_percentage: float = 98, - ) -> None: - self.console = console - self.table_width_percentage = table_width_percentage - - def print_table(self, table: Table): - terminal_width = console.width - table_width = int(terminal_width * (self.table_width_percentage / 100)) - table.width = table_width - - self.console.print(table) - self.console.rule() - - def extract_result_table_cols(self, results: list[dict]) -> list[str]: - return sorted({key for dictionary in results for key in dictionary.keys()}) - - def generate_result_cols(self, results_list: list[dict]) -> list[Column]: - return [ - Column(header=col_header, overflow='fold') - for col_header in self.extract_result_table_cols(results_list) - ] - - def generate_result_table(self, results: list, filter_passed_results: bool = True): - results = self._sanitize_results(results, filter_passed_results) - cols = self.generate_result_cols(results) - table = Table(*cols) - - for result in results: - table_row = [] - for col in cols: - table_row.append(str(result.get(col.header, '[red]:bug: - [/red]'))) - table.add_row(*table_row) - - return table - - def _sanitize_results( - self, - results: list, - filter_passed_results: bool = True, - is_leaking_data: bool = False, - ): - if filter_passed_results: - results = list( - filter( - lambda result: result.get('vulnerable') or result.get('data_leak'), - results, - ) - ) - - keys_to_remove = [ - 'url', - 'test_name', - 'response_filter', - 'body_params', - 'request_headers', - 'redirection', - 'query_params', - 'path_params', - 'curl_command', - 'response_match_regex', - 'regex_match_result', - 'success_codes', - ] - - # remove keys based on conditions or update their values - for result in results: - if result['vulnerable']: - result['vulnerable'] = '[bold red]True \u00d7[/bold red]' - else: - result['vulnerable'] = '[bold green]False \u2713[/bold green]' - - if not is_leaking_data: - del result['response_headers'] - del result['response_body'] - - if result.get('response_status_code'): - result['status_code'] = result.get('response_status_code') - del result['response_status_code'] - - if result.get('security') or result.get('security') == []: - del result['security'] - - if result.get('data_leak'): - result['data_leak'] = '[bold red]Leak Found \u00d7[/bold red]' - else: - result['data_leak'] = '[bold green]No Leak \u2713[/bold green]' - - if not isinstance(result.get('malicious_payload'), str): - del result['malicious_payload'] - - for key in keys_to_remove: - if key in result: - del result[key] - - return results diff --git a/src/offat/tester/__init__.py b/src/offat/tester/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/offat/tester/fuzzer.py b/src/offat/tester/fuzzer.py deleted file mode 100644 index ff8c086..0000000 --- a/src/offat/tester/fuzzer.py +++ /dev/null @@ -1,144 +0,0 @@ -import random -import string - - -def generate_random_int(max_value: int = 100): - """Generate Random Integer value between specified maximum value - note: maximum_value is not consider in range""" - return random.randint(0, max_value) - - -def generate_phone_number(): - """Generate Random 10 digit phone number starting with 72""" - return "72" + "".join(random.choice(string.digits) for _ in range(8)) - - -def generate_random_chars(length): - """Generate a random string of given length containing characters only.""" - characters = string.ascii_letters - return "".join(random.choice(characters) for _ in range(length)) - - -def generate_random_char_digits(length): - """Generate a random string of given length containing characters and digits only.""" - characters = string.ascii_letters + string.digits - return "".join(random.choice(characters) for _ in range(length)) - - -def generate_random_string(length): - """Generate a random string of given length.""" - characters = string.ascii_letters + string.digits + string.punctuation - return "".join(random.choice(characters) for _ in range(length)) - - -def fuzz_string_type(var_name: str): - var_name_lower = str(var_name).lower() - if "email" in var_name_lower: - var_value = generate_random_char_digits(6).lower() + "@example.com" - elif "password" in var_name_lower: - var_value = generate_random_string(15) - elif "phone" in var_name_lower: - var_value = generate_phone_number() - elif "name" in var_name_lower: - var_value = generate_random_chars(7) - elif "username" in var_name_lower: - var_value = generate_random_char_digits(6) - else: - var_value = generate_random_string(10) - - return var_value - - -def fill_schema_params( - params: dict[dict], param_in: str = None, is_required: bool = None -): - schema_params = [] - for var_name, var_data in params.items(): - var_type = var_data.get("type") - - match var_type: - case "string": - var_value = fuzz_string_type(var_name) - - case "integer": - var_value = generate_random_int() - - case _: - var_value = generate_random_string(10) - - var_data["value"] = var_value - var_data["name"] = var_name - - if is_required: - var_data["required"] = is_required - - if param_in: - var_data["in"] = param_in - - schema_params.append(var_data) - - return schema_params - - -def fuzz_type_value(param_type: str, param_name: str): - match param_type: - case "string": - param_value = fuzz_string_type(param_name) - - case "integer": - param_value = generate_random_int() - - # TODO: handle file and array type - - case _: # default case - param_value = generate_random_string(10) - - return param_value - - -def fill_params(params: list[dict], is_v3: bool) -> list[dict]: - """fills params for OAS/swagger specs""" - schema_params = [] - for index, _ in enumerate(params): - param_type = ( - params[index].get("schema", {}).get("type") - if is_v3 - else params[index].get("type") - ) - param_is_required = params[index].get("required") - param_in = params[index].get("in") - param_name = params[index].get("name", "") - - param_value = fuzz_type_value(param_type=param_type, param_name=param_name) - - if params[index].get("schema"): - schema_type = params[index].get("schema", {}).get("type") - if schema_type == "object": - schema_obj = params[index].get("schema", {}).get("properties", {}) - filled_schema_params = fill_schema_params( - schema_obj, param_in, param_is_required - ) - else: - filled_schema_params = [ - { - "in": param_in, - "name": param_name, - "required": param_is_required, - "value": param_value, - "type": param_type - } - ] - - schema_params.append(filled_schema_params) - else: - params[index]["value"] = param_value - - # delete schema params - for param in params: - if param.get("schema"): - params.remove(param) - - for schema_param in schema_params: - params += schema_param - - return params diff --git a/src/offat/tester/generator.py b/src/offat/tester/generator.py deleted file mode 100644 index 3c990dd..0000000 --- a/src/offat/tester/generator.py +++ /dev/null @@ -1,1084 +0,0 @@ -''' -This module contains the TestGenerator class which is used to generate API test checks. -''' -from copy import deepcopy -from .fuzzer import fill_params -from .post_test_processor import PostTestFiltersEnum -from .fuzzer import generate_random_int -from ..config_data_handler import populate_user_data -from ..parsers import SwaggerParser, OpenAPIv3Parser -from ..utils import join_uri_path, get_unique_params - - -class TestGenerator: - """ - Class to generate API test checks. - - This class provides methods to generate API test checks for various scenarios. - - Attributes: - None - - Methods: - check_unsupported_http_methods: Checks whether endpoint supports - undocumented/unsupported HTTP methods. - sqli_fuzz_params: Performs SQL injection (SQLi) parameter fuzzing - based on the provided OpenAPIParser instance. - """ - - def __init__(self, headers: dict = None) -> None: # type: ignore - """ - Initializes an instance of the TestGenerator class. - - Args: - headers (dict, optional): A dictionary of headers to be set - for the instance. Defaults to None. - - Returns: - None - - Example: - headers = {"Content-Type": "application/json", "Authorization": "Bearer xyz123"} - tester = TestGenerator(headers) - """ - self._headers = headers - - def check_unsupported_http_methods( - self, - openapi_parser: SwaggerParser | OpenAPIv3Parser, - *args, - success_codes: list[int] | None = None, - **kwargs, - ): - '''Checks whether endpoint supports undocumented/unsupported HTTP methods - - Args: - base_url (str): The base URL to check for unsupported HTTP methods. - endpoints (list[tuple]): A list of tuples representing the endpoints - to check. Each tuple should contain the endpoint path and the - corresponding supported HTTP methods. - success_codes (list[int], optional): A list of HTTP success codes to consider as - successful responses. Defaults to [ 200, 201, 301, 302 ]. - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - None - - Raises: - Any exceptions raised during the execution. - ''' - if success_codes is None: - success_codes = [200, 201, 301, 302] - - tasks = [] - fuzzed_endpoints = self.__fuzz_request_params(openapi_parser) - endpoints_index = {} - - for fuzzed_endpoint_data in fuzzed_endpoints: - endpoint = fuzzed_endpoint_data['endpoint'] - method = fuzzed_endpoint_data['method'] - - if endpoint not in endpoints_index: - endpoints_index[endpoint] = { - 'endpoints': [], - 'methods': [], - 'body_params': [], - 'query_params': [], - 'path_params': [], - } - - endpoints_index[endpoint]['endpoints'].append(fuzzed_endpoint_data) - if method not in endpoints_index[endpoint]['methods']: - endpoints_index[endpoint]['methods'].append(method.lower()) - - endpoints_index[endpoint]['body_params'].extend( - fuzzed_endpoint_data['body_params'] - ) - endpoints_index[endpoint]['query_params'].extend( - fuzzed_endpoint_data['query_params'] - ) - endpoints_index[endpoint]['path_params'].extend( - fuzzed_endpoint_data['path_params'] - ) - - for endpoint, endpoint_dict in endpoints_index.items(): - methods_allowed = endpoint_dict.get('methods', []) - body_params = endpoint_dict.get('body_params', []) - path_params = endpoint_dict.get('path_params', []) - query_params = endpoint_dict.get('query_params', []) - url = join_uri_path(openapi_parser.base_url, endpoint) - - http_methods: set = {'get', 'post', 'put', 'patch', 'delete', 'options'} - restricted_methods = http_methods - set(methods_allowed) - - for restricted_method in restricted_methods: - tasks.append( - { - 'test_name': 'UnSupported HTTP Method Check', - 'url': url, - 'endpoint': endpoint, - 'method': restricted_method.upper(), - 'malicious_payload': [], - 'args': args, - 'kwargs': kwargs, - 'vuln_details': { - True: 'Endpoint performs HTTP verb which is not documented', - False: "Endpoint doesn't perform any HTTP verb which is not documented", - }, - 'body_params': body_params, - 'query_params': query_params, - 'path_params': path_params, - 'success_codes': success_codes, - 'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name, - } - ) - - return tasks - - def __fuzz_request_params( - self, openapi_parser: SwaggerParser | OpenAPIv3Parser - ) -> list[dict]: - """ - Fuzzes Request params available in different positions and returns a list - of tasks - - Args: - openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class - containing the parsed OpenAPI specification. - - Returns: - list: returns list of dict (tasks) for API testing with fuzzed request params - """ - base_url: str = openapi_parser.base_url - request_response_params: list[dict] = openapi_parser.request_response_params - - tasks = [] - for path_obj in request_response_params: - # handle path params from request_params - request_params = path_obj.get('request_params', []) - request_params = fill_params(request_params, openapi_parser.is_v3) - security = path_obj.get('security', []) - - # get params based on their position in request - request_body_params = list( - filter(lambda x: x.get('in') == 'body', request_params) - ) - request_query_params = list( - filter(lambda x: x.get('in') == 'query', request_params) - ) - path_params_in_body = list( - filter(lambda x: x.get('in') == 'path', request_params) - ) - - # get endpoint path - endpoint_path: str = path_obj.get('path') # type: ignore - - # get path params and fill them - path_params = path_obj.get('path_params', []) - path_params = fill_params(path_params, openapi_parser.is_v3) - - # get unique path params - path_params = get_unique_params(path_params, path_params_in_body) - - for path_param in path_params: - path_param_name = path_param.get('name') - path_param_value = path_param.get('value') - - endpoint_path = endpoint_path.replace( - '{' + str(path_param_name) + '}', str(path_param_value) - ) - - tasks.append( - { - 'url': join_uri_path( - base_url, openapi_parser.api_base_path, endpoint_path - ), - 'endpoint': join_uri_path( - openapi_parser.api_base_path, endpoint_path - ), - 'method': path_obj.get('http_method', '').upper(), - 'body_params': request_body_params, - 'query_params': request_query_params, - 'path_params': path_params, - 'security': security, - } - ) - - return tasks - - def __inject_payload_in_params(self, request_params: list[dict], payload: str): - """ - Injects payload into the request parameters. - - This method modifies the provided request parameters by injecting the SQLi payload. - - Args: - request_params (list[dict]): A list of dictionaries representing the request parameters. - payload (str): The injection payload to be injected into the request parameters. - - Returns: - list: returns list of sqli injection parameters for API testing - """ - request_params = deepcopy(request_params) - - # inject sqli payload as param value - for request_param_data in request_params: - # TODO: inject sqli payloads in other data types as well - if request_param_data.get('type') == 'string': - request_param_data['value'] = payload - - return request_params - - def sqli_fuzz_params_test( - self, - openapi_parser: SwaggerParser | OpenAPIv3Parser, - *args, - success_codes: list[int] | None = None, - **kwargs, - ): - '''Performs SQL injection (SQLi) parameter fuzzing based on the - provided OpenAPIParser instance. - - Args: - openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class - containing the parsed OpenAPI specification. - success_codes (list[int], optional): A list of HTTP success codes to - consider as successful SQLi responses. Defaults to [500]. - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - List: List of dictionaries containing tests for SQLi - - Raises: - Any exceptions raised during the execution. - ''' - if success_codes is None: - success_codes = [500] - - # APPROACH: first send sqli in all params, if error is generated - # then enumerate one by one or ask user to pentest manually using - # sqlmap - tasks = [] - basic_sqli_payloads = [ - "' OR 1=1 ;--", - "' UNION SELECT 1,2,3 -- -", - "' OR '1'='1--", - "' AND (SELECT * FROM (SELECT(SLEEP(5)))abc)", - "' AND SLEEP(5) --", - ] - - fuzzed_request_list = self.__fuzz_request_params(openapi_parser) - - # inject SQLi payloads in string variables - for sqli_payload in basic_sqli_payloads: - for request_obj in fuzzed_request_list: - # handle body request params - body_request_params = request_obj.get('body_params', []) - malicious_body_request_params = self.__inject_payload_in_params( - body_request_params, sqli_payload - ) - - # handle query request params - query_request_params = request_obj.get('query_params', []) - malicious_query_request_params = self.__inject_payload_in_params( - query_request_params, sqli_payload - ) - - # BUG: for few SQLi test, path params injected value is not matching - # with final URI path params in output - request_obj['test_name'] = 'SQLi Test' - - request_obj['body_params'] = malicious_body_request_params - request_obj['query_params'] = malicious_query_request_params - request_obj['args'] = args - request_obj['kwargs'] = kwargs - - request_obj['malicious_payload'] = sqli_payload - - request_obj['vuln_details'] = { - True: 'One or more parameter is vulnerable to SQL Injection Attack', - False: 'Parameters are not vulnerable to SQLi Payload', - } - request_obj['success_codes'] = success_codes - request_obj[ - 'response_filter' - ] = PostTestFiltersEnum.STATUS_CODE_FILTER.name - tasks.append(deepcopy(request_obj)) - - return tasks - - def sqli_in_uri_path_fuzz_test( - self, - openapi_parser: SwaggerParser | OpenAPIv3Parser, - *args, - success_codes: list[int] | None = None, - **kwargs, - ): - '''Generate Tests for SQLi in endpoint path - - Args: - openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class - containing the parsed OpenAPI specification. - success_codes (list[int], optional): A list of HTTP success codes to - consider as successful BOLA responses. Defaults to [200, 201, 301]. - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - list[dict]: list of dict containing test case for endpoint - - Raises: - Any exceptions raised during the execution. - ''' - if success_codes is None: - success_codes = [500] - - base_url: str = openapi_parser.base_url - request_response_params: list[dict] = openapi_parser.request_response_params - - # filter path containing params in path - endpoints_with_param_in_path = list( - filter( - lambda path_obj: '/{' in path_obj.get('path'), # type: ignore - request_response_params, # type: ignore - ) # type: ignore - ) - - basic_sqli_payloads = [ - "' OR 1=1 ;--", - "' UNION SELECT 1,2,3 -- -", - "' OR '1'='1--", - "' AND (SELECT * FROM (SELECT(SLEEP(5)))abc)", - "' AND SLEEP(5) --", - ] - - tasks = [] - for sqli_payload in basic_sqli_payloads: - for path_obj in endpoints_with_param_in_path: - # handle path params from request_params - request_params = path_obj.get('request_params', []) - request_params = fill_params(request_params, openapi_parser.is_v3) - - # get request body params - request_body_params = list( - filter(lambda x: x.get('in') == 'body', request_params) - ) - - # handle path params from path_params - # and replace path params by value in - # endpoint path - endpoint_path: str = path_obj.get('path') - - path_params = path_obj.get('path_params', []) - path_params_in_body = list( - filter(lambda x: x.get('in') == 'path', request_params) - ) - path_params += path_params_in_body - path_params = fill_params(path_params, openapi_parser.is_v3) - - for path_param in path_params: - path_param_name = path_param.get('name') - endpoint_path = endpoint_path.replace( - '{' + str(path_param_name) + '}', str(sqli_payload) - ) - - request_query_params = list( - filter(lambda x: x.get('in') == 'query', request_params) - ) - - tasks.append( - { - 'test_name': 'SQLi Test in URI Path with Fuzzed Params', - 'url': join_uri_path( - base_url, openapi_parser.api_base_path, endpoint_path - ), - 'endpoint': join_uri_path( - openapi_parser.api_base_path, endpoint_path - ), - 'method': path_obj.get('http_method').upper(), - 'body_params': request_body_params, - 'query_params': request_query_params, - 'path_params': path_params, - 'malicious_payload': sqli_payload, - 'args': args, - 'kwargs': kwargs, - 'vuln_details': { - True: 'Endpoint might be vulnerable to SQli', - False: 'Endpoint is not vulnerable to SQLi', - }, - 'success_codes': success_codes, - 'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name, - } - ) - - return tasks - - def bola_fuzz_path_test( - self, - openapi_parser: SwaggerParser | OpenAPIv3Parser, - *args, - success_codes: list[int] | None = None, # type: ignore - **kwargs, - ): - '''Generate Tests for BOLA in endpoint path - - Args: - openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class - containing the parsed OpenAPI specification. - success_codes (list[int], optional): A list of HTTP success codes to consider - as successful BOLA responses. Defaults to [200, 201, 301]. - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - list[dict]: list of dict containing test case for endpoint - - Raises: - Any exceptions raised during the execution. - ''' - if success_codes is None: - success_codes: list[int] = [200, 201, 301] - - base_url: str = openapi_parser.base_url - request_response_params: list[dict] = openapi_parser.request_response_params - - # filter path containing params in path - endpoints_with_param_in_path = list( - filter( - lambda path_obj: '/{' in path_obj.get('path'), request_response_params # type: ignore - ) # type: ignore - ) - - tasks = [] - for path_obj in endpoints_with_param_in_path: - # handle path params from request_params - request_params = path_obj.get('request_params', []) - request_params = fill_params(request_params, openapi_parser.is_v3) - - # get request body params - request_body_params = list( - filter(lambda x: x.get('in') == 'body', request_params) - ) - - endpoint_path: str = path_obj.get('path') - - path_params = path_obj.get('path_params', []) - path_params_in_body = list( - filter(lambda x: x.get('in') == 'path', request_params) - ) - path_params = fill_params(path_params, openapi_parser.is_v3) - path_params = get_unique_params(path_params_in_body, path_params) - - for path_param in path_params: - path_param_name = path_param.get('name') - path_param_value = path_param.get('value') - endpoint_path = endpoint_path.replace( - '{' + str(path_param_name) + '}', str(path_param_value) - ) - - request_query_params = list( - filter(lambda x: x.get('in') == 'query', request_params) - ) - - tasks.append( - { - 'test_name': 'BOLA Path Test with Fuzzed Params', - 'url': join_uri_path( - base_url, openapi_parser.api_base_path, endpoint_path - ), - 'endpoint': join_uri_path( - openapi_parser.api_base_path, endpoint_path - ), - 'method': path_obj.get('http_method').upper(), - 'body_params': request_body_params, - 'query_params': request_query_params, - 'path_params': path_params, - 'malicious_payload': path_params, - 'args': args, - 'kwargs': kwargs, - 'vuln_details': { - True: 'Endpoint might be vulnerable to BOLA', - False: 'Endpoint is not vulnerable to BOLA', - }, - 'success_codes': success_codes, - 'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name, - } - ) - - return tasks - - def bola_fuzz_trailing_slash_path_test( - self, - openapi_parser: SwaggerParser | OpenAPIv3Parser, - *args, - success_codes: list[int] | None = None, # type: ignore - **kwargs, - ): - '''Generate Tests for BOLA in endpoint path - - Args: - openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class - containing the parsed OpenAPI specification. - success_codes (list[int], optional): A list of HTTP success codes to - consider as successful BOLA responses. Defaults to [200, 201, 301]. - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - list[dict]: list of dict containing test case for endpoint - - Raises: - Any exceptions raised during the execution. - ''' - if success_codes is None: - success_codes: list[int] = [200, 201, 301] - - base_url: str = openapi_parser.base_url - request_response_params: list[dict] = openapi_parser.request_response_params - - tasks = [] - for path_obj in request_response_params: - # handle path params from request_params - request_params = path_obj.get('request_params', []) - request_params = fill_params(request_params, openapi_parser.is_v3) - - # get params based on their position in request - request_body_params = list( - filter(lambda x: x.get('in') == 'body', request_params) - ) - request_query_params = list( - filter(lambda x: x.get('in') == 'query', request_params) - ) - path_params_in_body = list( - filter(lambda x: x.get('in') == 'path', request_params) - ) - - # get endpoint path - endpoint_path: str = path_obj.get('path') # type: ignore - - # get path params and fill them - path_params = path_obj.get('path_params', []) - path_params = fill_params(path_params, openapi_parser.is_v3) - - # get unique path params - path_params = get_unique_params(path_params, path_params_in_body) - - for path_param in path_params: - path_param_name = path_param.get('name') - path_param_value = path_param.get('value') - endpoint_path = endpoint_path.replace( - '{' + str(path_param_name) + '}', str(path_param_value) - ) - - # generate URL for BOLA attack - url = join_uri_path(base_url, openapi_parser.api_base_path, endpoint_path) - malicious_payload = generate_random_int() - if url.endswith('/'): - url = f'{url}{malicious_payload}' - else: - url = f'{url}/{malicious_payload}' - - tasks.append( - { - 'test_name': 'BOLA Path Trailing Slash Test', - 'url': url, - 'endpoint': join_uri_path( - openapi_parser.api_base_path, endpoint_path - ), - 'method': path_obj.get('http_method').upper(), # type: ignore - 'body_params': request_body_params, - 'query_params': request_query_params, - 'path_params': path_params, - 'malicious_payload': malicious_payload, - 'args': args, - 'kwargs': kwargs, - 'vuln_details': { - True: 'Endpoint might be vulnerable to BOLA', - False: 'Endpoint might not vulnerable to BOLA', - }, - 'success_codes': success_codes, - 'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name, - } - ) - - return tasks - - def _inject_response_params(self, response_params: dict, is_v3: bool = False): - '''Populate response params in body params for testing - BOPLA attacks. - - Args: - body_params ([dict]) : dict of response from openapi documentation - {'200':{'properties':{'schema':{'test_param':{'type':'str'}}}}} - - Returns: - list[dict]: list of dict containing test case for endpoint - - Raises: - Any exceptions raised during the execution. - ''' - # create list for data - params = [] - - for status_code, response_data in response_params.items(): - properties = response_data.get('schema', {}).get('properties', {}) - for name, param_data in properties.items(): - param_data['name'] = name - param_data['in'] = 'body' - param_data['status_code'] = status_code - params.append(deepcopy(param_data)) - - # fuzz data - params = fill_params(params, is_v3) - return params - - def bopla_fuzz_test( - self, - openapi_parser: SwaggerParser | OpenAPIv3Parser, - *args, - success_codes: list[int] | None = None, # type: ignore - **kwargs, - ): - '''Generate Tests for BOPLA/Mass Assignment Vulnerability - - Args: - openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class - containing the parsed OpenAPI specification. - success_codes (list[int], optional): A list of HTTP success codes to - consider as successful BOLA responses. Defaults to [200, 201, 301]. - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - list[dict]: list of dict containing test case for endpoint - - Raises: - Any exceptions raised during the execution. - ''' - if success_codes is None: - success_codes: list[int] = [200, 201, 301] - - base_url: str = openapi_parser.base_url - request_response_params: list[dict] = openapi_parser.request_response_params - - tasks = [] - for path_obj in request_response_params: - # handle path params from request_params - request_params = path_obj.get('request_params', []) - request_params = fill_params(request_params, openapi_parser.is_v3) - - # get params based on their position in request - request_body_params = list( - filter(lambda x: x.get('in') == 'body', request_params) - ) - request_query_params = list( - filter(lambda x: x.get('in') == 'query', request_params) - ) - path_params_in_body = list( - filter(lambda x: x.get('in') == 'path', request_params) - ) - - if len(request_body_params) == 0 and len(request_query_params) == 0: - continue - - # handle path params from path_params - # and replace path params by value in - # endpoint path - endpoint_path: str = path_obj.get('path') # type: ignore - path_params = path_obj.get('path_params', []) - path_params = fill_params(path_params, openapi_parser.is_v3) - path_params = get_unique_params(path_params_in_body, path_params) - - for path_param in path_params: - path_param_name = path_param.get('name') - path_param_value = path_param.get('value') - endpoint_path = endpoint_path.replace( - '{' + str(path_param_name) + '}', str(path_param_value) - ) - - # assign values to response params below and add them to JSON request body - response_body_params = self._inject_response_params( - path_obj.get('response_params', []), - openapi_parser.is_v3, - ) - request_body_params += response_body_params - - tasks.append( - { - 'test_name': 'BOPLA Test', - 'url': join_uri_path( - base_url, openapi_parser.api_base_path, endpoint_path - ), - 'endpoint': join_uri_path( - openapi_parser.api_base_path, endpoint_path - ), - 'method': path_obj.get('http_method', '').upper(), - 'body_params': request_body_params, - 'query_params': request_query_params, - 'path_params': path_params, - 'malicious_payload': response_body_params, - 'args': args, - 'kwargs': kwargs, - 'vuln_details': { - True: 'Endpoint might be vulnerable to BOPLA', - False: 'Endpoint might not vulnerable to BOPLA', - }, - 'success_codes': success_codes, - 'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name, - } - ) - - return tasks - - def test_with_user_data( - self, - user_data: dict, - test_generator_method, - *args, - test_for_actor1: bool = True, - test_for_actor2: bool = False, - **kwargs, - ): - '''Generate Tests with user sepecified data using provided test generator method - - Args: - user_data (dict): User specified YAML data as dict. - test_generator_method (class method): test generator class method to be - used for generating API pentest tests. - test_for_actor1 (bool): Generate tests for actor1 user data - test_for_actor2 (bool): Generate tests for actor2 user data - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - list[dict]: list of dict containing test case for endpoint - - Raises: - Any exceptions raised during the execution. - ''' - # generate tests using test generator method - tests = test_generator_method(*args, **kwargs) - new_tests = [] - - actor1_data = user_data.get('actors', [])[0].get('actor1', {}) - actor2_data = user_data.get('actors', [])[1].get('actor2', {}) - - if test_for_actor1: - new_tests += populate_user_data(actor1_data, 'actor1', tests) - - if test_for_actor2: - new_tests += populate_user_data(actor2_data, 'actor2', tests) - - return new_tests - - def __generate_injection_fuzz_params_test( - self, - openapi_parser: SwaggerParser | OpenAPIv3Parser, - test_name: str, - vuln_details: dict, - payloads_data: list[dict], - *args, - **kwargs, - ): - '''Performs injection parameter fuzzing based on the provided OpenAPIParser instance - and matches injected payload using regex in response. - - Args: - openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class containing - the parsed OpenAPI specification. - payloads_data (list[dict]): list of dictionary containing malicious request payload - and regex for matching injection in response. - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - List: List of dictionaries containing tests for SQLi - - Raises: - Any exceptions raised during the execution. - ''' - # fuzz params - fuzzed_request_list = self.__fuzz_request_params(openapi_parser) - - # inject command injection payloads in string variables - tasks = [] - for payload_dict in payloads_data: - for request_obj in fuzzed_request_list: - payload = payload_dict['request_payload'] - body_request_params = request_obj.get('body_params', []) - query_request_params = request_obj.get('query_params', []) - # endpoint can be fuzzed if it has query/body params - if len(body_request_params) == 0 and len(query_request_params) == 0: - continue - - # handle body and query request params - malicious_body_request_params = self.__inject_payload_in_params( - body_request_params, payload - ) - malicious_query_request_params = self.__inject_payload_in_params( - query_request_params, payload - ) - request_obj['test_name'] = test_name - - request_obj['body_params'] = malicious_body_request_params - request_obj['query_params'] = malicious_query_request_params - request_obj['args'] = args - request_obj['kwargs'] = kwargs - - request_obj['malicious_payload'] = payload - - request_obj['vuln_details'] = vuln_details - request_obj[ - 'response_filter' - ] = PostTestFiltersEnum.BODY_REGEX_FILTER.name - request_obj['response_match_regex'] = payload_dict.get( - 'response_match_regex' - ) - - tasks.append(deepcopy(request_obj)) - - return tasks - - def os_command_injection_fuzz_params_test( - self, openapi_parser: SwaggerParser | OpenAPIv3Parser - ): - '''Performs OS Command injection parameter fuzzing based on the provided - OpenAPIParser instance. - - Args: - openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class containing the - parsed OpenAPI specification. - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - List: List of dictionaries containing tests for OS command injection - - Raises: - Any exceptions raised during the execution. - ''' - test_name = 'OS Command Injection Test' - - root_regex = r'root:.*' - payloads_data = [ - {'request_payload': 'cat /etc/passwd', 'response_match_regex': root_regex}, - {'request_payload': 'cat /etc/shadow', 'response_match_regex': root_regex}, - {'request_payload': 'ls -la', 'response_match_regex': r'total\s\d+'}, - ] - - vuln_details = { - True: 'One or more parameter is vulnerable to OS Command Injection Attack', - False: 'Parameters are not vulnerable to OS Command Injection', - } - - return self.__generate_injection_fuzz_params_test( - openapi_parser=openapi_parser, - test_name=test_name, - vuln_details=vuln_details, - payloads_data=payloads_data, - ) - - def xss_html_injection_fuzz_params_test( - self, openapi_parser: SwaggerParser | OpenAPIv3Parser - ): - '''Performs OS Command injection parameter fuzzing based on the - provided OpenAPIParser instance. - - Args: - openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class - containing the parsed OpenAPI specification. - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - List: List of dictionaries containing tests for XSS - - Raises: - Any exceptions raised during the execution. - ''' - test_name = 'XSS/HTML Injection Test' - - payloads_data = [ - { - 'request_payload': '', - 'response_match_regex': r']*>.*<\/script>', - }, - { - 'request_payload': '', - 'response_match_regex': r']*>.*<\/script>', - }, - { - 'request_payload': "]*>', - }, - ] - - vuln_details = { - False: 'Parameters are not vulnerable to XSS/HTML Injection Attack', - True: 'One or more parameter is vulnerable to XSS/HTML Injection Attack', - } - - return self.__generate_injection_fuzz_params_test( - openapi_parser=openapi_parser, - test_name=test_name, - vuln_details=vuln_details, - payloads_data=payloads_data, - ) - - def ssti_fuzz_params_test(self, openapi_parser: SwaggerParser | OpenAPIv3Parser): - '''Performs SSTI fuzzing based on the provided OpenAPIParser instance. - - Args: - openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class - containing the parsed OpenAPI specification. - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - List: List of dictionaries containing tests for SSTI - - Raises: - Any exceptions raised during the execution. - ''' - test_name = 'SSTI Test' - - payloads_data = [ - {'request_payload': r'${7777+99999}', 'response_match_regex': r'107776'}, - {'request_payload': r"{{7*'7'}}", 'response_match_regex': r'49'}, - {'request_payload': r"{{7*'7'}}", 'response_match_regex': r'7777777'}, - { - 'request_payload': r"{{ '' }}", - 'response_match_regex': r'', - }, - { - 'request_payload': r"{{ '' | safe }}", - 'response_match_regex': r'', - }, - { - 'request_payload': r"{{'owasp offat'.toUpperCase()}}", - 'response_match_regex': r'OWASP OFFAT', - }, - { - 'request_payload': r"{{'owasp offat' | upper }}", - 'response_match_regex': r'OWASP OFFAT', - }, - { - 'request_payload': r"<%= system('cat /etc/passwd') %>", - 'response_match_regex': r'root:.*', - }, - {'request_payload': r'*{7*7}', 'response_match_regex': r'49'}, - ] - - vuln_details = { - True: 'One or more parameter is vulnerable to SSTI Attack', - False: 'Parameters are not vulnerable to SSTI Attack', - } - - return self.__generate_injection_fuzz_params_test( - openapi_parser=openapi_parser, - test_name=test_name, - vuln_details=vuln_details, - payloads_data=payloads_data, - ) - - def missing_auth_fuzz_test( - self, - openapi_parser: SwaggerParser | OpenAPIv3Parser, - *args, - success_codes: list[int] | None = None, # type: ignore - **kwargs, - ): - '''Generate Tests for API endpoints which documents security authentication but doesn't check for authentication properly - - Args: - openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class - containing the parsed OpenAPI specification. - success_codes (list[int], optional): A list of HTTP success codes to consider - as test failed responses. Defaults to [200, 201, 301]. - *args: Variable-length positional arguments. - **kwargs: Arbitrary keyword arguments. - - Returns: - list[dict]: list of dict containing test case for endpoint - - Raises: - Any exceptions raised during the execution. - ''' - if success_codes is None: - success_codes: list[int] = [200, 201, 301] - - base_url: str = openapi_parser.base_url - request_response_params: list[dict] = openapi_parser.request_response_params - - # pop authorization header from kwargs.headers dict - kwargs.get('headers', {}).pop('Authorization', None) - kwargs.get('headers', {}).pop('X-Api-Key', None) - - # filter endpoints which has security definition in their documentation - endpoints_with_security = list( - filter( - lambda path_obj: path_obj.get('security') - and path_obj.get('security') != [{}], - request_response_params, # type: ignore - ) # type: ignore - ) - - tasks = [] - for path_obj in endpoints_with_security: - # handle path params from request_params - request_params = path_obj.get('request_params', []) - request_params = fill_params(request_params, openapi_parser.is_v3) - - # get request body params - request_body_params = list( - filter(lambda x: x.get('in') == 'body', request_params) - ) - - endpoint_path: str = path_obj.get('path') # type: ignore - - path_params = path_obj.get('path_params', []) - path_params_in_body = list( - filter(lambda x: x.get('in') == 'path', request_params) - ) - path_params = fill_params(path_params, openapi_parser.is_v3) - path_params = get_unique_params(path_params_in_body, path_params) - - for path_param in path_params: - path_param_name = path_param.get('name') - path_param_value = path_param.get('value') - endpoint_path = endpoint_path.replace( - '{' + str(path_param_name) + '}', str(path_param_value) - ) - - request_query_params = list( - filter(lambda x: x.get('in') == 'query', request_params) - ) - - tasks.append( - { - 'test_name': 'Missing Authentication Test with Fuzzed Params', - 'url': join_uri_path( - base_url, openapi_parser.api_base_path, endpoint_path - ), - 'endpoint': join_uri_path( - openapi_parser.api_base_path, endpoint_path - ), - 'method': path_obj.get('http_method').upper(), # type: ignore - 'body_params': request_body_params, - 'query_params': request_query_params, - 'path_params': path_params, - 'malicious_payload': 'Security Payload Missing', - 'args': args, - 'kwargs': kwargs, - 'vuln_details': { - True: 'Endpoint fails to implement security authentication as defined', - False: 'Endpoint implements security authentication as defined', - }, - 'success_codes': success_codes, - 'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name, - } - ) - - return tasks diff --git a/src/offat/tester/handler.py b/src/offat/tester/handler.py deleted file mode 100644 index 9268d01..0000000 --- a/src/offat/tester/handler.py +++ /dev/null @@ -1,338 +0,0 @@ -""" -module to handle the test generation and running of tests -""" -from .generator import TestGenerator -from .tester_utils import run_test, is_host_up, reduce_data_list -from .post_test_processor import PostRunTests -from .runner import TestRunner -from ..parsers.openapi import OpenAPIv3Parser -from ..parsers.swagger import SwaggerParser -from ..report.generator import ReportGenerator -from ..report.summary import ResultSummarizer -from ..logger import logger, console - -# create tester obj -test_generator = TestGenerator() - - -# Note: redirects are allowed by default making it easier for pentesters/researchers -def generate_and_run_tests( - api_parser: SwaggerParser | OpenAPIv3Parser, - http_methods: list, - regex_pattern: str | None = None, - output_file: str | None = None, - output_file_format: str | None = None, - rate_limit: int | None = None, - req_headers: dict | None = None, - proxies: list[str] | None = None, - test_data_config: dict | None = None, - capture_failed: bool = False, - remove_unused_data: bool = True, - ssl_verify: bool = True, -): - """ - Generates and runs tests for the provided OAS/Swagger file. - - Args: - api_parser: An instance of SwaggerParser or OpenAPIv3Parser - representing the parsed API specification. - regex_pattern: A string representing the regex pattern to - match against the response body (optional). - output_file: A string representing the path to the output - file (optional). - output_file_format: A string representing the format of the - output file (optional). - rate_limit: An integer representing the rate limit for the - tests (optional). - req_headers: A dictionary representing the request headers - (optional). - proxies: A list of strings representing the proxies to be used - (optional). - test_data_config: A dictionary representing the configuration - for user-provided test data (optional). - capture_failed: A boolean indicating whether to capture failed - tests in the report (default: False). - remove_unused_data: A boolean indicating whether to remove - unused data (default: True). - - Returns: - A list of test results. - """ - if not is_host_up(openapi_parser=api_parser, ssl_verify=ssl_verify): - logger.error( - 'Stopping tests due to unavailability of host: %s', api_parser.host - ) - return - - logger.info('Host %s is up', api_parser.host) - - test_runner = TestRunner( - http_methods=http_methods, - rate_limit=rate_limit, - headers=req_headers, - proxies=proxies, - ssl_verify=ssl_verify, - ) - - results: list = [] - - test_list = [] - - # test for unsupported http methods - test_list.append( - { - 'test_name': 'Checking for Unsupported HTTP Methods/Verbs', - 'tests': test_generator.check_unsupported_http_methods(api_parser), - 'type': 'FUZZED', - } - ) - - # sqli fuzz test - test_list.append( - { - 'test_name': 'Checking for SQLi vulnerability', - 'tests': test_generator.sqli_fuzz_params_test(api_parser), - 'type': 'FUZZED', - } - ) - - test_list.append( - { - 'test_name': 'Checking for SQLi vulnerability in URI Path', - 'tests': test_generator.sqli_in_uri_path_fuzz_test(api_parser), - 'type': 'FUZZED', - } - ) - - # OS Command Injection Fuzz Test - test_list.append( - { - 'test_name': 'Checking for OS Command Injection Vulnerability with fuzzed params and checking response body', # noqa: E501 - 'tests': test_generator.os_command_injection_fuzz_params_test(api_parser), - 'type': 'FUZZED', - 'post_run_matcher_test': True, - } - ) - - # XSS/HTML Injection Fuzz Test - test_list.append( - { - 'test_name': 'Checking for XSS/HTML Injection Vulnerability with fuzzed params and checking response body', # noqa: E501 - 'tests': test_generator.xss_html_injection_fuzz_params_test(api_parser), - 'type': 'FUZZED', - 'post_run_matcher_test': True, - } - ) - - # BOLA path tests with fuzzed data - test_list.append( - { - 'test_name': 'Checking for BOLA in PATH using fuzzed params', - 'tests': test_generator.bola_fuzz_path_test( - api_parser, success_codes=[200, 201, 301] - ), - 'type': 'FUZZED', - } - ) - - # BOLA path test with fuzzed data + trailing slash - test_list.append( - { - 'test_name': 'Checking for BOLA in PATH with trailing slash and id using fuzzed params', - 'tests': test_generator.bola_fuzz_trailing_slash_path_test( - api_parser, success_codes=[200, 201, 301] - ), - 'type': 'FUZZED', - } - ) - - # Mass Assignment / BOPLA - test_list.append( - { - 'test_name': 'Checking for Mass Assignment Vulnerability with fuzzed params and checking response status codes:', # noqa: E501 - 'tests': test_generator.bopla_fuzz_test( - api_parser, success_codes=[200, 201, 301] - ), - 'type': 'FUZZED', - } - ) - - # SSTI Vulnerability - test_list.append( - { - 'test_name': 'Checking for SSTI vulnerability with fuzzed params and checking response body', # noqa: E501 - 'tests': test_generator.ssti_fuzz_params_test(api_parser), - 'type': 'FUZZED', - 'post_run_matcher_test': True, - } - ) - - # Missing Authorization Test - test_list.append( - { - 'test_name': 'Checking for Missing Authorization', - 'tests': test_generator.missing_auth_fuzz_test(api_parser), - 'type': 'FUZZED', - } - ) - - # Tests with User provided Data - if bool(test_data_config): - logger.info('[bold] Testing with user provided data [/bold]') - - # BOLA path tests with fuzzed + user provided data - test_list.append( - { - 'test_name': 'Checking for BOLA in PATH using fuzzed and user provided params', - 'tests': test_generator.test_with_user_data( - test_data_config, - test_generator.bola_fuzz_path_test, - openapi_parser=api_parser, - success_codes=[200, 201, 301], - ), - 'type': 'USER + FUZZED', - } - ) - - # BOLA path test with fuzzed + user data + trailing slash - test_list.append( - { - 'test_name': 'Checking for BOLA in PATH with trailing slash id using fuzzed and user provided params:', # noqa: E501 - 'tests': test_generator.test_with_user_data( - test_data_config, - test_generator.bola_fuzz_trailing_slash_path_test, - openapi_parser=api_parser, - success_codes=[200, 201, 301], - ), - 'type': 'USER + FUZZED', - } - ) - - # OS Command Injection Fuzz Test - test_list.append( - { - 'test_name': 'Checking for OS Command Injection Vulnerability with fuzzed & user params and checking response body', # noqa: E501 - 'tests': test_generator.test_with_user_data( - test_data_config, - test_generator.os_command_injection_fuzz_params_test, - openapi_parser=api_parser, - ), - 'type': 'USER + FUZZED', - 'post_run_matcher_test': True, - } - ) - - # XSS/HTML Injection Fuzz Test - test_list.append( - { - 'test_name': 'Checking for XSS/HTML Injection Vulnerability with fuzzed & user params and checking response body', # noqa: E501 - 'tests': test_generator.test_with_user_data( - test_data_config, - test_generator.xss_html_injection_fuzz_params_test, - openapi_parser=api_parser, - ), - 'type': 'USER + FUZZED', - 'post_run_matcher_test': True, - } - ) - - # STTI Vulnerability - test_list.append( - { - 'test_name': 'Checking for SSTI vulnerability with fuzzed params & user data and checking response body', # noqa: E501 - 'tests': test_generator.test_with_user_data( - test_data_config, - test_generator.ssti_fuzz_params_test, - openapi_parser=api_parser, - ), - 'type': 'USER + FUZZED', - 'post_run_matcher_test': True, - } - ) - - # Missing Authorization Test - test_list.append( - { - 'test_name': 'Checking for Missing Authorization with user data', - 'tests': test_generator.test_with_user_data( - test_data_config, - test_generator.missing_auth_fuzz_test, - openapi_parser=api_parser, - ), - 'type': 'USER + FUZZED', - 'post_run_matcher_test': True, - } - ) - - for test in test_list: - if 'post_run_matcher_test' not in test: - test['post_run_matcher_test'] = False - - logger.info(test['test_name']) - - results += run_test( - test_runner=test_runner, - tests=test['tests'], - regex_pattern=regex_pattern, - post_run_matcher_test=test['post_run_matcher_test'], - description=f'({test["type"]}) {test["test_name"]}', - ) - - # After we collected all the results, we can now test them for - # access without restrictions - if bool(test_data_config): - # Broken Access Control Test - test_name = 'Checking for Broken Access Control' - logger.info(test_name) - bac_results = PostRunTests.run_broken_access_control_tests( - results, test_data_config - ) - - results += run_test( - test_runner=test_runner, - tests=bac_results, - regex_pattern=regex_pattern, - skip_test_run=True, - description=test_name, - ) - - if remove_unused_data: - for result in results: - result.pop('kwargs', None) - result.pop('args', None) - - result['body_params'] = reduce_data_list(result.get('body_params', [{}])) - result['query_params'] = reduce_data_list(result.get('query_params', [{}])) - result['path_params'] = reduce_data_list(result.get('path_params', [{}])) - result['malicious_payload'] = reduce_data_list( - result.get('malicious_payload', []) - ) - - # save file to output if output flag is present - if output_file_format != 'table': - ReportGenerator.generate_report( - results=results, - report_format=output_file_format, - report_path=output_file, - capture_failed=capture_failed, - ) - - ReportGenerator.generate_report( - results=results, - report_format='table', - report_path=None, - capture_failed=capture_failed, - ) - - console.print( - "The columns for 'data_leak' and 'vulnerable' in the table represent independent aspects. It's possible for there to be a data leak in the endpoint, yet the result for that endpoint may still be marked as 'Success'. This is because the 'vulnerable' column doesn't necessarily reflect the overall test result; it may indicate success even in the presence of a data leak." - ) - - console.rule() - result_summary = ResultSummarizer.generate_count_summary( - results, table_title='Results Summary' - ) - - console.print(result_summary) - - return results diff --git a/src/offat/tester/post_test_processor.py b/src/offat/tester/post_test_processor.py deleted file mode 100644 index de0d812..0000000 --- a/src/offat/tester/post_test_processor.py +++ /dev/null @@ -1,196 +0,0 @@ -from copy import deepcopy -from enum import Enum -from re import search as re_search, findall - -from .regexs import sensitive_data_regex_patterns - - -class PostTestFiltersEnum(Enum): - STATUS_CODE_FILTER = 0 - BODY_REGEX_FILTER = 1 - HEADER_REGEX_FILTER = 2 - - -class PostRunTests: - """class Includes tests that should be ran after running all the active test""" - - @staticmethod - def run_broken_access_control_tests( - results: list[dict], test_data_config: dict - ) -> list[dict]: - """ - Runs tests for broken access control - - Args: - results (list[dict]): list of dict for tests results ran - test_data_config (dict): user based config for running tests - - Returns: - list[dict]: list of results - - Raises: - Any Exception occurred during the test. - """ - - def re_match(patterns: list[str], endpoint: str) -> bool: - """Matches endpoint for specified patterns - - Args: - patterns (list[str]): endpoint regex pattern for matching endpoints - endpoint (str): Endpoint to test for match - - Returns: - bool: True if match found from any of the regex pattern else False - - Exception: - Any Exception occurred during test procedure. - """ - for pattern in patterns: - if re_search(pattern, endpoint): - return True - - return False - - actor_based_tests = [] - actors = test_data_config.get('actors', [{}]) - actor_names = [] - for actor in actors: - actor_name = list(actor.keys())[-1] - unauth_endpoint_regex = actor[actor_name].get('unauthorized_endpoints', []) - - for result in results: - if result.get('test_actor_name') != actor_name: - continue - - endpoint = result.get('endpoint', 'endpoint path not found') - if not re_match(unauth_endpoint_regex, endpoint): - continue - - actor_names.append(actor_name) - - actor_test_result = deepcopy(result) - actor_test_result['test_name'] = 'Broken Access Control' - actor_test_result['vuln_details'] = { - True: f"BAC: Endpoint is accessible to {actor_name}", - False: 'Endpoint might not vulnerable to BAC', - } - actor_based_tests.append(actor_test_result) - - return PostRunTests.filter_status_code_based_results(actor_based_tests) - - @staticmethod - def detect_data_exposure(results: list[dict]) -> list[dict]: - """Detects data exposure against sensitive data regex - patterns and returns dict of matched results - - Args: - data (str): data to be analyzed for exposure - - Returns: - dict: dictionary with tag as dict key and matched pattern as dict value - """ - - def detect_exposure(data: str) -> dict: - # Dictionary to store detected data exposures - detected_exposures = {} - - for pattern_name, pattern in sensitive_data_regex_patterns.items(): - matches = findall(pattern, data) - if matches: - if isinstance(matches, list) and isinstance(matches[0], tuple): - matches = set.union( - *[set(match_tuple) for match_tuple in matches] - ) - matches.discard('') - matches = list(matches) - detected_exposures[pattern_name] = matches - return detected_exposures - - new_results = [] - - for result in results: - res_body = result.get('response_body') - data_exposures_dict = detect_exposure(str(res_body)) - result['data_leak'] = data_exposures_dict - new_results.append(result) - - return new_results - - @staticmethod - # take a list and filter all at once - def filter_status_code_based_results(results: list[dict]) -> list[dict]: - new_results = [] - - for result in results: - new_result = deepcopy(result) - response_status_code = result.get('response_status_code') - success_codes = result.get('success_codes') - - # if response status code or success code is not - # found then continue updating status of remaining - # results - if not response_status_code or not success_codes: - continue - - new_result['vulnerable'] = ( - response_status_code in success_codes - ) # True-> vulnerable, False-> not vulnerable - new_results.append(new_result) - - return new_results - - @staticmethod - def update_result_details(results: list[dict]): - new_results = [] - for result in results: - new_result = deepcopy(result) - new_result['vuln_details'] = result['vuln_details'].get( - result['vulnerable'] - ) - new_results.append(new_result) - - return new_results - - @staticmethod - def matcher(results: list[dict]): - """ - - Args: - results (list[dict]): list of dict for tests results ran - match_location (ResponseMatchLocation): Search for match at - specified location (`ResponseMatchLocation.BODY`, - `ResponseMatchLocation.HEADER`,`ResponseMatchLocation.STATUS_CODE`). - match_regex (str): regex to match as string - - Returns: - list[dict]: list of results - - Raises: - Any Exception occurred during the test. - """ - new_results = [] - - for result in results: - match_location = result.get('response_filter') - match_regex = result.get('response_match_regex') - - # skip test if match regex not found - if not match_regex or not match_location: - continue - - match match_location: - case PostTestFiltersEnum.STATUS_CODE_FILTER.name: - target_data = result.get('response_status_code') - case PostTestFiltersEnum.HEADER_REGEX_FILTER.name: - target_data = result.get('response_body') - case _: # PostTestFiltersEnum.BODY_REGEX_FILTER.name: - target_data = result.get('response_body') - - match_response = re_search(match_regex, target_data) - new_result = deepcopy(result) - new_result['regex_match_result'] = str(match_response) - # True (Vulnerable) / False (Not Vulnerable) - new_result['vulnerable'] = bool(match_response) - new_results.append(new_result) - - return new_results diff --git a/src/offat/tester/regexs.py b/src/offat/tester/regexs.py deleted file mode 100644 index b42090a..0000000 --- a/src/offat/tester/regexs.py +++ /dev/null @@ -1,37 +0,0 @@ -sensitive_data_regex_patterns = { - # General Data - 'email': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b', - # 'passwordOrToken': r'(^|\s|")(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&_])[A-Za-z\d@$!%*#?&_]{10,}($|\s|")', # Assuming the password contains at least 1 uppercase letter, 1 lowercase letter, 1 digit, 1 special character, and is at least 8 characters long. - 'date': r'\b\d{2}/\d{2}/\d{4}\b', - 'ip': r'(?:\d{1,3}\.){3}\d{1,3}\b|\b(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}\b', - 'ccn': r'\b\d{4}-\d{4}-\d{4}-\d{4}\b', - 'jwtToken': r'(^|\s|")[A-Za-z0-9_-]{2,}(?:\.[A-Za-z0-9_-]{2,}){2}($|\s|")', - 'ato_data': r'\b(auth_code|otp|password|password_hash|auth_token|access_token|refresh_token|secret|session_id|key|pin|accessToken|refreshToken|authenticationCode|authentication_code|jwt|api_secret|apiSecret)\b', - # BRAZIL - 'BrazilCPF': r'\b(\d{3}\.){2}\d{3}\-\d{2}\b', - # INDIA - # Assuming the format: AAAAB1234C (5 uppercase letters, 4 digits, 1 uppercase letter) - 'pan': r'\b[A-Z]{5}\d{4}[A-Z]{1}\b', - # Assuming the format XXXX XXXX XXXX (4 digits, space, 4 digits, space, 4 digits) - 'aadhaarCard': r'\b\d{4}\s\d{4}\s\d{4}\b', - 'PhoneNumberIN': r'\b((\+*)((0[ -]*)*|((91 )*))((\d{12})+|(\d{10})+))|\d{5}([- ]*)\d{6}\b', - # US - 'ssn': r'\b\d{3}-\d{2}-\d{4}\b', - 'PhoneNumberUS': r'\b(^|\s|")(1\s?)?(\d{3}|\(\d{3}\))[\s\-]?\d{3}[\s\-]?\d{4}(?:$|\s|")\b', - # AWS - # Assuming the format: AKIA followed by 16 uppercase alphanumeric characters - 'AWSAccessKey': r'\bAKIA[0-9A-Z]{16}\b', - # Assuming the format: 40 alphanumeric characters, including + and / - 'AWSSecretKey': r'\b[0-9a-zA-Z/+]{40}\b', - 'AWSResourceURL': r'\b([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.amazonaws.com*)\b', - 'AWSArnId': r'\barn:aws:[A-Za-z0-9-_]*\:[A-Za-z0-9-_]*\:[A-Za-z0-9-_]*\:[A-Za-z0-9-/_]*\b', - # Google Tokens - 'Google' : r'google_oauth_token|google_oauth|google_b64', - # Slack - 'Slack' : r'xoxo-[0-9a-z]+-[0-9a-z]+-[0-9a-z]+-[0-9a-z]+', - # Postgres DSN postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...] - 'PostgresDSN' : r'postgresql:\/\/|pgsql:', - 'MySQLDSN' : 'mysql://', - 'RedisDSN' : 'redis://', - 'OutlookWebhook' : 'https://outlook.office.com/webhook/' -} diff --git a/src/offat/tester/runner.py b/src/offat/tester/runner.py deleted file mode 100644 index e339c6e..0000000 --- a/src/offat/tester/runner.py +++ /dev/null @@ -1,207 +0,0 @@ -from asyncio import ensure_future, gather -from asyncio.exceptions import CancelledError -from enum import Enum -from sys import exc_info, exit -from traceback import format_exc -from rich.progress import Progress, TaskID - -from ..http import AsyncRequests -from ..logger import logger, console -from ..utils import result_to_curl - - -class PayloadFor(Enum): - BODY = 0 - QUERY = 1 - - -class TestRunner: - ''' - class to run tests generated by `TestGenerator` class - ''' - - def __init__( - self, - http_methods: list, - rate_limit: float = 60, - headers: dict | None = None, - proxies: list[str] | None = None, - ssl_verify: bool = True, - ) -> None: - self._client = AsyncRequests( - rate_limit=rate_limit, - headers=headers, - proxies=proxies, - ssl_verify=ssl_verify, - ) - self.http_methods = http_methods - self.progress = Progress(console=console) - self.progress_task_id: TaskID | None = None - - def _generate_payloads( - self, params: list[dict], payload_for: PayloadFor = PayloadFor.BODY - ): - '''Generate body payload from passed data for HTTP body and query. - - Args: - params (list[dict]): list of containing payload parameters - payload_for (PayloadFor): PayloadFor constant indicating - for which payload is be generated, default: `PayloadFor.BODY` - - Returns: - dict: dictionary containing payload as key value pairs generated from params. - - Raises: - ValueError: If incorrect `payload_for` argument is not of `PayloadFor` class. - ''' - if payload_for not in [PayloadFor.BODY, PayloadFor.QUERY]: - raise ValueError( - '`payload_for` arg only supports `PayloadFor.BODY, PayloadFor.QUERY` value' - ) - - body_payload = {} - query_payload = {} - - for param in params: - param_in = param.get('in') - param_name = param.get('name') - param_value = param.get('value') - - # TODO:handle schema - - match param_in: - case 'body': - body_payload[param_name] = param_value - case 'query': - query_payload[param_name] = param_value - case _: - continue - - match payload_for: - case PayloadFor.BODY: - return body_payload - - case PayloadFor.QUERY: - return query_payload - - return {} - - async def send_request(self, test_task: dict): - """ - Sends an HTTP request based on the provided test_task. - - Args: - test_task (dict): A dictionary containing the details of the test task. - It should include the following keys: - - 'url': The URL to send the request to. - - 'method': The HTTP method to use for the request. - - 'args': Additional positional arguments to pass to the request. - - 'kwargs': Additional keyword arguments to pass to the request. - - 'body_params': The body parameters for the request. - - 'query_params': The query parameters for the request. - - Returns: - dict: A dictionary containing the test result. It includes the following keys: - - 'request_headers': The headers sent in the request. - - 'response_headers': The headers received in the response. - - 'response_body': The body of the response. - - 'response_status_code': The status code of the response. - - 'redirection': The redirection URL, if any. - - 'error': True if an error occurred during the request, False otherwise. - """ - url = test_task.get('url') - http_method = test_task.get('method') - args = test_task.get('args') - kwargs = test_task.get('kwargs', {}) - body_params = test_task.get('body_params') - query_params = test_task.get('query_params') - - if body_params and str(http_method).upper() not in ['GET', 'OPTIONS']: - kwargs['json'] = self._generate_payloads( - body_params, payload_for=PayloadFor.BODY - ) - - if query_params: - kwargs['params'] = self._generate_payloads( - query_params, payload_for=PayloadFor.QUERY - ) - - test_result = test_task - if self.http_methods and str(http_method).upper() not in self.http_methods: - test_result['request_headers'] = [] - test_result['response_headers'] = [] - test_result['response_body'] = f"{str(http_method).upper()} request was not sent" - test_result['response_status_code'] = -1 - test_result['redirection'] = '' - test_result['error'] = False - - return test_result - try: - response = await self._client.request( - url=url, method=http_method, *args, **kwargs - ) - # add request headers to result - test_result['request_headers'] = response.get('req_headers', []) - # append response headers and body for analyzing data leak - res_body = response.get('res_body', 'No Response Body Found') - test_result['response_headers'] = response.get('res_headers') - test_result['response_body'] = res_body - test_result['response_status_code'] = response.get('status') - test_result['redirection'] = response.get('res_redirection', '') - test_result['error'] = False - - except Exception as e: - test_result['request_headers'] = [] - test_result['response_headers'] = [] - test_result['response_body'] = 'No Response Body Found' - test_result['response_status_code'] = -1 - test_result['redirection'] = '' - test_result['error'] = True - - logger.debug('Exception Debug Data:', exc_info=exc_info()) - logger.debug(format_exc()) - logger.debug(locals()) - logger.error('Unable to send request due to error: %s', e) - - # generate curl command for reproducing result - test_result['curl_command'] = result_to_curl(test_result) - - # advance progress bar - if self.progress_task_id: - self.progress.update(self.progress_task_id, advance=1, refresh=True) - - if self.progress and self.progress.finished: - self.progress.stop() - self.progress_task_id = None - - return test_result - - async def run_tests(self, test_tasks: list, description: str | None): - '''run tests generated from test generator module''' - self.progress.start() - self.progress_task_id = self.progress.add_task( - f'[orange] {description}', total=len(test_tasks) - ) - tasks = [] - - for test_task in test_tasks: - tasks.append(ensure_future(self.send_request(test_task))) - - try: - results = await gather(*tasks) - return results - - except ( - KeyboardInterrupt, - CancelledError, - ): - logger.error('[!] User Interruption Detected!') - exit(-1) - - except Exception as e: - logger.error( - '[*] Exception occurred while gathering results: %s', - e, - exc_info=exc_info(), - ) - return [] diff --git a/src/offat/tester/tester_utils.py b/src/offat/tester/tester_utils.py deleted file mode 100644 index 4194099..0000000 --- a/src/offat/tester/tester_utils.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -OWASP OFFAT Tester Utils Module -""" -from http import client as http_client -import ssl -from sys import exc_info -from typing import Optional -from asyncio import run -from asyncio.exceptions import CancelledError -from re import search as regex_search - - -from .post_test_processor import PostRunTests -from .runner import TestRunner -from ..logger import logger -from ..parsers import SwaggerParser, OpenAPIv3Parser - - -def is_host_up(openapi_parser: SwaggerParser | OpenAPIv3Parser, ssl_verify: bool = True) -> bool: - '''checks whether the host from openapi doc is available or not. - Returns True is host is available else returns False''' - tokens = openapi_parser.host.split(':') - use_ssl = False - match len(tokens): - case 1: - host = tokens[0] - port = 443 if openapi_parser.http_scheme == 'https' else 80 - case 2: - host = tokens[0] - port = int(tokens[1]) - case _: - logger.warning('Invalid host: %s', openapi_parser.host) - return False - - if openapi_parser.http_scheme == 'https': - use_ssl = True - - host = host.split('/')[0] - - match port: - case 443: - use_ssl = True - proto = http_client.HTTPSConnection - case _: - if use_ssl: - proto = http_client.HTTPSConnection - else: - proto = http_client.HTTPConnection - - logger.info('Checking whether host %s:%s is available', host, port) - try: - if not use_ssl: - conn = proto(host=host, port=port, timeout=5) - else: - if ssl_verify: - conn = proto(host=host, port=port, timeout=5) - else: - conn = proto( - host=host, - port=port, - timeout=5, - context = ssl._create_unverified_context()) - conn.request('GET', '/') - res = conn.getresponse() - logger.info('Host returned status code: %d', res.status) - return res.status in range(200, 499) - except Exception as e: - logger.error( - 'Unable to connect to host %s:%s due to error: %s', host, port, repr(e) - ) - return False - - -def run_test( - test_runner: TestRunner, - tests: list[dict], - regex_pattern: Optional[str] = None, - skip_test_run: Optional[bool] = False, - post_run_matcher_test: Optional[bool] = False, - description: Optional[str] = None, -) -> list: - '''Run tests and print result on console''' - logger.info('Tests Generated: %d', len(tests)) - - # filter data if regex is passed - if regex_pattern: - tests = list( - filter(lambda x: regex_search(regex_pattern, x.get('endpoint', '')), tests) - ) - - try: - if skip_test_run: - logger.warning('Skipping test run for: %s', description) - test_results = tests - else: - test_results = run(test_runner.run_tests(tests, description)) - - except ( - KeyboardInterrupt, - CancelledError, - ): - logger.error('[!] User Interruption Detected!') - exit(-1) - - except Exception as e: - logger.error( - '[*] Exception occurred while running tests: %s', e, exc_info=exc_info() - ) - return [] - - if post_run_matcher_test: - test_results = PostRunTests.matcher(test_results) - else: - # update test result for status based code filter - test_results = PostRunTests.filter_status_code_based_results(test_results) - - # update tests result success/failure details - test_results = PostRunTests.update_result_details(test_results) - - # run data leak tests - test_results = PostRunTests.detect_data_exposure(test_results) - - return test_results - - -def reduce_data_list(data_list: list[dict] | str) -> list[dict] | str: - """ - Reduces a list of dictionaries to only include 'name' and 'value' keys. - - Args: - data_list (list[dict] | str): The input data list to be reduced. - - Returns: - list[dict] | str: The reduced data list with only 'name' and 'value' keys. - - """ - if isinstance(data_list, list): - return [ - {'name': param.get('name'), 'value': param.get('value')} - for param in data_list - ] - - return data_list diff --git a/src/offat/tests/__init__.py b/src/offat/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/offat/tests/parsers/__init__.py b/src/offat/tests/parsers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/offat/tests/parsers/test_parser.py b/src/offat/tests/parsers/test_parser.py deleted file mode 100644 index aef7893..0000000 --- a/src/offat/tests/parsers/test_parser.py +++ /dev/null @@ -1,101 +0,0 @@ -import tempfile - -import unittest -from unittest.mock import patch, MagicMock -from ...parsers import BaseParser, create_parser -from ...parsers.openapi import OpenAPIv3Parser - - -class TestBaseParser(unittest.TestCase): - def test_BaseParser(self): - tmp_spec = tempfile.NamedTemporaryFile(mode="+a", encoding="utf-8") - tmp_spec.write(""" -{ - "openapi": "3.0.0", - "paths": { - "/api/render": { - "post": { - "operationId": "operationId", - "summary": "", - "description": "description", - "parameters": [], - "requestBody": { - "required": true, - "description": "Write your text here", - "content": { "text/plain": { "schema": { "type": "string" } } } - }, - "responses": { "201": { "description": "Rendered result" } }, - "tags": ["App controller"] - } - }, - }, - "info": { - "title": "Test", - "description": "info -> description", - "version": "1.0", - "contact": {} - }, - "tags": [], - "servers": [{ "url": "https://someserver.com" }], - "components": { - "schemas": { - } - } -} -""" - ) - tmp_spec.flush() - obj = BaseParser(tmp_spec.name) - - self.assertTrue( - obj.is_v3, - "Provided JSON is v3" - ) - - end_points = list(obj.specification.get('paths').keys()) - self.assertTrue( - '/api/render' in end_points, - "Spec has '/api/render'" - ) - - -class TestCreateParserFromUrl(unittest.TestCase): - _YAML_SPEC = """ -openapi: 3.0.0 -info: - title: Test - version: "1.0" -paths: - /api/render: - post: - operationId: operationId - responses: - "201": - description: Rendered result -servers: - - url: https://someserver.com -""" - - _JSON_SPEC = ( - '{"openapi": "3.0.0", "info": {"title": "Test", "version": "1.0"},' - ' "paths": {"/api/render": {"post": {"operationId": "operationId",' - ' "responses": {"201": {"description": "ok"}}}}},' - ' "servers": [{"url": "https://someserver.com"}]}' - ) - - def _create_from_url(self, body): - fake_res = MagicMock() - fake_res.status_code = 200 - fake_res.text = body - with patch("offat.parsers.http_get", return_value=fake_res): - return create_parser("https://example.com/openapi.yaml") - - def test_yaml_spec_from_url(self): - parser = self._create_from_url(self._YAML_SPEC) - self.assertIsInstance(parser, OpenAPIv3Parser) - self.assertEqual(parser.specification.get("openapi"), "3.0.0") - - def test_json_spec_from_url(self): - parser = self._create_from_url(self._JSON_SPEC) - self.assertIsInstance(parser, OpenAPIv3Parser) - self.assertEqual(parser.specification.get("openapi"), "3.0.0") diff --git a/src/offat/tests/self_signed/__init__.py b/src/offat/tests/self_signed/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/offat/tests/self_signed/self_signed_server_tester.py b/src/offat/tests/self_signed/self_signed_server_tester.py deleted file mode 100644 index aa6698e..0000000 --- a/src/offat/tests/self_signed/self_signed_server_tester.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/python3 - -# Generate a cert: -# openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 - -import http.server -import ssl - - -class SimpleHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): - pass - - -httpd = http.server.HTTPServer(("localhost", 4443), SimpleHTTPRequestHandler) -httpd.socket = ssl.wrap_socket( - httpd.socket, keyfile="key.pem", certfile="cert.pem", server_side=True -) - -print("Serving on https://localhost:4443") -httpd.serve_forever() diff --git a/src/offat/tests/test_cli.py b/src/offat/tests/test_cli.py deleted file mode 100644 index a9715d4..0000000 --- a/src/offat/tests/test_cli.py +++ /dev/null @@ -1,17 +0,0 @@ -import subprocess -import sys -import unittest - - -class TestCLI(unittest.TestCase): - def test_help_starts_with_installed_dependencies(self): - result = subprocess.run( - [sys.executable, '-m', 'offat', '--help'], - capture_output=True, - text=True, - timeout=30, - ) - - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn('usage: offat', result.stdout) - self.assertIn('--file', result.stdout) diff --git a/src/offat/tests/test_config_data_handler.py b/src/offat/tests/test_config_data_handler.py deleted file mode 100644 index 4ae2df2..0000000 --- a/src/offat/tests/test_config_data_handler.py +++ /dev/null @@ -1,43 +0,0 @@ -from pathlib import Path -import unittest - -import yaml - -from ..config_data_handler import populate_user_data, validate_config_file_data - - -class TestReadmeConfig(unittest.TestCase): - def setUp(self): - readme = Path(__file__).resolve().parents[2] / 'README.md' - example = readme.read_text(encoding='utf-8').split('```yaml', 1)[1].split('```', 1)[0] - self.config = yaml.safe_load(example) - - def test_readme_config_validates(self): - self.assertEqual(validate_config_file_data(self.config), self.config) - - def test_readme_actor_settings_populate_requests(self): - for index, name, user_id, city in ( - (0, 'actor1', 145, 'london'), - (1, 'actor2', 199, 'leeds'), - ): - with self.subTest(actor=name): - actor = self.config['actors'][index][name] - tests = [{ - 'body_params': [{'name': 'email', 'value': ''}], - 'query_params': [{'name': 'id', 'value': 0}, {'name': 'city', 'value': ''}], - 'path_params': [], - 'kwargs': {'headers': {'Accept': 'application/json'}}, - }] - populated = populate_user_data(actor, name, tests)[0] - self.assertEqual(populated['query_params'], [ - {'name': 'id', 'value': user_id}, {'name': 'city', 'value': city}, - ]) - self.assertEqual(populated['body_params'][0]['value'], - 'actorone@example.com' if index == 0 else 'actortwo@example.com') - self.assertEqual(populated['kwargs']['headers'], { - 'Accept': 'application/json', - 'Authorization': f'Bearer [Token{index + 1}]', - 'User-Agent': f'offat-{name}', - }) - self.assertEqual(populated['test_actor_name'], name) - self.assertEqual(tests[0]['query_params'][0]['value'], 0) diff --git a/src/offat/tests/testers/__init__.py b/src/offat/tests/testers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/offat/tests/testers/test_fuzzer.py b/src/offat/tests/testers/test_fuzzer.py deleted file mode 100644 index 45703cc..0000000 --- a/src/offat/tests/testers/test_fuzzer.py +++ /dev/null @@ -1,32 +0,0 @@ - -import unittest -from ...tester.fuzzer import fill_params - -class TestFuzzer(unittest.TestCase): - def test_fill_params(self): - params_in = [ - { - "in": "body", - "name": "text/plain", - "description": "Write your text here", - "required": True, - "schema": {"type": "string"} - } - ] - - params_out = fill_params(params = params_in[:], is_v3 = True) - - self.assertTrue( - 'value' in params_out[0].keys(), - "'value' should exist" - ) - - self.assertTrue( - 'type' in params_out[0].keys(), - "'type' should exist" - ) - - self.assertTrue( - 'schema' not in params_out[0].keys(), - "'schema' should be gone" - ) diff --git a/src/offat/tests/utils/__init__.py b/src/offat/tests/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/offat/tests/utils/test_parse_server_url.py b/src/offat/tests/utils/test_parse_server_url.py deleted file mode 100644 index dfea3e6..0000000 --- a/src/offat/tests/utils/test_parse_server_url.py +++ /dev/null @@ -1,35 +0,0 @@ -import unittest -from pytest import raises -from ...utils import parse_server_url - - -class TestParseUrls(unittest.TestCase): - def test_valid_urls(self): - urls = [ - "https://example.com", - "https://owasp.org/OFFAT/", - "http://localhost:8000/test", - "http://127.0.0.1:8001/url/1", - ] - for url in urls: - scheme, host, port, basepath = parse_server_url(url=url) - self.assertIn( - scheme, ["http", "https"], f"Failed to validate url scheme: {url}" - ) - self.assertIn( - host, - ["example.com", "owasp.org", "localhost", "127.0.0.1"], - "Host does not match expected test cases", - ) - self.assertIn( - port, - [80, 443, 8000, 8001], - "Port does not match according to test case", - ) - self.assertIn(basepath, ["", "/OFFAT/", "/test", "/url/1"]) - - def test_invalid_urls(self): - urls = ["owasp", "ftp://example/", "\0\0alkdsjlatest", '" OR 1==1 -- -'] - for url in urls: - with raises(ValueError): - parse_server_url(url=url) diff --git a/src/offat/tests/utils/test_url_validations.py b/src/offat/tests/utils/test_url_validations.py deleted file mode 100644 index 7f44fca..0000000 --- a/src/offat/tests/utils/test_url_validations.py +++ /dev/null @@ -1,19 +0,0 @@ -import unittest -from ...utils import is_valid_url - - -class TestUrls(unittest.TestCase): - def test_valid_urls(self): - urls = [ - "https://example.com", - "https://owasp.org/OFFAT/", - "http://localhost:8000/test", - "http://127.0.0.1:8001/url", - ] - for url in urls: - self.assertTrue(is_valid_url(url=url), f"Failed to validate url: {url}") - - def test_invalid_urls(self): - urls = ["owasp", "ftp://example/", "\0\0alkdsjlatest", '" OR 1==1 -- -'] - for url in urls: - assert is_valid_url(url=url) is False diff --git a/src/offat/utils.py b/src/offat/utils.py deleted file mode 100644 index eeb0198..0000000 --- a/src/offat/utils.py +++ /dev/null @@ -1,369 +0,0 @@ -""" -utils module -""" -from json import loads as json_load, dumps as json_dumps, JSONDecodeError -from re import compile as re_compile, match -from urllib.parse import urlparse, urljoin, quote_plus -from os.path import isfile -from importlib.metadata import version -from yaml import safe_load, YAMLError - - -from .logger import logger - - -def get_package_version(): - '''Returns package current version - - Args: - None - - Returns: - String: current package version - ''' - return version('offat') - - -def read_yaml(file_path: str) -> dict: - '''Reads YAML file and returns as python dict. - returns file not found or yaml errors as dict. - - Args: - file_path (str): path of yaml file - - Returns: - dict: YAML contents as dict else returns error - ''' - if not file_path: - return {'error': 'ValueError, path cannot be of None type'} - - if not isfile(file_path): - return {'error': 'File Not Found'} - - with open(file_path, 'r', encoding='utf-8') as f: - try: - return safe_load(f.read()) - except YAMLError: - return {'error': 'YAML error'} - - -def read_json(file_path: str) -> dict: - '''Reads JSON file and returns as python dict. - returns file not found or JSON errors as dict. - - Args: - file_path (str): path of yaml file - - Returns: - dict: YAML contents as dict else returns error - ''' - if not isfile(file_path): - return {'error': 'File Not Found'} - - with open(file_path, 'r', encoding='utf-8') as f: - try: - return json_load(f.read()) - except JSONDecodeError: - return {'error': 'JSON error'} - - -def read_openapi_file(file_path: str) -> dict: - '''Returns Open API Documentation file contents as json - returns file not found or yaml errors as dict. - - Args: - file_path (str): path of openapi file - - Returns: - dict: YAML contents as dict else returns error - ''' - if not isfile(file_path): - return {'error': 'File Not Found'} - - file_ext = file_path.split('.')[-1] - match file_ext: - case 'json': - return read_json(file_path) - case 'yaml': - return read_yaml(file_path) - case _: - return {'error': 'Invalid file extension'} - - -def write_json_to_file(json_data: dict, file_path: str): - '''Writes dict obj to file as json - - Args: - json_data (dict): JSON payload to be written into file - file_path (str): path of output json file - - Returns: - bool: True is `json_data` is written into `file_path` else - returns False (in case of any exception) - - Raises: - Any exception occurred during operation - ''' - if isfile(file_path): - logger.info('%s file will be overwritten.', file_path) - - logger.info('Writing data to file: %s', file_path) - try: - with open(file_path, 'w') as f: - f.write(json_dumps(json_data)) - logger.info('Completed writing data to file: %s', file_path) - return True - - except JSONDecodeError: - logger.error('Invalid JSON data, error while writing to %s file.', file_path) - - except Exception as e: - logger.error( - 'Unable to write JSON data to file due to below exception:\n%s', repr(e) - ) - - return False - - -def str_to_dict(key_values: str) -> dict: - '''Takes string object and converts to dict - String should in `Key1:Value1,Key2:Value2,Key3:Value3` format - - Args: - key_values (str): dict as str separated by commas `,` - - Returns: - dict: Returns dict from str after conversion - - Raises: - Any exception occurred during operation - ''' - new_dict = dict() - for key_value in key_values.split(','): - try: - key_value_list = key_value.split(':') - key = key_value_list[0].strip() - value = key_value_list[1].strip() - new_dict[key] = value - except (IndexError, KeyError) as e: - logger.error(str(e)) - - return new_dict - - -def headers_list_to_dict(headers_list_list: list[list[str]] | None) -> dict: - '''Takes list object and converts to dict - String should in `[['Key1:Value1'],['Key2:Value2'],['Key3:Value3']]` format - - Args: - headers_list_list (list): headers value as list[list[str]], where str - is in `key:value` format - - Returns: - dict: Returns dict from str after conversion - - Raises: - Any exception occurred during operation - ''' - if not headers_list_list: - return {} - - response_headers_dict: dict = dict() - - for header_list in headers_list_list: - for header_data in header_list: - header_key_value = header_data.split(':') - k = header_key_value[0].strip() - v = header_key_value[1].strip() - response_headers_dict[k] = v - - return response_headers_dict - - -def is_valid_url(url: str) -> bool: - '''Accepts string as an parameter and returns bool - whether str is url or not - - Args: - url (str): string value which could be url - - Returns: - bool: Returns True str is url else False - - Raises: - Any exception occurred during operation - ''' - url_regex = re_compile( - r'https?:\/\/([a-z.-]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})+(:\d+)?.*' - ) - return bool(match(url_regex, url)) - - -def parse_server_url(url: str) -> tuple: - '''Parses url and returns scheme, host, port and basepath. - - Args: - url (str): url to be parsed - - Returns: - tuple: (scheme:str, host:str, port:int|None, basepath:str|None) - - Raises: - Any exception occurred during operation - ''' - # TODO: implement url parse security https://docs.python.org/3/library/urllib.parse.html#url-parsing-security - parsed_url = urlparse(url) - - netloc = parsed_url.netloc - port = 443 if parsed_url.scheme == 'https' else 80 - if ':' in netloc: - tokens = netloc.split(':') - host = tokens[0] - try: - port = int(tokens[1]) - except ValueError: - logger.error( - 'Invalid Port Number: failed to parse port in url. Using port %d according to scheme %s', - port, - parsed_url.scheme, - ) - else: - host = netloc - - if parsed_url.scheme not in ['http', 'https']: - raise ValueError('only http and https schemes are allowed') - - return parsed_url.scheme, host, port, parsed_url.path - - -def join_uri_path(*args: str, remove_prefix: str = '/') -> str: - '''constructs url from passed args using urljoin - - Args: - *args (str): parts of uri - remove_prefix (str): prefix to be removed from uri path before - joining with previous uri path. - - Returns: - str: constructed uri - - Raises: - Any exception occurred during operation - - Example: - ```python - from offat.utils import join_uri_path - - url = join_uri_path('https://example.com:443','/v2/', '/pet/findByStatus/') - print(url) - # output: https://example.com:443/pet/findByStatus/ - ``` - ''' - url = args[0] - if not url.endswith('/'): - url += '/' - - for uri in args[1:]: - if not url.endswith('/'): - url += '/' - url = urljoin(url, uri.removeprefix(remove_prefix)) - - return url - - -def update_values(list1: list[dict], list2: list[dict]) -> list[dict]: - """ - Update values in list1 based on the corresponding "name" values in list2. - - Args: - list1 (list of dict): The list of dictionaries to be updated. - list2 (list of dict): The list of dictionaries containing values to update from. - - Returns: - list of dict: The updated list1 with values from list2. - - Example: - ```python - list1 = [{'name': 'id', 'value': 67}, {'name': 'email', 'value': 'old@example.com'}] - list2 = [{'name': 'id', 'value': 10}, {'name': 'email', 'value': 'new@example.com'}] - updated_list = update_values(list1, list2) - print(updated_list) - # Output: [{'name': 'id', 'value': 10}, {'name': 'email', 'value': 'new@example.com'}] - ``` - """ - # Create a dictionary for faster lookup - lookup_dict = {item['name']: item['value'] for item in list2} - - # Update values in list1 using index lookup - for item in list1: - if item['name'] in lookup_dict: - item['value'] = lookup_dict[item['name']] - - return list1 - - -def get_unique_params(list1: list[dict], list2: list[dict]) -> list[dict]: - '''Returns unique path params from list1 and list2 - - Args: - list1 (list of dict): The list of dictionaries to be updated. - list2 (list of dict): The list of dictionaries containing values to update from. - - Returns: - list of dict: The updated list1 with values from list2. - ''' - unique_params_names = [] - unique_params = [] - for path_param in list1 + list2: - param_name = path_param.get('name') - if param_name not in unique_params_names: - unique_params.append(path_param) - unique_params_names.append(param_name) - - return unique_params - - -def result_to_curl(result: dict): - """ - Converts a dictionary representing an HTTP request to a cURL command. - - Args: - result (dict): A dictionary containing the details of the HTTP request. - - Returns: - str: The cURL command generated from the given request details. - """ - url = result.get('url') - method = result.get('method') - query_params = result.get('query_params', None) - request_headers: dict = result.get('request_headers', {}) - body_params: dict = result.get('body_params', {}) - - # generate query params - query_param_str = ( - '&'.join( - [ - f'{param.get("name")}={quote_plus(str(param.get("value")))}' - for param in query_params - ] - ) - if query_params - else '' - ) - - # generate headers str - if isinstance(request_headers, dict): - request_headers.pop('Content-Length', None) - request_headers_str = ( - '-H '.join([f'"{hkey}: {hval}" ' for hkey, hval in request_headers.items()]) - if request_headers - else '' - ) - - # generate JSON body params - body = {bparam.get('name'): bparam.get('value') for bparam in body_params} - body_str = f"-d '{json_dumps(body)}'" if body else '' - - curl_command = f"curl -X {method} \"{url}?{query_param_str}\" {request_headers_str} {body_str}".strip() - - return curl_command diff --git a/src/poetry.lock b/src/poetry.lock deleted file mode 100644 index e15d7a5..0000000 --- a/src/poetry.lock +++ /dev/null @@ -1,2086 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, -] - -[[package]] -name = "aiohttp" -version = "3.12.14" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:906d5075b5ba0dd1c66fcaaf60eb09926a9fef3ca92d912d2a0bbdbecf8b1248"}, - {file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c875bf6fc2fd1a572aba0e02ef4e7a63694778c5646cdbda346ee24e630d30fb"}, - {file = "aiohttp-3.12.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbb284d15c6a45fab030740049d03c0ecd60edad9cd23b211d7e11d3be8d56fd"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e360381e02e1a05d36b223ecab7bc4a6e7b5ab15760022dc92589ee1d4238c"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aaf90137b5e5d84a53632ad95ebee5c9e3e7468f0aab92ba3f608adcb914fa95"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e532a25e4a0a2685fa295a31acf65e027fbe2bea7a4b02cdfbbba8a064577663"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eab9762c4d1b08ae04a6c77474e6136da722e34fdc0e6d6eab5ee93ac29f35d1"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abe53c3812b2899889a7fca763cdfaeee725f5be68ea89905e4275476ffd7e61"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5760909b7080aa2ec1d320baee90d03b21745573780a072b66ce633eb77a8656"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:02fcd3f69051467bbaa7f84d7ec3267478c7df18d68b2e28279116e29d18d4f3"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4dcd1172cd6794884c33e504d3da3c35648b8be9bfa946942d353b939d5f1288"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:224d0da41355b942b43ad08101b1b41ce633a654128ee07e36d75133443adcda"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e387668724f4d734e865c1776d841ed75b300ee61059aca0b05bce67061dcacc"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:dec9cde5b5a24171e0b0a4ca064b1414950904053fb77c707efd876a2da525d8"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bbad68a2af4877cc103cd94af9160e45676fc6f0c14abb88e6e092b945c2c8e3"}, - {file = "aiohttp-3.12.14-cp310-cp310-win32.whl", hash = "sha256:ee580cb7c00bd857b3039ebca03c4448e84700dc1322f860cf7a500a6f62630c"}, - {file = "aiohttp-3.12.14-cp310-cp310-win_amd64.whl", hash = "sha256:cf4f05b8cea571e2ccc3ca744e35ead24992d90a72ca2cf7ab7a2efbac6716db"}, - {file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597"}, - {file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393"}, - {file = "aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28"}, - {file = "aiohttp-3.12.14-cp311-cp311-win32.whl", hash = "sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b"}, - {file = "aiohttp-3.12.14-cp311-cp311-win_amd64.whl", hash = "sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced"}, - {file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22"}, - {file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a"}, - {file = "aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0"}, - {file = "aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729"}, - {file = "aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338"}, - {file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767"}, - {file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e"}, - {file = "aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758"}, - {file = "aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5"}, - {file = "aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa"}, - {file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b8cc6b05e94d837bcd71c6531e2344e1ff0fb87abe4ad78a9261d67ef5d83eae"}, - {file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1dcb015ac6a3b8facd3677597edd5ff39d11d937456702f0bb2b762e390a21b"}, - {file = "aiohttp-3.12.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3779ed96105cd70ee5e85ca4f457adbce3d9ff33ec3d0ebcdf6c5727f26b21b3"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:717a0680729b4ebd7569c1dcd718c46b09b360745fd8eb12317abc74b14d14d0"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b5dd3a2ef7c7e968dbbac8f5574ebeac4d2b813b247e8cec28174a2ba3627170"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4710f77598c0092239bc12c1fcc278a444e16c7032d91babf5abbf7166463f7b"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f3e9f75ae842a6c22a195d4a127263dbf87cbab729829e0bd7857fb1672400b2"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9c8d55d6802086edd188e3a7d85a77787e50d56ce3eb4757a3205fa4657922"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79b29053ff3ad307880d94562cca80693c62062a098a5776ea8ef5ef4b28d140"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23e1332fff36bebd3183db0c7a547a1da9d3b4091509f6d818e098855f2f27d3"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a564188ce831fd110ea76bcc97085dd6c625b427db3f1dbb14ca4baa1447dcbc"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a7a1b4302f70bb3ec40ca86de82def532c97a80db49cac6a6700af0de41af5ee"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1b07ccef62950a2519f9bfc1e5b294de5dd84329f444ca0b329605ea787a3de5"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:938bd3ca6259e7e48b38d84f753d548bd863e0c222ed6ee6ace3fd6752768a84"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8bc784302b6b9f163b54c4e93d7a6f09563bd01ff2b841b29ed3ac126e5040bf"}, - {file = "aiohttp-3.12.14-cp39-cp39-win32.whl", hash = "sha256:a3416f95961dd7d5393ecff99e3f41dc990fb72eda86c11f2a60308ac6dcd7a0"}, - {file = "aiohttp-3.12.14-cp39-cp39-win_amd64.whl", hash = "sha256:196858b8820d7f60578f8b47e5669b3195c21d8ab261e39b1d705346458f445f"}, - {file = "aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.5.0" -aiosignal = ">=1.4.0" -async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -propcache = ">=0.2.0" -yarl = ">=1.17.0,<2.0" - -[package.extras] -speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] - -[[package]] -name = "aiolimiter" -version = "1.2.1" -description = "asyncio rate limiter, a leaky bucket implementation" -optional = false -python-versions = "<4.0,>=3.8" -groups = ["main"] -files = [ - {file = "aiolimiter-1.2.1-py3-none-any.whl", hash = "sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7"}, - {file = "aiolimiter-1.2.1.tar.gz", hash = "sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9"}, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, - {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" -typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.9.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, - {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] -trio = ["trio (>=0.26.1)"] - -[[package]] -name = "async-timeout" -version = "5.0.1" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"api\" and python_full_version < \"3.11.3\" or python_version < \"3.11\"" -files = [ - {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, - {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, -] - -[[package]] -name = "attrs" -version = "25.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, -] - -[package.extras] -benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] - -[[package]] -name = "certifi" -version = "2025.4.26" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, - {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, -] - -[[package]] -name = "click" -version = "8.2.1" -description = "Composable command line interface toolkit" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -markers = {main = "extra == \"api\" and (platform_system == \"Windows\" or sys_platform == \"win32\")", dev = "sys_platform == \"win32\""} - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] -markers = {main = "extra == \"api\" and python_version < \"3.11\"", dev = "python_version < \"3.11\""} - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "fastapi" -version = "0.115.12" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, - {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, -] - -[package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.47.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "frozenlist" -version = "1.6.0" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e"}, - {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352"}, - {file = "frozenlist-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3"}, - {file = "frozenlist-1.6.0-cp310-cp310-win32.whl", hash = "sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812"}, - {file = "frozenlist-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1"}, - {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d"}, - {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0"}, - {file = "frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e"}, - {file = "frozenlist-1.6.0-cp311-cp311-win32.whl", hash = "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860"}, - {file = "frozenlist-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603"}, - {file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1"}, - {file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29"}, - {file = "frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770"}, - {file = "frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc"}, - {file = "frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878"}, - {file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e"}, - {file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117"}, - {file = "frozenlist-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e"}, - {file = "frozenlist-1.6.0-cp313-cp313-win32.whl", hash = "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4"}, - {file = "frozenlist-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd"}, - {file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64"}, - {file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91"}, - {file = "frozenlist-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497"}, - {file = "frozenlist-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f"}, - {file = "frozenlist-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348"}, - {file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:536a1236065c29980c15c7229fbb830dedf809708c10e159b8136534233545f0"}, - {file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ed5e3a4462ff25ca84fb09e0fada8ea267df98a450340ead4c91b44857267d70"}, - {file = "frozenlist-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e19c0fc9f4f030fcae43b4cdec9e8ab83ffe30ec10c79a4a43a04d1af6c5e1ad"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c608f833897501dac548585312d73a7dca028bf3b8688f0d712b7acfaf7fb3"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0dbae96c225d584f834b8d3cc688825911960f003a85cb0fd20b6e5512468c42"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625170a91dd7261a1d1c2a0c1a353c9e55d21cd67d0852185a5fef86587e6f5f"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1db8b2fc7ee8a940b547a14c10e56560ad3ea6499dc6875c354e2335812f739d"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4da6fc43048b648275a220e3a61c33b7fff65d11bdd6dcb9d9c145ff708b804c"}, - {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef8e7e8f2f3820c5f175d70fdd199b79e417acf6c72c5d0aa8f63c9f721646f"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa733d123cc78245e9bb15f29b44ed9e5780dc6867cfc4e544717b91f980af3b"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ba7f8d97152b61f22d7f59491a781ba9b177dd9f318486c5fbc52cde2db12189"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:56a0b8dd6d0d3d971c91f1df75e824986667ccce91e20dca2023683814344791"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5c9e89bf19ca148efcc9e3c44fd4c09d5af85c8a7dd3dbd0da1cb83425ef4983"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1330f0a4376587face7637dfd245380a57fe21ae8f9d360c1c2ef8746c4195fa"}, - {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2187248203b59625566cac53572ec8c2647a140ee2738b4e36772930377a533c"}, - {file = "frozenlist-1.6.0-cp39-cp39-win32.whl", hash = "sha256:2b8cf4cfea847d6c12af06091561a89740f1f67f331c3fa8623391905e878530"}, - {file = "frozenlist-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:1255d5d64328c5a0d066ecb0f02034d086537925f1f04b50b1ae60d37afbf572"}, - {file = "frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191"}, - {file = "frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68"}, -] - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "httptools" -version = "0.6.4" -description = "A collection of framework independent HTTP protocol utils." -optional = true -python-versions = ">=3.8.0" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, - {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, - {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, - {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, - {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, - {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, - {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, - {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, - {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, - {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, - {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, - {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, - {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, - {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, - {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, - {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, - {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, - {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, - {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, - {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, - {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, - {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, - {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, - {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, - {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, - {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, - {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, - {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, - {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, - {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, - {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, - {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, - {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, - {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, - {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, - {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, - {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, - {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, - {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, - {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, - {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, - {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, - {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, -] - -[package.extras] -test = ["Cython (>=0.29.24)"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "jsonschema" -version = "4.24.0" -description = "An implementation of JSON Schema validation for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d"}, - {file = "jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" -referencing = ">=0.28.4" -rpds-py = ">=0.7.1" - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] - -[[package]] -name = "jsonschema-path" -version = "0.3.4" -description = "JSONSchema Spec with object-oriented paths" -optional = false -python-versions = "<4.0.0,>=3.8.0" -groups = ["main"] -files = [ - {file = "jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8"}, - {file = "jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001"}, -] - -[package.dependencies] -pathable = ">=0.4.1,<0.5.0" -PyYAML = ">=5.1" -referencing = "<0.37.0" -requests = ">=2.31.0,<3.0.0" - -[[package]] -name = "jsonschema-specifications" -version = "2025.4.1" -description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af"}, - {file = "jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"}, -] - -[package.dependencies] -referencing = ">=0.31.0" - -[[package]] -name = "lazy-object-proxy" -version = "1.11.0" -description = "A fast and thorough lazy object proxy." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "lazy_object_proxy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:132bc8a34f2f2d662a851acfd1b93df769992ed1b81e2b1fda7db3e73b0d5a18"}, - {file = "lazy_object_proxy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:01261a3afd8621a1accb5682df2593dc7ec7d21d38f411011a5712dcd418fbed"}, - {file = "lazy_object_proxy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:090935756cc041e191f22f4f9c7fd4fe9a454717067adf5b1bbd2ce3046b556e"}, - {file = "lazy_object_proxy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:76ec715017f06410f57df442c1a8d66e6b5f7035077785b129817f5ae58810a4"}, - {file = "lazy_object_proxy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a9f39098e93a63618a79eef2889ae3cf0605f676cd4797fdfd49fcd7ddc318b"}, - {file = "lazy_object_proxy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee13f67f4fcd044ef27bfccb1c93d39c100046fec1fad6e9a1fcdfd17492aeb3"}, - {file = "lazy_object_proxy-1.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4c84eafd8dd15ea16f7d580758bc5c2ce1f752faec877bb2b1f9f827c329cd"}, - {file = "lazy_object_proxy-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:d2503427bda552d3aefcac92f81d9e7ca631e680a2268cbe62cd6a58de6409b7"}, - {file = "lazy_object_proxy-1.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0613116156801ab3fccb9e2b05ed83b08ea08c2517fdc6c6bc0d4697a1a376e3"}, - {file = "lazy_object_proxy-1.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bb03c507d96b65f617a6337dedd604399d35face2cdf01526b913fb50c4cb6e8"}, - {file = "lazy_object_proxy-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28c174db37946f94b97a97b579932ff88f07b8d73a46b6b93322b9ac06794a3b"}, - {file = "lazy_object_proxy-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:d662f0669e27704495ff1f647070eb8816931231c44e583f4d0701b7adf6272f"}, - {file = "lazy_object_proxy-1.11.0-py3-none-any.whl", hash = "sha256:a56a5093d433341ff7da0e89f9b486031ccd222ec8e52ec84d0ec1cdc819674b"}, - {file = "lazy_object_proxy-1.11.0.tar.gz", hash = "sha256:18874411864c9fbbbaa47f9fc1dd7aea754c86cfde21278ef427639d1dd78e9c"}, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "multidict" -version = "6.4.4" -description = "multidict implementation" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "multidict-6.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8adee3ac041145ffe4488ea73fa0a622b464cc25340d98be76924d0cda8545ff"}, - {file = "multidict-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b61e98c3e2a861035aaccd207da585bdcacef65fe01d7a0d07478efac005e028"}, - {file = "multidict-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75493f28dbadecdbb59130e74fe935288813301a8554dc32f0c631b6bdcdf8b0"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc3c6a37e048b5395ee235e4a2a0d639c2349dffa32d9367a42fc20d399772"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87cb72263946b301570b0f63855569a24ee8758aaae2cd182aae7d95fbc92ca7"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bbf7bd39822fd07e3609b6b4467af4c404dd2b88ee314837ad1830a7f4a8299"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1f7cbd4f1f44ddf5fd86a8675b7679176eae770f2fc88115d6dddb6cefb59bc"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5ac9e5bfce0e6282e7f59ff7b7b9a74aa8e5c60d38186a4637f5aa764046ad"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4efc31dfef8c4eeb95b6b17d799eedad88c4902daba39ce637e23a17ea078915"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fcad2945b1b91c29ef2b4050f590bfcb68d8ac8e0995a74e659aa57e8d78e01"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d877447e7368c7320832acb7159557e49b21ea10ffeb135c1077dbbc0816b598"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:33a12ebac9f380714c298cbfd3e5b9c0c4e89c75fe612ae496512ee51028915f"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0f14ea68d29b43a9bf37953881b1e3eb75b2739e896ba4a6aa4ad4c5b9ffa145"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0327ad2c747a6600e4797d115d3c38a220fdb28e54983abe8964fd17e95ae83c"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d1a20707492db9719a05fc62ee215fd2c29b22b47c1b1ba347f9abc831e26683"}, - {file = "multidict-6.4.4-cp310-cp310-win32.whl", hash = "sha256:d83f18315b9fca5db2452d1881ef20f79593c4aa824095b62cb280019ef7aa3d"}, - {file = "multidict-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:9c17341ee04545fd962ae07330cb5a39977294c883485c8d74634669b1f7fe04"}, - {file = "multidict-6.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95"}, - {file = "multidict-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a"}, - {file = "multidict-6.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08"}, - {file = "multidict-6.4.4-cp311-cp311-win32.whl", hash = "sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49"}, - {file = "multidict-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529"}, - {file = "multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2"}, - {file = "multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d"}, - {file = "multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e"}, - {file = "multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b"}, - {file = "multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781"}, - {file = "multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9"}, - {file = "multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf"}, - {file = "multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1"}, - {file = "multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd"}, - {file = "multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373"}, - {file = "multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156"}, - {file = "multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c"}, - {file = "multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd"}, - {file = "multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e"}, - {file = "multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb"}, - {file = "multidict-6.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:603f39bd1cf85705c6c1ba59644b480dfe495e6ee2b877908de93322705ad7cf"}, - {file = "multidict-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc60f91c02e11dfbe3ff4e1219c085695c339af72d1641800fe6075b91850c8f"}, - {file = "multidict-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:496bcf01c76a70a31c3d746fd39383aad8d685ce6331e4c709e9af4ced5fa221"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4219390fb5bf8e548e77b428bb36a21d9382960db5321b74d9d9987148074d6b"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef4e9096ff86dfdcbd4a78253090ba13b1d183daa11b973e842465d94ae1772"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49a29d7133b1fc214e818bbe025a77cc6025ed9a4f407d2850373ddde07fd04a"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e32053d6d3a8b0dfe49fde05b496731a0e6099a4df92154641c00aa76786aef5"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc403092a49509e8ef2d2fd636a8ecefc4698cc57bbe894606b14579bc2a955"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5363f9b2a7f3910e5c87d8b1855c478c05a2dc559ac57308117424dfaad6805c"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e543a40e4946cf70a88a3be87837a3ae0aebd9058ba49e91cacb0b2cd631e2b"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:60d849912350da557fe7de20aa8cf394aada6980d0052cc829eeda4a0db1c1db"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:19d08b4f22eae45bb018b9f06e2838c1e4b853c67628ef8ae126d99de0da6395"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d693307856d1ef08041e8b6ff01d5b4618715007d288490ce2c7e29013c12b9a"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fad6daaed41021934917f4fb03ca2db8d8a4d79bf89b17ebe77228eb6710c003"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c10d17371bff801af0daf8b073c30b6cf14215784dc08cd5c43ab5b7b8029bbc"}, - {file = "multidict-6.4.4-cp39-cp39-win32.whl", hash = "sha256:7e23f2f841fcb3ebd4724a40032d32e0892fbba4143e43d2a9e7695c5e50e6bd"}, - {file = "multidict-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d7b50b673ffb4ff4366e7ab43cf1f0aef4bd3608735c5fbdf0bdb6f690da411"}, - {file = "multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac"}, - {file = "multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "openapi-schema-validator" -version = "0.6.3" -description = "OpenAPI schema validation for Python" -optional = false -python-versions = "<4.0.0,>=3.8.0" -groups = ["main"] -files = [ - {file = "openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3"}, - {file = "openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee"}, -] - -[package.dependencies] -jsonschema = ">=4.19.1,<5.0.0" -jsonschema-specifications = ">=2023.5.2" -rfc3339-validator = "*" - -[[package]] -name = "openapi-spec-validator" -version = "0.7.1" -description = "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" -optional = false -python-versions = ">=3.8.0,<4.0.0" -groups = ["main"] -files = [ - {file = "openapi_spec_validator-0.7.1-py3-none-any.whl", hash = "sha256:3c81825043f24ccbcd2f4b149b11e8231abce5ba84f37065e14ec947d8f4e959"}, - {file = "openapi_spec_validator-0.7.1.tar.gz", hash = "sha256:8577b85a8268685da6f8aa30990b83b7960d4d1117e901d451b5d572605e5ec7"}, -] - -[package.dependencies] -jsonschema = ">=4.18.0,<5.0.0" -jsonschema-path = ">=0.3.1,<0.4.0" -lazy-object-proxy = ">=1.7.1,<2.0.0" -openapi-schema-validator = ">=0.6.0,<0.7.0" - -[[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "pathable" -version = "0.4.4" -description = "Object-oriented paths" -optional = false -python-versions = "<4.0.0,>=3.7.0" -groups = ["main"] -files = [ - {file = "pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2"}, - {file = "pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2"}, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "propcache" -version = "0.3.1" -description = "Accelerated property cache" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, - {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, - {file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"}, - {file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"}, - {file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"}, - {file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"}, - {file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"}, - {file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"}, - {file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"}, - {file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"}, - {file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"}, - {file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"}, - {file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"}, - {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6"}, - {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf"}, - {file = "propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe"}, - {file = "propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64"}, - {file = "propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566"}, - {file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"}, - {file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"}, -] - -[[package]] -name = "pydantic" -version = "2.11.5" -description = "Data validation using Python type hints" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"}, - {file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.33.2" -typing-extensions = ">=4.12.2" -typing-inspection = ">=0.4.0" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.33.2" -description = "Core functionality for Pydantic validation and serialization" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, - {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pygments" -version = "2.19.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyjwt" -version = "2.9.0" -description = "JSON Web Token implementation in Python" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, - {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, -] - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] - -[[package]] -name = "pytest" -version = "8.3.5" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "python-dotenv" -version = "1.1.0" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, - {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "redis" -version = "5.3.0" -description = "Python client for Redis database and key-value store" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "redis-5.3.0-py3-none-any.whl", hash = "sha256:f1deeca1ea2ef25c1e4e46b07f4ea1275140526b1feea4c6459c0ec27a10ef83"}, - {file = "redis-5.3.0.tar.gz", hash = "sha256:8d69d2dde11a12dc85d0dbf5c45577a5af048e2456f7077d87ad35c1c81c310e"}, -] - -[package.dependencies] -async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} -PyJWT = ">=2.9.0,<2.10.0" - -[package.extras] -hiredis = ["hiredis (>=3.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] - -[[package]] -name = "referencing" -version = "0.36.2" -description = "JSON Referencing + Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, - {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -rpds-py = ">=0.7.0" -typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} - -[[package]] -name = "requests" -version = "2.32.4" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -description = "A pure python RFC3339 validator" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main"] -files = [ - {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, - {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, -] - -[package.dependencies] -six = "*" - -[[package]] -name = "rich" -version = "13.9.4" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["main"] -files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "rpds-py" -version = "0.25.1" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "rpds_py-0.25.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f4ad628b5174d5315761b67f212774a32f5bad5e61396d38108bd801c0a8f5d9"}, - {file = "rpds_py-0.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c742af695f7525e559c16f1562cf2323db0e3f0fbdcabdf6865b095256b2d40"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:605ffe7769e24b1800b4d024d24034405d9404f0bc2f55b6db3362cd34145a6f"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc6f3ddef93243538be76f8e47045b4aad7a66a212cd3a0f23e34469473d36b"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f70316f760174ca04492b5ab01be631a8ae30cadab1d1081035136ba12738cfa"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1dafef8df605fdb46edcc0bf1573dea0d6d7b01ba87f85cd04dc855b2b4479e"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0701942049095741a8aeb298a31b203e735d1c61f4423511d2b1a41dcd8a16da"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e87798852ae0b37c88babb7f7bbbb3e3fecc562a1c340195b44c7e24d403e380"}, - {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3bcce0edc1488906c2d4c75c94c70a0417e83920dd4c88fec1078c94843a6ce9"}, - {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e2f6a2347d3440ae789505693a02836383426249d5293541cd712e07e7aecf54"}, - {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4fd52d3455a0aa997734f3835cbc4c9f32571345143960e7d7ebfe7b5fbfa3b2"}, - {file = "rpds_py-0.25.1-cp310-cp310-win32.whl", hash = "sha256:3f0b1798cae2bbbc9b9db44ee068c556d4737911ad53a4e5093d09d04b3bbc24"}, - {file = "rpds_py-0.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3ebd879ab996537fc510a2be58c59915b5dd63bccb06d1ef514fee787e05984a"}, - {file = "rpds_py-0.25.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5f048bbf18b1f9120685c6d6bb70cc1a52c8cc11bdd04e643d28d3be0baf666d"}, - {file = "rpds_py-0.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fbb0dbba559959fcb5d0735a0f87cdbca9e95dac87982e9b95c0f8f7ad10255"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4ca54b9cf9d80b4016a67a0193ebe0bcf29f6b0a96f09db942087e294d3d4c2"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ee3e26eb83d39b886d2cb6e06ea701bba82ef30a0de044d34626ede51ec98b0"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89706d0683c73a26f76a5315d893c051324d771196ae8b13e6ffa1ffaf5e574f"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2013ee878c76269c7b557a9a9c042335d732e89d482606990b70a839635feb7"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45e484db65e5380804afbec784522de84fa95e6bb92ef1bd3325d33d13efaebd"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48d64155d02127c249695abb87d39f0faf410733428d499867606be138161d65"}, - {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:048893e902132fd6548a2e661fb38bf4896a89eea95ac5816cf443524a85556f"}, - {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0317177b1e8691ab5879f4f33f4b6dc55ad3b344399e23df2e499de7b10a548d"}, - {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffcf57826d77a4151962bf1701374e0fc87f536e56ec46f1abdd6a903354042"}, - {file = "rpds_py-0.25.1-cp311-cp311-win32.whl", hash = "sha256:cda776f1967cb304816173b30994faaf2fd5bcb37e73118a47964a02c348e1bc"}, - {file = "rpds_py-0.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:dc3c1ff0abc91444cd20ec643d0f805df9a3661fcacf9c95000329f3ddf268a4"}, - {file = "rpds_py-0.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:5a3ddb74b0985c4387719fc536faced33cadf2172769540c62e2a94b7b9be1c4"}, - {file = "rpds_py-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5ffe453cde61f73fea9430223c81d29e2fbf412a6073951102146c84e19e34c"}, - {file = "rpds_py-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:115874ae5e2fdcfc16b2aedc95b5eef4aebe91b28e7e21951eda8a5dc0d3461b"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a714bf6e5e81b0e570d01f56e0c89c6375101b8463999ead3a93a5d2a4af91fa"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35634369325906bcd01577da4c19e3b9541a15e99f31e91a02d010816b49bfda"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cb2b3ddc16710548801c6fcc0cfcdeeff9dafbc983f77265877793f2660309"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ceca1cf097ed77e1a51f1dbc8d174d10cb5931c188a4505ff9f3e119dfe519b"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2cd1a4b0c2b8c5e31ffff50d09f39906fe351389ba143c195566056c13a7ea"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de336a4b164c9188cb23f3703adb74a7623ab32d20090d0e9bf499a2203ad65"}, - {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9fca84a15333e925dd59ce01da0ffe2ffe0d6e5d29a9eeba2148916d1824948c"}, - {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88ec04afe0c59fa64e2f6ea0dd9657e04fc83e38de90f6de201954b4d4eb59bd"}, - {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8bd2f19e312ce3e1d2c635618e8a8d8132892bb746a7cf74780a489f0f6cdcb"}, - {file = "rpds_py-0.25.1-cp312-cp312-win32.whl", hash = "sha256:e5e2f7280d8d0d3ef06f3ec1b4fd598d386cc6f0721e54f09109a8132182fbfe"}, - {file = "rpds_py-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:db58483f71c5db67d643857404da360dce3573031586034b7d59f245144cc192"}, - {file = "rpds_py-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:6d50841c425d16faf3206ddbba44c21aa3310a0cebc3c1cdfc3e3f4f9f6f5728"}, - {file = "rpds_py-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:659d87430a8c8c704d52d094f5ba6fa72ef13b4d385b7e542a08fc240cb4a559"}, - {file = "rpds_py-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68f6f060f0bbdfb0245267da014d3a6da9be127fe3e8cc4a68c6f833f8a23bb1"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083a9513a33e0b92cf6e7a6366036c6bb43ea595332c1ab5c8ae329e4bcc0a9c"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:816568614ecb22b18a010c7a12559c19f6fe993526af88e95a76d5a60b8b75fb"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c6564c0947a7f52e4792983f8e6cf9bac140438ebf81f527a21d944f2fd0a40"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c4a128527fe415d73cf1f70a9a688d06130d5810be69f3b553bf7b45e8acf79"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d74ec9bc0e2feb81d3f16946b005748119c0f52a153f6db6a29e8cd68636f295"}, - {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3af5b4cc10fa41e5bc64e5c198a1b2d2864337f8fcbb9a67e747e34002ce812b"}, - {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79dc317a5f1c51fd9c6a0c4f48209c6b8526d0524a6904fc1076476e79b00f98"}, - {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1521031351865e0181bc585147624d66b3b00a84109b57fcb7a779c3ec3772cd"}, - {file = "rpds_py-0.25.1-cp313-cp313-win32.whl", hash = "sha256:5d473be2b13600b93a5675d78f59e63b51b1ba2d0476893415dfbb5477e65b31"}, - {file = "rpds_py-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7b74e92a3b212390bdce1d93da9f6488c3878c1d434c5e751cbc202c5e09500"}, - {file = "rpds_py-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:dd326a81afe332ede08eb39ab75b301d5676802cdffd3a8f287a5f0b694dc3f5"}, - {file = "rpds_py-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a58d1ed49a94d4183483a3ce0af22f20318d4a1434acee255d683ad90bf78129"}, - {file = "rpds_py-0.25.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f251bf23deb8332823aef1da169d5d89fa84c89f67bdfb566c49dea1fccfd50d"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd586bfa270c1103ece2109314dd423df1fa3d9719928b5d09e4840cec0d72"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d273f136e912aa101a9274c3145dcbddbe4bac560e77e6d5b3c9f6e0ed06d34"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666fa7b1bd0a3810a7f18f6d3a25ccd8866291fbbc3c9b912b917a6715874bb9"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:921954d7fbf3fccc7de8f717799304b14b6d9a45bbeec5a8d7408ccbf531faf5"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d86373ff19ca0441ebeb696ef64cb58b8b5cbacffcda5a0ec2f3911732a194"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8980cde3bb8575e7c956a530f2c217c1d6aac453474bf3ea0f9c89868b531b6"}, - {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8eb8c84ecea987a2523e057c0d950bcb3f789696c0499290b8d7b3107a719d78"}, - {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e43a005671a9ed5a650f3bc39e4dbccd6d4326b24fb5ea8be5f3a43a6f576c72"}, - {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58f77c60956501a4a627749a6dcb78dac522f249dd96b5c9f1c6af29bfacfb66"}, - {file = "rpds_py-0.25.1-cp313-cp313t-win32.whl", hash = "sha256:2cb9e5b5e26fc02c8a4345048cd9998c2aca7c2712bd1b36da0c72ee969a3523"}, - {file = "rpds_py-0.25.1-cp313-cp313t-win_amd64.whl", hash = "sha256:401ca1c4a20cc0510d3435d89c069fe0a9ae2ee6495135ac46bdd49ec0495763"}, - {file = "rpds_py-0.25.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ce4c8e485a3c59593f1a6f683cf0ea5ab1c1dc94d11eea5619e4fb5228b40fbd"}, - {file = "rpds_py-0.25.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8222acdb51a22929c3b2ddb236b69c59c72af4019d2cba961e2f9add9b6e634"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4593c4eae9b27d22df41cde518b4b9e4464d139e4322e2127daa9b5b981b76be"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd035756830c712b64725a76327ce80e82ed12ebab361d3a1cdc0f51ea21acb0"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:114a07e85f32b125404f28f2ed0ba431685151c037a26032b213c882f26eb908"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dec21e02e6cc932538b5203d3a8bd6aa1480c98c4914cb88eea064ecdbc6396a"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09eab132f41bf792c7a0ea1578e55df3f3e7f61888e340779b06050a9a3f16e9"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c98f126c4fc697b84c423e387337d5b07e4a61e9feac494362a59fd7a2d9ed80"}, - {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0e6a327af8ebf6baba1c10fadd04964c1965d375d318f4435d5f3f9651550f4a"}, - {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc120d1132cff853ff617754196d0ac0ae63befe7c8498bd67731ba368abe451"}, - {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:140f61d9bed7839446bdd44852e30195c8e520f81329b4201ceead4d64eb3a9f"}, - {file = "rpds_py-0.25.1-cp39-cp39-win32.whl", hash = "sha256:9c006f3aadeda131b438c3092124bd196b66312f0caa5823ef09585a669cf449"}, - {file = "rpds_py-0.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:a61d0b2c7c9a0ae45732a77844917b427ff16ad5464b4d4f5e4adb955f582890"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b24bf3cd93d5b6ecfbedec73b15f143596c88ee249fa98cefa9a9dc9d92c6f28"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0eb90e94f43e5085623932b68840b6f379f26db7b5c2e6bcef3179bd83c9330f"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d50e4864498a9ab639d6d8854b25e80642bd362ff104312d9770b05d66e5fb13"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c9409b47ba0650544b0bb3c188243b83654dfe55dcc173a86832314e1a6a35d"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:796ad874c89127c91970652a4ee8b00d56368b7e00d3477f4415fe78164c8000"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85608eb70a659bf4c1142b2781083d4b7c0c4e2c90eff11856a9754e965b2540"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4feb9211d15d9160bc85fa72fed46432cdc143eb9cf6d5ca377335a921ac37b"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ccfa689b9246c48947d31dd9d8b16d89a0ecc8e0e26ea5253068efb6c542b76e"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3c5b317ecbd8226887994852e85de562f7177add602514d4ac40f87de3ae45a8"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:454601988aab2c6e8fd49e7634c65476b2b919647626208e376afcd22019eeb8"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1c0c434a53714358532d13539272db75a5ed9df75a4a090a753ac7173ec14e11"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f73ce1512e04fbe2bc97836e89830d6b4314c171587a99688082d090f934d20a"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee86d81551ec68a5c25373c5643d343150cc54672b5e9a0cafc93c1870a53954"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89c24300cd4a8e4a51e55c31a8ff3918e6651b241ee8876a42cc2b2a078533ba"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:771c16060ff4e79584dc48902a91ba79fd93eade3aa3a12d6d2a4aadaf7d542b"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:785ffacd0ee61c3e60bdfde93baa6d7c10d86f15655bd706c89da08068dc5038"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a40046a529cc15cef88ac5ab589f83f739e2d332cb4d7399072242400ed68c9"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85fc223d9c76cabe5d0bff82214459189720dc135db45f9f66aa7cffbf9ff6c1"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0be9965f93c222fb9b4cc254235b3b2b215796c03ef5ee64f995b1b69af0762"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8378fa4a940f3fb509c081e06cb7f7f2adae8cf46ef258b0e0ed7519facd573e"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:33358883a4490287e67a2c391dfaea4d9359860281db3292b6886bf0be3d8692"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1d1fadd539298e70cac2f2cb36f5b8a65f742b9b9f1014dd4ea1f7785e2470bf"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a46c2fb2545e21181445515960006e85d22025bd2fe6db23e76daec6eb689fe"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:50f2c501a89c9a5f4e454b126193c5495b9fb441a75b298c60591d8a2eb92e1b"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d779b325cc8238227c47fbc53964c8cc9a941d5dbae87aa007a1f08f2f77b23"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:036ded36bedb727beeabc16dc1dad7cb154b3fa444e936a03b67a86dc6a5066e"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:245550f5a1ac98504147cba96ffec8fabc22b610742e9150138e5d60774686d7"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff7c23ba0a88cb7b104281a99476cccadf29de2a0ef5ce864959a52675b1ca83"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e37caa8cdb3b7cf24786451a0bdb853f6347b8b92005eeb64225ae1db54d1c2b"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2f48ab00181600ee266a095fe815134eb456163f7d6699f525dee471f312cf"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e5fc7484fa7dce57e25063b0ec9638ff02a908304f861d81ea49273e43838c1"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d3c10228d6cf6fe2b63d2e7985e94f6916fa46940df46b70449e9ff9297bd3d1"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:5d9e40f32745db28c1ef7aad23f6fc458dc1e29945bd6781060f0d15628b8ddf"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:35a8d1a24b5936b35c5003313bc177403d8bdef0f8b24f28b1c4a255f94ea992"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6099263f526efff9cf3883dfef505518730f7a7a93049b1d90d42e50a22b4793"}, - {file = "rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3"}, -] - -[[package]] -name = "rq" -version = "1.16.2" -description = "RQ is a simple, lightweight, library for creating background jobs, and processing them." -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "rq-1.16.2-py3-none-any.whl", hash = "sha256:52e619f6cb469b00e04da74305045d244b75fecb2ecaa4f26422add57d3c5f09"}, - {file = "rq-1.16.2.tar.gz", hash = "sha256:5c5b9ad5fbaf792b8fada25cc7627f4d206a9a4455aced371d4f501cc3f13b34"}, -] - -[package.dependencies] -click = ">=5" -redis = ">=3.5" - -[[package]] -name = "setuptools" -version = "80.9.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "starlette" -version = "0.46.2" -description = "The little ASGI library that shines." -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, - {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, -] - -[package.dependencies] -anyio = ">=3.6.2,<5" - -[package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] - -[[package]] -name = "tenacity" -version = "8.5.0" -description = "Retry code until it succeeds" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"}, - {file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"}, -] - -[package.extras] -doc = ["reno", "sphinx"] -test = ["pytest", "tornado (>=4.5)", "typeguard"] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "typing-extensions" -version = "4.13.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, - {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, -] -markers = {main = "python_version < \"3.13\" or extra == \"api\"", dev = "python_version < \"3.11\""} - -[[package]] -name = "typing-inspection" -version = "0.4.1" -description = "Runtime typing introspection tools" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, - {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "urllib3" -version = "2.5.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "uvicorn" -version = "0.23.2" -description = "The lightning-fast ASGI server." -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, - {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} -h11 = ">=0.8" -httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} -python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} -watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} - -[package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "uvloop" -version = "0.21.0" -description = "Fast implementation of asyncio event loop on top of libuv" -optional = true -python-versions = ">=3.8.0" -groups = ["main"] -markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"api\"" -files = [ - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, - {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, - {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, - {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, - {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, - {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, - {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, - {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, - {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, - {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, - {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, - {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, - {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, - {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, -] - -[package.extras] -dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] - -[[package]] -name = "watchfiles" -version = "1.0.5" -description = "Simple, modern and high performance file watching and code reload in python." -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"}, - {file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b551d4fb482fc57d852b4541f911ba28957d051c8776e79c3b4a51eb5e2a1b11"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:830aa432ba5c491d52a15b51526c29e4a4b92bf4f92253787f9726fe01519487"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16512051a822a416b0d477d5f8c0e67b67c1a20d9acecb0aafa3aa4d6e7d256"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe0cbc787770e52a96c6fda6726ace75be7f840cb327e1b08d7d54eadc3bc85"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d363152c5e16b29d66cbde8fa614f9e313e6f94a8204eaab268db52231fe5358"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee32c9a9bee4d0b7bd7cbeb53cb185cf0b622ac761efaa2eba84006c3b3a614"}, - {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29c7fd632ccaf5517c16a5188e36f6612d6472ccf55382db6c7fe3fcccb7f59f"}, - {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e637810586e6fe380c8bc1b3910accd7f1d3a9a7262c8a78d4c8fb3ba6a2b3d"}, - {file = "watchfiles-1.0.5-cp310-cp310-win32.whl", hash = "sha256:cd47d063fbeabd4c6cae1d4bcaa38f0902f8dc5ed168072874ea11d0c7afc1ff"}, - {file = "watchfiles-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:86c0df05b47a79d80351cd179893f2f9c1b1cae49d96e8b3290c7f4bd0ca0a92"}, - {file = "watchfiles-1.0.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:237f9be419e977a0f8f6b2e7b0475ababe78ff1ab06822df95d914a945eac827"}, - {file = "watchfiles-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0da39ff917af8b27a4bdc5a97ac577552a38aac0d260a859c1517ea3dc1a7c4"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfcb3952350e95603f232a7a15f6c5f86c5375e46f0bd4ae70d43e3e063c13d"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b2dddba7a4e6151384e252a5632efcaa9bc5d1c4b567f3cb621306b2ca9f63"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cf944fcfc394c5f9de794ce581914900f82ff1f855326f25ebcf24d5397418"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf6cd9f83d7c023b1aba15d13f705ca7b7d38675c121f3cc4a6e25bd0857ee9"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852de68acd6212cd6d33edf21e6f9e56e5d98c6add46f48244bd479d97c967c6"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5730f3aa35e646103b53389d5bc77edfbf578ab6dab2e005142b5b80a35ef25"}, - {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:18b3bd29954bc4abeeb4e9d9cf0b30227f0f206c86657674f544cb032296acd5"}, - {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba5552a1b07c8edbf197055bc9d518b8f0d98a1c6a73a293bc0726dce068ed01"}, - {file = "watchfiles-1.0.5-cp311-cp311-win32.whl", hash = "sha256:2f1fefb2e90e89959447bc0420fddd1e76f625784340d64a2f7d5983ef9ad246"}, - {file = "watchfiles-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b6e76ceb1dd18c8e29c73f47d41866972e891fc4cc7ba014f487def72c1cf096"}, - {file = "watchfiles-1.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:266710eb6fddc1f5e51843c70e3bebfb0f5e77cf4f27129278c70554104d19ed"}, - {file = "watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2"}, - {file = "watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234"}, - {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2"}, - {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663"}, - {file = "watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249"}, - {file = "watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705"}, - {file = "watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417"}, - {file = "watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d"}, - {file = "watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827"}, - {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a"}, - {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936"}, - {file = "watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc"}, - {file = "watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11"}, - {file = "watchfiles-1.0.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2cfb371be97d4db374cba381b9f911dd35bb5f4c58faa7b8b7106c8853e5d225"}, - {file = "watchfiles-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a3904d88955fda461ea2531fcf6ef73584ca921415d5cfa44457a225f4a42bc1"}, - {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b7a21715fb12274a71d335cff6c71fe7f676b293d322722fe708a9ec81d91f5"}, - {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dfd6ae1c385ab481766b3c61c44aca2b3cd775f6f7c0fa93d979ddec853d29d5"}, - {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b659576b950865fdad31fa491d31d37cf78b27113a7671d39f919828587b429b"}, - {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1909e0a9cd95251b15bff4261de5dd7550885bd172e3536824bf1cf6b121e200"}, - {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:832ccc221927c860e7286c55c9b6ebcc0265d5e072f49c7f6456c7798d2b39aa"}, - {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85fbb6102b3296926d0c62cfc9347f6237fb9400aecd0ba6bbda94cae15f2b3b"}, - {file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:15ac96dd567ad6c71c71f7b2c658cb22b7734901546cd50a475128ab557593ca"}, - {file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b6227351e11c57ae997d222e13f5b6f1f0700d84b8c52304e8675d33a808382"}, - {file = "watchfiles-1.0.5-cp39-cp39-win32.whl", hash = "sha256:974866e0db748ebf1eccab17862bc0f0303807ed9cda465d1324625b81293a18"}, - {file = "watchfiles-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:9848b21ae152fe79c10dd0197304ada8f7b586d3ebc3f27f43c506e5a52a863c"}, - {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f59b870db1f1ae5a9ac28245707d955c8721dd6565e7f411024fa374b5362d1d"}, - {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9475b0093767e1475095f2aeb1d219fb9664081d403d1dff81342df8cd707034"}, - {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc533aa50664ebd6c628b2f30591956519462f5d27f951ed03d6c82b2dfd9965"}, - {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed1cd825158dcaae36acce7b2db33dcbfd12b30c34317a88b8ed80f0541cc57"}, - {file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:554389562c29c2c182e3908b149095051f81d28c2fec79ad6c8997d7d63e0009"}, - {file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a74add8d7727e6404d5dc4dcd7fac65d4d82f95928bbee0cf5414c900e86773e"}, - {file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb1489f25b051a89fae574505cc26360c8e95e227a9500182a7fe0afcc500ce0"}, - {file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0901429650652d3f0da90bad42bdafc1f9143ff3605633c455c999a2d786cac"}, - {file = "watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9"}, -] - -[package.dependencies] -anyio = ">=3.0.0" - -[[package]] -name = "websockets" -version = "15.0.1" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"api\"" -files = [ - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, - {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, - {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, - {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, - {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, - {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, - {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, - {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, - {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, - {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, - {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, - {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, - {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, -] - -[[package]] -name = "yarl" -version = "1.20.0" -description = "Yet another URL library" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"}, - {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62"}, - {file = "yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19"}, - {file = "yarl-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d"}, - {file = "yarl-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5"}, - {file = "yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6"}, - {file = "yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b"}, - {file = "yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64"}, - {file = "yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384"}, - {file = "yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62"}, - {file = "yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f"}, - {file = "yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac"}, - {file = "yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe"}, - {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914"}, - {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc"}, - {file = "yarl-1.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0"}, - {file = "yarl-1.20.0-cp39-cp39-win32.whl", hash = "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8"}, - {file = "yarl-1.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7"}, - {file = "yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124"}, - {file = "yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -propcache = ">=0.2.1" - -[extras] -api = ["fastapi", "python-dotenv", "redis", "rq", "uvicorn"] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.10,<4.0" -content-hash = "ea346ccfd35dcb90b6ea2035e7ccce0d286434d87b3c8c868d03fe199e596849" diff --git a/src/pyproject.toml b/src/pyproject.toml deleted file mode 100644 index 4b418dc..0000000 --- a/src/pyproject.toml +++ /dev/null @@ -1,49 +0,0 @@ -[tool.poetry] -name = "offat" -version = "0.19.4" -description = "Offensive API tester tool automates checks for common API vulnerabilities" -authors = ["Dhrumil Mistry "] -license = "MIT" -readme = "README.md" - -[tool.poetry.dependencies] -python = ">=3.10,<4.0" -aiohttp = ">=3.8.4" -pyyaml = ">=6.0" -requests = ">=2.32.4" -fastapi = {version = ">=0.115.2", optional = true} -uvicorn = {extras = ["standard"], version = ">=0.23.2", optional = true} -rq = {version = ">=1.15.1", optional = true} -redis = {version = ">=5.0.0", optional = true} -python-dotenv = {version = ">=1.0.0", optional = true} -rich = ">=13.7.0" -aiolimiter = ">=1.1.0" -openapi-spec-validator = ">=0.7.1" -setuptools = ">=69.0.3" -tenacity = ">=8.2.3" - -[tool.poetry.extras] -api = ["fastapi", "uvicorn", "redis", "rq", "python-dotenv"] - -[tool.poetry.urls] -"Home" = "https://github.com/OWASP/OFFAT" -"Bug Tracker" = "https://github.com/OWASP/OFFAT/issues" -"Support" = "https://github.com/sponsors/OWASP/" - - -[tool.poetry.scripts] -offat = "offat.__main__:start" -offat-api = "offat.api.__main__:start" - - -[tool.poetry.group.dev.dependencies] -pytest = ">=8.1.1" - -[tool.pytest.ini_options] -testpaths = [ - "offat/tests", -] - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api"