From 2ba78400a038f4a4ad470e31a2c70fac931e8670 Mon Sep 17 00:00:00 2001 From: Irving Popovetsky Date: Mon, 19 Jan 2026 14:19:18 -0800 Subject: [PATCH 1/2] Enable Sentry tracing and re-add CI and fix a bunch of lint things --- .github/dependabot.yml | 40 + .github/workflows/ci.yml | 189 +++ manage.py | 4 +- poetry.lock | 1061 ++++++++++++++++- pybot/__main__.py | 11 +- .../slack/actions/mentor_volunteer.py | 4 +- pybot/endpoints/slack/utils/slash_repeat.py | 6 +- pybot/plugins/airtable/api.py | 8 +- pybot/sentry.py | 144 +++ pyproject.toml | 2 + tests/conftest.py | 2 +- tests/data/blocks.py | 8 +- .../airtable/test_airtable_webhook.py | 39 +- tests/endpoints/slack/test_general_actions.py | 5 +- .../slack/test_mentor_request_flow.py | 84 +- .../slack/test_mentor_volunteer_flow.py | 59 +- .../endpoints/slack/test_message_handlers.py | 11 +- .../slack/test_new_member_actions.py | 13 +- tests/endpoints/slack/test_report_actions.py | 7 +- tests/endpoints/slack/test_slack_events.py | 29 +- tests/fixtures/__init__.py | 51 +- tests/integration/test_airtable_read.py | 3 +- tests/unit/test_high_severity_fixes.py | 15 +- tests/unit/test_mentor_request_model.py | 21 +- tests/unit/test_mentor_volunteer_model.py | 5 +- tests/unit/test_sentry_sampler.py | 128 ++ 26 files changed, 1803 insertions(+), 146 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 pybot/sentry.py create mode 100644 tests/unit/test_sentry_sampler.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1c65c2c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,40 @@ +version: 2 +updates: + # Python dependencies via Poetry + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + groups: + python-minor: + patterns: + - "*" + update-types: + - "minor" + - "patch" + labels: + - "dependencies" + - "python" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "github-actions" + + # Docker + - package-ecosystem: "docker" + directory: "/docker" + schedule: + interval: "weekly" + day: "monday" + labels: + - "dependencies" + - "docker" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b091182 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,189 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +env: + POETRY_VERSION: "2.3.0" + POETRY_VIRTUALENVS_IN_PROJECT: true + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Load cached Poetry installation + id: cached-poetry + uses: actions/cache@v4 + with: + path: ~/.local + key: poetry-${{ env.POETRY_VERSION }}-${{ runner.os }} + + - name: Install Poetry + if: steps.cached-poetry.outputs.cache-hit != 'true' + uses: snok/install-poetry@v1 + with: + version: ${{ env.POETRY_VERSION }} + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Load cached venv + id: cached-venv + uses: actions/cache@v4 + with: + path: .venv + key: venv-lint-${{ runner.os }}-py3.14-${{ hashFiles('poetry.lock') }} + restore-keys: | + venv-lint-${{ runner.os }}-py3.14- + + - name: Install dependencies + if: steps.cached-venv.outputs.cache-hit != 'true' + run: poetry install --only dev --no-interaction + + - name: Run Ruff linter + run: poetry run ruff check . + + - name: Run Ruff formatter check + run: poetry run ruff format --check . + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.14 + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Load cached Poetry installation + id: cached-poetry + uses: actions/cache@v4 + with: + path: ~/.local + key: poetry-${{ env.POETRY_VERSION }}-${{ runner.os }} + + - name: Install Poetry + if: steps.cached-poetry.outputs.cache-hit != 'true' + uses: snok/install-poetry@v1 + with: + version: ${{ env.POETRY_VERSION }} + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Load cached venv + id: cached-venv + uses: actions/cache@v4 + with: + path: .venv + key: venv-test-${{ runner.os }}-py3.14-${{ hashFiles('poetry.lock') }} + restore-keys: | + venv-test-${{ runner.os }}-py3.14- + + - name: Install dependencies + if: steps.cached-venv.outputs.cache-hit != 'true' + run: poetry install --no-interaction + + - name: Run tests with coverage + run: | + poetry run pytest \ + --cov=pybot \ + --cov-report=xml \ + --cov-report=term-missing \ + -v \ + --tb=short + env: + SLACK_TOKEN: "xoxb-test-token" + SLACK_ADMIN_TOKEN: "xoxb-admin-test-token" + AIRTABLE_API_KEY: "test-key" + AIRTABLE_BASE_ID: "test-base" + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + files: ./coverage.xml + fail_ci_if_error: false + verbose: true + + security: + name: Security Scan + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.14 + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Load cached Poetry installation + id: cached-poetry + uses: actions/cache@v4 + with: + path: ~/.local + key: poetry-${{ env.POETRY_VERSION }}-${{ runner.os }} + + - name: Install Poetry + if: steps.cached-poetry.outputs.cache-hit != 'true' + uses: snok/install-poetry@v1 + with: + version: ${{ env.POETRY_VERSION }} + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Load cached venv + id: cached-venv + uses: actions/cache@v4 + with: + path: .venv + key: venv-security-${{ runner.os }}-py3.14-${{ hashFiles('poetry.lock') }} + restore-keys: | + venv-security-${{ runner.os }}-py3.14- + + - name: Install dependencies + if: steps.cached-venv.outputs.cache-hit != 'true' + run: poetry install --no-interaction + + - name: Run Bandit security linter + run: poetry run bandit -r pybot -x pybot/_vendor --skip B101 -f json -o bandit-report.json || true + + - name: Display Bandit results + run: poetry run bandit -r pybot -x pybot/_vendor --skip B101 -f txt || true + + - name: Check for known vulnerabilities + run: poetry run safety scan --output text || true + continue-on-error: true + + # Final status check for branch protection + ci-success: + name: CI Success + needs: [lint, test, security] + runs-on: ubuntu-latest + if: always() + steps: + - name: Check all jobs passed + run: | + if [[ "${{ needs.lint.result }}" != "success" ]]; then + echo "Lint job failed" + exit 1 + fi + if [[ "${{ needs.test.result }}" != "success" ]]; then + echo "Test job failed" + exit 1 + fi + # Security is informational, doesn't fail CI + echo "All required jobs passed!" diff --git a/manage.py b/manage.py index 626518b..26a19ee 100644 --- a/manage.py +++ b/manage.py @@ -125,7 +125,9 @@ async def replay_team_join( except Exception as e: logger.error(f" Failed to link backend user: {e}") else: - logger.error(" Backend authentication failed - check BACKEND_USERNAME/BACKEND_PASS") + logger.error( + " Backend authentication failed - check BACKEND_USERNAME/BACKEND_PASS" + ) else: logger.info("Skipping backend linking (--skip-backend)") diff --git a/poetry.lock b/poetry.lock index 6cd382f..06f1da2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -170,6 +170,37 @@ files = [ 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 = false +python-versions = ">=3.8" +groups = ["dev"] +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.12.1" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, + {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, +] + +[package.dependencies] +idna = ">=2.8" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] + [[package]] name = "attrs" version = "25.4.0" @@ -182,6 +213,46 @@ files = [ {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, ] +[[package]] +name = "authlib" +version = "1.6.6" +description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd"}, + {file = "authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e"}, +] + +[package.dependencies] +cryptography = "*" + +[[package]] +name = "bandit" +version = "1.9.3" +description = "Security oriented static analyser for python code." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "bandit-1.9.3-py3-none-any.whl", hash = "sha256:4745917c88d2246def79748bde5e08b9d5e9b92f877863d43fab70cd8814ce6a"}, + {file = "bandit-1.9.3.tar.gz", hash = "sha256:ade4b9b7786f89ef6fc7344a52b34558caec5da74cb90373aed01de88472f774"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +PyYAML = ">=5.3.1" +rich = "*" +stevedore = ">=1.20.0" + +[package.extras] +baseline = ["GitPython (>=3.1.30)"] +sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] +toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] +yaml = ["PyYAML"] + [[package]] name = "black" version = "25.12.0" @@ -245,6 +316,104 @@ files = [ {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, ] +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -501,6 +670,116 @@ files = [ [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "cryptography" +version = "46.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["dev"] +files = [ + {file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"}, + {file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"}, + {file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"}, + {file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"}, + {file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"}, + {file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"}, + {file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"}, + {file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"}, + {file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"}, + {file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"}, + {file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "dparse" +version = "0.6.4" +description = "A parser for Python dependency files" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57"}, + {file = "dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +all = ["pipenv", "poetry", "pyyaml"] +conda = ["pyyaml"] +pipenv = ["pipenv"] +poetry = ["poetry"] + +[[package]] +name = "filelock" +version = "3.20.3" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, + {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -641,6 +920,65 @@ files = [ {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, ] +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +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 = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "idna" version = "3.11" @@ -668,6 +1006,188 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "joblib" +version = "1.5.3" +description = "Lightweight pipelining with Python functions" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713"}, + {file = "joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3"}, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "marshmallow" +version = "4.2.0" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "marshmallow-4.2.0-py3-none-any.whl", hash = "sha256:1dc369bd13a8708a9566d6f73d1db07d50142a7580f04fd81e1c29a4d2e10af4"}, + {file = "marshmallow-4.2.0.tar.gz", hash = "sha256:908acabd5aa14741419d3678d3296bda6abe28a167b7dcd05969ceb8256943ac"}, +] + +[package.extras] +dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] +docs = ["autodocsumm (==0.2.14)", "furo (==2025.12.19)", "sphinx (==8.2.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.1)", "sphinxext-opengraph (==0.13.0)"] +tests = ["pytest", "simplejson"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +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.7.0" @@ -836,6 +1356,32 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "nltk" +version = "3.9.2" +description = "Natural Language Toolkit" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a"}, + {file = "nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419"}, +] + +[package.dependencies] +click = "*" +joblib = "*" +regex = ">=2021.8.3" +tqdm = "*" + +[package.extras] +all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"] +corenlp = ["requests"] +machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"] +plot = ["matplotlib"] +tgrep = ["pyparsing"] +twitter = ["twython"] + [[package]] name = "packaging" version = "25.0" @@ -1031,6 +1577,175 @@ files = [ {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, ] +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + [[package]] name = "pygments" version = "2.19.2" @@ -1182,7 +1897,7 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -1259,6 +1974,147 @@ files = [ {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] +[[package]] +name = "regex" +version = "2026.1.15" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"}, + {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"}, + {file = "regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3"}, + {file = "regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218"}, + {file = "regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a"}, + {file = "regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3"}, + {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"}, + {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"}, + {file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"}, + {file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"}, + {file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"}, + {file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"}, + {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"}, + {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"}, + {file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"}, + {file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"}, + {file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"}, + {file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"}, + {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"}, + {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"}, + {file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"}, + {file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"}, + {file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"}, + {file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"}, + {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"}, + {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"}, + {file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"}, + {file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"}, + {file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"}, + {file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"}, + {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"}, + {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"}, + {file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"}, + {file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"}, + {file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"}, + {file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"}, + {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"}, + {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"}, + {file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"}, + {file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"}, + {file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"}, + {file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"}, + {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5"}, + {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815"}, + {file = "regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3"}, + {file = "regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a"}, + {file = "regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc"}, + {file = "regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5"}, + {file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"}, +] + [[package]] name = "requests" version = "2.32.5" @@ -1281,6 +2137,43 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rich" +version = "14.2.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["dev"] +files = [ + {file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"}, + {file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93"}, + {file = "ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993"}, +] + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] +libyaml = ["ruamel.yaml.clibz (>=0.3.7) ; platform_python_implementation == \"CPython\""] +oldlibyaml = ["ruamel.yaml.clib ; platform_python_implementation == \"CPython\""] + [[package]] name = "ruff" version = "0.14.13" @@ -1310,6 +2203,61 @@ files = [ {file = "ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47"}, ] +[[package]] +name = "safety" +version = "3.7.0" +description = "Scan dependencies for known vulnerabilities and licenses." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf"}, + {file = "safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e"}, +] + +[package.dependencies] +authlib = ">=1.2.0" +click = ">=8.0.2" +dparse = ">=0.6.4" +filelock = ">=3.16.1,<4.0" +httpx = "*" +jinja2 = ">=3.1.0" +marshmallow = ">=3.15.0" +nltk = ">=3.9" +packaging = ">=21.0" +pydantic = ">=2.6.0" +requests = "*" +ruamel-yaml = ">=0.17.21" +safety-schemas = "0.0.16" +tenacity = ">=8.1.0" +tomlkit = "*" +typer = ">=0.16.0" +typing-extensions = ">=4.7.1" + +[package.extras] +github = ["pygithub (>=1.43.3)"] +gitlab = ["python-gitlab (>=1.3.0)"] +spdx = ["spdx-tools (>=0.8.2)"] + +[[package]] +name = "safety-schemas" +version = "0.0.16" +description = "Schemas for Safety tools" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44"}, + {file = "safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e"}, +] + +[package.dependencies] +dparse = ">=0.6.4" +packaging = ">=21.0" +pydantic = ">=2.6.0" +ruamel-yaml = ">=0.17.21" +typing-extensions = ">=4.7.1" + [[package]] name = "sentry-sdk" version = "2.49.0" @@ -1373,6 +2321,98 @@ statsig = ["statsig (>=0.55.3)"] tornado = ["tornado (>=6)"] unleash = ["UnleashClient (>=6.0.1)"] +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "stevedore" +version = "5.6.0" +description = "Manage dynamic plugins for Python applications" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820"}, + {file = "stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945"}, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, + {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, +] + +[package.extras] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] + +[[package]] +name = "tomlkit" +version = "0.14.0" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, + {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "typer" +version = "0.21.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01"}, + {file = "typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1380,11 +2420,26 @@ description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev"] -markers = "python_version == \"3.12\"" files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +markers = {main = "python_version == \"3.12\""} + +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" [[package]] name = "urllib3" @@ -1564,4 +2619,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "12accc93c69dc4206c8f778e50f9adc4d75cdcf87c3abee8fe11946bdac1b904" +content-hash = "77b0d25eac59046bd66c60544bff73de060cc6d1898b61a842f970618ef9297a" diff --git a/pybot/__main__.py b/pybot/__main__.py index 44e3d8a..8ff8795 100644 --- a/pybot/__main__.py +++ b/pybot/__main__.py @@ -1,14 +1,13 @@ import logging.config import os -import sentry_sdk import yaml -from sentry_sdk.integrations.aiohttp import AioHttpIntegration from pybot._vendor.sirbot import SirBot from pybot._vendor.sirbot.plugins.slack import SlackPlugin from pybot.endpoints import handle_health_check from pybot.endpoints.slack.utils import HOST, PORT, slack_configs +from pybot.sentry import init_sentry from . import endpoints from .plugins import AirtablePlugin, APIPlugin @@ -25,13 +24,7 @@ logging.basicConfig(level=logging.DEBUG) logger.exception(e) - if "SENTRY_DSN" in os.environ: - sentry_sdk.init( - dsn=os.environ["SENTRY_DSN"], - release=os.environ.get("VERSION", "1.0.0"), - environment=os.environ.get("ENVIRONMENT", "production"), - integrations=[AioHttpIntegration()], - ) + init_sentry() bot = SirBot() diff --git a/pybot/endpoints/slack/actions/mentor_volunteer.py b/pybot/endpoints/slack/actions/mentor_volunteer.py index fc22514..162d429 100644 --- a/pybot/endpoints/slack/actions/mentor_volunteer.py +++ b/pybot/endpoints/slack/actions/mentor_volunteer.py @@ -56,7 +56,9 @@ async def submit_mentor_volunteer(action: Action, app: SirBot) -> None: {"channel": MENTOR_CHANNEL, "users": [user_id]}, ) except SlackAPIError as error: - logger.debug("Error during mentor channel invite %s", error.data.get("errors", error.data)) + logger.debug( + "Error during mentor channel invite %s", error.data.get("errors", error.data) + ) request.on_submit_success() diff --git a/pybot/endpoints/slack/utils/slash_repeat.py b/pybot/endpoints/slack/utils/slash_repeat.py index e421b74..0e45b31 100644 --- a/pybot/endpoints/slack/utils/slash_repeat.py +++ b/pybot/endpoints/slack/utils/slash_repeat.py @@ -36,9 +36,9 @@ def modify_params(modify_options: dict) -> dict: ], } - message["attachments"][0][ - "pretext" - ] = f"<@{modify_options['slack_id']}>: {modify_options['pretext']}" + message["attachments"][0]["pretext"] = ( + f"<@{modify_options['slack_id']}>: {modify_options['pretext']}" + ) message["attachments"][0]["title"] = modify_options["title"] message["attachments"][0]["title_link"] = modify_options["link"] diff --git a/pybot/plugins/airtable/api.py b/pybot/plugins/airtable/api.py index a86b13a..0563e0b 100644 --- a/pybot/plugins/airtable/api.py +++ b/pybot/plugins/airtable/api.py @@ -84,7 +84,9 @@ async def get_row_from_record_id(self, table_name: str, record_id: str) -> dict: # Check for Airtable API error response if "error" in res_json: error_msg = res_json["error"].get("message", "Unknown error") - logger.error(f"Airtable API error for record {record_id} in {table_name}: {error_msg}") + logger.error( + f"Airtable API error for record {record_id} in {table_name}: {error_msg}" + ) return {} return res_json["fields"] @@ -104,9 +106,7 @@ async def get_all_records(self, table_name, field=None): if "error" in res_json: error_msg = res_json["error"].get("message", "Unknown error") error_type = res_json["error"].get("type", "Unknown type") - logger.error( - f"Airtable API error for table '{table_name}': {error_type} - {error_msg}" - ) + logger.error(f"Airtable API error for table '{table_name}': {error_type} - {error_msg}") raise ValueError( f"Airtable API error: {error_type} - {error_msg}. " f"Check AIRTABLE_API_KEY (must be a Personal Access Token starting with 'pat...') " diff --git a/pybot/sentry.py b/pybot/sentry.py new file mode 100644 index 0000000..cdab631 --- /dev/null +++ b/pybot/sentry.py @@ -0,0 +1,144 @@ +""" +Sentry configuration for pybot. + +Provides tracing, profiling, and logging integration following +the same standards as OperationCode/back-end. +""" + +import logging +import os + +import sentry_sdk +from sentry_sdk.integrations.aiohttp import AioHttpIntegration +from sentry_sdk.integrations.logging import LoggingIntegration + + +def strtobool(value): + """Convert string to boolean.""" + if isinstance(value, bool): + return value + value_lower = str(value).lower() + if value_lower in ("true", "1", "yes", "y", "on"): + return True + if value_lower in ("false", "0", "no", "n", "off", ""): + return False + raise ValueError(f"Cannot convert '{value}' to boolean") + + +def config(key, default=None, cast=None): + """Get configuration from environment with optional type casting.""" + value = os.environ.get(key, default) + if cast is not None and value is not None: + return cast(value) + return value + + +def traces_sampler(sampling_context): + """ + Custom sampler to control which transactions are traced. + Returns a sample rate between 0.0 and 1.0. + + Respects parent sampling decisions for distributed tracing. + """ + logger = logging.getLogger(__name__) + + # Respect parent sampling decision for distributed tracing + parent_sampled = sampling_context.get("parent_sampled") + if parent_sampled is not None: + return float(parent_sampled) + + # Get transaction context + transaction_context = sampling_context.get("transaction_context", {}) + transaction_name = transaction_context.get("name", "").lower() + + # Get the request path from various possible sources + request_path = "" + + # Check ASGI scope (for ASGI-based frameworks) + asgi_scope = sampling_context.get("asgi_scope", {}) + if asgi_scope: + request_path = asgi_scope.get("path", "").lower() + + # Check WSGI environ (for WSGI-based frameworks like Django) + wsgi_environ = sampling_context.get("wsgi_environ", {}) + if wsgi_environ and not request_path: + request_path = wsgi_environ.get("PATH_INFO", "").lower() + + # Check aiohttp_request (for aiohttp - this is the actual Request object) + aiohttp_request = sampling_context.get("aiohttp_request") + if aiohttp_request is not None: + try: + request_path = str(aiohttp_request.path).lower() + except Exception: + pass + + # Debug logging to understand what context is being passed + logger.debug( + "traces_sampler called: transaction_name=%s, request_path=%s, context_keys=%s", + transaction_name, + request_path, + list(sampling_context.keys()), + ) + + # Sample health check endpoints at 1% (to catch errors but reduce noise) + # Check both URL paths and handler function names + health_patterns = ["healthz", "health", "readiness", "liveness", "health_check"] + if request_path and any(pattern in request_path for pattern in health_patterns): + logger.debug("Sampling health check path at 1%%: %s", request_path) + return 0.01 + if any(pattern in transaction_name for pattern in health_patterns): + logger.debug("Sampling health check transaction at 1%%: %s", transaction_name) + return 0.01 + + # Use the configured sample rate for everything else + return config("SENTRY_TRACES_SAMPLE_RATE", default=1.0, cast=float) + + +def before_send_transaction(event, hint): # noqa: ARG001 + """ + Filter transactions before sending to Sentry. + Returns None to drop the transaction, or the event to send it. + + Args: + event: The transaction event + hint: Additional context (unused but required by Sentry signature) + """ + # Drop 404 transactions (not found errors are noise, not actionable issues) + if event.get("contexts", {}).get("response", {}).get("status_code") == 404: + return None + + return event + + +def init_sentry(): + """ + Initialize Sentry with tracing, profiling, and logging. + + Only initializes if SENTRY_DSN is set in environment. + """ + sentry_dsn = config("SENTRY_DSN", default="") + + if not sentry_dsn: + return + + sentry_sdk.init( + dsn=sentry_dsn, + integrations=[ + AioHttpIntegration(), + LoggingIntegration( + level=logging.INFO, # Capture info and above as breadcrumbs + event_level=logging.ERROR, # Send errors and above as events + ), + ], + environment=config("ENVIRONMENT", default="production"), + release=config("VERSION", default="1.0.0"), + # Performance Monitoring (Tracing) - use custom sampler to filter health checks + traces_sampler=traces_sampler, + # Filter transactions before sending (e.g., drop 404s) + before_send_transaction=before_send_transaction, + # Set profiles_sample_rate to 1.0 to profile 100% of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=config("SENTRY_PROFILES_SAMPLE_RATE", default=1.0, cast=float), + # Send default PII like user IP and user ID to Sentry + send_default_pii=config("SENTRY_SEND_DEFAULT_PII", default=True, cast=strtobool), + ) diff --git a/pyproject.toml b/pyproject.toml index 22580c9..084ca2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,8 @@ black = "^25.0" ruff = "^0.14" requests = "^2.31" pytest-cov = "^7.0.0" +bandit = "^1.9.3" +safety = "^3.7.0" [tool.poetry.scripts] pybot-manage = "manage:main" diff --git a/tests/conftest.py b/tests/conftest.py index d755d74..c67e759 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ from pybot._vendor.sirbot.plugins.slack import SlackPlugin from pybot.plugins import AirtablePlugin, APIPlugin from tests import data -from tests.fixtures import SlackMock, AirtableMock, AdminSlackMock +from tests.fixtures import AdminSlackMock, AirtableMock, SlackMock pytest_plugins = ("pybot._vendor.slack.tests.plugin",) diff --git a/tests/data/blocks.py b/tests/data/blocks.py index f9dc41b..c3be9fe 100644 --- a/tests/data/blocks.py +++ b/tests/data/blocks.py @@ -109,9 +109,7 @@ def make_mentor_request_blocks( # Add skillsets if provided if skillsets: - blocks[4]["fields"] = [ - {"type": "plain_text", "text": s, "emoji": True} for s in skillsets - ] + blocks[4]["fields"] = [{"type": "plain_text", "text": s, "emoji": True} for s in skillsets] # Add details if provided if details: @@ -462,9 +460,7 @@ class BlockActionPayload(Enum): make_mentor_volunteer_action(skillsets=["Python", "JavaScript"]) ) - MENTOR_VOLUNTEER_NO_SKILLSETS = json.dumps( - make_mentor_volunteer_action(skillsets=None) - ) + MENTOR_VOLUNTEER_NO_SKILLSETS = json.dumps(make_mentor_volunteer_action(skillsets=None)) CLAIM_MENTEE = json.dumps(make_claim_mentee_action(is_claim=True)) diff --git a/tests/endpoints/airtable/test_airtable_webhook.py b/tests/endpoints/airtable/test_airtable_webhook.py index c72e7f6..b1068af 100644 --- a/tests/endpoints/airtable/test_airtable_webhook.py +++ b/tests/endpoints/airtable/test_airtable_webhook.py @@ -5,26 +5,29 @@ finding mentors, and posting to Slack. """ -import pytest from unittest.mock import AsyncMock, MagicMock, patch +import pytest + from pybot._vendor.sirbot import SirBot from pybot.endpoints.airtable.requests import mentor_request from pybot.endpoints.airtable.utils import ( - _get_requested_mentor, - _slack_user_id_from_email, - _get_matching_skillset_mentors, _create_messages, + _get_matching_skillset_mentors, + _get_requested_mentor, _post_messages, + _slack_user_id_from_email, ) from tests.data.blocks import ZAPIER_MENTOR_REQUEST, ZAPIER_MENTOR_REQUEST_WITH_MENTOR -from tests.fixtures import SlackMock, AirtableMock +from tests.fixtures import AirtableMock, SlackMock class TestMentorRequestWebhook: """Tests for the mentor_request webhook handler.""" - async def test_posts_to_mentor_channel(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_posts_to_mentor_channel( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Webhook posts a message to the mentor channel.""" request = ZAPIER_MENTOR_REQUEST.copy() @@ -36,7 +39,9 @@ async def test_posts_to_mentor_channel(self, bot: SirBot, slack_mock: SlackMock, # Should have posted a message slack_mock.assert_called_with_method("chat.postMessage") - async def test_message_includes_claim_button(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_message_includes_claim_button( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Posted message includes a claim button.""" request = ZAPIER_MENTOR_REQUEST.copy() @@ -54,7 +59,9 @@ async def test_message_includes_claim_button(self, bot: SirBot, slack_mock: Slac attachments = data["attachments"] assert len(attachments) > 0 - async def test_includes_requested_mentor_when_provided(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_includes_requested_mentor_when_provided( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Message includes the requested mentor when specified.""" request = ZAPIER_MENTOR_REQUEST_WITH_MENTOR.copy() @@ -67,14 +74,20 @@ async def test_includes_requested_mentor_when_provided(self, bot: SirBot, slack_ slack_mock.assert_called_with_method("chat.postMessage") - async def test_finds_matching_mentors(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_finds_matching_mentors( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Handler finds mentors matching the requested skillsets.""" request = ZAPIER_MENTOR_REQUEST.copy() request["skillsets"] = "Python,AWS" airtable_mock.setup_service("Resume Review", "recSVC001") - airtable_mock.setup_mentor("Jane Mentor", "mentor1@example.com", skillsets=["Python", "JavaScript"]) - airtable_mock.setup_mentor("John Mentor", "mentor2@example.com", skillsets=["AWS", "DevOps"]) + airtable_mock.setup_mentor( + "Jane Mentor", "mentor1@example.com", skillsets=["Python", "JavaScript"] + ) + airtable_mock.setup_mentor( + "John Mentor", "mentor2@example.com", skillsets=["AWS", "DevOps"] + ) slack_mock.setup_lookup_by_email("requester@example.com", "U456REQUESTER") slack_mock.setup_lookup_by_email("mentor1@example.com", "U001") slack_mock.setup_lookup_by_email("mentor2@example.com", "U002") @@ -83,7 +96,9 @@ async def test_finds_matching_mentors(self, bot: SirBot, slack_mock: SlackMock, slack_mock.assert_called_with_method("chat.postMessage") - async def test_user_email_fallback(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_user_email_fallback( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """When user email lookup fails, uses fallback message.""" request = ZAPIER_MENTOR_REQUEST.copy() diff --git a/tests/endpoints/slack/test_general_actions.py b/tests/endpoints/slack/test_general_actions.py index b17c0f1..65e231e 100644 --- a/tests/endpoints/slack/test_general_actions.py +++ b/tests/endpoints/slack/test_general_actions.py @@ -4,14 +4,15 @@ Covers: claimed(), reset_claim(), delete_message() """ -import pytest from unittest.mock import AsyncMock +import pytest + from pybot._vendor.sirbot import SirBot from pybot.endpoints.slack.actions.general_actions import ( claimed, - reset_claim, delete_message, + reset_claim, ) from tests.fixtures import SlackMock diff --git a/tests/endpoints/slack/test_mentor_request_flow.py b/tests/endpoints/slack/test_mentor_request_flow.py index cabd752..19a9140 100644 --- a/tests/endpoints/slack/test_mentor_request_flow.py +++ b/tests/endpoints/slack/test_mentor_request_flow.py @@ -6,33 +6,36 @@ set_requested_mentor, add_skillset, claim_mentee """ -import pytest from unittest.mock import AsyncMock, MagicMock, patch +import pytest + from pybot._vendor.sirbot import SirBot from pybot.endpoints.slack.actions.mentor_request import ( - mentor_request_submit, + add_skillset, + claim_mentee, + clear_mentor, + clear_skillsets, mentor_details_submit, + mentor_request_submit, open_details_dialog, - clear_skillsets, - clear_mentor, set_group, set_requested_service, - add_skillset, - claim_mentee, ) from tests.data.blocks import ( - make_mentor_request_action, make_claim_mentee_action, make_mentor_details_dialog_submission, + make_mentor_request_action, ) -from tests.fixtures import SlackMock, AirtableMock +from tests.fixtures import AirtableMock, SlackMock class TestMentorRequestSubmit: """Tests for mentor_request_submit handler.""" - async def test_submit_success_with_all_fields(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_submit_success_with_all_fields( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Successful submission with all required fields.""" action = make_mentor_request_action( user_id="U123", @@ -51,7 +54,9 @@ async def test_submit_success_with_all_fields(self, bot: SirBot, slack_mock: Sla # Should have added a record to Airtable airtable_mock.assert_record_added("Mentor Request") - async def test_submit_with_skillsets_included(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_submit_with_skillsets_included( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Submission includes skillsets in the Airtable record.""" action = make_mentor_request_action( user_id="U123", @@ -72,7 +77,9 @@ async def test_submit_with_skillsets_included(self, bot: SirBot, slack_mock: Sla fields = added[0][1].get("fields", {}) assert "Skillsets" in fields - async def test_submit_validation_failure_missing_service(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_submit_validation_failure_missing_service( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Submission fails validation when service is missing.""" action = make_mentor_request_action( user_id="U123", @@ -89,7 +96,9 @@ async def test_submit_validation_failure_missing_service(self, bot: SirBot, slac assert len(airtable_mock.get_added_records("Mentor Request")) == 0 slack_mock.assert_called_with_method("chat.update") - async def test_submit_validation_failure_missing_affiliation(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_submit_validation_failure_missing_affiliation( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Submission fails validation when affiliation is missing.""" action = make_mentor_request_action( user_id="U123", @@ -106,7 +115,9 @@ async def test_submit_validation_failure_missing_affiliation(self, bot: SirBot, # Should have updated message with error assert len(airtable_mock.get_added_records("Mentor Request")) == 0 - async def test_submit_validation_failure_missing_details(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_submit_validation_failure_missing_details( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Submission fails validation when details are missing.""" action = make_mentor_request_action( user_id="U123", @@ -123,7 +134,9 @@ async def test_submit_validation_failure_missing_details(self, bot: SirBot, slac # Should not have added a record assert len(airtable_mock.get_added_records("Mentor Request")) == 0 - async def test_submit_fails_when_user_has_no_email(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_submit_fails_when_user_has_no_email( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Submission fails when user's Slack profile has no email.""" action = make_mentor_request_action( user_id="U123", @@ -142,7 +155,9 @@ async def test_submit_fails_when_user_has_no_email(self, bot: SirBot, slack_mock assert len(airtable_mock.get_added_records("Mentor Request")) == 0 slack_mock.assert_called_with_method("chat.update") - async def test_submit_handles_airtable_error(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_submit_handles_airtable_error( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Submission handles Airtable errors gracefully.""" action = make_mentor_request_action( user_id="U123", @@ -167,7 +182,10 @@ class TestMentorRequestFieldHandlers: async def test_set_group_updates_affiliation(self, bot: SirBot, slack_mock: SlackMock): """set_group handler updates affiliation in the message.""" action = make_mentor_request_action() - action["actions"][0]["selected_option"] = {"value": "spouse", "text": {"type": "plain_text", "text": "Spouse"}} + action["actions"][0]["selected_option"] = { + "value": "spouse", + "text": {"type": "plain_text", "text": "Spouse"}, + } await set_group(action, bot) @@ -176,7 +194,10 @@ async def test_set_group_updates_affiliation(self, bot: SirBot, slack_mock: Slac async def test_set_requested_service_updates_service(self, bot: SirBot, slack_mock: SlackMock): """set_requested_service handler updates service in the message.""" action = make_mentor_request_action() - action["actions"][0]["selected_option"] = {"value": "Mock Interview", "text": {"type": "plain_text", "text": "Mock Interview"}} + action["actions"][0]["selected_option"] = { + "value": "Mock Interview", + "text": {"type": "plain_text", "text": "Mock Interview"}, + } await set_requested_service(action, bot) @@ -185,7 +206,10 @@ async def test_set_requested_service_updates_service(self, bot: SirBot, slack_mo async def test_add_skillset_appends_skillset(self, bot: SirBot, slack_mock: SlackMock): """add_skillset handler adds a skillset to the request.""" action = make_mentor_request_action(skillsets=["Python"]) - action["actions"][0]["selected_option"] = {"value": "AWS", "text": {"type": "plain_text", "text": "AWS"}} + action["actions"][0]["selected_option"] = { + "value": "AWS", + "text": {"type": "plain_text", "text": "AWS"}, + } await add_skillset(action, bot) @@ -247,7 +271,9 @@ async def test_submit_details_updates_message(self, bot: SirBot, slack_mock: Sla class TestClaimMentee: """Tests for claim_mentee handler.""" - async def test_claim_success_updates_airtable(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_claim_success_updates_airtable( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Claiming a mentee updates the Airtable record.""" action = make_claim_mentee_action( mentor_id="U123MENTOR", @@ -267,7 +293,9 @@ async def test_claim_success_updates_airtable(self, bot: SirBot, slack_mock: Sla # Should have updated the request with the mentor assert len(airtable_mock._update_history) > 0 - async def test_claim_shows_warning_when_no_email(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_claim_shows_warning_when_no_email( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Claiming shows warning when mentor has no email in profile.""" action = make_claim_mentee_action( mentor_id="U123MENTOR", @@ -283,7 +311,9 @@ async def test_claim_shows_warning_when_no_email(self, bot: SirBot, slack_mock: # Should have called chat.update (to show warning) slack_mock.assert_called_with_method("chat.update") - async def test_claim_shows_warning_when_mentor_not_found(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_claim_shows_warning_when_mentor_not_found( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Claiming shows warning when mentor is not found in Airtable.""" action = make_claim_mentee_action( mentor_id="U123MENTOR", @@ -299,7 +329,9 @@ async def test_claim_shows_warning_when_mentor_not_found(self, bot: SirBot, slac # Should still update message (with warning) slack_mock.assert_called_with_method("chat.update") - async def test_unclaim_resets_attachment(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_unclaim_resets_attachment( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Unclaiming resets the message attachment.""" action = make_claim_mentee_action( mentor_id="U123MENTOR", @@ -312,7 +344,9 @@ async def test_unclaim_resets_attachment(self, bot: SirBot, slack_mock: SlackMoc # Should have updated the message slack_mock.assert_called_with_method("chat.update") - async def test_claim_exception_is_caught(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_claim_exception_is_caught( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Exceptions during claim are caught and logged.""" action = make_claim_mentee_action( mentor_id="U123MENTOR", @@ -330,7 +364,9 @@ async def test_claim_exception_is_caught(self, bot: SirBot, slack_mock: SlackMoc class TestMentorRequestIntegration: """Integration tests for the full mentor request flow.""" - async def test_full_flow_submit_to_claim(self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock): + async def test_full_flow_submit_to_claim( + self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock + ): """Test the full flow from request submission to mentor claim.""" # Step 1: User submits request submit_action = make_mentor_request_action( diff --git a/tests/endpoints/slack/test_mentor_volunteer_flow.py b/tests/endpoints/slack/test_mentor_volunteer_flow.py index f6da569..1a63b29 100644 --- a/tests/endpoints/slack/test_mentor_volunteer_flow.py +++ b/tests/endpoints/slack/test_mentor_volunteer_flow.py @@ -5,26 +5,31 @@ build_airtable_fields """ -import pytest from unittest.mock import AsyncMock, MagicMock, patch +import pytest + from pybot._vendor.sirbot import SirBot from pybot._vendor.slack.exceptions import SlackAPIError from pybot.endpoints.slack.actions.mentor_volunteer import ( add_volunteer_skillset, + build_airtable_fields, clear_volunteer_skillsets, submit_mentor_volunteer, - build_airtable_fields, ) from tests.data.blocks import make_mentor_volunteer_action -from tests.fixtures import SlackMock, AirtableMock, AdminSlackMock +from tests.fixtures import AdminSlackMock, AirtableMock, SlackMock class TestSubmitMentorVolunteer: """Tests for submit_mentor_volunteer handler.""" async def test_submit_success_creates_airtable_record( - self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock, admin_slack_mock: AdminSlackMock + self, + bot: SirBot, + slack_mock: SlackMock, + airtable_mock: AirtableMock, + admin_slack_mock: AdminSlackMock, ): """Successful submission creates an Airtable record.""" action = make_mentor_volunteer_action( @@ -33,7 +38,9 @@ async def test_submit_success_creates_airtable_record( skillsets=["Python", "JavaScript"], ) - slack_mock.setup_user_info("U123", email="volunteer@example.com", name="testuser", real_name="Test User") + slack_mock.setup_user_info( + "U123", email="volunteer@example.com", name="testuser", real_name="Test User" + ) await submit_mentor_volunteer(action, bot) @@ -41,7 +48,11 @@ async def test_submit_success_creates_airtable_record( airtable_mock.assert_record_added("Mentors") async def test_submit_success_invites_to_channel( - self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock, admin_slack_mock: AdminSlackMock + self, + bot: SirBot, + slack_mock: SlackMock, + airtable_mock: AirtableMock, + admin_slack_mock: AdminSlackMock, ): """Successful submission invites user to mentor channel.""" action = make_mentor_volunteer_action( @@ -57,7 +68,11 @@ async def test_submit_success_invites_to_channel( admin_slack_mock.assert_invited_to_channel("mentors-internal", "U123") async def test_submit_handles_invite_error_gracefully( - self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock, admin_slack_mock: AdminSlackMock + self, + bot: SirBot, + slack_mock: SlackMock, + airtable_mock: AirtableMock, + admin_slack_mock: AdminSlackMock, ): """Submission continues even if channel invite fails.""" action = make_mentor_volunteer_action( @@ -77,7 +92,11 @@ async def test_submit_handles_invite_error_gracefully( airtable_mock.assert_record_added("Mentors") async def test_submit_validation_fails_without_skillsets( - self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock, admin_slack_mock: AdminSlackMock + self, + bot: SirBot, + slack_mock: SlackMock, + airtable_mock: AirtableMock, + admin_slack_mock: AdminSlackMock, ): """Submission fails validation when no skillsets selected.""" action = make_mentor_volunteer_action( @@ -95,7 +114,11 @@ async def test_submit_validation_fails_without_skillsets( slack_mock.assert_called_with_method("chat.update") async def test_submit_handles_airtable_error( - self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock, admin_slack_mock: AdminSlackMock + self, + bot: SirBot, + slack_mock: SlackMock, + airtable_mock: AirtableMock, + admin_slack_mock: AdminSlackMock, ): """Submission handles Airtable errors gracefully.""" action = make_mentor_volunteer_action( @@ -112,7 +135,11 @@ async def test_submit_handles_airtable_error( slack_mock.assert_called_with_method("chat.update") async def test_submit_updates_message_on_success( - self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock, admin_slack_mock: AdminSlackMock + self, + bot: SirBot, + slack_mock: SlackMock, + airtable_mock: AirtableMock, + admin_slack_mock: AdminSlackMock, ): """Successful submission updates message with success state.""" action = make_mentor_volunteer_action( @@ -178,6 +205,7 @@ async def test_build_fields_includes_all_required_data(self): ) from pybot.endpoints.slack.message_templates.mentor_volunteer import MentorVolunteer + request = MentorVolunteer(action) user_info = { @@ -202,6 +230,7 @@ async def test_build_fields_filters_empty_skillset(self): ) from pybot.endpoints.slack.message_templates.mentor_volunteer import MentorVolunteer + request = MentorVolunteer(action) user_info = { @@ -221,7 +250,11 @@ class TestMentorVolunteerIntegration: """Integration tests for the full mentor volunteer flow.""" async def test_full_volunteer_flow( - self, bot: SirBot, slack_mock: SlackMock, airtable_mock: AirtableMock, admin_slack_mock: AdminSlackMock + self, + bot: SirBot, + slack_mock: SlackMock, + airtable_mock: AirtableMock, + admin_slack_mock: AdminSlackMock, ): """Test the full flow from adding skillsets to submission.""" # Step 1: Add first skillset @@ -246,7 +279,9 @@ async def test_full_volunteer_flow( skillsets=["Python", "JavaScript"], ) - slack_mock.setup_user_info("U123", email="volunteer@example.com", real_name="Test Volunteer") + slack_mock.setup_user_info( + "U123", email="volunteer@example.com", real_name="Test Volunteer" + ) await submit_mentor_volunteer(submit_action, bot) diff --git a/tests/endpoints/slack/test_message_handlers.py b/tests/endpoints/slack/test_message_handlers.py index 7c13465..3e537ad 100644 --- a/tests/endpoints/slack/test_message_handlers.py +++ b/tests/endpoints/slack/test_message_handlers.py @@ -6,19 +6,20 @@ """ import logging -import pytest from unittest.mock import AsyncMock, MagicMock, patch +import pytest + from pybot._vendor.sirbot import SirBot from pybot._vendor.slack.events import Message from pybot.endpoints.slack.messages import ( advertise_pybot, here_bad, - tech_tips, message_changed, message_deleted, - not_bot_message, not_bot_delete, + not_bot_message, + tech_tips, ) from tests.fixtures import SlackMock @@ -187,9 +188,7 @@ async def test_tech_tips_posts_response(self, bot: SirBot, slack_mock: SlackMock """tech_tips() posts a response when triggered.""" event = make_message_event(channel_id="C123", user_id="U456", text="!tech python") - with patch( - "pybot.endpoints.slack.messages.TechTerms" - ) as mock_tech: + with patch("pybot.endpoints.slack.messages.TechTerms") as mock_tech: mock_instance = MagicMock() mock_instance.grab_values = AsyncMock( return_value={"message": {"channel": "C123", "text": "Python info"}} diff --git a/tests/endpoints/slack/test_new_member_actions.py b/tests/endpoints/slack/test_new_member_actions.py index ca52cf6..b1612e1 100644 --- a/tests/endpoints/slack/test_new_member_actions.py +++ b/tests/endpoints/slack/test_new_member_actions.py @@ -5,18 +5,19 @@ member_greeted(), reset_greet(), member_messaged(), reset_message() """ -import pytest from unittest.mock import AsyncMock +import pytest + from pybot._vendor.sirbot import SirBot from pybot.endpoints.slack.actions.new_member import ( - resource_buttons, + member_greeted, + member_messaged, open_suggestion, post_suggestion, - member_greeted, reset_greet, - member_messaged, reset_message, + resource_buttons, ) from tests.fixtures import SlackMock @@ -183,7 +184,9 @@ async def test_open_suggestion_uses_trigger_id(self, bot: SirBot, slack_mock: Sl class TestPostSuggestion: """Tests for post_suggestion() handler.""" - async def test_post_suggestion_posts_to_community_channel(self, bot: SirBot, slack_mock: SlackMock): + async def test_post_suggestion_posts_to_community_channel( + self, bot: SirBot, slack_mock: SlackMock + ): """post_suggestion() posts to the community channel.""" action = make_suggestion_submission( user_id="U456", diff --git a/tests/endpoints/slack/test_report_actions.py b/tests/endpoints/slack/test_report_actions.py index f7096b6..73c6b81 100644 --- a/tests/endpoints/slack/test_report_actions.py +++ b/tests/endpoints/slack/test_report_actions.py @@ -5,9 +5,10 @@ """ import json -import pytest from unittest.mock import AsyncMock, MagicMock +import pytest + from pybot._vendor.sirbot import SirBot from pybot._vendor.slack.actions import Action from pybot.endpoints.slack.actions.report_message import ( @@ -81,7 +82,9 @@ async def test_open_report_dialog_uses_trigger_id(self, bot: SirBot, slack_mock: data = calls[0][1] assert data["trigger_id"] == "trigger999" - async def test_open_report_dialog_includes_dialog_config(self, bot: SirBot, slack_mock: SlackMock): + async def test_open_report_dialog_includes_dialog_config( + self, bot: SirBot, slack_mock: SlackMock + ): """open_report_dialog() includes dialog configuration.""" action = make_report_action() diff --git a/tests/endpoints/slack/test_slack_events.py b/tests/endpoints/slack/test_slack_events.py index 0a791ef..c6f239c 100644 --- a/tests/endpoints/slack/test_slack_events.py +++ b/tests/endpoints/slack/test_slack_events.py @@ -12,15 +12,15 @@ import pytest from pybot import endpoints +from pybot._vendor.slack.events import Event from pybot.endpoints.slack.events import team_join from pybot.endpoints.slack.utils.event_utils import ( build_messages, - send_user_greetings, - send_community_notification, - link_backend_user, get_backend_auth_headers, + link_backend_user, + send_community_notification, + send_user_greetings, ) -from pybot._vendor.slack.events import Event from tests.data.events import MESSAGE_DELETE, MESSAGE_EDIT, PLAIN_MESSAGE, TEAM_JOIN @@ -67,9 +67,7 @@ async def test_team_join_asyncio_gather_does_not_raise_typeerror(bot): with ( patch("pybot.endpoints.slack.events.asyncio.sleep", new_callable=AsyncMock), - patch( - "pybot.endpoints.slack.events.send_user_greetings", new_callable=AsyncMock - ), + patch("pybot.endpoints.slack.events.send_user_greetings", new_callable=AsyncMock), patch( "pybot.endpoints.slack.events.send_community_notification", new_callable=AsyncMock, @@ -160,12 +158,12 @@ class TestLinkBackendUser: async def test_link_backend_user_handles_missing_email(self): """link_backend_user returns early when user has no email.""" mock_slack_api = AsyncMock() - mock_slack_api.query.return_value = { - "user": {"profile": {}} # No email - } + mock_slack_api.query.return_value = {"user": {"profile": {}}} # No email mock_session = AsyncMock() - await link_backend_user("U123", {"Authorization": "Bearer token"}, mock_slack_api, mock_session) + await link_backend_user( + "U123", {"Authorization": "Bearer token"}, mock_slack_api, mock_session + ) # Should not call session.patch when no email mock_session.patch.assert_not_called() @@ -173,9 +171,7 @@ async def test_link_backend_user_handles_missing_email(self): async def test_link_backend_user_calls_backend_api(self): """link_backend_user calls the backend API with correct params.""" mock_slack_api = AsyncMock() - mock_slack_api.query.return_value = { - "user": {"profile": {"email": "user@example.com"}} - } + mock_slack_api.query.return_value = {"user": {"profile": {"email": "user@example.com"}}} mock_response = AsyncMock() mock_response.json.return_value = {"success": True} @@ -184,10 +180,7 @@ async def test_link_backend_user_calls_backend_api(self): mock_session.patch.return_value.__aenter__.return_value = mock_response await link_backend_user( - "U123", - {"Authorization": "Bearer token"}, - mock_slack_api, - mock_session + "U123", {"Authorization": "Bearer token"}, mock_slack_api, mock_session ) mock_session.patch.assert_called_once() diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index 857b448..d6891f4 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -2,8 +2,8 @@ Reusable mock classes for testing Slack and Airtable integrations. """ -from unittest.mock import AsyncMock, MagicMock from typing import Any +from unittest.mock import AsyncMock, MagicMock class SlackMock: @@ -33,9 +33,9 @@ async def _handle_query(self, method: str = "", data: dict | None = None, **kwar actual_method = kwargs.get("url", method) # Extract URL from Methods enum (the value is a namedtuple with url attribute) - if hasattr(actual_method, 'value') and hasattr(actual_method.value, 'url'): + if hasattr(actual_method, "value") and hasattr(actual_method.value, "url"): method_url = actual_method.value.url - elif hasattr(actual_method, 'url'): + elif hasattr(actual_method, "url"): method_url = actual_method.url elif isinstance(actual_method, str): method_url = actual_method @@ -59,7 +59,10 @@ async def _handle_query(self, method: str = "", data: dict | None = None, **kwar return {"ok": True, "user": {"id": self._email_lookup[email]}} # Raise error if email not found from pybot._vendor.slack.exceptions import SlackAPIError - raise SlackAPIError(error={"ok": False, "error": "users_not_found"}, headers={}, data={}) + + raise SlackAPIError( + error={"ok": False, "error": "users_not_found"}, headers={}, data={} + ) # Check for user.info requests if "users.info" in method_str and data and "user" in data: @@ -94,8 +97,13 @@ async def _handle_query(self, method: str = "", data: dict | None = None, **kwar default_response["ts"] = "1234567890.123456" return default_response - def setup_user_info(self, user_id: str, email: str | None = None, - name: str = "Test User", real_name: str = "Test User") -> "SlackMock": + def setup_user_info( + self, + user_id: str, + email: str | None = None, + name: str = "Test User", + real_name: str = "Test User", + ) -> "SlackMock": """Configure response for users.info API call.""" profile = {"real_name": real_name} if email: @@ -108,7 +116,7 @@ def setup_user_info(self, user_id: str, email: str | None = None, "name": name, "real_name": real_name, "profile": profile, - } + }, } return self @@ -131,11 +139,15 @@ def assert_called_with_method(self, method: str, times: int | None = None) -> No """Assert a Slack API method was called a specific number of times.""" call_count = sum(1 for m, _ in self._call_history if method in str(m)) if times is not None: - assert call_count == times, f"Expected {method} to be called {times} times, got {call_count}" + assert call_count == times, ( + f"Expected {method} to be called {times} times, got {call_count}" + ) else: assert call_count > 0, f"Expected {method} to be called at least once" - def assert_message_sent_to_channel(self, channel_id: str, text_contains: str | None = None) -> None: + def assert_message_sent_to_channel( + self, channel_id: str, text_contains: str | None = None + ) -> None: """Assert a message was sent to a specific channel.""" for method, data in self._call_history: if "chat" in method and data.get("channel") == channel_id: @@ -144,8 +156,8 @@ def assert_message_sent_to_channel(self, channel_id: str, text_contains: str | N if text_contains in data.get("text", ""): return raise AssertionError( - f"No message found for channel {channel_id}" + - (f" containing '{text_contains}'" if text_contains else "") + f"No message found for channel {channel_id}" + + (f" containing '{text_contains}'" if text_contains else "") ) def get_calls(self, method: str | None = None) -> list[tuple[str, dict]]: @@ -281,8 +293,14 @@ def setup_service(self, name: str, record_id: str | None = None) -> "AirtableMoc self._services[record_id] = {"Name": name} return self - def setup_mentor(self, name: str, email: str, skillsets: list[str] | None = None, - record_id: str | None = None, slack_name: str | None = None) -> "AirtableMock": + def setup_mentor( + self, + name: str, + email: str, + skillsets: list[str] | None = None, + record_id: str | None = None, + slack_name: str | None = None, + ) -> "AirtableMock": """Add a mentor to the mock.""" record_id = record_id or f"recmentor{len(self._mentors)}" self._mentors[record_id] = { @@ -343,6 +361,7 @@ def __init__(self, bot): # Create a mock admin_slack plugin if it doesn't exist if "admin_slack" not in bot["plugins"]: from unittest.mock import MagicMock + bot["plugins"]["admin_slack"] = MagicMock() bot["plugins"]["admin_slack"].api = MagicMock() @@ -352,7 +371,7 @@ def __init__(self, bot): async def _handle_query(self, method: str, data: dict | None = None, **kwargs) -> dict: """Handle admin Slack API queries.""" # Extract URL from Methods enum if needed - if hasattr(method, 'value') and hasattr(method.value, 'url'): + if hasattr(method, "value") and hasattr(method.value, "url"): method_url = method.value.url elif isinstance(method, str): method_url = method @@ -372,6 +391,7 @@ async def _handle_query(self, method: str, data: dict | None = None, **kwargs) - def setup_invite_error(self, error: Exception) -> "AdminSlackMock": """Configure an error for channel invites.""" from pybot._vendor.slack import methods + self._errors[methods.CONVERSATIONS_INVITE.value.url] = error return self @@ -382,8 +402,7 @@ def assert_invited_to_channel(self, channel: str, user_id: str | None = None) -> if user_id is None or user_id in users: return raise AssertionError( - f"No invite found for channel {channel}" + - (f" with user {user_id}" if user_id else "") + f"No invite found for channel {channel}" + (f" with user {user_id}" if user_id else "") ) def reset(self) -> None: diff --git a/tests/integration/test_airtable_read.py b/tests/integration/test_airtable_read.py index 27b4733..4052695 100644 --- a/tests/integration/test_airtable_read.py +++ b/tests/integration/test_airtable_read.py @@ -10,8 +10,9 @@ """ import os -import pytest + import aiohttp +import pytest from pybot.plugins.airtable.api import AirtableAPI diff --git a/tests/unit/test_high_severity_fixes.py b/tests/unit/test_high_severity_fixes.py index 7c1b2e8..41a7799 100644 --- a/tests/unit/test_high_severity_fixes.py +++ b/tests/unit/test_high_severity_fixes.py @@ -5,9 +5,10 @@ cause crashes or stacktraces in production. """ -import pytest from unittest.mock import AsyncMock, MagicMock, patch +import pytest + from pybot.endpoints.slack.utils.slash_lunch import LunchCommand from pybot.plugins.airtable.api import AirtableAPI @@ -137,9 +138,7 @@ async def mock_get(url, params=None): return {"error": {"message": "Rate limited"}} with patch.object(airtable_api, "get", side_effect=mock_get): - result = await airtable_api._depaginate_records( - "http://test", {}, "initial_offset" - ) + result = await airtable_api._depaginate_records("http://test", {}, "initial_offset") # Should return the records from the first page only assert len(result) == 1 @@ -224,9 +223,7 @@ async def test_get_requested_mentor_handles_missing_email(self): mock_slack = AsyncMock() mock_airtable = AsyncMock() - mock_airtable.get_row_from_record_id.return_value = { - "Name": "John Doe" - } # No Email field + mock_airtable.get_row_from_record_id.return_value = {"Name": "John Doe"} # No Email field result = await _get_requested_mentor("rec123", mock_slack, mock_airtable) @@ -262,9 +259,7 @@ async def test_link_backend_user_handles_missing_email(self): from pybot.endpoints.slack.utils.event_utils import link_backend_user mock_slack_api = AsyncMock() - mock_slack_api.query.return_value = { - "user": {"profile": {}} # No email field - } + mock_slack_api.query.return_value = {"user": {"profile": {}}} # No email field mock_session = AsyncMock() # Should not raise, should return early diff --git a/tests/unit/test_mentor_request_model.py b/tests/unit/test_mentor_request_model.py index a35b457..d4016ea 100644 --- a/tests/unit/test_mentor_request_model.py +++ b/tests/unit/test_mentor_request_model.py @@ -5,15 +5,16 @@ and error attachment handling. """ -import pytest from unittest.mock import AsyncMock, MagicMock +import pytest + from pybot.endpoints.slack.message_templates.mentor_request import ( + BlockIndex, MentorRequest, MentorRequestClaim, - BlockIndex, ) -from tests.data.blocks import make_mentor_request_action, make_claim_mentee_action +from tests.data.blocks import make_claim_mentee_action, make_mentor_request_action class TestMentorRequestProperties: @@ -38,7 +39,10 @@ def test_service_setter_updates_block(self): action = make_mentor_request_action() request = MentorRequest(action) - new_option = {"text": {"type": "plain_text", "text": "Mock Interview"}, "value": "Mock Interview"} + new_option = { + "text": {"type": "plain_text", "text": "Mock Interview"}, + "value": "Mock Interview", + } request.service = new_option assert request.blocks[BlockIndex.SERVICE]["accessory"]["initial_option"] == new_option @@ -399,7 +403,9 @@ async def test_claim_request_updates_attachment_when_mentor_found(self): claim = MentorRequestClaim(action, slack_mock, airtable_mock) await claim.claim_request("recMENTOR001") - assert "claimed by" in claim.attachment["text"].lower() or ":100:" in claim.attachment["text"] + assert ( + "claimed by" in claim.attachment["text"].lower() or ":100:" in claim.attachment["text"] + ) assert claim.should_update is True @pytest.mark.asyncio @@ -414,7 +420,10 @@ async def test_claim_request_shows_error_when_mentor_not_found(self): await claim.claim_request(None) assert ":warning:" in claim.attachment["text"] - assert "not found" in claim.attachment["text"].lower() or "email" in claim.attachment["text"].lower() + assert ( + "not found" in claim.attachment["text"].lower() + or "email" in claim.attachment["text"].lower() + ) assert claim.should_update is False @pytest.mark.asyncio diff --git a/tests/unit/test_mentor_volunteer_model.py b/tests/unit/test_mentor_volunteer_model.py index 62b90d3..56426c8 100644 --- a/tests/unit/test_mentor_volunteer_model.py +++ b/tests/unit/test_mentor_volunteer_model.py @@ -184,10 +184,7 @@ def test_on_submit_success_includes_dismiss_button(self): # Find the actions block actions_block = volunteer.blocks[1] assert actions_block["type"] == "actions" - assert any( - e.get("action_id") == "cancel_btn" - for e in actions_block.get("elements", []) - ) + assert any(e.get("action_id") == "cancel_btn" for e in actions_block.get("elements", [])) def test_on_submit_success_mentions_mentor_channel(self): """Success message mentions the mentor channel.""" diff --git a/tests/unit/test_sentry_sampler.py b/tests/unit/test_sentry_sampler.py new file mode 100644 index 0000000..9936d00 --- /dev/null +++ b/tests/unit/test_sentry_sampler.py @@ -0,0 +1,128 @@ +"""Tests for Sentry traces_sampler configuration.""" + +import pytest + +from pybot.sentry import traces_sampler + + +class TestTracesSampler: + """Test the traces_sampler function with various contexts.""" + + def test_respects_parent_sampled_true(self): + """Should return 1.0 when parent is sampled.""" + context = {"parent_sampled": True} + assert traces_sampler(context) == 1.0 + + def test_respects_parent_sampled_false(self): + """Should return 0.0 when parent is not sampled.""" + context = {"parent_sampled": False} + assert traces_sampler(context) == 0.0 + + def test_health_check_by_transaction_name(self): + """Should sample health check transactions at 1%.""" + context = {"transaction_context": {"name": "pybot.endpoints.handle_health_check"}} + assert traces_sampler(context) == 0.01 + + def test_health_path_in_asgi_scope(self): + """Should sample /health path at 1%.""" + context = { + "transaction_context": {"name": "some_handler"}, + "asgi_scope": {"path": "/health"}, + } + assert traces_sampler(context) == 0.01 + + def test_healthz_path_in_asgi_scope(self): + """Should sample /healthz path at 1%.""" + context = { + "transaction_context": {"name": "some_handler"}, + "asgi_scope": {"path": "/healthz"}, + } + assert traces_sampler(context) == 0.01 + + def test_liveness_path(self): + """Should sample liveness endpoints at 1%.""" + context = {"transaction_context": {"name": "liveness_check"}} + assert traces_sampler(context) == 0.01 + + def test_readiness_path(self): + """Should sample readiness endpoints at 1%.""" + context = {"transaction_context": {"name": "readiness_probe"}} + assert traces_sampler(context) == 0.01 + + def test_normal_transaction_uses_default(self): + """Should use default sample rate for non-health transactions.""" + context = {"transaction_context": {"name": "pybot.endpoints.slack.handle_event"}} + # Default is 1.0 + assert traces_sampler(context) == 1.0 + + def test_empty_context_uses_default(self): + """Should use default sample rate for empty context.""" + assert traces_sampler({}) == 1.0 + + def test_case_insensitive_matching(self): + """Should match health patterns case-insensitively.""" + context = {"transaction_context": {"name": "HANDLE_HEALTH_CHECK"}} + assert traces_sampler(context) == 0.01 + + def test_aiohttp_request_object_with_health_path(self): + """Test with aiohttp_request object (what aiohttp integration actually provides).""" + + # Mock aiohttp Request object with .path attribute + class MockRequest: + path = "/health" + + context = { + "transaction_context": {"name": "generic AIOHTTP request"}, + "aiohttp_request": MockRequest(), + } + # Should match on aiohttp_request.path + assert traces_sampler(context) == 0.01 + + def test_aiohttp_request_object_with_healthz_path(self): + """Test with /healthz path.""" + + class MockRequest: + path = "/healthz" + + context = { + "transaction_context": {"name": "generic AIOHTTP request"}, + "aiohttp_request": MockRequest(), + } + assert traces_sampler(context) == 0.01 + + def test_aiohttp_request_object_with_other_path(self): + """Test that non-health paths get default sample rate.""" + + class MockRequest: + path = "/api/users" + + context = { + "transaction_context": {"name": "generic AIOHTTP request"}, + "aiohttp_request": MockRequest(), + } + # Should NOT match health pattern + assert traces_sampler(context) == 1.0 + + def test_aiohttp_request_object_with_liveness_path(self): + """Test with /liveness path.""" + + class MockRequest: + path = "/liveness" + + context = { + "transaction_context": {"name": "generic AIOHTTP request"}, + "aiohttp_request": MockRequest(), + } + assert traces_sampler(context) == 0.01 + + def test_aiohttp_request_object_with_readiness_path(self): + """Test with /readiness path.""" + + class MockRequest: + path = "/readiness" + + context = { + "transaction_context": {"name": "generic AIOHTTP request"}, + "aiohttp_request": MockRequest(), + } + assert traces_sampler(context) == 0.01 From 13585ff1aa96cf77901047415cb958978da9660b Mon Sep 17 00:00:00 2001 From: Irving Popovetsky Date: Mon, 19 Jan 2026 14:32:33 -0800 Subject: [PATCH 2/2] readme updates --- README.md | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3f2b277..7caa949 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,17 @@
- + Operation Code Hacktoberfest Banner
-
-
- -
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -[![Twitter Follow](https://img.shields.io/twitter/follow/operation_code.svg?style=social&label=Follow&style=social)](https://twitter.com/operation_code) -[![Code-style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) - - -[![CircleCI](https://circleci.com/gh/OperationCode/operationcode-pybot.svg?style=svg)](https://circleci.com/gh/OperationCode/operationcode-pybot) -[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=OperationCode/operationcode-pybot)](https://dependabot.com) +[![CI](https://github.com/OperationCode/operationcode-pybot/actions/workflows/ci.yml/badge.svg)](https://github.com/OperationCode/operationcode-pybot/actions/workflows/ci.yml) +[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://contributor-covenant.org/) # [OperationCode-Pybot](https://github.com/OperationCode/operationcode-pybot) @@ -41,7 +34,7 @@ The vendored code has been modernized with: - Removed deprecated `asyncio.coroutine()` usage - Fixed deprecated `loop=` parameter patterns - Replaced removed `cgi` module with `email.message` -- Added Python 3.12 type hints +- Added Python 3.12+ type hints ## Resources * [Slack Bot Tutorial](https://www.digitalocean.com/community/tutorials/how-to-build-a-slackbot-in-python-on-ubuntu-20-04) @@ -54,9 +47,9 @@ Bug reports and pull requests are welcome on [Github](https://github.com/Operati ## Quick Start Recommended versions of tools used within the repo: -- `python@3.12` or greater (Python 3.13+ also supported) +- `python@3.14` or greater - `git@2.17.1` or greater -- `poetry@1.0` or greater +- `poetry@2.0` or greater - [Poetry](https://python-poetry.org/) is a packaging and dependency manager, similar to pip or pipenv - Install via: `curl -sSL https://install.python-poetry.org | python3 -` - See https://python-poetry.org/docs/ @@ -73,7 +66,7 @@ poetry run python -m pybot poetry run pytest # Run formatting and linting -poetry run black pybot/ tests/ +poetry run ruff format pybot/ tests/ poetry run ruff check pybot/ tests/ ``` @@ -253,7 +246,7 @@ Command | Description | Usage Hint /ticket | submit ticket to admins | (text of ticket) -**👋 IMPORTANT!** +**IMPORTANT!** The `/lunch` command requires a valid Yelp API token stored in the `YELP_TOKEN` environment variable. See https://www.yelp.com/developers/faq @@ -264,7 +257,7 @@ functionality please reach out to the `#oc-python-projects` channel for help get ### Airtable Authentication -**⚠️ IMPORTANT:** Airtable deprecated API keys on February 1, 2024. You must use a **Personal Access Token (PAT)**: +**IMPORTANT:** Airtable deprecated API keys on February 1, 2024. You must use a **Personal Access Token (PAT)**: 1. Go to [Airtable Developer Hub](https://airtable.com/create/tokens) 2. Click **Create new token**