diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..ac6b69b143 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,34 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(ls:*)", + "Bash(git:*)", + "Bash(git status:*)", + "Bash(git log:*)", + "Bash(git diff:*)", + "Bash(git show:*)", + "Bash(git branch:*)", + "Bash(git remote:*)", + "Bash(git tag:*)", + "Bash(git stash list:*)", + "Bash(git rev-parse:*)", + "Bash(gh pr view:*)", + "Bash(gh pr list:*)", + "Bash(gh pr checks:*)", + "Bash(gh pr diff:*)", + "Bash(gh issue view:*)", + "Bash(gh issue list:*)", + "Bash(gh run view:*)", + "Bash(gh run list:*)", + "Bash(gh run logs:*)", + "Bash(gh repo view:*)", + "WebFetch(domain:github.com)", + "WebFetch(domain:docs.sentry.io)", + "WebFetch(domain:develop.sentry.dev)", + "Bash(grep:*)", + "Bash(mv:*)" + ], + "deny": [] + } +} diff --git a/.craft.yml b/.craft.yml index 665f06834a..ea98f2d402 100644 --- a/.craft.yml +++ b/.craft.yml @@ -1,4 +1,4 @@ -minVersion: 0.34.1 +minVersion: 2.17.0 targets: - name: pypi includeNames: /^sentry[_\-]sdk.*$/ diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 8efbe19ec3..e354c1ed92 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -2,3 +2,4 @@ afea4a017bf13f78e82f725ea9d6a56a8e02cb34 23a340a9dca60eea36de456def70c00952a33556 973dda79311cf6b9cb8f1ba67ca0515dfaf9f49c +e275c9e94323b429f39196881fb992d81a2e52ea diff --git a/.github/release.yml b/.github/release.yml index 058bc4d5bb..4383f70e7d 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -10,23 +10,28 @@ changelog: - "Changelog: Feature" commit_patterns: - "^feat\\b" + semver: minor - title: Bug Fixes 🐛 labels: - "Changelog: Bugfix" commit_patterns: - "^(fix|bugfix)\\b" + semver: patch - title: Deprecations 🏗️ labels: - "Changelog: Deprecation" commit_patterns: - "deprecat" # deprecation, deprecated + semver: patch - title: Documentation 📚 labels: - "Changelog: Docs" commit_patterns: - "^docs?\\b" + semver: patch - title: Internal Changes 🔧 labels: - "Changelog: Internal" commit_patterns: - "^(build|ref|chore|ci|tests?)\\b" + semver: patch diff --git a/.github/workflows/ai-integration-test.yml b/.github/workflows/ai-integration-test.yml new file mode 100644 index 0000000000..bd1f1c6f85 --- /dev/null +++ b/.github/workflows/ai-integration-test.yml @@ -0,0 +1,45 @@ +name: AI integration tests + +on: + workflow_dispatch: + schedule: + # every weekday + - cron: '23 3 * * 1-5' + +jobs: + ai-integration-tests: + name: AI integration tests + runs-on: ubuntu-latest + environment: "AI Integrations Tests" + timeout-minutes: 10 + + permissions: + contents: write + issues: write + + steps: + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: 3.14t + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: Checkout repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Python SDK Tests + uses: getsentry/testing-ai-sdk-integrations@dba21cbfb57482556338983d8c35e6b09b534667 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + language: py + sentry-python-path: ${{ github.workspace }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + google-api-key: ${{ secrets.GOOGLE_API_KEY }} diff --git a/.github/workflows/changelog-preview.yml b/.github/workflows/changelog-preview.yml new file mode 100644 index 0000000000..3788b29609 --- /dev/null +++ b/.github/workflows/changelog-preview.yml @@ -0,0 +1,19 @@ +name: Changelog Preview +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + - edited + - labeled + - unlabeled +permissions: + contents: write + pull-requests: write + statuses: write + +jobs: + changelog-preview: + uses: getsentry/craft/.github/workflows/changelog-preview.yml@v2 + secrets: inherit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44fe331b34..c3fc1ae316 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-python@v6 with: python-version: 3.14 @@ -39,7 +39,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-python@v6 with: python-version: 3.12 @@ -70,7 +70,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-python@v6 with: python-version: 3.12 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 79af0a3039..a8f3e199bd 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release-comment-issues.yml b/.github/workflows/release-comment-issues.yml index 8870f25bc0..e18aeab155 100644 --- a/.github/workflows/release-comment-issues.yml +++ b/.github/workflows/release-comment-issues.yml @@ -10,6 +10,11 @@ on: required: false # This workflow is triggered when a release is published +permissions: + issues: write + contents: write + pull-requests: write + jobs: release-comment-issues: runs-on: ubuntu-20.04 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a5b89d2734..fe56354e7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,38 +1,40 @@ name: Release - on: workflow_dispatch: inputs: version: - description: Version to release - required: true + description: Version to release (or "auto") + required: false force: - description: Force a release even when there are release-blockers (optional) + description: Force a release even when there are release-blockers required: false merge_target: - description: Target branch to merge into. Uses the default branch as a fallback (optional) + description: Target branch to merge into required: false +permissions: + contents: write + pull-requests: write jobs: release: runs-on: ubuntu-latest - name: "Release a new version" + name: Release a new version steps: - - name: Get auth token - id: token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 - with: - app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} - private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} - - uses: actions/checkout@v6.0.1 - with: - token: ${{ steps.token.outputs.token }} - fetch-depth: 0 - - name: Prepare release - uses: getsentry/action-prepare-release@v1 - env: - GITHUB_TOKEN: ${{ steps.token.outputs.token }} - with: - version: ${{ github.event.inputs.version }} - force: ${{ github.event.inputs.force }} - merge_target: ${{ github.event.inputs.merge_target }} + - name: Get auth token + id: token + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 + with: + app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} + private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ steps.token.outputs.token }} + fetch-depth: 0 + - name: Prepare release + uses: getsentry/craft@beea4aba589c66381258cbd131c5551ae8245b82 # v2 + env: + GITHUB_TOKEN: ${{ steps.token.outputs.token }} + with: + version: ${{ inputs.version }} + force: ${{ inputs.force }} + merge_target: ${{ inputs.merge_target }} diff --git a/.github/workflows/test-integrations-agents.yml b/.github/workflows/test-integrations-agents.yml index e2be8508ea..a05649a5f0 100644 --- a/.github/workflows/test-integrations-agents.yml +++ b/.github/workflows/test-integrations-agents.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -69,21 +72,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All Agents tests passed diff --git a/.github/workflows/test-integrations-ai-workflow.yml b/.github/workflows/test-integrations-ai-workflow.yml index d641becd25..7cd4cb86df 100644 --- a/.github/workflows/test-integrations-ai-workflow.yml +++ b/.github/workflows/test-integrations-ai-workflow.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -73,21 +76,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All AI Workflow tests passed diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index fc1a9f7b90..0b305a3775 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -89,21 +92,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All AI tests passed diff --git a/.github/workflows/test-integrations-cloud.yml b/.github/workflows/test-integrations-cloud.yml index d655af0a1a..d57034d4e3 100644 --- a/.github/workflows/test-integrations-cloud.yml +++ b/.github/workflows/test-integrations-cloud.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -42,7 +45,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -85,21 +88,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All Cloud tests passed diff --git a/.github/workflows/test-integrations-common.yml b/.github/workflows/test-integrations-common.yml index b87b8e56d2..9b333435bd 100644 --- a/.github/workflows/test-integrations-common.yml +++ b/.github/workflows/test-integrations-common.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -65,21 +68,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All Common tests passed diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index 4638525be7..b1dadb0ca5 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -56,7 +59,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -105,21 +108,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All DBs tests passed diff --git a/.github/workflows/test-integrations-flags.yml b/.github/workflows/test-integrations-flags.yml index 9c3c647937..dded26658a 100644 --- a/.github/workflows/test-integrations-flags.yml +++ b/.github/workflows/test-integrations-flags.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -77,21 +80,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All Flags tests passed diff --git a/.github/workflows/test-integrations-gevent.yml b/.github/workflows/test-integrations-gevent.yml index 0d57e14224..525140dfa7 100644 --- a/.github/workflows/test-integrations-gevent.yml +++ b/.github/workflows/test-integrations-gevent.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -65,21 +68,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All Gevent tests passed diff --git a/.github/workflows/test-integrations-graphql.yml b/.github/workflows/test-integrations-graphql.yml index 8e27210148..322a95ff54 100644 --- a/.github/workflows/test-integrations-graphql.yml +++ b/.github/workflows/test-integrations-graphql.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -77,21 +80,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All GraphQL tests passed diff --git a/.github/workflows/test-integrations-mcp.yml b/.github/workflows/test-integrations-mcp.yml index e986d1e358..4b576a897f 100644 --- a/.github/workflows/test-integrations-mcp.yml +++ b/.github/workflows/test-integrations-mcp.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -69,21 +72,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All MCP tests passed diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index 16d8a5f1a9..021d6cda79 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -97,21 +100,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All Misc tests passed diff --git a/.github/workflows/test-integrations-network.yml b/.github/workflows/test-integrations-network.yml index af0ed3cd09..ee4579f50f 100644 --- a/.github/workflows/test-integrations-network.yml +++ b/.github/workflows/test-integrations-network.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -73,21 +76,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All Network tests passed diff --git a/.github/workflows/test-integrations-tasks.yml b/.github/workflows/test-integrations-tasks.yml index bf464d8f5c..bab5ddf335 100644 --- a/.github/workflows/test-integrations-tasks.yml +++ b/.github/workflows/test-integrations-tasks.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -100,21 +103,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All Tasks tests passed diff --git a/.github/workflows/test-integrations-web-1.yml b/.github/workflows/test-integrations-web-1.yml index 7f4c3d681f..82632632e7 100644 --- a/.github/workflows/test-integrations-web-1.yml +++ b/.github/workflows/test-integrations-web-1.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -56,7 +59,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -95,21 +98,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All Web 1 tests passed diff --git a/.github/workflows/test-integrations-web-2.yml b/.github/workflows/test-integrations-web-2.yml index 7de840df55..9dec6bff24 100644 --- a/.github/workflows/test-integrations-web-2.yml +++ b/.github/workflows/test-integrations-web-2.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: true permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | @@ -38,7 +41,7 @@ jobs: # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: @@ -101,21 +104,13 @@ jobs: run: | coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true check_required_tests: name: All Web 2 tests passed diff --git a/.github/workflows/update-tox.yml b/.github/workflows/update-tox.yml index 5d3931cf9e..914109eae8 100644 --- a/.github/workflows/update-tox.yml +++ b/.github/workflows/update-tox.yml @@ -23,7 +23,7 @@ jobs: python-version: 3.14t - name: Checkout repo - uses: actions/checkout@v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8592487cb8..7840cfa595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,167 @@ # Changelog +## 2.51.0 + +### New Features ✨ + +#### Openai + +- feat(openai): Set system instruction attribute for Responses API by @alexander-alderman-webb in [#5376](https://github.com/getsentry/sentry-python/pull/5376) +- feat(openai): Set system instruction attribute for Completions API by @alexander-alderman-webb in [#5358](https://github.com/getsentry/sentry-python/pull/5358) +- feat(integrations): OpenAI/OpenAI Agents detect and report the time to first token metric (TTFT) as `gen_ai.response.time_to_first_token` by @constantinius in [#5348](https://github.com/getsentry/sentry-python/pull/5348) + +#### Openai Agents + +- feat(openai-agents): Set system instruction attribute on `gen_ai.chat` spans by @alexander-alderman-webb in [#5370](https://github.com/getsentry/sentry-python/pull/5370) +- feat(openai-agents): Set system instruction attribute by @alexander-alderman-webb in [#5355](https://github.com/getsentry/sentry-python/pull/5355) +- feat(integrations): openai-agents streaming support by @constantinius in [#5291](https://github.com/getsentry/sentry-python/pull/5291) + +#### Other + +- feat(ai): Add original input length meta attribute by @alexander-alderman-webb in [#5375](https://github.com/getsentry/sentry-python/pull/5375) +- feat(anthropic): Set system instruction attribute by @alexander-alderman-webb in [#5353](https://github.com/getsentry/sentry-python/pull/5353) +- feat(asyncio): Allow to turn task spans off by @sentrivana in [#5367](https://github.com/getsentry/sentry-python/pull/5367) +- feat(gen_ai): add function `set_conversation_id` and managing functions on the Scope and apply it on the Span on `.finish()` by @constantinius in [#5362](https://github.com/getsentry/sentry-python/pull/5362) +- feat(google-genai): Set system instruction attribute by @alexander-alderman-webb in [#5354](https://github.com/getsentry/sentry-python/pull/5354) +- feat(langchain): Set system instruction attribute by @alexander-alderman-webb in [#5357](https://github.com/getsentry/sentry-python/pull/5357) +- feat(pydantic-ai): Set system instruction attribute by @alexander-alderman-webb in [#5356](https://github.com/getsentry/sentry-python/pull/5356) +- feat(transport): Report 413 responses for oversized envelopes by @alexander-alderman-webb in [#5380](https://github.com/getsentry/sentry-python/pull/5380) + +### Bug Fixes 🐛 + +- fix(ai): Keep single content input message by @alexander-alderman-webb in [#5345](https://github.com/getsentry/sentry-python/pull/5345) + +### Internal Changes 🔧 + +- ci(release): Fix changelog-preview permissions by @BYK in [#5368](https://github.com/getsentry/sentry-python/pull/5368) +- ref: Replace `set_data_normalized()` with `Span.set_data()` for system instructions by @alexander-alderman-webb in [#5374](https://github.com/getsentry/sentry-python/pull/5374) +- ci: Fix path in AI integration tests by @alexander-alderman-webb in [#5347](https://github.com/getsentry/sentry-python/pull/5347) + +## 2.50.0 + +### New Features ✨ + +#### Ai + +- feat(ai): add cache writes for gen_ai by @shellmayr in [#5319](https://github.com/getsentry/sentry-python/pull/5319) +- feat(ai): add parse_data_uri function to parse a data URI by @constantinius in [#5311](https://github.com/getsentry/sentry-python/pull/5311) + +#### Other + +- feat(asyncio): Add on-demand way to enable AsyncioIntegration by @sentrivana in [#5288](https://github.com/getsentry/sentry-python/pull/5288) + + You can now enable the `AsyncioIntegration` on demand, after calling `sentry_sdk.init()`. This is useful in scenarios where you don't have + the event loop running early on, or when you need to instrument multiple event loops. + +```python +import sentry_sdk +from sentry_sdk.integrations.asyncio import enable_asyncio_integration + +# Initializing the SDK as early as possible, when there is no event loop yet +sentry_sdk.init( + ... + # No AsyncioIntegration in explicitly provided `integrations` +) + +async def main(): + enable_asyncio_integration() # instruments the current event loop + # ...your code... +``` + +- feat(openai-agents): Inject propagation headers for `HostedMCPTool` by @alexander-alderman-webb in [#5297](https://github.com/getsentry/sentry-python/pull/5297) +- feat(stdlib): Handle proxy tunnels in httlib integration by @sl0thentr0py in [#5303](https://github.com/getsentry/sentry-python/pull/5303) +- feat: Support array types for logs and metrics attributes by @alexander-alderman-webb in [#5314](https://github.com/getsentry/sentry-python/pull/5314) + +### Bug Fixes 🐛 + +#### Integrations + +- fix(integrations): google genai report image inputs by @constantinius in [#5337](https://github.com/getsentry/sentry-python/pull/5337) +- fix(integrations): google-genai: reworked `gen_ai.request.messages` extraction from parameters by @constantinius in [#5275](https://github.com/getsentry/sentry-python/pull/5275) +- fix(integrations): pydantic-ai: properly format binary input message parts to be conformant with the `gen_ai.request.messages` structure by @constantinius in [#5251](https://github.com/getsentry/sentry-python/pull/5251) +- fix(integrations): Anthropic: add content transformation for images and documents by @constantinius in [#5276](https://github.com/getsentry/sentry-python/pull/5276) +- fix(integrations): langchain add multimodal content transformation functions for images, audio, and files by @constantinius in [#5278](https://github.com/getsentry/sentry-python/pull/5278) + +#### Litellm + +- fix(litellm): fix `gen_ai.request.messages` to be as expected by @constantinius in [#5255](https://github.com/getsentry/sentry-python/pull/5255) +- fix(litellm): Guard against module shadowing by @alexander-alderman-webb in [#5249](https://github.com/getsentry/sentry-python/pull/5249) + +#### Other + +- fix(ai): redact message parts content of type blob by @constantinius in [#5243](https://github.com/getsentry/sentry-python/pull/5243) +- fix(clickhouse): Guard against module shadowing by @alexander-alderman-webb in [#5250](https://github.com/getsentry/sentry-python/pull/5250) +- fix(gql): Revert signature change of patched gql.Client.execute by @alexander-alderman-webb in [#5289](https://github.com/getsentry/sentry-python/pull/5289) +- fix(grpc): Derive interception state from channel fields by @alexander-alderman-webb in [#5302](https://github.com/getsentry/sentry-python/pull/5302) +- fix(pure-eval): Guard against module shadowing by @alexander-alderman-webb in [#5252](https://github.com/getsentry/sentry-python/pull/5252) +- fix(ray): Guard against module shadowing by @alexander-alderman-webb in [#5254](https://github.com/getsentry/sentry-python/pull/5254) +- fix(threading): Handle channels shadowing by @sentrivana in [#5299](https://github.com/getsentry/sentry-python/pull/5299) +- fix(typer): Guard against module shadowing by @alexander-alderman-webb in [#5253](https://github.com/getsentry/sentry-python/pull/5253) +- fix: Stop suppressing exception chains in AI integrations by @alexander-alderman-webb in [#5309](https://github.com/getsentry/sentry-python/pull/5309) +- fix: Send client reports for span recorder overflow by @sentrivana in [#5310](https://github.com/getsentry/sentry-python/pull/5310) + +### Documentation 📚 + +- docs(metrics): Remove experimental notice by @alexander-alderman-webb in [#5304](https://github.com/getsentry/sentry-python/pull/5304) +- docs: Update Python versions banner in README by @sentrivana in [#5287](https://github.com/getsentry/sentry-python/pull/5287) + +### Internal Changes 🔧 + +#### Fastmcp + +- test(fastmcp): Narrow `AttributeError` try-except by @alexander-alderman-webb in [#5339](https://github.com/getsentry/sentry-python/pull/5339) +- test(fastmcp): Stop accessing non-existent attribute by @alexander-alderman-webb in [#5338](https://github.com/getsentry/sentry-python/pull/5338) + +#### Release + +- ci(release): Bump Craft version to fix issues by @BYK in [#5305](https://github.com/getsentry/sentry-python/pull/5305) +- ci(release): Switch from action-prepare-release to Craft by @BYK in [#5290](https://github.com/getsentry/sentry-python/pull/5290) + +#### Other + +- chore(gen_ai): add auto-enablement for google genai by @shellmayr in [#5295](https://github.com/getsentry/sentry-python/pull/5295) +- chore(repo): Add Claude Code settings with basic permissions by @philipphofmann in [#5342](https://github.com/getsentry/sentry-python/pull/5342) +- ci: 🤖 Update test matrix with new releases (01/19) by @github-actions in [#5330](https://github.com/getsentry/sentry-python/pull/5330) +- ci: Add periodic AI integration tests by @alexander-alderman-webb in [#5313](https://github.com/getsentry/sentry-python/pull/5313) +- chore: Use pull_request_target for changelog preview by @BYK in [#5323](https://github.com/getsentry/sentry-python/pull/5323) +- chore: add unlabeled trigger to changelog-preview by @BYK in [#5315](https://github.com/getsentry/sentry-python/pull/5315) +- chore: Add type for metric units by @sentrivana in [#5312](https://github.com/getsentry/sentry-python/pull/5312) +- ci: Update tox and handle generic classifiers by @sentrivana in [#5306](https://github.com/getsentry/sentry-python/pull/5306) + +## 2.49.0 + +### New Features ✨ + +- feat(api): Add `Scope.set_attribute` by @sentrivana in [#5256](https://github.com/getsentry/sentry-python/pull/5256) + +### Bug Fixes 🐛 + +- fix(grpc): Gate third-party imports by @alexander-alderman-webb in [#5246](https://github.com/getsentry/sentry-python/pull/5246) +- fix(opentelemetry): Gate third-party imports by @alexander-alderman-webb in [#5247](https://github.com/getsentry/sentry-python/pull/5247) +- fix(ray): Keep variadic kwargs last in signatures by @alexander-alderman-webb in [#5244](https://github.com/getsentry/sentry-python/pull/5244) +- fix(trytond): Gate third-party imports by @alexander-alderman-webb in [#5245](https://github.com/getsentry/sentry-python/pull/5245) +- Fix openai count_tokens by @sl0thentr0py in [#5281](https://github.com/getsentry/sentry-python/pull/5281) + +### Documentation 📚 + +- docs: Fix typo in comment by @sentrivana in [#5280](https://github.com/getsentry/sentry-python/pull/5280) +- docs: Fix `middleware_spans` docstring by @sentrivana in [#5279](https://github.com/getsentry/sentry-python/pull/5279) + +### Internal Changes 🔧 + +- ref(scope): Set global attrs on global scope by @sentrivana in [#5259](https://github.com/getsentry/sentry-python/pull/5259) +- chore: Ignore type migration for scripts/ and tests/ in blame by @alexander-alderman-webb in [#5284](https://github.com/getsentry/sentry-python/pull/5284) +- ref: Properly override parent func by @sentrivana in [#5283](https://github.com/getsentry/sentry-python/pull/5283) +- ci: Allow to use Craft's new auto-versioning by @sentrivana in [#5218](https://github.com/getsentry/sentry-python/pull/5218) +- ref: Deduplicate batchers by @sentrivana in [#5263](https://github.com/getsentry/sentry-python/pull/5263) +- tests: Add dedicated transport format test for metrics, logs by @sentrivana in [#5264](https://github.com/getsentry/sentry-python/pull/5264) +- ci: 🤖 Update test matrix with new releases (01/05) by @github-actions in [#5273](https://github.com/getsentry/sentry-python/pull/5273) +- tests: General logs tests should use Sentry logs API by @sentrivana in [#5262](https://github.com/getsentry/sentry-python/pull/5262) +- tests: Test preserialization of attributes by @sentrivana in [#5260](https://github.com/getsentry/sentry-python/pull/5260) +- ci: Unpin Pydantic 1.x version in tests by @alexander-alderman-webb in [#5261](https://github.com/getsentry/sentry-python/pull/5261) +- ref: Make logs, metrics go via scope by @sentrivana in [#5213](https://github.com/getsentry/sentry-python/pull/5213) +- ci: Fix failing arq, fastapi tests on 3.7; update test matrix by @sentrivana in [#5258](https://github.com/getsentry/sentry-python/pull/5258) + ## 2.48.0 Middleware spans are now disabled by default in Django, Starlette and FastAPI integrations. Set the `middleware_spans` integration-level diff --git a/README.md b/README.md index 6c1db3b25f..7a7bf8b44f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ _Bad software is everywhere, and we're tired of it. Sentry is on a mission to he [![Discord](https://img.shields.io/discord/621778831602221064?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.com/invite/Ww9hbqr) [![X Follow](https://img.shields.io/twitter/follow/sentry?label=sentry&style=social)](https://x.com/intent/follow?screen_name=sentry) [![PyPi page link -- version](https://img.shields.io/pypi/v/sentry-sdk.svg)](https://pypi.python.org/pypi/sentry-sdk) -python +python [![Build Status](https://github.com/getsentry/sentry-python/actions/workflows/ci.yml/badge.svg)](https://github.com/getsentry/sentry-python/actions/workflows/ci.yml)
diff --git a/codecov.yml b/codecov.yml index b7abcf8c86..4cb22edee8 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,18 +10,7 @@ ignore: - "tests" - "sentry_sdk/_types.py" -# Read more here: https://docs.codecov.com/docs/pull-request-comments -comment: - after_n_builds: 99 - layout: 'diff, files' - # Update, if comment exists. Otherwise post new. - behavior: default - # Comments will only post when coverage changes. Furthermore, if a comment - # already exists, and a newer commit results in no coverage change for the - # entire pull, the comment will be deleted. - require_changes: true - require_base: true # must have a base report to post - require_head: true # must have a head report to post +comment: true github_checks: annotations: false diff --git a/docs/conf.py b/docs/conf.py index 4ce462103f..2049969d4c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.48.0" +release = "2.51.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/scripts/find_raise_from_none.py b/scripts/find_raise_from_none.py new file mode 100644 index 0000000000..63b2b84333 --- /dev/null +++ b/scripts/find_raise_from_none.py @@ -0,0 +1,65 @@ +import ast +import pathlib +from collections import defaultdict + + +class RaiseFromNoneVisitor(ast.NodeVisitor): + line_numbers = defaultdict(list) + + def __init__(self, filename): + self.filename = filename + + def visit_Raise(self, node: ast.Raise): + if node.cause is not None: + if isinstance(node.cause, ast.Constant) and node.cause.value is None: + RaiseFromNoneVisitor.line_numbers[self.filename].append(node.lineno) + self.generic_visit(node) + + +def scan_file(module_path: pathlib.Path): + source = pathlib.Path(module_path).read_text(encoding="utf-8") + tree = ast.parse(source, filename=module_path) + + RaiseFromNoneVisitor(module_path).visit(tree) + + +def walk_package_modules(): + for p in pathlib.Path("sentry_sdk").rglob("*.py"): + yield p + + +def format_detected_raises(line_numbers) -> str: + lines = [] + for filepath, line_numbers_in_file in line_numbers.items(): + lines_string = ", ".join(f"line {ln}" for ln in sorted(line_numbers_in_file)) + lines.append( + f"{filepath}: {len(line_numbers_in_file)} occurrence(s) at {lines_string}" + ) + return "\n".join(lines) + + +def main(): + for module_path in walk_package_modules(): + scan_file(module_path) + + # TODO: Investigate why we suppress exception chains here. + ignored_raises = { + pathlib.Path("sentry_sdk/integrations/asgi.py"): 2, + pathlib.Path("sentry_sdk/integrations/asyncio.py"): 1, + } + + raise_from_none_count = { + file: len(occurences) + for file, occurences in RaiseFromNoneVisitor.line_numbers.items() + } + if raise_from_none_count != ignored_raises: + exc = Exception("Detected unexpected raise ... from None.") + exc.add_note( + "Raise ... from None suppresses chained exceptions, removing valuable context." + ) + exc.add_note(format_detected_raises(RaiseFromNoneVisitor.line_numbers)) + raise exc + + +if __name__ == "__main__": + main() diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 9d5e97846b..37d3a6a64d 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -311,6 +311,9 @@ "deps": { "*": ["mockupdb"], }, + "python": { + "<3.6": "<3.7", + }, }, "pyramid": { "package": "pyramid", @@ -368,6 +371,9 @@ ">=0.9,<0.14": ["fakeredis>=1.0,<1.7.4"], "py3.6,py3.7": ["fakeredis!=2.26.0"], }, + "python": { + "<0.13": "<3.7", + }, }, "sanic": { "package": "sanic", @@ -385,6 +391,9 @@ }, "sqlalchemy": { "package": "sqlalchemy", + "python": { + "<1.4": "<3.10", + }, }, "starlette": { "package": "starlette", diff --git a/scripts/populate_tox/package_dependencies.jsonl b/scripts/populate_tox/package_dependencies.jsonl index 3c32b3cb93..52724708f4 100644 --- a/scripts/populate_tox/package_dependencies.jsonl +++ b/scripts/populate_tox/package_dependencies.jsonl @@ -1,29 +1,35 @@ -{"name": "boto3", "version": "1.42.13", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/9e/a8/51fb7b8078864f673169456ce16eecd2abd9d40010a65c6fa910b41c0088/boto3-1.42.13-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b1/52/b4235bd6cd9b86fa73be92bad1039fd533b666921c32d0d94ffdb220a871/botocore-1.42.13-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl"}}]} -{"name": "django", "version": "5.2.9", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/17/b0/7f42bfc38b8f19b78546d47147e083ed06e12fc29c42da95655e0962c6c2/django-5.2.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/25/70/001ee337f7aa888fb2e3f5fd7592a6afc5283adb1ed44ce8df5764070f22/sqlparse-0.5.4-py3-none-any.whl"}}]} -{"name": "django", "version": "6.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/d7/ae/f19e24789a5ad852670d6885f5480f5e5895576945fcc01817dfd9bc002a/django-6.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/25/70/001ee337f7aa888fb2e3f5fd7592a6afc5283adb1ed44ce8df5764070f22/sqlparse-0.5.4-py3-none-any.whl"}}]} -{"name": "dramatiq", "version": "2.0.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/38/4b/4a538e5c324d5d2f788f437531419c7331c7f958591c7b6075b5ce931520/dramatiq-2.0.0-py3-none-any.whl"}}]} -{"name": "fastapi", "version": "0.125.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/34/2f/ff2fcc98f500713368d8b650e1bbc4a0b3ebcdd3e050dcdaad5f5a13fd7e/fastapi-0.125.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}]} +{"name": "anthropic", "version": "0.77.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/ac/27/9df785d3f94df9ac72f43ee9e14b8120b37d992b18f4952774ed46145022/anthropic-0.77.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl"}}]} +{"name": "ariadne", "version": "0.27.1", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/ec/ad/1218af7c0ee7f1a79adf10d37d9c62f3a1309dff95292ee7cb5c0894f6d6/ariadne-0.27.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}]} +{"name": "ariadne", "version": "0.28.0rc2", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/54/70/f51f75d18a7f8c8c524c95128088bfd288a92b949a511581ecc64d3fea08/ariadne-0.28.0rc2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}]} +{"name": "boto3", "version": "1.42.39", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/b2/c4/3493b5c86e32d6dd558b30d16b55503e24a6e6cd7115714bc102b247d26e/boto3-1.42.39-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ef/71/9a2c88abb5fe47b46168b262254d5b5d635de371eba4bd01ea5c8c109575/botocore-1.42.39-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl"}}]} +{"name": "django", "version": "5.2.10", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/fa/de/f1a7cd896daec85832136ab509d9b2a6daed4939dbe26313af3e95fc5f5e/django-5.2.10-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl"}}]} +{"name": "django", "version": "6.0.1", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/95/b5/814ed98bd21235c116fd3436a7ed44d47560329a6d694ec8aac2982dbb93/django-6.0.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl"}}]} +{"name": "dramatiq", "version": "2.0.1", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/ca/28/4bfc19a3b12177febcb3d28767933c823c056727872a8792a87d6f68df67/dramatiq-2.0.1-py3-none-any.whl"}}]} +{"name": "fastapi", "version": "0.128.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}]} {"name": "fastmcp", "version": "0.1.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/f5/07/bc69e65b45d638822190bce0defb497a50d240291b8467cb79078d0064b7/fastmcp-0.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9f/9e/26e1d2d2c6afe15dfba5ca6799eeeea7656dce625c22766e4c57305e9cc2/mcp-1.23.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl"}}]} {"name": "fastmcp", "version": "0.4.1", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/79/0b/008a340435fe8f0879e9d608f48af2737ad48440e09bd33b83b3fd03798b/fastmcp-0.4.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9f/9e/26e1d2d2c6afe15dfba5ca6799eeeea7656dce625c22766e4c57305e9cc2/mcp-1.23.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl"}}]} {"name": "fastmcp", "version": "1.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/b9/bf/0a77688242f30f81e3633d3765289966d9c7e408f9dcb4928a85852b9fde/fastmcp-1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9f/9e/26e1d2d2c6afe15dfba5ca6799eeeea7656dce625c22766e4c57305e9cc2/mcp-1.23.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl"}}]} {"name": "flask", "version": "2.3.3", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl"}}]} {"name": "flask", "version": "3.1.2", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl"}}]} -{"name": "google-genai", "version": "1.47.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/89/ef/e080e8d67c270ea320956bb911a9359664fc46d3b87d1f029decd33e5c4c/google_genai-1.47.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c6/97/451d55e05487a5cd6279a01a7e34921858b16f7dc8aa38a2c684743cd2b3/google_auth-2.45.0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}]} -{"name": "google-genai", "version": "1.56.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/84/93/94bc7a89ef4e7ed3666add55cd859d1483a22737251df659bf1aa46e9405/google_genai-1.56.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c6/97/451d55e05487a5cd6279a01a7e34921858b16f7dc8aa38a2c684743cd2b3/google_auth-2.45.0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl"}}]} -{"name": "huey", "version": "2.5.5", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/de/c2/0543039071259cfdab525757022de8dad6d22c15a0e7352f1a50a1444a13/huey-2.5.5-py3-none-any.whl"}}]} -{"name": "huggingface_hub", "version": "1.2.3", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/df/8d/7ca723a884d55751b70479b8710f06a317296b1fa1c1dec01d0420d13e43/huggingface_hub-1.2.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}]} -{"name": "langchain", "version": "1.2.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/23/00/4e3fa0d90f5a5c376ccb8ca983d0f0f7287783dfac48702e18f01d24673b/langchain-1.2.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/cc/95/98c47dbb4b6098934ff70e0f52efef3a85505dbcccc9eb63587e21fde4c9/langchain_core-1.2.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/23/1b/e318ee76e42d28f515d87356ac5bd7a7acc8bad3b8f54ee377bef62e1cbf/langgraph-1.0.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/48/e3/616e3a7ff737d98c1bbb5700dd62278914e2a9ded09a79a1fa93cf24ce12/langgraph_checkpoint-3.0.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/87/5e/aeba4a5b39fe6e874e0dd003a82da71c7153e671312671a8dacc5cb7c1af/langgraph_prebuilt-1.0.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/69/48/ee4d7afb3c3d38bd2ebe51a4d37f1ed7f1058dd242f35994b562203067aa/langgraph_sdk-0.3.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/63/54/4577ef9424debea2fa08af338489d593276520d2e2f8950575d292be612c/langsmith-0.4.59-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0b/0e/512fb221e4970c2f75ca9dae412d320b7d9ddc9f2b15e04ea8e44710396c/uuid_utils-0.12.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/b3/ef4494438c90359e1547eaed3c5ec46e2c431d59a3de2af4e70ebd594c49/ormsgpack-1.12.1-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}]} +{"name": "google-genai", "version": "1.51.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/c6/28/0185dcda66f1994171067cfdb0e44a166450239d5b11b3a8a281dd2da459/google_genai-1.51.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl"}}]} +{"name": "google-genai", "version": "1.61.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/0e/87/78dd70cb59f7acf3350f53c5144a7aa7bc39c6f425cd7dc1224b59fcdac3/google_genai-1.61.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl"}}]} +{"name": "gql", "version": "4.3.0b0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/3a/9a/0f888771634de03e679e3de0bbbe7580966d8ee3854ee6bf6c24e85a8330/gql-4.3.0b0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c3/a3/b14060e541cd3a94da2b2a89f0e7815284b52010aa3adb06c07a3b7c23b5/graphql_core-3.3.0a11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl"}}]} +{"name": "huey", "version": "2.6.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/1a/34/fae9ac8f1c3a552fd3f7ff652b94c78d219dedc5fce0c0a4232457760a00/huey-2.6.0-py3-none-any.whl"}}]} +{"name": "huggingface_hub", "version": "1.3.7", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/54/89/bfbfde252d649fae8d5f09b14a2870e5672ed160c1a6629301b3e5302621/huggingface_hub-1.3.7-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f5/e2/31eac96de2915cf20ccaed0225035db149dfb9165a9ed28d4b252ef3f7f7/tqdm-4.67.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}]} +{"name": "langchain", "version": "1.2.7", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/dd/c8/9ce37ae34870834c7d00bb14ff4876b700db31b928635e3307804dc41d74/langchain-1.2.7-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6e/6f/34a9fba14d191a67f7e2ee3dbce3e9b86d2fa7310e2c7f2c713583481bd2/langchain_core-1.2.7-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/0e/fe80144e3e4048e5d19ccdb91ac547c1a7dc3da8dbd1443e210048194c14/langgraph-1.0.7-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/4a/de/ddd53b7032e623f3c7bcdab2b44e8bf635e468f62e10e5ff1946f62c9356/langgraph_checkpoint-4.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/47/49/5e37abb3f38a17a3487634abc2a5da87c208cc1d14577eb8d7184b25c886/langgraph_prebuilt-1.0.7-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6e/be/4ad511bacfdd854afb12974f407cb30010dceb982dc20c55491867b34526/langgraph_sdk-0.3.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/af/6d/526fee48986499400075436728499307fa7c37fa88c9d5450b4500b640e3/langsmith-0.6.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/57/7c/3a926e847516e67bc6838634f2e54e24381105b4e80f9338dc35cca0086b/uuid_utils-0.14.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl"}}]} {"name": "langgraph", "version": "0.6.11", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/df/94/430f0341c5c2fe3e3b9f5ab2622f35e2bda12c4a7d655c519468e853d1b0/langgraph-0.6.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/48/e3/616e3a7ff737d98c1bbb5700dd62278914e2a9ded09a79a1fa93cf24ce12/langgraph_checkpoint-3.0.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/8e/d1/e4727f4822943befc3b7046f79049b1086c9493a34b4d44a1adf78577693/langgraph_prebuilt-0.6.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d8/2f/5c97b3fc799730179f2061cca633c0dc03d9e74f0372a783d4d2be924110/langgraph_sdk-0.2.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/71/1e/e129fc471a2d2a7b3804480a937b5ab9319cab9f4142624fcb115f925501/langchain_core-1.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fc/48/37cc533e2d16e4ec1d01f30b41933c9319af18389ea0f6866835ace7d331/langsmith-0.4.53-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0b/0e/512fb221e4970c2f75ca9dae412d320b7d9ddc9f2b15e04ea8e44710396c/uuid_utils-0.12.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b3/cf/5d58d9b132128d2fe5d586355dde76af386554abef00d608f66b913bff1f/ormsgpack-1.12.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}]} {"name": "launchdarkly-server-sdk", "version": "9.14.1", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/3e/64/3b0ca36edef5f795e9367ce727a8b761697e7306030f4105b29796ec9fd5/launchdarkly_server_sdk-9.14.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e6/94/a46d76ff5738fb9a842c27a1f95fbdae8397621596bdfc5c582079958567/launchdarkly_eventsource-1.5.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/cb/84/a04c59324445f4bcc98dc05b39a1cd07c242dde643c1a3c21e4f7beaf2f2/expiringdict-1.2.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/34/90/0200184d2124484f918054751ef997ed6409cb05b7e8dcbf5a22da4c4748/pyrfc3339-2.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl"}}]} -{"name": "openai", "version": "2.14.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl"}}]} -{"name": "openai-agents", "version": "0.6.4", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/2b/ed/f2282debf62c52241959b111d2e35105b43b2ea6ff060833734405bb4d0f/openai_agents-0.6.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/61/0d/5cf14e177c8ae655a2fd9324a6ef657ca4cafd3fc2201c87716055e29641/mcp-1.24.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/71/22/8ab1066358601163e1ac732837adba3672f703818f693e179b24e0d3b65c/sse_starlette-3.0.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl"}}]} +{"name": "openai", "version": "2.16.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/16/83/0315bf2cfd75a2ce8a7e54188e9456c60cec6c0cf66728ed07bd9859ff26/openai-2.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f5/e2/31eac96de2915cf20ccaed0225035db149dfb9165a9ed28d4b252ef3f7f7/tqdm-4.67.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl"}}]} +{"name": "openai-agents", "version": "0.7.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/a2/92/9cbbdd604f858056d4e4f105a1b99779128bae61b6a3681db0f035ef73b4/openai_agents-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl"}}]} {"name": "openfeature-sdk", "version": "0.7.5", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/9b/20/cb043f54b11505d993e4dd84652cfc44c1260dc94b7f41aa35489af58277/openfeature_sdk-0.7.5-py3-none-any.whl"}}]} {"name": "openfeature-sdk", "version": "0.8.4", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/9c/80/f6532778188c573cc83790b11abccde717d4c1442514e722d6bb6140e55c/openfeature_sdk-0.8.4-py3-none-any.whl"}}]} {"name": "quart", "version": "0.20.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/7e/e9/cc28f21f52913adf333f653b9e0a3bf9cb223f5083a26422968ba73edd8d/quart-0.20.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/93/35/850277d1b17b206bd10874c8a9a3f52e059452fb49bb0d22cbb908f6038b/hypercorn-0.18.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/5e/5f/82c8074f7e84978129347c2c6ec8b6c59f3584ff1a20bc3c940a3e061790/priority-2.0.0-py3-none-any.whl"}}]} {"name": "redis", "version": "7.1.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl"}}]} {"name": "requests", "version": "2.32.5", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}]} -{"name": "starlette", "version": "0.50.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}]} +{"name": "sanic", "version": "25.12.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/9e/8a/16adaf66d358abfd0d24f2b76857196cf7effbf75c01306306bf39904e30/sanic-25.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9b/12/2f5d43ee912ea14a6baba4b3db6d309b02d932e3b7074c3339b4aded98ff/html5tagger-1.3.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/cf/e3/3425c9a8773807ac2c01d6a56c8521733f09b627e5827e733c5cd36b9ac5/sanic_routing-23.12.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b6/62/3f385a67ff3cc91209f107d20bbebdecf7a4e4aba55a43f9f71bddc424a9/tracerite-2.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2e/e5/af5491dfda4f8b77e24cf3da68ee0d1552f99a13e5c622f4cef1380925c3/ujson-5.11.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl"}}]} +{"name": "sqlalchemy", "version": "2.1.0b1", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/45/eb/07e192fa2e1deb500e86e0b86883037116447360951a6c3eda2ce4f176f7/sqlalchemy-2.1.0b1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}]} +{"name": "starlette", "version": "0.52.1", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}]} {"name": "statsig", "version": "0.55.3", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/9d/17/de62fdea8aab8aa7c4a833378e0e39054b728dfd45ef279e975ed5ef4e86/statsig-0.55.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/97/e88295f9456ba939d90d4603af28fcabda3b443ef55e709e9381df3daa58/ijson-3.4.0.post0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d6/b7/48a7f1ab9eee62f1113114207df7e7e6bc29227389d554f42cc11bc98108/ip3country-0.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/94/37/be6dfbfa45719aa82c008fb4772cfe5c46db765a2ca4b6f524a1fdfee4d7/ua_parser-1.0.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6f/d3/13adff37f15489c784cc7669c35a6c3bf94b87540229eedf52ef2a1d0175/ua_parser_builtins-0.18.0.post1-py3-none-any.whl"}}]} -{"name": "statsig", "version": "0.66.2", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/0d/e6/788773678cef0f84758c648a3e085c57f953dc1a6f1d55f37e6844fcb655/statsig-0.66.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/97/e88295f9456ba939d90d4603af28fcabda3b443ef55e709e9381df3daa58/ijson-3.4.0.post0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d6/b7/48a7f1ab9eee62f1113114207df7e7e6bc29227389d554f42cc11bc98108/ip3country-0.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/94/37/be6dfbfa45719aa82c008fb4772cfe5c46db765a2ca4b6f524a1fdfee4d7/ua_parser-1.0.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/6f/d3/13adff37f15489c784cc7669c35a6c3bf94b87540229eedf52ef2a1d0175/ua_parser_builtins-0.18.0.post1-py3-none-any.whl"}}]} -{"name": "strawberry-graphql", "version": "0.287.3", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/7c/8e/ffd20e179cc8218465599922660323f453c7b955aca2b909e5b86ba61eb0/strawberry_graphql-0.287.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/00/f2/c68a97c727c795119f1056ad2b7e716c23f26f004292517c435accf90b5c/lia_web-0.2.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}]} -{"name": "typer", "version": "0.20.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}]} +{"name": "statsig", "version": "0.67.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/0d/12/9c6e2b9ba24b4945fd17b48c2bc5b397c653af7c501c3a34c193155f2d8c/statsig-0.67.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/97/e88295f9456ba939d90d4603af28fcabda3b443ef55e709e9381df3daa58/ijson-3.4.0.post0-cp314-cp314t-macosx_11_0_arm64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d6/b7/48a7f1ab9eee62f1113114207df7e7e6bc29227389d554f42cc11bc98108/ip3country-0.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/75/b1/1dc83c2c661b4c62d56cc081706ee33a4fc2835bd90f965baa2663ef7676/protobuf-6.33.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/94/37/be6dfbfa45719aa82c008fb4772cfe5c46db765a2ca4b6f524a1fdfee4d7/ua_parser-1.0.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fd/82/aab481e2fc6dee0a13ce35c750e97dbe3f270fb327089c99a8f5e6900e0c/ua_parser_builtins-202601-py3-none-any.whl"}}]} +{"name": "strawberry-graphql", "version": "0.291.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/65/1c/b3307f421c78ba0578b07abc93dbc07a3bca2da79c79ef6a6e2b027b557d/strawberry_graphql-0.291.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/67/49/92b46b6e65f09b717a66c4e5a9bc47a45ebc83dd0e0ed126f8258363479d/cross_web-0.4.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}]} +{"name": "typer", "version": "0.21.1", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl"}}]} diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index ce0983ad50..196a4d76fe 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -77,6 +77,14 @@ MIN_FREE_THREADING_SUPPORT = Version("3.14") +class PackageVersion(Version): + # Convenience wrapper around Version. It's convenient to be able to set + # attributes on a Version in toxgen, but we can't because the class now + # defines __slots__. By rewrapping Version in this custom class, we get + # around that. + pass + + @dataclass(order=True) class ThreadedVersion: version: Version @@ -218,7 +226,7 @@ def _save_to_package_dependencies_cache( def _prefilter_releases( integration: str, releases: dict[str, dict], -) -> tuple[list[Version], Optional[Version]]: +) -> tuple[list[PackageVersion], Optional[PackageVersion]]: """ Filter `releases`, removing releases that are for sure unsupported. @@ -238,7 +246,7 @@ def _prefilter_releases( min_supported = _MIN_VERSIONS.get(integration_name) if min_supported is not None: - min_supported = Version(".".join(map(str, min_supported))) + min_supported = PackageVersion(".".join(map(str, min_supported))) else: print( f" {integration} doesn't have a minimum version defined in " @@ -291,10 +299,10 @@ def _prefilter_releases( ): # Don't save all patch versions of a release, just the newest one if version.micro > saved_version.micro: - filtered_releases[i] = version + filtered_releases[i] = PackageVersion(str(version)) break else: - filtered_releases.append(version) + filtered_releases.append(PackageVersion(str(version))) filtered_releases.sort() @@ -302,14 +310,14 @@ def _prefilter_releases( # than the last released version); if not, don't consider it if last_prerelease is not None: if not filtered_releases or last_prerelease > filtered_releases[-1]: - return filtered_releases, last_prerelease + return filtered_releases, PackageVersion(str(last_prerelease)) return filtered_releases, None def get_supported_releases( integration: str, pypi_data: dict -) -> tuple[list[Version], Optional[Version]]: +) -> tuple[list[PackageVersion], Optional[PackageVersion]]: """ Get a list of releases that are currently supported by the SDK. @@ -331,7 +339,7 @@ def get_supported_releases( pypi_data["releases"], ) - def _supports_lowest(release: Version) -> bool: + def _supports_lowest(release: PackageVersion) -> bool: time.sleep(PYPI_COOLDOWN) # don't DoS PYPI pypi_data = fetch_release(package, release) @@ -504,6 +512,9 @@ def pick_python_versions_to_test( - a free-threaded wheel is distributed; and - the SDK supports free-threading. """ + if not python_versions: + return [] + filtered_python_versions = { python_versions[0], } @@ -545,7 +556,8 @@ def _parse_python_versions_from_classifiers(classifiers: list[str]) -> list[Vers if python_versions: python_versions.sort() - return python_versions + + return python_versions def determine_python_versions(pypi_data: dict) -> Union[SpecifierSet, list[Version]]: @@ -579,6 +591,14 @@ def determine_python_versions(pypi_data: dict) -> Union[SpecifierSet, list[Versi if requires_python: return SpecifierSet(requires_python) + # If we haven't found neither specific 3.x classifiers nor a requires_python, + # check if there is a generic "Python 3" classifier and if so, assume the + # package supports all Python versions the SDK does. If this is not the case + # in reality, add the actual constraints manually to config.py. + for classifier in classifiers: + if CLASSIFIER_PREFIX + "3" in classifiers: + return SpecifierSet(f">={MIN_PYTHON_VERSION}") + return [] @@ -770,7 +790,7 @@ def _compare_min_version_with_defined( def _add_python_versions_to_release( - integration: str, package: str, release: Version + integration: str, package: str, release: PackageVersion ) -> None: release_pypi_data = fetch_release(package, release) if release_pypi_data is None: diff --git a/scripts/populate_tox/releases.jsonl b/scripts/populate_tox/releases.jsonl index 3c6b99e8ff..67abdf36d2 100644 --- a/scripts/populate_tox/releases.jsonl +++ b/scripts/populate_tox/releases.jsonl @@ -6,39 +6,40 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.6", "version": "3.1.14", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "Django-3.1.14-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "Django-3.1.14.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.6", "version": "3.2.25", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "Django-3.2.25-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "Django-3.2.25.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.8", "version": "4.2.27", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "django-4.2.27-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "django-4.2.27.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.10", "version": "5.2.9", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "django-5.2.9-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "django-5.2.9.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.12", "version": "6.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "django-6.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "django-6.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.10", "version": "5.2.10", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "django-5.2.10-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "django-5.2.10.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.12", "version": "6.0.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "django-6.0.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "django-6.0.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Flask", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", "version": "1.1.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "Flask-1.1.4-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "Flask-1.1.4.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "Flask", "requires_python": ">=3.8", "version": "2.3.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "flask-2.3.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "flask-2.3.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", "Typing :: Typed"], "name": "Flask", "requires_python": ">=3.9", "version": "3.1.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "flask-3.1.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "flask-3.1.2.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Quart", "requires_python": ">=3.7", "version": "0.16.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "Quart-0.16.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "Quart-0.16.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Application Frameworks", "Typing :: Typed"], "name": "Quart", "requires_python": ">=3.9", "version": "0.20.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "quart-0.20.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "quart-0.20.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": "", "version": "1.2.19", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "SQLAlchemy-1.2.19.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "version": "1.3.24", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp37-cp37m-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp38-cp38-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp38-cp38-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp39-cp39-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp39-cp39-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "SQLAlchemy-1.3.24.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7", "version": "1.4.54", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp310-cp310-macosx_12_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp310-cp310-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp311-cp311-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp312-cp312-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp36-cp36m-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp36-cp36m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp37-cp37m-macosx_11_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp37-cp37m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp38-cp38-macosx_12_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp38-cp38-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp39-cp39-macosx_12_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp39-cp39-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "SQLAlchemy-1.4.54-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "sqlalchemy-1.4.54.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": ">=3.7", "version": "2.0.45", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp314-cp314-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp38-cp38-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp38-cp38-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp38-cp38-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp39-cp39-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp39-cp39-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-cp39-cp39-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.45-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "sqlalchemy-2.0.45.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": ">=3.7", "version": "2.0.46", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp314-cp314-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp38-cp38-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp38-cp38-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp38-cp38-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-cp39-cp39-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.0.46-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "sqlalchemy-2.0.46.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": ">=3.10", "version": "2.1.0b1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp310-cp310-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp311-cp311-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp312-cp312-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp313-cp313-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp314-cp314-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp314-cp314-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp314-cp314-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp314-cp314-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-cp314-cp314-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "sqlalchemy-2.1.0b1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "sqlalchemy-2.1.0b1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed"], "name": "UnleashClient", "requires_python": ">=3.8", "version": "6.0.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "UnleashClient-6.0.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "unleashclient-6.0.1.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed"], "name": "UnleashClient", "requires_python": ">=3.8", "version": "6.4.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "unleashclient-6.4.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "unleashclient-6.4.1.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed"], "name": "UnleashClient", "requires_python": ">=3.8", "version": "6.5.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "unleashclient-6.5.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "unleashclient-6.5.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "aiohttp", "requires_python": ">=3.8", "version": "3.10.11", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.10.11-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "aiohttp-3.10.11.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "aiohttp", "requires_python": ">=3.9", "version": "3.13.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314t-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.2-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "aiohttp-3.13.2.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "aiohttp", "requires_python": ">=3.9", "version": "3.13.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314t-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.13.3-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "aiohttp-3.13.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet :: WWW/HTTP"], "name": "aiohttp", "requires_python": ">=3.5.3", "version": "3.4.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp35-cp35m-macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp35-cp35m-macosx_10_11_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp35-cp35m-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp35-cp35m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp35-cp35m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp35-cp35m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp36-cp36m-macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp36-cp36m-macosx_10_11_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp36-cp36m-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp37-cp37m-macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp37-cp37m-macosx_10_11_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp37-cp37m-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.4.4-cp37-cp37m-win_amd64.whl"}, {"packagetype": "sdist", "filename": "aiohttp-3.4.4.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "aiohttp", "requires_python": ">=3.6", "version": "3.7.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp36-cp36m-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp36-cp36m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp36-cp36m-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp36-cp36m-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp36-cp36m-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp36-cp36m-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp37-cp37m-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp37-cp37m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp37-cp37m-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp37-cp37m-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp37-cp37m-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp37-cp37m-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp38-cp38-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp38-cp38-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp38-cp38-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp38-cp38-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp38-cp38-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp38-cp38-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp38-cp38-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp39-cp39-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp39-cp39-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp39-cp39-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp39-cp39-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp39-cp39-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp39-cp39-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "aiohttp-3.7.4-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "aiohttp-3.7.4.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "anthropic", "requires_python": ">=3.7", "version": "0.16.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "anthropic-0.16.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "anthropic-0.16.0.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "anthropic", "requires_python": ">=3.7", "version": "0.36.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "anthropic-0.36.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "anthropic-0.36.2.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "anthropic", "requires_python": ">=3.8", "version": "0.56.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "anthropic-0.56.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "anthropic-0.56.0.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "anthropic", "requires_python": ">=3.9", "version": "0.75.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "anthropic-0.75.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "anthropic-0.75.0.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "anthropic", "requires_python": ">=3.9", "version": "0.77.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "anthropic-0.77.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "anthropic-0.77.0.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.12.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp27-cp27m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp27-cp27m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp27-cp27mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp27-cp27mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp35-cp35m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.12.0-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "sdist", "filename": "apache-beam-2.12.0.zip"}]} {"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.13.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp27-cp27m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp27-cp27m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp27-cp27mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp27-cp27mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp35-cp35m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.13.0-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "sdist", "filename": "apache-beam-2.13.0.zip"}]} {"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.14.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp27-cp27m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp27-cp27m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp27-cp27mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp27-cp27mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp35-cp35m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.14.0-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "sdist", "filename": "apache-beam-2.14.0.zip"}]} {"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.15.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp27-cp27m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp27-cp27m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp27-cp27mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp27-cp27mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp35-cp35m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.15.0-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "sdist", "filename": "apache-beam-2.15.0.zip"}]} {"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.19.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp27-cp27m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp27-cp27m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp27-cp27mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp27-cp27mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp35-cp35m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.19.0-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "sdist", "filename": "apache-beam-2.19.0.zip"}]} -{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.6", "version": "2.26.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp36-cp36m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp36-cp36m-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp36-cp36m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp37-cp37m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp37-cp37m-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp37-cp37m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp38-cp38-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp38-cp38-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp38-cp38-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp38-cp38-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.26.0-cp38-cp38-win_amd64.whl"}, {"packagetype": "sdist", "filename": "apache-beam-2.26.0.zip"}]} -{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.7", "version": "2.41.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp37-cp37m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp37-cp37m-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp37-cp37m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp37-cp37m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp38-cp38-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp38-cp38-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp38-cp38-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp38-cp38-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp38-cp38-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp39-cp39-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp39-cp39-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp39-cp39-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp39-cp39-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.41.0-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "apache-beam-2.41.0.zip"}]} -{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.10", "version": "2.70.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.70.0-cp313-cp313-win_amd64.whl"}, {"packagetype": "sdist", "filename": "apache_beam-2.70.0.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.6", "version": "2.27.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp36-cp36m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp36-cp36m-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp36-cp36m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp37-cp37m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp37-cp37m-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp37-cp37m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp38-cp38-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp38-cp38-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp38-cp38-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp38-cp38-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.27.0-cp38-cp38-win_amd64.whl"}, {"packagetype": "sdist", "filename": "apache-beam-2.27.0.zip"}]} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.7", "version": "2.42.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp37-cp37m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp37-cp37m-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp37-cp37m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp37-cp37m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp38-cp38-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp38-cp38-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp38-cp38-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp38-cp38-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp38-cp38-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp39-cp39-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp39-cp39-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp39-cp39-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp39-cp39-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.42.0-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "apache-beam-2.42.0.zip"}]} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.10", "version": "2.71.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "apache_beam-2.71.0-cp313-cp313-win_amd64.whl"}, {"packagetype": "sdist", "filename": "apache_beam-2.71.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": "", "version": "0.20.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ariadne-0.20.1-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "ariadne-0.20.1.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": ">=3.9", "version": "0.26.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ariadne-0.26.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "ariadne-0.26.2.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": ">=3.10", "version": "0.27.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ariadne-0.27.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "ariadne-0.27.1.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": ">=3.10", "version": "0.28.0rc2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ariadne-0.28.0rc2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "ariadne-0.28.0rc2.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "arq", "requires_python": ">=3.6", "version": "0.23", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "arq-0.23-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "arq-0.23.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "arq", "requires_python": ">=3.8", "version": "0.26.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "arq-0.26.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "arq-0.26.3.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "arq", "requires_python": ">=3.9", "version": "0.27.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "arq-0.27.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "arq-0.27.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database :: Front-Ends"], "name": "asyncpg", "requires_python": ">=3.5.0", "version": "0.23.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp35-cp35m-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp36-cp36m-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp37-cp37m-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp38-cp38-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp39-cp39-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.23.0-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "asyncpg-0.23.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database :: Front-Ends"], "name": "asyncpg", "requires_python": ">=3.6.0", "version": "0.26.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp310-cp310-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp36-cp36m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp36-cp36m-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp37-cp37m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp37-cp37m-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp38-cp38-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp39-cp39-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.26.0-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "asyncpg-0.26.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database :: Front-Ends"], "name": "asyncpg", "requires_python": ">=3.8.0", "version": "0.29.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "asyncpg-0.29.0-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "asyncpg-0.29.0.tar.gz"}]} @@ -46,46 +47,49 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "boto3", "requires_python": "", "version": "1.12.49", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "boto3-1.12.49-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "boto3-1.12.49.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "boto3", "requires_python": ">= 3.6", "version": "1.21.46", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "boto3-1.21.46-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "boto3-1.21.46.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "boto3", "requires_python": ">= 3.7", "version": "1.33.13", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "boto3-1.33.13-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "boto3-1.33.13.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9"], "name": "boto3", "requires_python": ">=3.9", "version": "1.42.13", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "boto3-1.42.13-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "boto3-1.42.13.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9"], "name": "boto3", "requires_python": ">=3.9", "version": "1.42.39", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "boto3-1.42.39-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "boto3-1.42.39.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", "Topic :: Internet :: WWW/HTTP :: WSGI :: Server", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "bottle", "requires_python": "", "version": "0.12.25", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "bottle-0.12.25-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "bottle-0.12.25.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", "Topic :: Internet :: WWW/HTTP :: WSGI :: Server", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "bottle", "requires_python": null, "version": "0.13.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "bottle-0.13.4-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "bottle-0.13.4.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: C", "Programming Language :: C++", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Unix Shell", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Archiving", "Topic :: System :: Archiving :: Compression", "Topic :: Text Processing :: Fonts", "Topic :: Utilities"], "name": "brotli", "requires_python": null, "version": "1.2.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp27-cp27m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp27-cp27m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp27-cp27m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp27-cp27mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp27-cp27mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp27-cp27m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp27-cp27m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp314-cp314-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp36-cp36m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp36-cp36m-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp36-cp36m-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp36-cp36m-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp36-cp36m-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp37-cp37m-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp37-cp37m-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp37-cp37m-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp37-cp37m-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp38-cp38-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp38-cp38-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp38-cp38-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp39-cp39-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp39-cp39-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp39-cp39-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "brotli-1.2.0-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "brotli-1.2.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Object Brokering", "Topic :: System :: Distributed Computing"], "name": "celery", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", "version": "4.4.7", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "celery-4.4.7-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "celery-4.4.7.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Celery", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Object Brokering", "Topic :: System :: Distributed Computing"], "name": "celery", "requires_python": ">=3.9", "version": "5.6.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "celery-5.6.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "celery-5.6.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Celery", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Object Brokering", "Topic :: System :: Distributed Computing"], "name": "celery", "requires_python": ">=3.9", "version": "5.6.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "celery-5.6.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "celery-5.6.2.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "chalice", "requires_python": "", "version": "1.16.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "chalice-1.16.0-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "chalice-1.16.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "chalice", "requires_python": null, "version": "1.32.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "chalice-1.32.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "chalice-1.32.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: SQL", "Topic :: Database", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "clickhouse-driver", "requires_python": "<4,>=3.9", "version": "0.2.10", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-musllinux_1_2_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-musllinux_1_2_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-cp39-cp39-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp310-pypy310_pp73-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp310-pypy310_pp73-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp311-pypy311_pp73-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp311-pypy311_pp73-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp39-pypy39_pp73-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp39-pypy39_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "clickhouse_driver-0.2.10-pp39-pypy39_pp73-win_amd64.whl"}, {"packagetype": "sdist", "filename": "clickhouse_driver-0.2.10.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "cohere", "requires_python": "<4.0,>=3.8", "version": "5.10.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "cohere-5.10.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "cohere-5.10.0.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "cohere", "requires_python": "<4.0,>=3.9", "version": "5.15.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "cohere-5.15.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "cohere-5.15.0.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "cohere", "requires_python": "<4.0,>=3.9", "version": "5.20.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "cohere-5.20.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "cohere-5.20.1.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "cohere", "requires_python": "<4.0,>=3.9", "version": "5.20.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "cohere-5.20.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "cohere-5.20.2.tar.gz"}]} {"info": {"classifiers": ["Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "cohere", "requires_python": "<4.0,>=3.8", "version": "5.4.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "cohere-5.4.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "cohere-5.4.0.tar.gz"}]} {"info": {"classifiers": ["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: System :: Distributed Computing"], "name": "dramatiq", "requires_python": ">=3.5", "version": "1.9.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "dramatiq-1.9.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "dramatiq-1.9.0.tar.gz"}]} -{"info": {"classifiers": ["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: System :: Distributed Computing"], "name": "dramatiq", "requires_python": ">=3.10", "version": "2.0.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "dramatiq-2.0.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "dramatiq-2.0.0.tar.gz"}]} +{"info": {"classifiers": ["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: System :: Distributed Computing"], "name": "dramatiq", "requires_python": ">=3.10", "version": "2.0.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "dramatiq-2.0.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "dramatiq-2.0.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: Jython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "falcon", "requires_python": "", "version": "1.4.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "falcon-1.4.1-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "falcon-1.4.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "falcon", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "version": "2.0.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp27-cp27m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp27-cp27m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp27-cp27mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp27-cp27mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp34-cp34m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp34-cp34m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp35-cp35m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-2.0.0-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "falcon-2.0.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Cython", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "falcon", "requires_python": ">=3.5", "version": "3.1.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp310-cp310-macosx_11_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp311-cp311-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp312-cp312-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp36-cp36m-macosx_10_14_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp37-cp37m-macosx_11_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp38-cp38-macosx_11_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp39-cp39-macosx_11_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-3.1.3-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "falcon-3.1.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Cython", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Free Threading", "Programming Language :: Python :: Free Threading :: 2 - Beta", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Typing :: Typed"], "name": "falcon", "requires_python": ">=3.9", "version": "4.2.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp314-cp314-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp314-cp314-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp314-cp314-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "falcon-4.2.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "falcon-4.2.0.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 1", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.8", "version": "0.109.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastapi-0.109.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastapi-0.109.2.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 1", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.9", "version": "0.125.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastapi-0.125.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastapi-0.125.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 1", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.8", "version": "0.111.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastapi-0.111.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastapi-0.111.1.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.9", "version": "0.128.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastapi-0.128.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastapi-0.128.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.6.1", "version": "0.79.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastapi-0.79.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastapi-0.79.1.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 1", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.7", "version": "0.94.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastapi-0.94.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastapi-0.94.1.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 1", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.7", "version": "0.95.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastapi-0.95.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastapi-0.95.2.tar.gz"}]} {"info": {"classifiers": [], "name": "fastmcp", "requires_python": ">=3.10", "version": "0.1.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastmcp-0.1.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastmcp-0.1.0.tar.gz"}]} {"info": {"classifiers": [], "name": "fastmcp", "requires_python": ">=3.10", "version": "0.4.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastmcp-0.4.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastmcp-0.4.1.tar.gz"}]} {"info": {"classifiers": [], "name": "fastmcp", "requires_python": ">=3.10", "version": "1.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastmcp-1.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastmcp-1.0.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Typing :: Typed"], "name": "fastmcp", "requires_python": ">=3.10", "version": "2.14.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastmcp-2.14.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastmcp-2.14.1.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Typing :: Typed"], "name": "fastmcp", "requires_python": ">=3.10", "version": "2.14.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastmcp-2.14.4-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastmcp-2.14.4.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Typing :: Typed"], "name": "fastmcp", "requires_python": ">=3.10", "version": "3.0.0b1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastmcp-3.0.0b1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastmcp-3.0.0b1.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "google-genai", "requires_python": ">=3.9", "version": "1.29.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "google_genai-1.29.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "google_genai-1.29.0.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "google-genai", "requires_python": ">=3.9", "version": "1.38.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "google_genai-1.38.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "google_genai-1.38.0.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "google-genai", "requires_python": ">=3.9", "version": "1.47.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "google_genai-1.47.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "google_genai-1.47.0.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "google-genai", "requires_python": ">=3.10", "version": "1.56.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "google_genai-1.56.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "google_genai-1.56.0.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "google-genai", "requires_python": ">=3.9", "version": "1.40.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "google_genai-1.40.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "google_genai-1.40.0.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "google-genai", "requires_python": ">=3.10", "version": "1.51.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "google_genai-1.51.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "google_genai-1.51.0.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "google-genai", "requires_python": ">=3.10", "version": "1.61.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "google_genai-1.61.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "google_genai-1.61.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": "", "version": "3.4.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "gql-3.4.1-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "gql-3.4.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": ">=3.8.1", "version": "4.0.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "gql-4.0.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "gql-4.0.0.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": ">=3.8.1", "version": "4.2.0b0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "gql-4.2.0b0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "gql-4.2.0b0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": ">=3.8.1", "version": "4.3.0b0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "gql-4.3.0b0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "gql-4.3.0b0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries"], "name": "graphene", "requires_python": "", "version": "3.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "graphene-3.3-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "graphene-3.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries"], "name": "graphene", "requires_python": null, "version": "3.4.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "graphene-3.4.3-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "graphene-3.4.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "grpcio", "requires_python": "", "version": "1.32.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp27-cp27m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp27-cp27m-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp27-cp27m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp27-cp27mu-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp27-cp27mu-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp27-cp27mu-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp27-cp27m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp27-cp27m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp35-cp35m-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp35-cp35m-macosx_10_7_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp35-cp35m-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp35-cp35m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp35-cp35m-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp35-cp35m-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp35-cp35m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp35-cp35m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp36-cp36m-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp36-cp36m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp36-cp36m-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp36-cp36m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp36-cp36m-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp36-cp36m-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp37-cp37m-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp37-cp37m-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp37-cp37m-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp37-cp37m-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp37-cp37m-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp38-cp38-manylinux2010_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp38-cp38-manylinux2010_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp38-cp38-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp38-cp38-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.32.0-cp38-cp38-win_amd64.whl"}, {"packagetype": "sdist", "filename": "grpcio-1.32.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "grpcio", "requires_python": ">=3.6", "version": "1.47.5", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp310-cp310-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp310-cp310-macosx_12_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp310-cp310-manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp310-cp310-musllinux_1_1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp310-cp310-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp36-cp36m-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp36-cp36m-macosx_10_10_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp36-cp36m-manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp36-cp36m-musllinux_1_1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp36-cp36m-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp37-cp37m-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp37-cp37m-macosx_10_10_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp37-cp37m-manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp37-cp37m-musllinux_1_1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp37-cp37m-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp38-cp38-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp38-cp38-macosx_10_10_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp38-cp38-manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp38-cp38-musllinux_1_1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp38-cp38-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp39-cp39-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp39-cp39-macosx_10_10_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp39-cp39-manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp39-cp39-musllinux_1_1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp39-cp39-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.47.5-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "grpcio-1.47.5.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "grpcio", "requires_python": ">=3.7", "version": "1.62.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp310-cp310-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp310-cp310-macosx_12_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp310-cp310-manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp310-cp310-musllinux_1_1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp310-cp310-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp311-cp311-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp311-cp311-macosx_10_10_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp311-cp311-manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp311-cp311-musllinux_1_1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp311-cp311-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp312-cp312-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp312-cp312-macosx_10_10_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp312-cp312-manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp312-cp312-musllinux_1_1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp312-cp312-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp37-cp37m-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp37-cp37m-macosx_10_10_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp37-cp37m-manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp37-cp37m-musllinux_1_1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp37-cp37m-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp38-cp38-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp38-cp38-macosx_10_10_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp38-cp38-manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp38-cp38-musllinux_1_1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp38-cp38-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp39-cp39-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp39-cp39-macosx_10_10_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp39-cp39-manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp39-cp39-musllinux_1_1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp39-cp39-musllinux_1_1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.62.3-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "grpcio-1.62.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9"], "name": "grpcio", "requires_python": ">=3.9", "version": "1.76.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp310-cp310-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp311-cp311-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp312-cp312-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp313-cp313-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp314-cp314-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp314-cp314-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp39-cp39-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp39-cp39-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp39-cp39-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp39-cp39-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp39-cp39-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.76.0-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "grpcio-1.76.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9"], "name": "grpcio", "requires_python": ">=3.9", "version": "1.78.0rc2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp310-cp310-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp310-cp310-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp310-cp310-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp311-cp311-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp311-cp311-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp311-cp311-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp312-cp312-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp312-cp312-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp312-cp312-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp313-cp313-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp313-cp313-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp313-cp313-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp314-cp314-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp314-cp314-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp314-cp314-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp314-cp314-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp314-cp314-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp314-cp314-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp39-cp39-linux_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp39-cp39-macosx_11_0_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp39-cp39-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp39-cp39-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp39-cp39-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "grpcio-1.78.0rc2-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "grpcio-1.78.0rc2.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX", "Programming Language :: Python :: 3"], "name": "httptools", "requires_python": ">=3.9", "version": "0.7.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp39-cp39-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp39-cp39-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp39-cp39-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "httptools-0.7.1-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "httptools-0.7.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Trio", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "httpx", "requires_python": ">=3.6", "version": "0.16.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "httpx-0.16.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "httpx-0.16.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Trio", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "httpx", "requires_python": ">=3.6", "version": "0.20.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "httpx-0.20.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "httpx-0.20.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Trio", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "httpx", "requires_python": ">=3.6", "version": "0.22.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "httpx-0.22.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "httpx-0.22.0.tar.gz"}]} @@ -97,59 +101,57 @@ {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python"], "name": "huey", "requires_python": "", "version": "1.7.0", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "huey-1.7.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python"], "name": "huey", "requires_python": "", "version": "2.0.1", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "huey-2.0.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "huey", "requires_python": "", "version": "2.1.3", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "huey-2.1.3.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "huey", "requires_python": null, "version": "2.5.5", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "huey-2.5.5-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "huey-2.5.5.tar.gz"}]} +{"info": {"classifiers": ["Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "huey", "requires_python": null, "version": "2.6.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "huey-2.6.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "huey-2.6.0.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.24.7", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "huggingface_hub-0.24.7-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "huggingface_hub-0.24.7.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.36.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "huggingface_hub-0.36.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "huggingface_hub-0.36.0.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.9.0", "version": "1.2.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "huggingface_hub-1.2.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "huggingface_hub-1.2.3.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.36.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "huggingface_hub-0.36.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "huggingface_hub-0.36.1.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.9.0", "version": "1.3.7", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "huggingface_hub-1.3.7-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "huggingface_hub-1.3.7.tar.gz"}]} {"info": {"classifiers": ["License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.9"], "name": "langchain", "requires_python": "<4.0,>=3.8.1", "version": "0.1.20", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "langchain-0.1.20-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "langchain-0.1.20.tar.gz"}]} {"info": {"classifiers": [], "name": "langchain", "requires_python": "<4.0,>=3.9", "version": "0.3.27", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "langchain-0.3.27-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "langchain-0.3.27.tar.gz"}]} {"info": {"classifiers": [], "name": "langchain", "requires_python": "<4.0.0,>=3.10.0", "version": "1.0.8", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "langchain-1.0.8-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "langchain-1.0.8.tar.gz"}]} -{"info": {"classifiers": [], "name": "langchain", "requires_python": "<4.0.0,>=3.10.0", "version": "1.2.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "langchain-1.2.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "langchain-1.2.0.tar.gz"}]} +{"info": {"classifiers": [], "name": "langchain", "requires_python": "<4.0.0,>=3.10.0", "version": "1.2.7", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "langchain-1.2.7-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "langchain-1.2.7.tar.gz"}]} {"info": {"classifiers": [], "name": "langgraph", "requires_python": ">=3.9", "version": "0.6.11", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "langgraph-0.6.11-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "langgraph-0.6.11.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "langgraph", "requires_python": ">=3.10", "version": "1.0.5", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "langgraph-1.0.5-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "langgraph-1.0.5.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "langgraph", "requires_python": ">=3.10", "version": "1.0.7", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "langgraph-1.0.7-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "langgraph-1.0.7.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries"], "name": "launchdarkly-server-sdk", "requires_python": ">=3.9", "version": "9.14.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "launchdarkly_server_sdk-9.14.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "launchdarkly_server_sdk-9.14.1.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries"], "name": "launchdarkly-server-sdk", "requires_python": ">=3.8", "version": "9.8.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "launchdarkly_server_sdk-9.8.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "launchdarkly_server_sdk-9.8.1.tar.gz"}]} {"info": {"classifiers": ["License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "litellm", "requires_python": "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8", "version": "1.77.7", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "litellm-1.77.7-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "litellm-1.77.7.tar.gz"}]} {"info": {"classifiers": ["License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "litellm", "requires_python": "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8", "version": "1.78.7", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "litellm-1.78.7-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "litellm-1.78.7.tar.gz"}]} {"info": {"classifiers": ["License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "litellm", "requires_python": "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8", "version": "1.79.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "litellm-1.79.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "litellm-1.79.3.tar.gz"}]} -{"info": {"classifiers": ["License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "litellm", "requires_python": "<4.0,>=3.9", "version": "1.80.10", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "litellm-1.80.10-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "litellm-1.80.10.tar.gz"}]} +{"info": {"classifiers": ["License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "litellm", "requires_python": "<4.0,>=3.9", "version": "1.81.6", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "litellm-1.81.6-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "litellm-1.81.6.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Pydantic", "Framework :: Pydantic :: 1", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": ">=3.8,<4.0", "version": "2.0.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "litestar-2.0.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "litestar-2.0.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": "<4.0,>=3.8", "version": "2.12.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "litestar-2.12.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "litestar-2.12.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": "<4.0,>=3.8", "version": "2.19.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "litestar-2.19.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "litestar-2.19.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": "<4.0,>=3.8", "version": "2.6.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "litestar-2.6.4-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "litestar-2.6.4.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: System :: Logging"], "name": "loguru", "requires_python": "<4.0,>=3.5", "version": "0.7.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "loguru-0.7.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "loguru-0.7.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13"], "name": "mcp", "requires_python": ">=3.10", "version": "1.15.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "mcp-1.15.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "mcp-1.15.0.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13"], "name": "mcp", "requires_python": ">=3.10", "version": "1.18.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "mcp-1.18.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "mcp-1.18.0.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13"], "name": "mcp", "requires_python": ">=3.10", "version": "1.21.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "mcp-1.21.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "mcp-1.21.2.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13"], "name": "mcp", "requires_python": ">=3.10", "version": "1.24.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "mcp-1.24.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "mcp-1.24.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13"], "name": "mcp", "requires_python": ">=3.10", "version": "1.19.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "mcp-1.19.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "mcp-1.19.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13"], "name": "mcp", "requires_python": ">=3.10", "version": "1.23.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "mcp-1.23.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "mcp-1.23.3.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13"], "name": "mcp", "requires_python": ">=3.10", "version": "1.26.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "mcp-1.26.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "mcp-1.26.0.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.7.1", "version": "1.0.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-1.0.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-1.0.1.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.109.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-1.109.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-1.109.1.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.62.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-1.62.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-1.62.0.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.93.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-1.93.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-1.93.3.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.9", "version": "2.12.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-2.12.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-2.12.0.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.63.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-1.63.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-1.63.2.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.95.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-1.95.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-1.95.1.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "2.1.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-2.1.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-2.1.0.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.9", "version": "2.14.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-2.14.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-2.14.0.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.9", "version": "2.7.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-2.7.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-2.7.2.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.9", "version": "2.16.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-2.16.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-2.16.0.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.9", "version": "2.9.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai-2.9.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai-2.9.0.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.0.19", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai_agents-0.0.19-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai_agents-0.0.19.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.2.11", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai_agents-0.2.11-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai_agents-0.2.11.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.4.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai_agents-0.4.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai_agents-0.4.2.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.6.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai_agents-0.6.4-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai_agents-0.6.4.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.7.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai_agents-0.7.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai_agents-0.7.0.tar.gz"}]} {"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3"], "name": "openfeature-sdk", "requires_python": ">=3.8", "version": "0.7.5", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openfeature_sdk-0.7.5-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openfeature_sdk-0.7.5.tar.gz"}]} {"info": {"classifiers": ["Programming Language :: Python", "Programming Language :: Python :: 3"], "name": "openfeature-sdk", "requires_python": ">=3.9", "version": "0.8.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openfeature_sdk-0.8.4-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openfeature_sdk-0.8.4.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "pure-eval", "requires_python": "", "version": "0.0.3", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "pure_eval-0.0.3.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "pure-eval", "requires_python": null, "version": "0.2.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pure_eval-0.2.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pure_eval-0.2.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Pydantic", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "pydantic-ai", "requires_python": ">=3.10", "version": "1.0.18", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.0.18-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pydantic_ai-1.0.18.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Pydantic", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "pydantic-ai", "requires_python": ">=3.10", "version": "1.12.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.12.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pydantic_ai-1.12.0.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Pydantic", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "pydantic-ai", "requires_python": ">=3.10", "version": "1.24.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.24.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pydantic_ai-1.24.0.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Pydantic", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "pydantic-ai", "requires_python": ">=3.10", "version": "1.36.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.36.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pydantic_ai-1.36.0.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Topic :: Database"], "name": "pymongo", "requires_python": null, "version": "0.6", "yanked": false}, "urls": [{"packagetype": "bdist_egg", "filename": "pymongo-0.6-py2.5-macosx-10.5-i386.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-0.6-py2.6-macosx-10.5-i386.egg"}, {"packagetype": "sdist", "filename": "pymongo-0.6.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.4", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.1", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: Jython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": null, "version": "2.8.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp26-none-macosx_10_10_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp26-none-macosx_10_8_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp26-none-macosx_10_9_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp26-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp26-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp27-none-macosx_10_10_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp27-none-macosx_10_8_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp27-none-macosx_10_9_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp27-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp27-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp32-cp32m-macosx_10_6_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp32-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp32-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp33-cp33m-macosx_10_6_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp33-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp33-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp34-cp34m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp34-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-2.8.1-cp34-none-win_amd64.whl"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.4-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.5-macosx-10.8-x86_64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.5-macosx-10.9-x86_64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.5-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.6-macosx-10.10-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.6-macosx-10.8-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.6-macosx-10.9-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.6-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.6-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.7-macosx-10.10-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.7-macosx-10.8-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.7-macosx-10.9-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.7-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py2.7-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py3.2-macosx-10.6-x86_64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py3.2-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py3.2-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py3.3-macosx-10.6-x86_64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py3.3-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py3.3-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py3.4-macosx-10.6-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py3.4-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-2.8.1-py3.4-win-amd64.egg"}, {"packagetype": "sdist", "filename": "pymongo-2.8.1.tar.gz"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win32-py2.4.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win32-py2.5.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win32-py2.6.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win32-py2.7.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win32-py3.2.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win32-py3.3.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win32-py3.4.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win-amd64-py2.6.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win-amd64-py2.7.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win-amd64-py3.2.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win-amd64-py3.3.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-2.8.1.win-amd64-py3.4.exe"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Pydantic", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "pydantic-ai", "requires_python": ">=3.10", "version": "1.17.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.17.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pydantic_ai-1.17.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Pydantic", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "pydantic-ai", "requires_python": ">=3.10", "version": "1.34.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.34.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pydantic_ai-1.34.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Pydantic", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "pydantic-ai", "requires_python": ">=3.10", "version": "1.51.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.51.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pydantic_ai-1.51.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": "", "version": "3.13.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp27-cp27m-macosx_10_14_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp27-cp27m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp27-cp27m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp27-cp27mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp27-cp27mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp27-cp27m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp27-cp27m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp311-cp311-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp35-cp35m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp35-cp35m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp35-cp35m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp35-cp35m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.13.0-cp39-cp39-win_amd64.whl"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.13.0-py2.7-macosx-10.14-intel.egg"}, {"packagetype": "sdist", "filename": "pymongo-3.13.0.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": null, "version": "3.2.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp26-none-macosx_10_10_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp26-none-macosx_10_11_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp26-none-macosx_10_8_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp26-none-macosx_10_9_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp26-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp26-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp27-none-macosx_10_10_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp27-none-macosx_10_11_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp27-none-macosx_10_8_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp27-none-macosx_10_9_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp27-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp27-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp32-cp32m-macosx_10_6_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp32-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp32-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp33-cp33m-macosx_10_6_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp33-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp33-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp34-cp34m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp34-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp34-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp35-cp35m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp35-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.2.2-cp35-none-win_amd64.whl"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.6-macosx-10.10-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.6-macosx-10.11-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.6-macosx-10.8-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.6-macosx-10.9-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.6-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.6-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.7-macosx-10.10-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.7-macosx-10.11-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.7-macosx-10.8-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.7-macosx-10.9-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.7-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py2.7-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.2-macosx-10.6-x86_64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.2-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.2-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.3-macosx-10.6-x86_64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.3-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.3-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.4-macosx-10.6-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.4-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.4-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.5-macosx-10.6-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.5-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.2.2-py3.5-win-amd64.egg"}, {"packagetype": "sdist", "filename": "pymongo-3.2.2.tar.gz"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win32-py2.6.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win32-py2.7.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win32-py3.2.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win32-py3.3.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win32-py3.4.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win32-py3.5.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win-amd64-py2.6.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win-amd64-py2.7.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win-amd64-py3.2.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win-amd64-py3.3.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win-amd64-py3.4.exe"}, {"packagetype": "bdist_wininst", "filename": "pymongo-3.2.2.win-amd64-py3.5.exe"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": "", "version": "3.4.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp26-cp26m-macosx_10_11_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp26-cp26m-macosx_10_12_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp26-cp26m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp26-cp26m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp26-cp26mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp26-cp26mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp26-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp26-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp27-cp27m-macosx_10_11_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp27-cp27m-macosx_10_12_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp27-cp27m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp27-cp27m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp27-cp27mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp27-cp27mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp27-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp27-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp33-cp33m-macosx_10_6_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp33-cp33m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp33-cp33m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp33-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp33-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp34-cp34m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp34-cp34m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp34-cp34m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp34-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp34-none-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp35-cp35m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp35-cp35m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp35-none-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.4.0-cp35-none-win_amd64.whl"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py2.6-macosx-10.11-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py2.6-macosx-10.12-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py2.6-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py2.6-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py2.7-macosx-10.11-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py2.7-macosx-10.12-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py2.7-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py2.7-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py3.3-macosx-10.6-x86_64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py3.3-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py3.3-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py3.4-macosx-10.6-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py3.4-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py3.4-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py3.5-macosx-10.6-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py3.5-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.4.0-py3.5-win-amd64.egg"}, {"packagetype": "sdist", "filename": "pymongo-3.4.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": "", "version": "3.5.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp26-cp26m-macosx_10_12_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp26-cp26m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp26-cp26m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp26-cp26mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp26-cp26mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp26-cp26m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp26-cp26m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp27-cp27m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp27-cp27m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp27-cp27mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp27-cp27mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp27-cp27m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp27-cp27m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp27-none-macosx_10_12_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp33-cp33m-macosx_10_6_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp33-cp33m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp33-cp33m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp33-cp33m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp33-cp33m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp34-cp34m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp34-cp34m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp34-cp34m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp34-cp34m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp34-cp34m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp35-cp35m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp35-cp35m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp35-cp35m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp35-cp35m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp36-cp36m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.5.1-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py2.6-macosx-10.12-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py2.6-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py2.6-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py2.7-macosx-10.12-x86_64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py2.7-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py2.7-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.3-macosx-10.6-x86_64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.3-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.3-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.4-macosx-10.6-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.4-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.4-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.5-macosx-10.6-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.5-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.5-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.6-macosx-10.6-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.6-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.5.1-py3.6-win-amd64.egg"}, {"packagetype": "sdist", "filename": "pymongo-3.5.1.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": "", "version": "3.6.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp26-cp26m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp26-cp26m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp26-cp26mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp26-cp26mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp26-cp26m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp26-cp26m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp27-cp27m-macosx_10_13_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp27-cp27m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp27-cp27m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp27-cp27mu-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp27-cp27mu-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp27-cp27m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp27-cp27m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp34-cp34m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp34-cp34m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp34-cp34m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp34-cp34m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp34-cp34m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp35-cp35m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp35-cp35m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp35-cp35m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp35-cp35m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp35-cp35m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp36-cp36m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-3.6.1-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py2.6-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py2.6-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py2.7-macosx-10.13-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py2.7-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py2.7-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py3.4-macosx-10.6-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py3.4-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py3.4-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py3.5-macosx-10.6-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py3.5-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py3.5-win-amd64.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py3.6-macosx-10.6-intel.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py3.6-win32.egg"}, {"packagetype": "bdist_egg", "filename": "pymongo-3.6.1-py3.6-win-amd64.egg"}, {"packagetype": "sdist", "filename": "pymongo-3.6.1.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": ">=3.6", "version": "4.0.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.0.2-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "pymongo-4.0.2.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database", "Typing :: Typed"], "name": "pymongo", "requires_python": ">=3.9", "version": "4.15.5", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp310-cp310-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp311-cp311-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp312-cp312-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp312-cp312-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp313-cp313-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp313-cp313-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314t-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314t-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314t-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314t-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314t-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp314-cp314-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp39-cp39-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.15.5-cp39-cp39-win_arm64.whl"}, {"packagetype": "sdist", "filename": "pymongo-4.15.5.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database", "Typing :: Typed"], "name": "pymongo", "requires_python": ">=3.9", "version": "4.11.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp312-cp312-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313t-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313t-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313t-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313t-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.11.3-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "pymongo-4.11.3.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database", "Typing :: Typed"], "name": "pymongo", "requires_python": ">=3.9", "version": "4.14.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp312-cp312-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313t-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313t-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313t-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313t-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.14.1-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "pymongo-4.14.1.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database", "Typing :: Typed"], "name": "pymongo", "requires_python": ">=3.9", "version": "4.16.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp310-cp310-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp311-cp311-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp312-cp312-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp312-cp312-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp312-cp312-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp313-cp313-macosx_10_13_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp313-cp313-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp313-cp313-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314t-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314t-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314t-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314t-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314t-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp314-cp314-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp39-cp39-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp39-cp39-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.16.0-cp39-cp39-win_arm64.whl"}, {"packagetype": "sdist", "filename": "pymongo-4.16.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database", "Typing :: Typed"], "name": "pymongo", "requires_python": ">=3.7", "version": "4.4.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp311-cp311-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp311-cp311-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-macosx_10_6_intel.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "pymongo-4.4.1-cp39-cp39-win_amd64.whl"}, {"packagetype": "sdist", "filename": "pymongo-4.4.1.tar.gz"}]} {"info": {"classifiers": ["Framework :: Pylons", "Intended Audience :: Developers", "License :: Repoze Public License", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI"], "name": "pyramid", "requires_python": null, "version": "1.0.2", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "pyramid-1.0.2.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 6 - Mature", "Framework :: Pyramid", "Intended Audience :: Developers", "License :: Repoze Public License", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI"], "name": "pyramid", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", "version": "1.10.8", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pyramid-1.10.8-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pyramid-1.10.8.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 6 - Mature", "Framework :: Pyramid", "Intended Audience :: Developers", "License :: Repoze Public License", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI"], "name": "pyramid", "requires_python": "", "version": "1.6.5", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pyramid-1.6.5-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pyramid-1.6.5.tar.gz"}]} @@ -162,13 +164,13 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "pyspark", "requires_python": "", "version": "3.0.3", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "pyspark-3.0.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.6", "version": "3.1.3", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "pyspark-3.1.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.6", "version": "3.2.4", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "pyspark-3.2.4.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.8", "version": "3.5.7", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "pyspark-3.5.7.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.10", "version": "4.1.0", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "pyspark-4.1.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.8", "version": "3.5.8", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "pyspark-3.5.8.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.10", "version": "4.1.1", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "pyspark-4.1.1.tar.gz"}]} {"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": ">=3.8", "version": "2.36.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp310-cp310-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp310-cp310-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp310-cp310-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp311-cp311-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp311-cp311-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp311-cp311-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp312-cp312-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp312-cp312-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp312-cp312-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp39-cp39-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp39-cp39-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.36.1-cp39-cp39-win_amd64.whl"}]} {"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": ">=3.9", "version": "2.45.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp310-cp310-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp310-cp310-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp310-cp310-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp311-cp311-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp311-cp311-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp311-cp311-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp312-cp312-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp312-cp312-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp312-cp312-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp313-cp313-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp313-cp313-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp313-cp313-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp39-cp39-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp39-cp39-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.45.0-cp39-cp39-win_amd64.whl"}]} -{"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": ">=3.9", "version": "2.49.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp310-cp310-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp310-cp310-macosx_12_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp310-cp310-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp310-cp310-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp311-cp311-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp311-cp311-macosx_12_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp311-cp311-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp311-cp311-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp312-cp312-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp312-cp312-macosx_12_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp312-cp312-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp312-cp312-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp313-cp313-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp313-cp313-macosx_12_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp313-cp313-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp313-cp313-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp39-cp39-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp39-cp39-macosx_12_0_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp39-cp39-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.49.2-cp39-cp39-win_amd64.whl"}]} -{"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": ">=3.9", "version": "2.51.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp310-cp310-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp310-cp310-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp310-cp310-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp311-cp311-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp311-cp311-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp311-cp311-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp312-cp312-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp312-cp312-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp312-cp312-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp313-cp313-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp313-cp313-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp313-cp313-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp39-cp39-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp39-cp39-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.51.2-cp39-cp39-win_amd64.whl"}]} +{"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": ">=3.9", "version": "2.50.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp310-cp310-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp310-cp310-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp310-cp310-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp311-cp311-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp311-cp311-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp311-cp311-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp312-cp312-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp312-cp312-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp312-cp312-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp313-cp313-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp313-cp313-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp313-cp313-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp39-cp39-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp39-cp39-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.50.1-cp39-cp39-win_amd64.whl"}]} {"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": ">=3.9", "version": "2.52.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp310-cp310-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp310-cp310-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp310-cp310-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp311-cp311-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp311-cp311-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp311-cp311-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp312-cp312-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp312-cp312-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp312-cp312-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp313-cp313-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp313-cp313-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.52.1-cp313-cp313-manylinux2014_x86_64.whl"}]} +{"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": ">=3.9", "version": "2.53.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp310-cp310-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp310-cp310-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp310-cp310-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp311-cp311-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp311-cp311-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp311-cp311-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp312-cp312-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp312-cp312-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp312-cp312-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp313-cp313-macosx_12_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp313-cp313-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.53.0-cp313-cp313-manylinux2014_x86_64.whl"}]} {"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": "", "version": "2.7.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp310-cp310-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp310-cp310-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp310-cp310-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp311-cp311-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp311-cp311-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp311-cp311-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp37-cp37m-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp37-cp37m-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp37-cp37m-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp38-cp38-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp38-cp38-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp38-cp38-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp38-cp38-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp38-cp38-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp39-cp39-macosx_10_15_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp39-cp39-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp39-cp39-manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp39-cp39-manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "ray-2.7.2-cp39-cp39-win_amd64.whl"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python"], "name": "redis", "requires_python": null, "version": "0.6.1", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "redis-0.6.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6"], "name": "redis", "requires_python": "", "version": "2.10.6", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "redis-2.10.6-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "redis-2.10.6.tar.gz"}]} @@ -195,29 +197,27 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "requests", "requires_python": "", "version": "2.16.5", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "requests-2.16.5-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "requests-2.16.5.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries"], "name": "requests", "requires_python": ">=3.9", "version": "2.32.5", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "requests-2.32.5-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "requests-2.32.5.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5"], "name": "requests", "requires_python": null, "version": "2.8.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "requests-2.8.1-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "requests-2.8.1.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": "", "version": "0.10.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "rq-0.10.0-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "rq-0.10.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": "", "version": "0.13.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "rq-0.13.0-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "rq-0.13.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": "", "version": "0.6.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "rq-0.6.0-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "rq-0.6.0.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": "", "version": "0.7.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "rq-0.7.1-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "rq-0.7.1.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": "", "version": "0.8.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "rq-0.8.2-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "rq-0.8.2.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": ">=2.7", "version": "1.0", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "rq-1.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": ">=3.7", "version": "1.16.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "rq-1.16.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "rq-1.16.2.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": ">=3.5", "version": "1.8.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "rq-1.8.1-py2.py3-none-any.whl"}, {"packagetype": "sdist", "filename": "rq-1.8.1.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": ">=3.8", "version": "2.3.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "rq-2.3.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "rq-2.3.3.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": ">=3.9", "version": "2.5.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "rq-2.5.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "rq-2.5.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": ">=3.9", "version": "2.6.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "rq-2.6.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "rq-2.6.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6"], "name": "sanic", "requires_python": "", "version": "0.8.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "sanic-0.8.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "sanic-0.8.3.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "sanic", "requires_python": ">=3.6", "version": "20.12.7", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "sanic-20.12.7-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "sanic-20.12.7.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "sanic", "requires_python": ">=3.8", "version": "23.12.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "sanic-23.12.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "sanic-23.12.2.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "sanic", "requires_python": ">=3.8", "version": "25.3.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "sanic-25.3.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "sanic-25.3.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14"], "name": "sanic", "requires_python": ">=3.10", "version": "25.12.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "sanic-25.12.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "sanic-25.12.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.6", "version": "0.16.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "starlette-0.16.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "starlette-0.16.0.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AnyIO", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.7", "version": "0.27.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "starlette-0.27.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "starlette-0.27.0.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AnyIO", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.8", "version": "0.38.6", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "starlette-0.38.6-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "starlette-0.38.6.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AnyIO", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.10", "version": "0.50.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "starlette-0.50.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "starlette-0.50.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AnyIO", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.7", "version": "0.28.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "starlette-0.28.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "starlette-0.28.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AnyIO", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.8", "version": "0.40.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "starlette-0.40.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "starlette-0.40.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AnyIO", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.10", "version": "0.52.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "starlette-0.52.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "starlette-0.52.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Typing :: Typed"], "name": "starlite", "requires_python": ">=3.8,<4.0", "version": "1.48.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "starlite-1.48.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "starlite-1.48.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Typing :: Typed"], "name": "starlite", "requires_python": "<4.0,>=3.8", "version": "1.51.16", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "starlite-1.51.16-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "starlite-1.51.16.tar.gz"}]} {"info": {"classifiers": ["Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries"], "name": "statsig", "requires_python": ">=3.7", "version": "0.55.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "statsig-0.55.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "statsig-0.55.3.tar.gz"}]} -{"info": {"classifiers": ["Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries"], "name": "statsig", "requires_python": ">=3.7", "version": "0.66.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "statsig-0.66.2-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "statsig-0.66.2.tar.gz"}]} +{"info": {"classifiers": ["Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries"], "name": "statsig", "requires_python": ">=3.7", "version": "0.67.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "statsig-0.67.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "statsig-0.67.0.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "strawberry-graphql", "requires_python": ">=3.8,<4.0", "version": "0.209.8", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "strawberry_graphql-0.209.8-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "strawberry_graphql-0.209.8.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "strawberry-graphql", "requires_python": "<4.0,>=3.10", "version": "0.287.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "strawberry_graphql-0.287.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "strawberry_graphql-0.287.3.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "strawberry-graphql", "requires_python": "<4.0,>=3.10", "version": "0.291.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "strawberry_graphql-0.291.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "strawberry_graphql-0.291.0.tar.gz"}]} {"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "tornado", "requires_python": ">= 3.5", "version": "6.0.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "tornado-6.0.4-cp35-cp35m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.0.4-cp35-cp35m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.0.4-cp36-cp36m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.0.4-cp36-cp36m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.0.4-cp37-cp37m-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.0.4-cp37-cp37m-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.0.4-cp38-cp38-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.0.4-cp38-cp38-win_amd64.whl"}, {"packagetype": "sdist", "filename": "tornado-6.0.4.tar.gz"}]} {"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "tornado", "requires_python": ">=3.9", "version": "6.5.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.5.4-cp39-abi3-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.5.4-cp39-abi3-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "tornado-6.5.4-cp39-abi3-win_arm64.whl"}, {"packagetype": "sdist", "filename": "tornado-6.5.4.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License (GPL)", "Natural Language :: English", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Spanish", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": null, "version": "1.2.10", "yanked": false}, "urls": [{"packagetype": "sdist", "filename": "trytond-1.2.10.tar.gz"}]} @@ -229,7 +229,8 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": "", "version": "4.8.18", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "trytond-4.8.18-py2-none-any.whl"}, {"packagetype": "bdist_wheel", "filename": "trytond-4.8.18-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "trytond-4.8.18.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: Finnish", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Indonesian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Natural Language :: Turkish", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.6", "version": "5.8.16", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "trytond-5.8.16-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "trytond-5.8.16.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: Finnish", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Indonesian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Romanian", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Natural Language :: Turkish", "Natural Language :: Ukrainian", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.8", "version": "6.8.17", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "trytond-6.8.17-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "trytond-6.8.17.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: Finnish", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Indonesian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Romanian", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Natural Language :: Turkish", "Natural Language :: Ukrainian", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.9", "version": "7.8.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "trytond-7.8.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "trytond-7.8.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: Finnish", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Indonesian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Romanian", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Natural Language :: Turkish", "Natural Language :: Ukrainian", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.9", "version": "7.8.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "trytond-7.8.4-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "trytond-7.8.4.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "typer", "requires_python": ">=3.7", "version": "0.15.4", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "typer-0.15.4-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "typer-0.15.4.tar.gz"}]} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "typer", "requires_python": ">=3.8", "version": "0.20.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "typer-0.20.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "typer-0.20.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "typer", "requires_python": ">=3.9", "version": "0.21.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "typer-0.21.1-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "typer-0.21.1.tar.gz"}]} {"info": {"classifiers": ["Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Rust"], "name": "uuid-utils", "requires_python": ">=3.9", "version": "0.12.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-macosx_10_12_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-cp39-abi3-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "sdist", "filename": "uuid_utils-0.12.0.tar.gz"}]} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Programming Language :: Rust"], "name": "uuid_utils", "requires_python": ">=3.9", "version": "0.14.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-cp39-abi3-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "sdist", "filename": "uuid_utils-0.14.0.tar.gz"}]} diff --git a/scripts/populate_tox/requirements.txt b/scripts/populate_tox/requirements.txt index 0402fac5ab..77f67f24d2 100644 --- a/scripts/populate_tox/requirements.txt +++ b/scripts/populate_tox/requirements.txt @@ -1,3 +1,3 @@ jinja2 -packaging +packaging<26.0 requests diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index e01832abcb..7e49c2156d 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -230,3 +230,4 @@ commands = ruff check tests sentry_sdk ruff format --check tests sentry_sdk mypy sentry_sdk + python scripts/find_raise_from_none.py diff --git a/scripts/split_tox_gh_actions/templates/base.jinja b/scripts/split_tox_gh_actions/templates/base.jinja index 8d618d228c..c3bc528a7c 100644 --- a/scripts/split_tox_gh_actions/templates/base.jinja +++ b/scripts/split_tox_gh_actions/templates/base.jinja @@ -23,6 +23,9 @@ concurrency: permissions: contents: read + actions: read + pull-requests: write + statuses: write env: BUILD_CACHE_KEY: {% raw %}${{ github.sha }}{% endraw %} diff --git a/scripts/split_tox_gh_actions/templates/test_group.jinja b/scripts/split_tox_gh_actions/templates/test_group.jinja index 3e1ab30290..e9581d2d8f 100644 --- a/scripts/split_tox_gh_actions/templates/test_group.jinja +++ b/scripts/split_tox_gh_actions/templates/test_group.jinja @@ -42,7 +42,7 @@ # Use Docker container only for Python 3.6 {% raw %}container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }}{% endraw %} steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 {% raw %}if: ${{ matrix.python-version != '3.6' }}{% endraw %} with: @@ -94,20 +94,11 @@ coverage combine .coverage-sentry-* coverage xml - - name: Upload coverage to Codecov + - name: Parse and Upload Coverage if: {% raw %}${{ !cancelled() }}{% endraw %} - uses: codecov/codecov-action@v5.5.2 + uses: getsentry/codecov-action@main with: - token: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %} + token: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %} files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - - name: Upload test results to Codecov - if: {% raw %}${{ !cancelled() }}{% endraw %} - uses: codecov/test-results-action@v1 - with: - token: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %} - files: .junitxml + junit-xml-pattern: .junitxml verbose: true diff --git a/sentry_sdk/_batcher.py b/sentry_sdk/_batcher.py new file mode 100644 index 0000000000..4a6ee07e67 --- /dev/null +++ b/sentry_sdk/_batcher.py @@ -0,0 +1,139 @@ +import os +import random +import threading +from datetime import datetime, timezone +from typing import TYPE_CHECKING, TypeVar, Generic + +from sentry_sdk.utils import format_timestamp, safe_repr, serialize_attribute +from sentry_sdk.envelope import Envelope, Item, PayloadRef + +if TYPE_CHECKING: + from typing import Optional, Callable, Any + +T = TypeVar("T") + + +class Batcher(Generic[T]): + MAX_BEFORE_FLUSH = 100 + MAX_BEFORE_DROP = 1_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "" + CONTENT_TYPE = "" + + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + record_lost_func: "Callable[..., None]", + ) -> None: + self._buffer: "list[T]" = [] + self._capture_func = capture_func + self._record_lost_func = record_lost_func + self._running = True + self._lock = threading.Lock() + + self._flush_event: "threading.Event" = threading.Event() + + self._flusher: "Optional[threading.Thread]" = None + self._flusher_pid: "Optional[int]" = None + + def _ensure_thread(self) -> bool: + """For forking processes we might need to restart this thread. + This ensures that our process actually has that thread running. + """ + if not self._running: + return False + + pid = os.getpid() + if self._flusher_pid == pid: + return True + + with self._lock: + # Recheck to make sure another thread didn't get here and start the + # the flusher in the meantime + if self._flusher_pid == pid: + return True + + self._flusher_pid = pid + + self._flusher = threading.Thread(target=self._flush_loop) + self._flusher.daemon = True + + try: + self._flusher.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self._running = False + return False + + return True + + def _flush_loop(self) -> None: + while self._running: + self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) + self._flush_event.clear() + self._flush() + + def add(self, item: "T") -> None: + if not self._ensure_thread() or self._flusher is None: + return None + + with self._lock: + if len(self._buffer) >= self.MAX_BEFORE_DROP: + self._record_lost(item) + return None + + self._buffer.append(item) + if len(self._buffer) >= self.MAX_BEFORE_FLUSH: + self._flush_event.set() + + def kill(self) -> None: + if self._flusher is None: + return + + self._running = False + self._flush_event.set() + self._flusher = None + + def flush(self) -> None: + self._flush() + + def _add_to_envelope(self, envelope: "Envelope") -> None: + envelope.add_item( + Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": len(self._buffer), + }, + payload=PayloadRef( + json={ + "items": [ + self._to_transport_format(item) for item in self._buffer + ] + } + ), + ) + ) + + def _flush(self) -> "Optional[Envelope]": + envelope = Envelope( + headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} + ) + with self._lock: + if len(self._buffer) == 0: + return None + + self._add_to_envelope(envelope) + self._buffer.clear() + + self._capture_func(envelope) + return envelope + + def _record_lost(self, item: "T") -> None: + pass + + @staticmethod + def _to_transport_format(item: "T") -> "Any": + pass diff --git a/sentry_sdk/_log_batcher.py b/sentry_sdk/_log_batcher.py index 51886f48f9..1c59f7379c 100644 --- a/sentry_sdk/_log_batcher.py +++ b/sentry_sdk/_log_batcher.py @@ -1,164 +1,56 @@ -import os -import random -import threading -from datetime import datetime, timezone -from typing import Optional, List, Callable, TYPE_CHECKING, Any +from typing import TYPE_CHECKING -from sentry_sdk.utils import format_timestamp, safe_repr, serialize_attribute +from sentry_sdk._batcher import Batcher +from sentry_sdk.utils import serialize_attribute from sentry_sdk.envelope import Envelope, Item, PayloadRef if TYPE_CHECKING: + from typing import Any from sentry_sdk._types import Log -class LogBatcher: - MAX_LOGS_BEFORE_FLUSH = 100 - MAX_LOGS_BEFORE_DROP = 1_000 +class LogBatcher(Batcher["Log"]): + MAX_BEFORE_FLUSH = 100 + MAX_BEFORE_DROP = 1_000 FLUSH_WAIT_TIME = 5.0 - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - record_lost_func: "Callable[..., None]", - ) -> None: - self._log_buffer: "List[Log]" = [] - self._capture_func = capture_func - self._record_lost_func = record_lost_func - self._running = True - self._lock = threading.Lock() - - self._flush_event: "threading.Event" = threading.Event() - - self._flusher: "Optional[threading.Thread]" = None - self._flusher_pid: "Optional[int]" = None - - def _ensure_thread(self) -> bool: - """For forking processes we might need to restart this thread. - This ensures that our process actually has that thread running. - """ - if not self._running: - return False - - pid = os.getpid() - if self._flusher_pid == pid: - return True - - with self._lock: - # Recheck to make sure another thread didn't get here and start the - # the flusher in the meantime - if self._flusher_pid == pid: - return True - - self._flusher_pid = pid - - self._flusher = threading.Thread(target=self._flush_loop) - self._flusher.daemon = True - - try: - self._flusher.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self._running = False - return False - - return True - - def _flush_loop(self) -> None: - while self._running: - self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) - self._flush_event.clear() - self._flush() - - def add( - self, - log: "Log", - ) -> None: - if not self._ensure_thread() or self._flusher is None: - return None - - with self._lock: - if len(self._log_buffer) >= self.MAX_LOGS_BEFORE_DROP: - # Construct log envelope item without sending it to report lost bytes - log_item = Item( - type="log", - content_type="application/vnd.sentry.items.log+json", - headers={ - "item_count": 1, - }, - payload=PayloadRef( - json={"items": [LogBatcher._log_to_transport_format(log)]} - ), - ) - self._record_lost_func( - reason="queue_overflow", - data_category="log_item", - item=log_item, - quantity=1, - ) - return None - - self._log_buffer.append(log) - if len(self._log_buffer) >= self.MAX_LOGS_BEFORE_FLUSH: - self._flush_event.set() - - def kill(self) -> None: - if self._flusher is None: - return - - self._running = False - self._flush_event.set() - self._flusher = None - - def flush(self) -> None: - self._flush() + TYPE = "log" + CONTENT_TYPE = "application/vnd.sentry.items.log+json" @staticmethod - def _log_to_transport_format(log: "Log") -> "Any": - if "sentry.severity_number" not in log["attributes"]: - log["attributes"]["sentry.severity_number"] = log["severity_number"] - if "sentry.severity_text" not in log["attributes"]: - log["attributes"]["sentry.severity_text"] = log["severity_text"] + def _to_transport_format(item: "Log") -> "Any": + if "sentry.severity_number" not in item["attributes"]: + item["attributes"]["sentry.severity_number"] = item["severity_number"] + if "sentry.severity_text" not in item["attributes"]: + item["attributes"]["sentry.severity_text"] = item["severity_text"] res = { - "timestamp": int(log["time_unix_nano"]) / 1.0e9, - "trace_id": log.get("trace_id", "00000000-0000-0000-0000-000000000000"), - "span_id": log.get("span_id"), - "level": str(log["severity_text"]), - "body": str(log["body"]), + "timestamp": int(item["time_unix_nano"]) / 1.0e9, + "trace_id": item.get("trace_id", "00000000-0000-0000-0000-000000000000"), + "span_id": item.get("span_id"), + "level": str(item["severity_text"]), + "body": str(item["body"]), "attributes": { - k: serialize_attribute(v) for (k, v) in log["attributes"].items() + k: serialize_attribute(v) for (k, v) in item["attributes"].items() }, } return res - def _flush(self) -> "Optional[Envelope]": - envelope = Envelope( - headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} + def _record_lost(self, item: "Log") -> None: + # Construct log envelope item without sending it to report lost bytes + log_item = Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": 1, + }, + payload=PayloadRef(json={"items": [self._to_transport_format(item)]}), ) - with self._lock: - if len(self._log_buffer) == 0: - return None - envelope.add_item( - Item( - type="log", - content_type="application/vnd.sentry.items.log+json", - headers={ - "item_count": len(self._log_buffer), - }, - payload=PayloadRef( - json={ - "items": [ - self._log_to_transport_format(log) - for log in self._log_buffer - ] - } - ), - ) - ) - self._log_buffer.clear() - - self._capture_func(envelope) - return envelope + self._record_lost_func( + reason="queue_overflow", + data_category="log_item", + item=log_item, + quantity=1, + ) diff --git a/sentry_sdk/_metrics_batcher.py b/sentry_sdk/_metrics_batcher.py index 6cbac0cbce..e60a4c86ec 100644 --- a/sentry_sdk/_metrics_batcher.py +++ b/sentry_sdk/_metrics_batcher.py @@ -1,146 +1,46 @@ -import os -import random -import threading -from datetime import datetime, timezone -from typing import Optional, List, Callable, TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING -from sentry_sdk.utils import format_timestamp, safe_repr, serialize_attribute -from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk._batcher import Batcher +from sentry_sdk.utils import serialize_attribute +from sentry_sdk.envelope import Item if TYPE_CHECKING: + from typing import Any from sentry_sdk._types import Metric -class MetricsBatcher: - MAX_METRICS_BEFORE_FLUSH = 1000 - MAX_METRICS_BEFORE_DROP = 10_000 +class MetricsBatcher(Batcher["Metric"]): + MAX_BEFORE_FLUSH = 1000 + MAX_BEFORE_DROP = 10_000 FLUSH_WAIT_TIME = 5.0 - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - record_lost_func: "Callable[..., None]", - ) -> None: - self._metric_buffer: "List[Metric]" = [] - self._capture_func = capture_func - self._record_lost_func = record_lost_func - self._running = True - self._lock = threading.Lock() - - self._flush_event: "threading.Event" = threading.Event() - - self._flusher: "Optional[threading.Thread]" = None - self._flusher_pid: "Optional[int]" = None - - def _ensure_thread(self) -> bool: - if not self._running: - return False - - pid = os.getpid() - if self._flusher_pid == pid: - return True - - with self._lock: - if self._flusher_pid == pid: - return True - - self._flusher_pid = pid - - self._flusher = threading.Thread(target=self._flush_loop) - self._flusher.daemon = True - - try: - self._flusher.start() - except RuntimeError: - self._running = False - return False - - return True - - def _flush_loop(self) -> None: - while self._running: - self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) - self._flush_event.clear() - self._flush() - - def add( - self, - metric: "Metric", - ) -> None: - if not self._ensure_thread() or self._flusher is None: - return None - - with self._lock: - if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_DROP: - self._record_lost_func( - reason="queue_overflow", - data_category="trace_metric", - quantity=1, - ) - return None - - self._metric_buffer.append(metric) - if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_FLUSH: - self._flush_event.set() - - def kill(self) -> None: - if self._flusher is None: - return - - self._running = False - self._flush_event.set() - self._flusher = None - - def flush(self) -> None: - self._flush() + TYPE = "trace_metric" + CONTENT_TYPE = "application/vnd.sentry.items.trace-metric+json" @staticmethod - def _metric_to_transport_format(metric: "Metric") -> "Any": + def _to_transport_format(item: "Metric") -> "Any": res = { - "timestamp": metric["timestamp"], - "trace_id": metric["trace_id"], - "name": metric["name"], - "type": metric["type"], - "value": metric["value"], + "timestamp": item["timestamp"], + "trace_id": item["trace_id"], + "name": item["name"], + "type": item["type"], + "value": item["value"], "attributes": { - k: serialize_attribute(v) for (k, v) in metric["attributes"].items() + k: serialize_attribute(v) for (k, v) in item["attributes"].items() }, } - if metric.get("span_id") is not None: - res["span_id"] = metric["span_id"] + if item.get("span_id") is not None: + res["span_id"] = item["span_id"] - if metric.get("unit") is not None: - res["unit"] = metric["unit"] + if item.get("unit") is not None: + res["unit"] = item["unit"] return res - def _flush(self) -> "Optional[Envelope]": - envelope = Envelope( - headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} + def _record_lost(self, item: "Metric") -> None: + self._record_lost_func( + reason="queue_overflow", + data_category="trace_metric", + quantity=1, ) - with self._lock: - if len(self._metric_buffer) == 0: - return None - - envelope.add_item( - Item( - type="trace_metric", - content_type="application/vnd.sentry.items.trace-metric+json", - headers={ - "item_count": len(self._metric_buffer), - }, - payload=PayloadRef( - json={ - "items": [ - self._metric_to_transport_format(metric) - for metric in self._metric_buffer - ] - } - ), - ) - ) - self._metric_buffer.clear() - - self._capture_func(envelope) - return envelope diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py new file mode 100644 index 0000000000..38ecc8da51 --- /dev/null +++ b/sentry_sdk/_span_batcher.py @@ -0,0 +1,126 @@ +import threading +from collections import defaultdict +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.consts import SPANSTATUS +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import format_timestamp, serialize_attribute, safe_repr + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + from sentry_sdk.traces import StreamedSpan + from sentry_sdk._types import SerializedAttributeValue + + +class SpanBatcher(Batcher["StreamedSpan"]): + # TODO[span-first]: size-based flushes + # TODO[span-first]: adjust flush/drop defaults + MAX_BEFORE_FLUSH = 1000 + MAX_BEFORE_DROP = 5000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "span" + CONTENT_TYPE = "application/vnd.sentry.items.span.v2+json" + + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + record_lost_func: "Callable[..., None]", + ) -> None: + # Spans from different traces cannot be emitted in the same envelope + # since the envelope contains a shared trace header. That's why we bucket + # by trace_id, so that we can then send the buckets each in its own + # envelope. + # trace_id -> span buffer + self._span_buffer: dict[str, list["StreamedSpan"]] = defaultdict(list) + self._capture_func = capture_func + self._record_lost_func = record_lost_func + self._running = True + self._lock = threading.Lock() + + self._flush_event: "threading.Event" = threading.Event() + + self._flusher: "Optional[threading.Thread]" = None + self._flusher_pid: "Optional[int]" = None + + def get_size(self) -> int: + # caller is responsible for locking before checking this + return sum(len(buffer) for buffer in self._span_buffer.values()) + + def add(self, span: "StreamedSpan") -> None: + if not self._ensure_thread() or self._flusher is None: + return None + + with self._lock: + size = self.get_size() + if size >= self.MAX_BEFORE_DROP: + self._record_lost_func( + reason="queue_overflow", + data_category="span", + quantity=1, + ) + return None + + self._span_buffer[span.trace_id].append(span) + if size + 1 >= self.MAX_BEFORE_FLUSH: + self._flush_event.set() + + @staticmethod + def _to_transport_format(item: "StreamedSpan") -> "Any": + # TODO[span-first] + res: "dict[str, Any]" = { + "name": item.name, + } + + if item._attributes: + res["attributes"] = { + k: serialize_attribute(v) for (k, v) in item._attributes.items() + } + + return res + + def _flush(self) -> None: + with self._lock: + if len(self._span_buffer) == 0: + return None + + envelopes = [] + for trace_id, spans in self._span_buffer.items(): + if spans: + # TODO[span-first] + # dsc = spans[0].dynamic_sampling_context() + dsc = None + + envelope = Envelope( + headers={ + "sent_at": format_timestamp(datetime.now(timezone.utc)), + "trace": dsc, + } + ) + + envelope.add_item( + Item( + type="span", + content_type="application/vnd.sentry.items.span.v2+json", + headers={ + "item_count": len(spans), + }, + payload=PayloadRef( + json={ + "items": [ + self._to_transport_format(span) + for span in spans + ] + } + ), + ) + ) + + envelopes.append(envelope) + + self._span_buffer.clear() + + for envelope in envelopes: + self._capture_func(envelope) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 5a8cb936ac..ecb8abcd10 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -6,6 +6,7 @@ SENSITIVE_DATA_SUBSTITUTE = "[Filtered]" +BLOB_DATA_SUBSTITUTE = "[Blob substitute]" class AnnotatedValue: @@ -216,9 +217,18 @@ class SDKInfo(TypedDict): Hint = Dict[str, Any] AttributeValue = ( - str | bool | float | int - # TODO: relay support coming soon for - # | list[str] | list[bool] | list[float] | list[int] + str + | bool + | float + | int + | list[str] + | list[bool] + | list[float] + | list[int] + | tuple[str, ...] + | tuple[bool, ...] + | tuple[float, ...] + | tuple[int, ...] ) Attributes = dict[str, AttributeValue] @@ -231,11 +241,10 @@ class SDKInfo(TypedDict): "boolean", "double", "integer", - # TODO: relay support coming soon for: - # "string[]", - # "boolean[]", - # "double[]", - # "integer[]", + "string[]", + "boolean[]", + "double[]", + "integer[]", ], "value": AttributeValue, }, @@ -255,6 +264,7 @@ class SDKInfo(TypedDict): ) MetricType = Literal["counter", "gauge", "distribution"] + MetricUnit = Union[DurationUnit, InformationUnit, str] Metric = TypedDict( "Metric", @@ -265,7 +275,7 @@ class SDKInfo(TypedDict): "name": str, "type": MetricType, "value": float, - "unit": Optional[str], + "unit": Optional[MetricUnit], "attributes": Attributes, }, ) @@ -349,3 +359,7 @@ class SDKInfo(TypedDict): ) HttpStatusCodeRange = Union[int, Container[int]] + + class TextPart(TypedDict): + type: Literal["text"] + content: str diff --git a/sentry_sdk/ai/__init__.py b/sentry_sdk/ai/__init__.py index fbcb9c061d..7f0d9f92f7 100644 --- a/sentry_sdk/ai/__init__.py +++ b/sentry_sdk/ai/__init__.py @@ -4,4 +4,5 @@ GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING, normalize_message_role, normalize_message_roles, + set_conversation_id, ) # noqa: F401 diff --git a/sentry_sdk/ai/_openai_completions_api.py b/sentry_sdk/ai/_openai_completions_api.py new file mode 100644 index 0000000000..8150925b21 --- /dev/null +++ b/sentry_sdk/ai/_openai_completions_api.py @@ -0,0 +1,48 @@ +from collections.abc import Iterable + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sentry_sdk._types import TextPart + + from openai.types.chat import ( + ChatCompletionMessageParam, + ChatCompletionSystemMessageParam, + ) + + +def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool: + return isinstance(message, dict) and message.get("role") == "system" + + +def _get_system_instructions( + messages: "Iterable[ChatCompletionMessageParam]", +) -> "list[ChatCompletionMessageParam]": + if not isinstance(messages, Iterable): + return [] + + return [message for message in messages if _is_system_instruction(message)] + + +def _transform_system_instructions( + system_instructions: "list[ChatCompletionSystemMessageParam]", +) -> "list[TextPart]": + instruction_text_parts: "list[TextPart]" = [] + + for instruction in system_instructions: + if not isinstance(instruction, dict): + continue + + content = instruction.get("content") + + if isinstance(content, str): + instruction_text_parts.append({"type": "text", "content": content}) + + elif isinstance(content, list): + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + text = part.get("text", None) + if text is not None: + instruction_text_parts.append({"type": "text", "content": text}) + + return instruction_text_parts diff --git a/sentry_sdk/ai/_openai_responses_api.py b/sentry_sdk/ai/_openai_responses_api.py new file mode 100644 index 0000000000..50fddf1d2f --- /dev/null +++ b/sentry_sdk/ai/_openai_responses_api.py @@ -0,0 +1,22 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + + from openai.types.responses import ResponseInputParam, ResponseInputItemParam + + +def _is_system_instruction(message: "ResponseInputItemParam") -> bool: + if not isinstance(message, dict) or not message.get("role") == "system": + return False + + return "type" not in message or message["type"] == "message" + + +def _get_system_instructions( + messages: "Union[str, ResponseInputParam]", +) -> "list[ResponseInputItemParam]": + if not isinstance(messages, list): + return [] + + return [message for message in messages if _is_system_instruction(message)] diff --git a/sentry_sdk/ai/monitoring.py b/sentry_sdk/ai/monitoring.py index e7e00ad462..581e967bd4 100644 --- a/sentry_sdk/ai/monitoring.py +++ b/sentry_sdk/ai/monitoring.py @@ -1,11 +1,12 @@ import inspect +import sys from functools import wraps from sentry_sdk.consts import SPANDATA import sentry_sdk.utils from sentry_sdk import start_span from sentry_sdk.tracing import Span -from sentry_sdk.utils import ContextVar +from sentry_sdk.utils import ContextVar, reraise, capture_internal_exceptions from typing import TYPE_CHECKING @@ -44,13 +45,15 @@ def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": try: res = f(*args, **kwargs) except Exception as e: - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "ai_monitoring", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - raise e from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + event, hint = sentry_sdk.utils.event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "ai_monitoring", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + reraise(*exc_info) finally: _ai_pipeline_name.set(None) return res @@ -72,13 +75,15 @@ async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": try: res = await f(*args, **kwargs) except Exception as e: - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "ai_monitoring", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - raise e from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + event, hint = sentry_sdk.utils.event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "ai_monitoring", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + reraise(*exc_info) finally: _ai_pipeline_name.set(None) return res @@ -95,6 +100,7 @@ def record_token_usage( span: "Span", input_tokens: "Optional[int]" = None, input_tokens_cached: "Optional[int]" = None, + input_tokens_cache_write: "Optional[int]" = None, output_tokens: "Optional[int]" = None, output_tokens_reasoning: "Optional[int]" = None, total_tokens: "Optional[int]" = None, @@ -113,6 +119,12 @@ def record_token_usage( input_tokens_cached, ) + if input_tokens_cache_write is not None: + span.set_data( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, + input_tokens_cache_write, + ) + if output_tokens is not None: span.set_data(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) diff --git a/sentry_sdk/ai/utils.py b/sentry_sdk/ai/utils.py index 1d2b4483c9..684b7323da 100644 --- a/sentry_sdk/ai/utils.py +++ b/sentry_sdk/ai/utils.py @@ -5,6 +5,8 @@ from sys import getsizeof from typing import TYPE_CHECKING +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE + if TYPE_CHECKING: from typing import Any, Callable, Dict, List, Optional, Tuple @@ -12,6 +14,7 @@ import sentry_sdk from sentry_sdk.utils import logger +from sentry_sdk.consts import SPANDATA MAX_GEN_AI_MESSAGE_BYTES = 20_000 # 20KB # Maximum characters when only a single message is left after bytes truncation @@ -38,6 +41,429 @@ class GEN_AI_ALLOWED_MESSAGE_ROLES: GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role +def parse_data_uri(url: str) -> "Tuple[str, str]": + """ + Parse a data URI and return (mime_type, content). + + Data URI format (RFC 2397): data:[][;base64], + + Examples: + data:image/jpeg;base64,/9j/4AAQ... → ("image/jpeg", "/9j/4AAQ...") + data:text/plain,Hello → ("text/plain", "Hello") + data:;base64,SGVsbG8= → ("", "SGVsbG8=") + + Raises: + ValueError: If the URL is not a valid data URI (missing comma separator) + """ + if "," not in url: + raise ValueError("Invalid data URI: missing comma separator") + + header, content = url.split(",", 1) + + # Extract mime type from header + # Format: "data:[;param1][;param2]..." e.g. "data:image/jpeg;base64" + # Remove "data:" prefix, then take everything before the first semicolon + if header.startswith("data:"): + mime_part = header[5:] # Remove "data:" prefix + else: + mime_part = header + + mime_type = mime_part.split(";")[0] + + return mime_type, content + + +def get_modality_from_mime_type(mime_type: str) -> str: + """ + Infer the content modality from a MIME type string. + + Args: + mime_type: A MIME type string (e.g., "image/jpeg", "audio/mp3") + + Returns: + One of: "image", "audio", "video", or "document" + Defaults to "image" for unknown or empty MIME types. + + Examples: + "image/jpeg" -> "image" + "audio/mp3" -> "audio" + "video/mp4" -> "video" + "application/pdf" -> "document" + "text/plain" -> "document" + """ + if not mime_type: + return "image" # Default fallback + + mime_lower = mime_type.lower() + if mime_lower.startswith("image/"): + return "image" + elif mime_lower.startswith("audio/"): + return "audio" + elif mime_lower.startswith("video/"): + return "video" + elif mime_lower.startswith("application/") or mime_lower.startswith("text/"): + return "document" + else: + return "image" # Default fallback for unknown types + + +def transform_openai_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform an OpenAI/LiteLLM content part to Sentry's standardized format. + + This handles the OpenAI image_url format used by OpenAI and LiteLLM SDKs. + + Input format: + - {"type": "image_url", "image_url": {"url": "..."}} + - {"type": "image_url", "image_url": "..."} (string shorthand) + + Output format (one of): + - {"type": "blob", "modality": "image", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "image", "mime_type": "", "uri": "..."} + + Args: + content_part: A dictionary representing a content part from OpenAI/LiteLLM + + Returns: + A transformed dictionary in standardized format, or None if the format + is not OpenAI image_url format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type != "image_url": + return None + + image_url_data = content_part.get("image_url") + if isinstance(image_url_data, str): + url = image_url_data + elif isinstance(image_url_data, dict): + url = image_url_data.get("url", "") + else: + return None + + if not url: + return None + + # Check if it's a data URI (base64 encoded) + if url.startswith("data:"): + try: + mime_type, content = parse_data_uri(url) + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": content, + } + except ValueError: + # If parsing fails, return as URI + return { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": url, + } + else: + # Regular URL + return { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": url, + } + + +def transform_anthropic_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform an Anthropic content part to Sentry's standardized format. + + This handles the Anthropic image and document formats with source dictionaries. + + Input format: + - {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} + - {"type": "image", "source": {"type": "url", "media_type": "...", "url": "..."}} + - {"type": "image", "source": {"type": "file", "media_type": "...", "file_id": "..."}} + - {"type": "document", "source": {...}} (same source formats) + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part from Anthropic + + Returns: + A transformed dictionary in standardized format, or None if the format + is not Anthropic format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type not in ("image", "document") or "source" not in content_part: + return None + + source = content_part.get("source") + if not isinstance(source, dict): + return None + + source_type = source.get("type") + media_type = source.get("media_type", "") + modality = ( + "document" + if block_type == "document" + else get_modality_from_mime_type(media_type) + ) + + if source_type == "base64": + return { + "type": "blob", + "modality": modality, + "mime_type": media_type, + "content": source.get("data", ""), + } + elif source_type == "url": + return { + "type": "uri", + "modality": modality, + "mime_type": media_type, + "uri": source.get("url", ""), + } + elif source_type == "file": + return { + "type": "file", + "modality": modality, + "mime_type": media_type, + "file_id": source.get("file_id", ""), + } + + return None + + +def transform_google_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a Google GenAI content part to Sentry's standardized format. + + This handles the Google GenAI inline_data and file_data formats. + + Input format: + - {"inline_data": {"mime_type": "...", "data": "..."}} + - {"file_data": {"mime_type": "...", "file_uri": "..."}} + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + + Args: + content_part: A dictionary representing a content part from Google GenAI + + Returns: + A transformed dictionary in standardized format, or None if the format + is not Google format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + # Handle Google inline_data format + if "inline_data" in content_part: + inline_data = content_part.get("inline_data") + if isinstance(inline_data, dict): + mime_type = inline_data.get("mime_type", "") + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": inline_data.get("data", ""), + } + return None + + # Handle Google file_data format + if "file_data" in content_part: + file_data = content_part.get("file_data") + if isinstance(file_data, dict): + mime_type = file_data.get("mime_type", "") + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_data.get("file_uri", ""), + } + return None + + return None + + +def transform_generic_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a generic/LangChain-style content part to Sentry's standardized format. + + This handles generic formats where the type indicates the modality and + the data is provided via direct base64, url, or file_id fields. + + Input format: + - {"type": "image", "base64": "...", "mime_type": "..."} + - {"type": "audio", "url": "...", "mime_type": "..."} + - {"type": "video", "base64": "...", "mime_type": "..."} + - {"type": "file", "file_id": "...", "mime_type": "..."} + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part in generic format + + Returns: + A transformed dictionary in standardized format, or None if the format + is not generic format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type not in ("image", "audio", "video", "file"): + return None + + # Ensure it's not Anthropic format (which also uses type: "image") + if "source" in content_part: + return None + + mime_type = content_part.get("mime_type", "") + modality = block_type if block_type != "file" else "document" + + # Check for base64 encoded content + if "base64" in content_part: + return { + "type": "blob", + "modality": modality, + "mime_type": mime_type, + "content": content_part.get("base64", ""), + } + # Check for URL reference + elif "url" in content_part: + return { + "type": "uri", + "modality": modality, + "mime_type": mime_type, + "uri": content_part.get("url", ""), + } + # Check for file_id reference + elif "file_id" in content_part: + return { + "type": "file", + "modality": modality, + "mime_type": mime_type, + "file_id": content_part.get("file_id", ""), + } + + return None + + +def transform_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a content part from various AI SDK formats to Sentry's standardized format. + + This is a heuristic dispatcher that detects the format and delegates to the + appropriate SDK-specific transformer. For direct SDK integration, prefer using + the specific transformers directly: + - transform_openai_content_part() for OpenAI/LiteLLM + - transform_anthropic_content_part() for Anthropic + - transform_google_content_part() for Google GenAI + - transform_generic_content_part() for LangChain and other generic formats + + Detection order: + 1. OpenAI: type == "image_url" + 2. Google: "inline_data" or "file_data" keys present + 3. Anthropic: type in ("image", "document") with "source" key + 4. Generic: type in ("image", "audio", "video", "file") with base64/url/file_id + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part from an AI SDK + + Returns: + A transformed dictionary in standardized format, or None if the format + is unrecognized or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + # Try OpenAI format first (most common, clear indicator) + result = transform_openai_content_part(content_part) + if result is not None: + return result + + # Try Google format (unique keys make it easy to detect) + result = transform_google_content_part(content_part) + if result is not None: + return result + + # Try Anthropic format (has "source" key) + result = transform_anthropic_content_part(content_part) + if result is not None: + return result + + # Try generic format as fallback + result = transform_generic_content_part(content_part) + if result is not None: + return result + + # Unrecognized format + return None + + +def transform_message_content(content: "Any") -> "Any": + """ + Transform message content, handling both string content and list of content blocks. + + For list content, each item is transformed using transform_content_part(). + Items that cannot be transformed (return None) are kept as-is. + + Args: + content: Message content - can be a string, list of content blocks, or other + + Returns: + - String content: returned as-is + - List content: list with each transformable item converted to standardized format + - Other: returned as-is + """ + if isinstance(content, str): + return content + + if isinstance(content, (list, tuple)): + transformed = [] + for item in content: + if isinstance(item, dict): + result = transform_content_part(item) + # If transformation succeeded, use the result; otherwise keep original + transformed.append(result if result is not None else item) + else: + transformed.append(item) + return transformed + + return content + + def _normalize_data(data: "Any", unpack: bool = True) -> "Any": # convert pydantic data (e.g. OpenAI v1+) to json compatible format if hasattr(data, "model_dump"): @@ -141,6 +567,85 @@ def _find_truncation_index(messages: "List[Dict[str, Any]]", max_bytes: int) -> return 0 +def redact_blob_message_parts( + messages: "List[Dict[str, Any]]", +) -> "List[Dict[str, Any]]": + """ + Redact blob message parts from the messages by replacing blob content with "[Filtered]". + + This function creates a deep copy of messages that contain blob content to avoid + mutating the original message dictionaries. Messages without blob content are + returned as-is to minimize copying overhead. + + e.g: + { + "role": "user", + "content": [ + { + "text": "How many ponies do you see in the image?", + "type": "text" + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "data:image/jpeg;base64,..." + } + ] + } + becomes: + { + "role": "user", + "content": [ + { + "text": "How many ponies do you see in the image?", + "type": "text" + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "[Filtered]" + } + ] + } + """ + + # First pass: check if any message contains blob content + has_blobs = False + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if isinstance(content, list): + for item in content: + if isinstance(item, dict) and item.get("type") == "blob": + has_blobs = True + break + if has_blobs: + break + + # If no blobs found, return original messages to avoid unnecessary copying + if not has_blobs: + return messages + + # Deep copy messages to avoid mutating the original + messages_copy = deepcopy(messages) + + # Second pass: redact blob content in the copy + for message in messages_copy: + if not isinstance(message, dict): + continue + + content = message.get("content") + if isinstance(content, list): + for item in content: + if isinstance(item, dict) and item.get("type") == "blob": + item["content"] = BLOB_DATA_SUBSTITUTE + + return messages_copy + + def truncate_messages_by_size( messages: "List[Dict[str, Any]]", max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, @@ -178,6 +683,28 @@ def truncate_messages_by_size( def truncate_and_annotate_messages( + messages: "Optional[List[Dict[str, Any]]]", + span: "Any", + scope: "Any", + max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, +) -> "Optional[List[Dict[str, Any]]]": + if not messages: + return None + + messages = redact_blob_message_parts(messages) + + truncated_message = _truncate_single_message_content_if_present( + deepcopy(messages[-1]), max_chars=max_single_message_chars + ) + if len(messages) > 1: + scope._gen_ai_original_message_count[span.span_id] = len(messages) + + span.set_data(SPANDATA.META_GEN_AI_ORIGINAL_INPUT_MESSAGES_LENGTH, len(messages)) + + return [truncated_message] + + +def truncate_and_annotate_embedding_inputs( messages: "Optional[List[Dict[str, Any]]]", span: "Any", scope: "Any", @@ -186,8 +713,18 @@ def truncate_and_annotate_messages( if not messages: return None + messages = redact_blob_message_parts(messages) + truncated_messages, removed_count = truncate_messages_by_size(messages, max_bytes) if removed_count > 0: scope._gen_ai_original_message_count[span.span_id] = len(messages) return truncated_messages + + +def set_conversation_id(conversation_id: str) -> None: + """ + Set the conversation_id in the scope. + """ + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id(conversation_id) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 259196d1c6..f540a49f35 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -11,6 +11,7 @@ import sentry_sdk from sentry_sdk._compat import PY37, check_uwsgi_thread_support from sentry_sdk._metrics_batcher import MetricsBatcher +from sentry_sdk._span_batcher import SpanBatcher from sentry_sdk.utils import ( AnnotatedValue, ContextVar, @@ -31,6 +32,7 @@ ) from sentry_sdk.serializer import serialize from sentry_sdk.tracing import trace +from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.transport import BaseHttpTransport, make_transport from sentry_sdk.consts import ( SPANDATA, @@ -67,6 +69,7 @@ from sentry_sdk.scope import Scope from sentry_sdk.session import Session from sentry_sdk.spotlight import SpotlightClient + from sentry_sdk.traces import StreamedSpan from sentry_sdk.transport import Transport, Item from sentry_sdk._log_batcher import LogBatcher from sentry_sdk._metrics_batcher import MetricsBatcher @@ -188,6 +191,8 @@ def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None: self.monitor: "Optional[Monitor]" = None self.log_batcher: "Optional[LogBatcher]" = None self.metrics_batcher: "Optional[MetricsBatcher]" = None + self.span_batcher: "Optional[SpanBatcher]" = None + self.integrations: "dict[str, Integration]" = {} def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any": return {"options": {}} @@ -223,6 +228,9 @@ def _capture_log(self, log: "Log", scope: "Scope") -> None: def _capture_metric(self, metric: "Metric", scope: "Scope") -> None: pass + def _capture_span(self, span: "StreamedSpan", scope: "Scope") -> None: + pass + def capture_session(self, *args: "Any", **kwargs: "Any") -> None: return None @@ -398,6 +406,13 @@ def _record_lost_event( record_lost_func=_record_lost_event, ) + self.span_batcher = None + if has_span_streaming_enabled(self.options): + self.span_batcher = SpanBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + max_request_body_size = ("always", "never", "small", "medium") if self.options["max_request_body_size"] not in max_request_body_size: raise ValueError( @@ -456,6 +471,8 @@ def _record_lost_event( if ( self.monitor or self.log_batcher + or self.metrics_batcher + or self.span_batcher or has_profiling_enabled(self.options) or isinstance(self.transport, BaseHttpTransport) ): @@ -526,12 +543,21 @@ def _prepare_event( spans_delta = spans_before - len( cast(List[Dict[str, object]], event.get("spans", [])) ) - if is_transaction and spans_delta > 0 and self.transport is not None: - self.transport.record_lost_event( - "event_processor", data_category="span", quantity=spans_delta - ) + span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0) - dropped_spans: int = event.pop("_dropped_spans", 0) + spans_delta + if is_transaction and self.transport is not None: + if spans_delta > 0: + self.transport.record_lost_event( + "event_processor", data_category="span", quantity=spans_delta + ) + if span_recorder_dropped_spans > 0: + self.transport.record_lost_event( + "buffer_overflow", + data_category="span", + quantity=span_recorder_dropped_spans, + ) + + dropped_spans: int = span_recorder_dropped_spans + spans_delta if dropped_spans > 0: previous_total_spans = spans_before + dropped_spans if scope._n_breadcrumbs_truncated > 0: @@ -899,7 +925,10 @@ def capture_event( return return_value def _capture_telemetry( - self, telemetry: "Optional[Union[Log, Metric]]", ty: str, scope: "Scope" + self, + telemetry: "Optional[Union[Log, Metric, StreamedSpan]]", + ty: str, + scope: "Scope", ) -> None: # Capture attributes-based telemetry (logs, metrics, spansV2) if telemetry is None: @@ -924,6 +953,8 @@ def _capture_telemetry( batcher = self.log_batcher elif ty == "metric": batcher = self.metrics_batcher # type: ignore + elif ty == "span": + batcher = self.span_batcher # type: ignore if batcher is not None: batcher.add(telemetry) # type: ignore @@ -934,6 +965,9 @@ def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None: def _capture_metric(self, metric: "Optional[Metric]", scope: "Scope") -> None: self._capture_telemetry(metric, "metric", scope) + def _capture_span(self, span: "Optional[StreamedSpan]", scope: "Scope") -> None: + self._capture_telemetry(span, "span", scope) + def capture_session( self, session: "Session", @@ -983,6 +1017,8 @@ def close( self.log_batcher.kill() if self.metrics_batcher is not None: self.metrics_batcher.kill() + if self.span_batcher is not None: + self.span_batcher.kill() if self.monitor: self.monitor.kill() self.transport.kill() @@ -1008,6 +1044,8 @@ def flush( self.log_batcher.flush() if self.metrics_batcher is not None: self.metrics_batcher.flush() + if self.span_batcher is not None: + self.span_batcher.flush() self.transport.flush(timeout=timeout, callback=callback) def __enter__(self) -> "_Client": diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 78c3a2912f..f294414edf 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -82,6 +82,7 @@ class CompressionAlgo(Enum): "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]], "enable_metrics": Optional[bool], "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]], + "trace_lifecycle": Optional[Literal["static", "stream"]], }, total=False, ) @@ -518,6 +519,12 @@ class SPANDATA: Example: ["The weather in Paris is rainy and overcast, with temperatures around 57°F", "The weather in London is sunny and warm, with temperatures around 65°F"] """ + GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token" + """ + The time it took to receive the first token from the model. + Example: 0.1 + """ + GEN_AI_RESPONSE_TOOL_CALLS = "gen_ai.response.tool_calls" """ The tool calls in the model's response. @@ -542,6 +549,12 @@ class SPANDATA: Example: 2048 """ + GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" + """ + The system instructions passed to the model. + Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}] + """ + GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages" """ The messages passed to the model. The "content" can be a string or an array of objects. @@ -632,6 +645,12 @@ class SPANDATA: Example: 50 """ + GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE = "gen_ai.usage.input_tokens.cache_write" + """ + The number of tokens written to the cache when processing the AI input (prompt). + Example: 100 + """ + GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" """ The number of tokens in the output. @@ -851,6 +870,14 @@ class SPANDATA: Example: "a1b2c3d4e5f6" """ + META_GEN_AI_ORIGINAL_INPUT_MESSAGES_LENGTH = ( + "sentry.sdk_meta.gen_ai.input.messages.original_length" + ) + """ + The original number of input non-system instruction messages, before SDK trimming. + Example: 4 + """ + class SPANSTATUS: """ @@ -1465,4 +1492,4 @@ def _get_default_options() -> "dict[str, Any]": del _get_default_options -VERSION = "2.48.0" +VERSION = "2.51.0" diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 5ab181df25..dd12a6011f 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -81,6 +81,7 @@ def iter_default_integrations( "sentry_sdk.integrations.fastapi.FastApiIntegration", "sentry_sdk.integrations.flask.FlaskIntegration", "sentry_sdk.integrations.gql.GQLIntegration", + "sentry_sdk.integrations.google_genai.GoogleGenAIIntegration", "sentry_sdk.integrations.graphene.GrapheneIntegration", "sentry_sdk.integrations.httpx.HttpxIntegration", "sentry_sdk.integrations.huey.HueyIntegration", @@ -148,6 +149,7 @@ def iter_default_integrations( "openai_agents": (0, 0, 19), "openfeature": (0, 7, 1), "pydantic_ai": (1, 0, 0), + "pymongo": (3, 5, 0), "quart": (0, 16, 0), "ray": (2, 7, 0), "requests": (2, 0, 0), @@ -165,7 +167,7 @@ def iter_default_integrations( _INTEGRATION_DEACTIVATES = { - "langchain": {"openai", "anthropic"}, + "langchain": {"openai", "anthropic", "google_genai"}, "openai_agents": {"openai"}, "pydantic_ai": {"openai", "anthropic"}, } diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 5257e3bf60..74ac71a399 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -1,3 +1,5 @@ +import sys +import json from collections.abc import Iterable from functools import wraps from typing import TYPE_CHECKING @@ -10,6 +12,7 @@ normalize_message_roles, truncate_and_annotate_messages, get_start_span_function, + transform_anthropic_content_part, ) from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration @@ -20,6 +23,7 @@ event_from_exception, package_version, safe_serialize, + reraise, ) try: @@ -36,13 +40,14 @@ from anthropic.resources import AsyncMessages, Messages if TYPE_CHECKING: - from anthropic.types import MessageStreamEvent + from anthropic.types import MessageStreamEvent, TextBlockParam except ImportError: raise DidNotEnable("Anthropic not installed") if TYPE_CHECKING: from typing import Any, AsyncIterator, Iterator, List, Optional, Union from sentry_sdk.tracing import Span + from sentry_sdk._types import TextPart class AnthropicIntegration(Integration): @@ -72,20 +77,36 @@ def _capture_exception(exc: "Any") -> None: sentry_sdk.capture_event(event, hint=hint) -def _get_token_usage(result: "Messages") -> "tuple[int, int]": +def _get_token_usage(result: "Messages") -> "tuple[int, int, int, int]": """ Get token usage from the Anthropic response. + Returns: (input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens) """ input_tokens = 0 output_tokens = 0 + cache_read_input_tokens = 0 + cache_write_input_tokens = 0 if hasattr(result, "usage"): usage = result.usage if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): input_tokens = usage.input_tokens if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): output_tokens = usage.output_tokens - - return input_tokens, output_tokens + if hasattr(usage, "cache_read_input_tokens") and isinstance( + usage.cache_read_input_tokens, int + ): + cache_read_input_tokens = usage.cache_read_input_tokens + if hasattr(usage, "cache_creation_input_tokens") and isinstance( + usage.cache_creation_input_tokens, int + ): + cache_write_input_tokens = usage.cache_creation_input_tokens + + return ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) def _collect_ai_data( @@ -93,8 +114,10 @@ def _collect_ai_data( model: "str | None", input_tokens: int, output_tokens: int, + cache_read_input_tokens: int, + cache_write_input_tokens: int, content_blocks: "list[str]", -) -> "tuple[str | None, int, int, list[str]]": +) -> "tuple[str | None, int, int, int, int, list[str]]": """ Collect model information, token usage, and collect content blocks from the AI streaming response. """ @@ -104,6 +127,14 @@ def _collect_ai_data( usage = event.message.usage input_tokens += usage.input_tokens output_tokens += usage.output_tokens + if hasattr(usage, "cache_read_input_tokens") and isinstance( + usage.cache_read_input_tokens, int + ): + cache_read_input_tokens += usage.cache_read_input_tokens + if hasattr(usage, "cache_creation_input_tokens") and isinstance( + usage.cache_creation_input_tokens, int + ): + cache_write_input_tokens += usage.cache_creation_input_tokens model = event.message.model or model elif event.type == "content_block_start": pass @@ -117,7 +148,56 @@ def _collect_ai_data( elif event.type == "message_delta": output_tokens += event.usage.output_tokens - return model, input_tokens, output_tokens, content_blocks + return ( + model, + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + content_blocks, + ) + + +def _transform_anthropic_content_block( + content_block: "dict[str, Any]", +) -> "dict[str, Any]": + """ + Transform an Anthropic content block using the Anthropic-specific transformer, + with special handling for Anthropic's text-type documents. + """ + # Handle Anthropic's text-type documents specially (not covered by shared function) + if content_block.get("type") == "document": + source = content_block.get("source") + if isinstance(source, dict) and source.get("type") == "text": + return { + "type": "text", + "text": source.get("data", ""), + } + + # Use Anthropic-specific transformation + result = transform_anthropic_content_part(content_block) + return result if result is not None else content_block + + +def _transform_system_instructions( + system_instructions: "Union[str, Iterable[TextBlockParam]]", +) -> "list[TextPart]": + if isinstance(system_instructions, str): + return [ + { + "type": "text", + "content": system_instructions, + } + ] + + return [ + { + "type": "text", + "content": instruction["text"], + } + for instruction in system_instructions + if isinstance(instruction, dict) and "text" in instruction + ] def _set_input_data( @@ -127,7 +207,7 @@ def _set_input_data( Set input data for the span based on the provided keyword arguments for the anthropic message creation. """ set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - system_prompt = kwargs.get("system") + system_instructions: "Union[str, Iterable[TextBlockParam]]" = kwargs.get("system") # type: ignore messages = kwargs.get("messages") if ( messages is not None @@ -135,48 +215,56 @@ def _set_input_data( and should_send_default_pii() and integration.include_prompts ): - normalized_messages = [] - if system_prompt: - system_prompt_content: "Optional[Union[str, List[dict[str, Any]]]]" = None - if isinstance(system_prompt, str): - system_prompt_content = system_prompt - elif isinstance(system_prompt, Iterable): - system_prompt_content = [] - for item in system_prompt: - if ( - isinstance(item, dict) - and item.get("type") == "text" - and item.get("text") - ): - system_prompt_content.append(item.copy()) - - if system_prompt_content: - normalized_messages.append( - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM, - "content": system_prompt_content, - } - ) + if isinstance(system_instructions, str) or isinstance( + system_instructions, Iterable + ): + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + normalized_messages = [] for message in messages: if ( message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER and "content" in message and isinstance(message["content"], (list, tuple)) ): + transformed_content = [] for item in message["content"]: - if item.get("type") == "tool_result": - normalized_messages.append( - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, - "content": { # type: ignore[dict-item] - "tool_use_id": item.get("tool_use_id"), - "output": item.get("content"), - }, - } - ) + # Skip tool_result items - they can contain images/documents + # with nested structures that are difficult to redact properly + if isinstance(item, dict) and item.get("type") == "tool_result": + continue + + # Transform content blocks (images, documents, etc.) + transformed_content.append( + _transform_anthropic_content_block(item) + if isinstance(item, dict) + else item + ) + + # If there are non-tool-result items, add them as a message + if transformed_content: + normalized_messages.append( + { + "role": message.get("role"), + "content": transformed_content, + } + ) else: - normalized_messages.append(message) + # Transform content for non-list messages or assistant messages + transformed_message = message.copy() + if "content" in transformed_message: + content = transformed_message["content"] + if isinstance(content, (list, tuple)): + transformed_message["content"] = [ + _transform_anthropic_content_block(item) + if isinstance(item, dict) + else item + for item in content + ] + normalized_messages.append(transformed_message) role_normalized_messages = normalize_message_roles(normalized_messages) scope = sentry_sdk.get_current_scope() @@ -219,6 +307,8 @@ def _set_output_data( model: "str | None", input_tokens: "int | None", output_tokens: "int | None", + cache_read_input_tokens: "int | None", + cache_write_input_tokens: "int | None", content_blocks: "list[Any]", finish_span: bool = False, ) -> None: @@ -254,6 +344,8 @@ def _set_output_data( span, input_tokens=input_tokens, output_tokens=output_tokens, + input_tokens_cached=cache_read_input_tokens, + input_tokens_cache_write=cache_write_input_tokens, ) if finish_span: @@ -288,7 +380,12 @@ def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A with capture_internal_exceptions(): if hasattr(result, "content"): - input_tokens, output_tokens = _get_token_usage(result) + ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) = _get_token_usage(result) content_blocks = [] for content_block in result.content: @@ -305,6 +402,8 @@ def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A model=getattr(result, "model", None), input_tokens=input_tokens, output_tokens=output_tokens, + cache_read_input_tokens=cache_read_input_tokens, + cache_write_input_tokens=cache_write_input_tokens, content_blocks=content_blocks, finish_span=True, ) @@ -317,13 +416,26 @@ def new_iterator() -> "Iterator[MessageStreamEvent]": model = None input_tokens = 0 output_tokens = 0 + cache_read_input_tokens = 0 + cache_write_input_tokens = 0 content_blocks: "list[str]" = [] for event in old_iterator: - model, input_tokens, output_tokens, content_blocks = ( - _collect_ai_data( - event, model, input_tokens, output_tokens, content_blocks - ) + ( + model, + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + content_blocks, + ) = _collect_ai_data( + event, + model, + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + content_blocks, ) yield event @@ -333,6 +445,8 @@ def new_iterator() -> "Iterator[MessageStreamEvent]": model=model, input_tokens=input_tokens, output_tokens=output_tokens, + cache_read_input_tokens=cache_read_input_tokens, + cache_write_input_tokens=cache_write_input_tokens, content_blocks=[{"text": "".join(content_blocks), "type": "text"}], finish_span=True, ) @@ -341,13 +455,26 @@ async def new_iterator_async() -> "AsyncIterator[MessageStreamEvent]": model = None input_tokens = 0 output_tokens = 0 + cache_read_input_tokens = 0 + cache_write_input_tokens = 0 content_blocks: "list[str]" = [] async for event in old_iterator: - model, input_tokens, output_tokens, content_blocks = ( - _collect_ai_data( - event, model, input_tokens, output_tokens, content_blocks - ) + ( + model, + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + content_blocks, + ) = _collect_ai_data( + event, + model, + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + content_blocks, ) yield event @@ -357,6 +484,8 @@ async def new_iterator_async() -> "AsyncIterator[MessageStreamEvent]": model=model, input_tokens=input_tokens, output_tokens=output_tokens, + cache_read_input_tokens=cache_read_input_tokens, + cache_write_input_tokens=cache_write_input_tokens, content_blocks=[{"text": "".join(content_blocks), "type": "text"}], finish_span=True, ) @@ -386,8 +515,10 @@ def _execute_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": try: result = f(*args, **kwargs) except Exception as exc: - _capture_exception(exc) - raise exc from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) return gen.send(result) except StopIteration as e: @@ -422,8 +553,10 @@ async def _execute_async(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": try: result = await f(*args, **kwargs) except Exception as exc: - _capture_exception(exc) - raise exc from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) return gen.send(result) except StopIteration as e: diff --git a/sentry_sdk/integrations/arq.py b/sentry_sdk/integrations/arq.py index ee8aa393cf..35c0a9c6f0 100644 --- a/sentry_sdk/integrations/arq.py +++ b/sentry_sdk/integrations/arq.py @@ -185,7 +185,7 @@ def patch_create_worker() -> None: @ensure_integration_enabled(ArqIntegration, old_create_worker) def _sentry_create_worker(*args: "Any", **kwargs: "Any") -> "Worker": - settings_cls = args[0] + settings_cls = args[0] if args else kwargs.get("settings_cls") if isinstance(settings_cls, dict): if "functions" in settings_cls: @@ -200,13 +200,14 @@ def _sentry_create_worker(*args: "Any", **kwargs: "Any") -> "Worker": ] if hasattr(settings_cls, "functions"): - settings_cls.functions = [ - _get_arq_function(func) for func in settings_cls.functions + settings_cls.functions = [ # type: ignore[union-attr] + _get_arq_function(func) + for func in settings_cls.functions # type: ignore[union-attr] ] if hasattr(settings_cls, "cron_jobs"): - settings_cls.cron_jobs = [ + settings_cls.cron_jobs = [ # type: ignore[union-attr] _get_arq_cron_job(cron_job) - for cron_job in (settings_cls.cron_jobs or []) + for cron_job in (settings_cls.cron_jobs or []) # type: ignore[union-attr] ] if "functions" in kwargs: diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index 39c7e3f879..b7aa0a7202 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -4,6 +4,7 @@ import sentry_sdk from sentry_sdk.consts import OP from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations._wsgi_common import nullcontext from sentry_sdk.utils import event_from_exception, logger, reraise try: @@ -47,6 +48,10 @@ def patch_asyncio() -> None: loop = asyncio.get_running_loop() orig_task_factory = loop.get_task_factory() + # Check if already patched + if getattr(orig_task_factory, "_is_sentry_task_factory", False): + return + def _sentry_task_factory( loop: "asyncio.AbstractEventLoop", coro: "Coroutine[Any, Any, Any]", @@ -56,11 +61,20 @@ def _sentry_task_factory( async def _task_with_sentry_span_creation() -> "Any": result = None + integration = sentry_sdk.get_client().get_integration( + AsyncioIntegration + ) + task_spans = integration.task_spans if integration else False + with sentry_sdk.isolation_scope(): - with sentry_sdk.start_span( - op=OP.FUNCTION, - name=get_name(coro), - origin=AsyncioIntegration.origin, + with ( + sentry_sdk.start_span( + op=OP.FUNCTION, + name=get_name(coro), + origin=AsyncioIntegration.origin, + ) + if task_spans + else nullcontext() ): try: result = await coro @@ -102,6 +116,7 @@ async def _task_with_sentry_span_creation() -> "Any": return task + _sentry_task_factory._is_sentry_task_factory = True # type: ignore loop.set_task_factory(_sentry_task_factory) # type: ignore except RuntimeError: @@ -135,6 +150,54 @@ class AsyncioIntegration(Integration): identifier = "asyncio" origin = f"auto.function.{identifier}" + def __init__(self, task_spans: bool = True) -> None: + self.task_spans = task_spans + @staticmethod def setup_once() -> None: patch_asyncio() + + +def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: + """ + Enable AsyncioIntegration with the provided options. + + This is useful in scenarios where Sentry needs to be initialized before + an event loop is set up, but you still want to instrument asyncio once there + is an event loop. In that case, you can sentry_sdk.init() early on without + the AsyncioIntegration and then, once the event loop has been set up, + execute: + + ```python + from sentry_sdk.integrations.asyncio import enable_asyncio_integration + + async def async_entrypoint(): + enable_asyncio_integration() + ``` + + Any arguments provided will be passed to AsyncioIntegration() as is. + + If AsyncioIntegration has already patched the current event loop, this + function won't have any effect. + + If AsyncioIntegration was provided in + sentry_sdk.init(disabled_integrations=[...]), this function will ignore that + and the integration will be enabled. + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + # This function purposefully bypasses the integration machinery in + # integrations/__init__.py. _installed_integrations/_processed_integrations + # is used to prevent double patching the same module, but in the case of + # the AsyncioIntegration, we don't monkeypatch the standard library directly, + # we patch the currently running event loop, and we keep the record of doing + # that on the loop itself. + logger.debug("Setting up integration asyncio") + + integration = AsyncioIntegration(*args, **kwargs) + integration.setup_once() + + if "asyncio" not in client.integrations: + client.integrations["asyncio"] = integration diff --git a/sentry_sdk/integrations/clickhouse_driver.py b/sentry_sdk/integrations/clickhouse_driver.py index b4cc2860e7..7bbea94210 100644 --- a/sentry_sdk/integrations/clickhouse_driver.py +++ b/sentry_sdk/integrations/clickhouse_driver.py @@ -30,7 +30,9 @@ def __getitem__(self, _): try: - import clickhouse_driver # type: ignore[import-not-found] + from clickhouse_driver import VERSION # type: ignore[import-not-found] + from clickhouse_driver.client import Client # type: ignore[import-not-found] + from clickhouse_driver.connection import Connection # type: ignore[import-not-found] except ImportError: raise DidNotEnable("clickhouse-driver not installed.") @@ -42,29 +44,23 @@ class ClickhouseDriverIntegration(Integration): @staticmethod def setup_once() -> None: - _check_minimum_version(ClickhouseDriverIntegration, clickhouse_driver.VERSION) + _check_minimum_version(ClickhouseDriverIntegration, VERSION) # Every query is done using the Connection's `send_query` function - clickhouse_driver.connection.Connection.send_query = _wrap_start( - clickhouse_driver.connection.Connection.send_query - ) + Connection.send_query = _wrap_start(Connection.send_query) # If the query contains parameters then the send_data function is used to send those parameters to clickhouse _wrap_send_data() # Every query ends either with the Client's `receive_end_of_query` (no result expected) # or its `receive_result` (result expected) - clickhouse_driver.client.Client.receive_end_of_query = _wrap_end( - clickhouse_driver.client.Client.receive_end_of_query - ) - if hasattr(clickhouse_driver.client.Client, "receive_end_of_insert_query"): + Client.receive_end_of_query = _wrap_end(Client.receive_end_of_query) + if hasattr(Client, "receive_end_of_insert_query"): # In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query` - clickhouse_driver.client.Client.receive_end_of_insert_query = _wrap_end( - clickhouse_driver.client.Client.receive_end_of_insert_query + Client.receive_end_of_insert_query = _wrap_end( + Client.receive_end_of_insert_query ) - clickhouse_driver.client.Client.receive_result = _wrap_end( - clickhouse_driver.client.Client.receive_result - ) + Client.receive_result = _wrap_end(Client.receive_result) P = ParamSpec("P") @@ -128,7 +124,7 @@ def _inner_end(*args: "P.args", **kwargs: "P.kwargs") -> "T": def _wrap_send_data() -> None: - original_send_data = clickhouse_driver.client.Client.send_data + original_send_data = Client.send_data def _inner_send_data( # type: ignore[no-untyped-def] # clickhouse-driver does not type send_data self, sample_block, data, types_check=False, columnar=False, *args, **kwargs @@ -164,12 +160,10 @@ def wrapped_generator() -> "Iterator[Any]": self, sample_block, data, types_check, columnar, *args, **kwargs ) - clickhouse_driver.client.Client.send_data = _inner_send_data + Client.send_data = _inner_send_data -def _set_db_data( - span: "Span", connection: "clickhouse_driver.connection.Connection" -) -> None: +def _set_db_data(span: "Span", connection: "Connection") -> None: span.set_data(SPANDATA.DB_SYSTEM, "clickhouse") span.set_data(SPANDATA.SERVER_ADDRESS, connection.host) span.set_data(SPANDATA.SERVER_PORT, connection.port) diff --git a/sentry_sdk/integrations/cohere.py b/sentry_sdk/integrations/cohere.py index bac2ce5655..f45a02f2b5 100644 --- a/sentry_sdk/integrations/cohere.py +++ b/sentry_sdk/integrations/cohere.py @@ -1,3 +1,4 @@ +import sys from functools import wraps from sentry_sdk import consts @@ -16,7 +17,7 @@ import sentry_sdk from sentry_sdk.scope import should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception, reraise try: from cohere.client import Client @@ -151,9 +152,11 @@ def new_chat(*args: "Any", **kwargs: "Any") -> "Any": try: res = f(*args, **kwargs) except Exception as e: - _capture_exception(e) - span.__exit__(None, None, None) - raise e from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + span.__exit__(None, None, None) + reraise(*exc_info) with capture_internal_exceptions(): if should_send_default_pii() and integration.include_prompts: @@ -247,8 +250,10 @@ def new_embed(*args: "Any", **kwargs: "Any") -> "Any": try: res = f(*args, **kwargs) except Exception as e: - _capture_exception(e) - raise e from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + reraise(*exc_info) if ( hasattr(res, "meta") and hasattr(res.meta, "billed_units") diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 0cc4bc4d16..2595c33ea8 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -109,7 +109,7 @@ class DjangoIntegration(Integration): Auto instrument a Django application. :param transaction_style: How to derive transaction names. Either `"function_name"` or `"url"`. Defaults to `"url"`. - :param middleware_spans: Whether to create spans for middleware. Defaults to `True`. + :param middleware_spans: Whether to create spans for middleware. Defaults to `False`. :param signals_spans: Whether to create spans for signals. Defaults to `True`. :param signals_denylist: A list of signals to ignore when creating spans. :param cache_spans: Whether to create spans for cache operations. Defaults to `False`. diff --git a/sentry_sdk/integrations/dramatiq.py b/sentry_sdk/integrations/dramatiq.py index ae87de7525..f954d4fb98 100644 --- a/sentry_sdk/integrations/dramatiq.py +++ b/sentry_sdk/integrations/dramatiq.py @@ -188,6 +188,8 @@ def after_process_message( transaction.__exit__(type(exception), exception, None) scope_manager.__exit__(type(exception), exception, None) + after_skip_message = after_process_message + def _make_message_event_processor( message: "Message[R]", integration: "DramatiqIntegration" diff --git a/sentry_sdk/integrations/google_genai/utils.py b/sentry_sdk/integrations/google_genai/utils.py index 03423c385a..b2d6499843 100644 --- a/sentry_sdk/integrations/google_genai/utils.py +++ b/sentry_sdk/integrations/google_genai/utils.py @@ -1,7 +1,9 @@ import copy +import json import inspect from functools import wraps from .consts import ORIGIN, TOOL_ATTRIBUTES_MAP, GEN_AI_SYSTEM +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE from typing import ( cast, TYPE_CHECKING, @@ -12,6 +14,7 @@ Optional, Union, TypedDict, + Dict, ) import sentry_sdk @@ -19,6 +22,9 @@ set_data_normalized, truncate_and_annotate_messages, normalize_message_roles, + redact_blob_message_parts, + transform_google_content_part, + get_modality_from_mime_type, ) from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.scope import should_send_default_pii @@ -27,16 +33,20 @@ event_from_exception, safe_serialize, ) -from google.genai.types import GenerateContentConfig +from google.genai.types import GenerateContentConfig, Part, Content +from itertools import chain if TYPE_CHECKING: from sentry_sdk.tracing import Span + from sentry_sdk._types import TextPart from google.genai.types import ( GenerateContentResponse, ContentListUnion, + ContentUnionDict, Tool, Model, EmbedContentResponse, + ContentUnion, ) @@ -145,44 +155,326 @@ def get_model_name(model: "Union[str, Model]") -> str: return str(model) -def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]": - """Extract text from contents parameter which can have various formats.""" +def extract_contents_messages(contents: "ContentListUnion") -> "List[Dict[str, Any]]": + """Extract messages from contents parameter which can have various formats. + + Returns a list of message dictionaries in the format: + - System: {"role": "system", "content": "string"} + - User/Assistant: {"role": "user"|"assistant", "content": [{"text": "...", "type": "text"}, ...]} + """ if contents is None: - return None + return [] + + messages = [] - # Simple string case + # Handle string case if isinstance(contents, str): - return contents + return [{"role": "user", "content": contents}] - # List of contents or parts + # Handle list case - process each item (non-recursive, flatten at top level) if isinstance(contents, list): - texts = [] for item in contents: - # Recursively extract text from each item - extracted = extract_contents_text(item) - if extracted: - texts.append(extracted) - return " ".join(texts) if texts else None + item_messages = extract_contents_messages(item) + messages.extend(item_messages) + return messages - # Dictionary case + # Handle dictionary case (ContentDict) if isinstance(contents, dict): - if "text" in contents: - return contents["text"] - # Try to extract from parts if present in dict - if "parts" in contents: - return extract_contents_text(contents["parts"]) + role = contents.get("role", "user") + parts = contents.get("parts") + + if parts: + content_parts = [] + tool_messages = [] + + for part in parts: + part_result = _extract_part_content(part) + if part_result is None: + continue + + if isinstance(part_result, dict) and part_result.get("role") == "tool": + # Tool message - add separately + tool_messages.append(part_result) + else: + # Regular content part + content_parts.append(part_result) + + # Add main message if we have content parts + if content_parts: + # Normalize role: "model" -> "assistant" + normalized_role = "assistant" if role == "model" else role or "user" + messages.append({"role": normalized_role, "content": content_parts}) + + # Add tool messages + messages.extend(tool_messages) + elif "text" in contents: + # Simple text in dict + messages.append( + { + "role": role or "user", + "content": [{"text": contents["text"], "type": "text"}], + } + ) + + return messages - # Content object with parts - recurse into parts - if getattr(contents, "parts", None): - return extract_contents_text(contents.parts) + # Handle Content object + if hasattr(contents, "parts") and contents.parts: + role = getattr(contents, "role", None) or "user" + content_parts = [] + tool_messages = [] - # Direct text attribute - if hasattr(contents, "text"): - return contents.text + for part in contents.parts: + part_result = _extract_part_content(part) + if part_result is None: + continue + + if isinstance(part_result, dict) and part_result.get("role") == "tool": + tool_messages.append(part_result) + else: + content_parts.append(part_result) + + if content_parts: + normalized_role = "assistant" if role == "model" else role + messages.append({"role": normalized_role, "content": content_parts}) + + messages.extend(tool_messages) + return messages + + # Handle Part object directly + part_result = _extract_part_content(contents) + if part_result: + if isinstance(part_result, dict) and part_result.get("role") == "tool": + return [part_result] + else: + return [{"role": "user", "content": [part_result]}] + + # Handle PIL.Image.Image + try: + from PIL import Image as PILImage # type: ignore[import-not-found] + + if isinstance(contents, PILImage.Image): + blob_part = _extract_pil_image(contents) + if blob_part: + return [{"role": "user", "content": [blob_part]}] + except ImportError: + pass + + # Handle File object + if hasattr(contents, "uri") and hasattr(contents, "mime_type"): + # File object + file_uri = getattr(contents, "uri", None) + mime_type = getattr(contents, "mime_type", None) + # Process if we have file_uri, even if mime_type is missing + if file_uri is not None: + # Default to empty string if mime_type is None + if mime_type is None: + mime_type = "" + + blob_part = { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + return [{"role": "user", "content": [blob_part]}] + + # Handle direct text attribute + if hasattr(contents, "text") and contents.text: + return [ + {"role": "user", "content": [{"text": str(contents.text), "type": "text"}]} + ] + + return [] + + +def _extract_part_content(part: "Any") -> "Optional[dict[str, Any]]": + """Extract content from a Part object or dict. + + Returns: + - dict for content part (text/blob) or tool message + - None if part should be skipped + """ + if part is None: + return None + + # Handle dict Part + if isinstance(part, dict): + # Check for function_response first (tool message) + if "function_response" in part: + return _extract_tool_message_from_part(part) + + if part.get("text"): + return {"text": part["text"], "type": "text"} + + # Try using Google-specific transform for dict formats (inline_data, file_data) + result = transform_google_content_part(part) + if result is not None: + # For inline_data with bytes data, substitute the content + if "inline_data" in part: + inline_data = part["inline_data"] + if isinstance(inline_data, dict) and isinstance( + inline_data.get("data"), bytes + ): + result["content"] = BLOB_DATA_SUBSTITUTE + return result + + return None + + # Handle Part object + # Check for function_response (tool message) + if hasattr(part, "function_response") and part.function_response: + return _extract_tool_message_from_part(part) + + # Handle text + if hasattr(part, "text") and part.text: + return {"text": part.text, "type": "text"} + + # Handle file_data + if hasattr(part, "file_data") and part.file_data: + file_data = part.file_data + file_uri = getattr(file_data, "file_uri", None) + mime_type = getattr(file_data, "mime_type", None) + # Process if we have file_uri, even if mime_type is missing (consistent with dict handling) + if file_uri is not None: + # Default to empty string if mime_type is None (consistent with transform_google_content_part) + if mime_type is None: + mime_type = "" + + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + + # Handle inline_data + if hasattr(part, "inline_data") and part.inline_data: + inline_data = part.inline_data + data = getattr(inline_data, "data", None) + mime_type = getattr(inline_data, "mime_type", None) + # Process if we have data, even if mime_type is missing/empty (consistent with dict handling) + if data is not None: + # Default to empty string if mime_type is None (consistent with transform_google_content_part) + if mime_type is None: + mime_type = "" + + # Handle both bytes (binary data) and str (base64-encoded data) + if isinstance(data, bytes): + content = BLOB_DATA_SUBSTITUTE + else: + # For non-bytes data (e.g., base64 strings), use as-is + content = data + + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": content, + } return None +def _extract_tool_message_from_part(part: "Any") -> "Optional[dict[str, Any]]": + """Extract tool message from a Part with function_response. + + Returns: + {"role": "tool", "content": {"toolCallId": "...", "toolName": "...", "output": "..."}} + or None if not a valid tool message + """ + function_response = None + + if isinstance(part, dict): + function_response = part.get("function_response") + elif hasattr(part, "function_response"): + function_response = part.function_response + + if not function_response: + return None + + # Extract fields from function_response + tool_call_id = None + tool_name = None + output = None + + if isinstance(function_response, dict): + tool_call_id = function_response.get("id") + tool_name = function_response.get("name") + response_dict = function_response.get("response", {}) + # Prefer "output" key if present, otherwise use entire response + output = response_dict.get("output", response_dict) + else: + # FunctionResponse object + tool_call_id = getattr(function_response, "id", None) + tool_name = getattr(function_response, "name", None) + response_obj = getattr(function_response, "response", None) + if response_obj is None: + response_obj = {} + if isinstance(response_obj, dict): + output = response_obj.get("output", response_obj) + else: + output = response_obj + + if not tool_name: + return None + + return { + "role": "tool", + "content": { + "toolCallId": str(tool_call_id) if tool_call_id else None, + "toolName": str(tool_name), + "output": safe_serialize(output) if output is not None else None, + }, + } + + +def _extract_pil_image(image: "Any") -> "Optional[dict[str, Any]]": + """Extract blob part from PIL.Image.Image.""" + try: + from PIL import Image as PILImage + + if not isinstance(image, PILImage.Image): + return None + + # Get format, default to JPEG + format_str = image.format or "JPEG" + suffix = format_str.lower() + mime_type = f"image/{suffix}" + + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": BLOB_DATA_SUBSTITUTE, + } + except Exception: + return None + + +def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]": + """Extract text from contents parameter which can have various formats. + + This is a compatibility function that extracts text from messages. + For new code, use extract_contents_messages instead. + """ + messages = extract_contents_messages(contents) + if not messages: + return None + + texts = [] + for message in messages: + content = message.get("content") + if isinstance(content, str): + texts.append(content) + elif isinstance(content, list): + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + texts.append(part.get("text", "")) + + return " ".join(texts) if texts else None + + def _format_tools_for_span( tools: "Iterable[Tool | Callable[..., Any]]", ) -> "Optional[List[dict[str, Any]]]": @@ -433,6 +725,62 @@ def extract_finish_reasons( return finish_reasons if finish_reasons else None +def _transform_system_instruction_one_level( + system_instructions: "Union[ContentUnionDict, ContentUnion]", + can_be_content: bool, +) -> "list[TextPart]": + text_parts: "list[TextPart]" = [] + + if isinstance(system_instructions, str): + return [{"type": "text", "content": system_instructions}] + + if isinstance(system_instructions, Part) and system_instructions.text: + return [{"type": "text", "content": system_instructions.text}] + + if can_be_content and isinstance(system_instructions, Content): + if isinstance(system_instructions.parts, list): + for part in system_instructions.parts: + if isinstance(part.text, str): + text_parts.append({"type": "text", "content": part.text}) + return text_parts + + if isinstance(system_instructions, dict) and system_instructions.get("text"): + return [{"type": "text", "content": system_instructions["text"]}] + + elif can_be_content and isinstance(system_instructions, dict): + parts = system_instructions.get("parts", []) + for part in parts: + if isinstance(part, Part) and isinstance(part.text, str): + text_parts.append({"type": "text", "content": part.text}) + elif isinstance(part, dict) and isinstance(part.get("text"), str): + text_parts.append({"type": "text", "content": part["text"]}) + return text_parts + + return text_parts + + +def _transform_system_instructions( + system_instructions: "Union[ContentUnionDict, ContentUnion]", +) -> "list[TextPart]": + text_parts: "list[TextPart]" = [] + + if isinstance(system_instructions, list): + text_parts = list( + chain.from_iterable( + _transform_system_instruction_one_level( + instructions, can_be_content=False + ) + for instructions in system_instructions + ) + ) + + return text_parts + + return _transform_system_instruction_one_level( + system_instructions, can_be_content=True + ) + + def set_span_data_for_request( span: "Span", integration: "Any", @@ -454,17 +802,21 @@ def set_span_data_for_request( messages = [] # Add system instruction if present + system_instructions = None if config and hasattr(config, "system_instruction"): - system_instruction = config.system_instruction - if system_instruction: - system_text = extract_contents_text(system_instruction) - if system_text: - messages.append({"role": "system", "content": system_text}) - - # Add user message - contents_text = extract_contents_text(contents) - if contents_text: - messages.append({"role": "user", "content": contents_text}) + system_instructions = config.system_instruction + elif isinstance(config, dict) and "system_instruction" in config: + system_instructions = config.get("system_instruction") + + if system_instructions is not None: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + # Extract messages from contents + contents_messages = extract_contents_messages(contents) + messages.extend(contents_messages) if messages: normalized_messages = normalize_message_roles(messages) diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index 083ceaf517..c19a518f46 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -95,18 +95,23 @@ def _request_info_from_transport( def _patch_execute() -> None: real_execute = gql.Client.execute + # Maintain signature for backwards compatibility. + # gql.Client.execute() accepts a positional-only "request" + # parameter with version 4.0.0. @ensure_integration_enabled(GQLIntegration, real_execute) def sentry_patched_execute( self: "gql.Client", - document_or_request: "DocumentNode", + document: "DocumentNode", *args: "Any", **kwargs: "Any", ) -> "Any": scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_make_gql_event_processor(self, document_or_request)) + # document is a gql.GraphQLRequest with gql v4.0.0. + scope.add_event_processor(_make_gql_event_processor(self, document)) try: - return real_execute(self, document_or_request, *args, **kwargs) + # document is a gql.GraphQLRequest with gql v4.0.0. + return real_execute(self, document, *args, **kwargs) except TransportQueryError as e: event, hint = event_from_exception( e, diff --git a/sentry_sdk/integrations/grpc/__init__.py b/sentry_sdk/integrations/grpc/__init__.py index b6641163a9..a41631c37e 100644 --- a/sentry_sdk/integrations/grpc/__init__.py +++ b/sentry_sdk/integrations/grpc/__init__.py @@ -50,14 +50,29 @@ def __getitem__(self, _): GRPC_VERSION = parse_version(grpc.__version__) +def _is_channel_intercepted(channel: "Channel") -> bool: + interceptor = getattr(channel, "_interceptor", None) + while interceptor is not None: + if isinstance(interceptor, ClientInterceptor): + return True + + inner_channel = getattr(channel, "_channel", None) + if inner_channel is None: + return False + + channel = inner_channel + interceptor = getattr(channel, "_interceptor", None) + + return False + + def _wrap_channel_sync(func: "Callable[P, Channel]") -> "Callable[P, Channel]": "Wrapper for synchronous secure and insecure channel." @wraps(func) def patched_channel(*args: "Any", **kwargs: "Any") -> "Channel": channel = func(*args, **kwargs) - if not ClientInterceptor._is_intercepted: - ClientInterceptor._is_intercepted = True + if not _is_channel_intercepted(channel): return intercept_channel(channel, ClientInterceptor()) else: return channel @@ -70,7 +85,7 @@ def _wrap_intercept_channel(func: "Callable[P, Channel]") -> "Callable[P, Channe def patched_intercept_channel( channel: "Channel", *interceptors: "grpc.ServerInterceptor" ) -> "Channel": - if ClientInterceptor._is_intercepted: + if _is_channel_intercepted(channel): interceptors = tuple( [ interceptor diff --git a/sentry_sdk/integrations/grpc/client.py b/sentry_sdk/integrations/grpc/client.py index 69b3f3d318..b6cbc54f10 100644 --- a/sentry_sdk/integrations/grpc/client.py +++ b/sentry_sdk/integrations/grpc/client.py @@ -22,8 +22,6 @@ class ClientInterceptor( grpc.UnaryUnaryClientInterceptor, # type: ignore grpc.UnaryStreamClientInterceptor, # type: ignore ): - _is_intercepted = False - def intercept_unary_unary( self: "ClientInterceptor", continuation: "Callable[[ClientCallDetails, Message], _UnaryOutcome]", diff --git a/sentry_sdk/integrations/httpx.py b/sentry_sdk/integrations/httpx.py index 38e9c1a3d7..38c4f437bc 100644 --- a/sentry_sdk/integrations/httpx.py +++ b/sentry_sdk/integrations/httpx.py @@ -4,9 +4,9 @@ from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.tracing import BAGGAGE_HEADER_NAME from sentry_sdk.tracing_utils import ( - Baggage, should_propagate_trace, add_http_request_source, + add_sentry_baggage_to_headers, ) from sentry_sdk.utils import ( SENSITIVE_DATA_SUBSTITUTE, @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from collections.abc import MutableMapping from typing import Any @@ -81,7 +80,7 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": ) if key == BAGGAGE_HEADER_NAME: - _add_sentry_baggage_to_headers(request.headers, value) + add_sentry_baggage_to_headers(request.headers, value) else: request.headers[key] = value @@ -155,22 +154,3 @@ async def send( return rv AsyncClient.send = send - - -def _add_sentry_baggage_to_headers( - headers: "MutableMapping[str, str]", sentry_baggage: str -) -> None: - """Add the Sentry baggage to the headers. - - This function directly mutates the provided headers. The provided sentry_baggage - is appended to the existing baggage. If the baggage already contains Sentry items, - they are stripped out first. - """ - existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "") - stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage) - - separator = "," if len(stripped_existing_baggage) > 0 else "" - - headers[BAGGAGE_HEADER_NAME] = ( - stripped_existing_baggage + separator + sentry_baggage - ) diff --git a/sentry_sdk/integrations/huggingface_hub.py b/sentry_sdk/integrations/huggingface_hub.py index 39a667dde9..8509cadefa 100644 --- a/sentry_sdk/integrations/huggingface_hub.py +++ b/sentry_sdk/integrations/huggingface_hub.py @@ -1,3 +1,4 @@ +import sys import inspect from functools import wraps @@ -11,6 +12,7 @@ from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, + reraise, ) from typing import TYPE_CHECKING @@ -126,9 +128,11 @@ def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any": try: res = f(*args, **kwargs) except Exception as e: - _capture_exception(e) - span.__exit__(None, None, None) - raise e from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + span.__exit__(None, None, None) + reraise(*exc_info) # Output attributes finish_reason = None diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 950f437d4c..7a5e863e4d 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -1,6 +1,7 @@ import contextvars import itertools import sys +import json import warnings from collections import OrderedDict from functools import wraps @@ -14,6 +15,7 @@ normalize_message_roles, set_data_normalized, truncate_and_annotate_messages, + transform_content_part, ) from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration @@ -35,6 +37,7 @@ from uuid import UUID from sentry_sdk.tracing import Span + from sentry_sdk._types import TextPart try: @@ -117,6 +120,39 @@ } +def _transform_langchain_content_block( + content_block: "Dict[str, Any]", +) -> "Dict[str, Any]": + """ + Transform a LangChain content block using the shared transform_content_part function. + + Returns the original content block if transformation is not applicable + (e.g., for text blocks or unrecognized formats). + """ + result = transform_content_part(content_block) + return result if result is not None else content_block + + +def _transform_langchain_message_content(content: "Any") -> "Any": + """ + Transform LangChain message content, handling both string content and + list of content blocks. + """ + if isinstance(content, str): + return content + + if isinstance(content, (list, tuple)): + transformed = [] + for block in content: + if isinstance(block, dict): + transformed.append(_transform_langchain_content_block(block)) + else: + transformed.append(block) + return transformed + + return content + + # Contextvar to track agent names in a stack for re-entrant agent support _agent_stack: "contextvars.ContextVar[Optional[List[Optional[str]]]]" = ( contextvars.ContextVar("langchain_agent_stack", default=None) @@ -155,6 +191,40 @@ def _get_current_agent() -> "Optional[str]": return None +def _get_system_instructions(messages: "List[List[BaseMessage]]") -> "List[str]": + system_instructions = [] + + for list_ in messages: + for message in list_: + # type of content: str | list[str | dict] | None + if message.type == "system" and isinstance(message.content, str): + system_instructions.append(message.content) + + elif message.type == "system" and isinstance(message.content, list): + for item in message.content: + if isinstance(item, str): + system_instructions.append(item) + + elif isinstance(item, dict) and item.get("type") == "text": + instruction = item.get("text") + if isinstance(instruction, str): + system_instructions.append(instruction) + + return system_instructions + + +def _transform_system_instructions( + system_instructions: "List[str]", +) -> "List[TextPart]": + return [ + { + "type": "text", + "content": instruction, + } + for instruction in system_instructions + ] + + class LangchainIntegration(Integration): identifier = "langchain" origin = f"auto.ai.{identifier}" @@ -234,7 +304,9 @@ def _handle_error(self, run_id: "UUID", error: "Any") -> None: del self.span_map[run_id] def _normalize_langchain_message(self, message: "BaseMessage") -> "Any": - parsed = {"role": message.type, "content": message.content} + # Transform content to handle multimodal data (images, audio, video, files) + transformed_content = _transform_langchain_message_content(message.content) + parsed = {"role": message.type, "content": transformed_content} parsed.update(message.additional_kwargs) return parsed @@ -394,9 +466,19 @@ def on_chat_model_start( _set_tools_on_span(span, all_params.get("tools")) if should_send_default_pii() and self.include_prompts: + system_instructions = _get_system_instructions(messages) + if len(system_instructions) > 0: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + normalized_messages = [] for list_ in messages: for message in list_: + if message.type == "system": + continue + normalized_messages.append( self._normalize_langchain_message(message) ) diff --git a/sentry_sdk/integrations/litellm.py b/sentry_sdk/integrations/litellm.py index 08cb217962..28bcc34d3e 100644 --- a/sentry_sdk/integrations/litellm.py +++ b/sentry_sdk/integrations/litellm.py @@ -1,3 +1,4 @@ +import copy from typing import TYPE_CHECKING import sentry_sdk @@ -7,6 +8,8 @@ get_start_span_function, set_data_normalized, truncate_and_annotate_messages, + transform_openai_content_part, + truncate_and_annotate_embedding_inputs, ) from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration @@ -14,11 +17,12 @@ from sentry_sdk.utils import event_from_exception if TYPE_CHECKING: - from typing import Any, Dict + from typing import Any, Dict, List from datetime import datetime try: import litellm # type: ignore[import-not-found] + from litellm import input_callback, success_callback, failure_callback except ImportError: raise DidNotEnable("LiteLLM not installed") @@ -35,6 +39,33 @@ def _get_metadata_dict(kwargs: "Dict[str, Any]") -> "Dict[str, Any]": return metadata +def _convert_message_parts(messages: "List[Dict[str, Any]]") -> "List[Dict[str, Any]]": + """ + Convert the message parts from OpenAI format to the `gen_ai.request.messages` format + using the OpenAI-specific transformer (LiteLLM uses OpenAI's message format). + + Deep copies messages to avoid mutating original kwargs. + """ + # Deep copy to avoid mutating original messages from kwargs + messages = copy.deepcopy(messages) + + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if isinstance(content, (list, tuple)): + transformed = [] + for item in content: + if isinstance(item, dict): + result = transform_openai_content_part(item) + # If transformation succeeded, use the result; otherwise keep original + transformed.append(result if result is not None else item) + else: + transformed.append(item) + message["content"] = transformed + return messages + + def _input_callback(kwargs: "Dict[str, Any]") -> None: """Handle the start of a request.""" integration = sentry_sdk.get_client().get_integration(LiteLLMIntegration) @@ -88,7 +119,9 @@ def _input_callback(kwargs: "Dict[str, Any]") -> None: if isinstance(embedding_input, list) else [embedding_input] ) - messages_data = truncate_and_annotate_messages(input_list, span, scope) + messages_data = truncate_and_annotate_embedding_inputs( + input_list, span, scope + ) if messages_data is not None: set_data_normalized( span, @@ -101,6 +134,7 @@ def _input_callback(kwargs: "Dict[str, Any]") -> None: messages = kwargs.get("messages", []) if messages: scope = sentry_sdk.get_current_scope() + messages = _convert_message_parts(messages) messages_data = truncate_and_annotate_messages(messages, span, scope) if messages_data is not None: set_data_normalized( @@ -278,14 +312,14 @@ def __init__(self: "LiteLLMIntegration", include_prompts: bool = True) -> None: @staticmethod def setup_once() -> None: """Set up LiteLLM callbacks for monitoring.""" - litellm.input_callback = litellm.input_callback or [] + litellm.input_callback = input_callback or [] if _input_callback not in litellm.input_callback: litellm.input_callback.append(_input_callback) - litellm.success_callback = litellm.success_callback or [] + litellm.success_callback = success_callback or [] if _success_callback not in litellm.success_callback: litellm.success_callback.append(_success_callback) - litellm.failure_callback = litellm.failure_callback or [] + litellm.failure_callback = failure_callback or [] if _failure_callback not in litellm.failure_callback: litellm.failure_callback.append(_failure_callback) diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 53d464c3c4..081744bfd5 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -1,4 +1,8 @@ +import sys +import json +import time from functools import wraps +from collections.abc import Iterable import sentry_sdk from sentry_sdk import consts @@ -7,6 +11,16 @@ set_data_normalized, normalize_message_roles, truncate_and_annotate_messages, + truncate_and_annotate_embedding_inputs, +) +from sentry_sdk.ai._openai_completions_api import ( + _is_system_instruction as _is_system_instruction_completions, + _get_system_instructions as _get_system_instructions_completions, + _transform_system_instructions, +) +from sentry_sdk.ai._openai_responses_api import ( + _is_system_instruction as _is_system_instruction_responses, + _get_system_instructions as _get_system_instructions_responses, ) from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration @@ -16,13 +30,27 @@ capture_internal_exceptions, event_from_exception, safe_serialize, + reraise, ) from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Iterable, List, Optional, Callable, AsyncIterator, Iterator + from typing import ( + Any, + List, + Optional, + Callable, + AsyncIterator, + Iterator, + Union, + Iterable, + ) from sentry_sdk.tracing import Span + from sentry_sdk._types import TextPart + + from openai.types.responses import ResponseInputParam + from openai import Omit try: try: @@ -39,7 +67,11 @@ from openai.resources import Embeddings, AsyncEmbeddings if TYPE_CHECKING: - from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk + from openai.types.chat import ( + ChatCompletionMessageParam, + ChatCompletionChunk, + ChatCompletionSystemMessageParam, + ) except ImportError: raise DidNotEnable("OpenAI not installed") @@ -84,9 +116,12 @@ def setup_once() -> None: AsyncResponses.create = _wrap_async_responses_create(AsyncResponses.create) def count_tokens(self: "OpenAIIntegration", s: str) -> int: - if self.tiktoken_encoding is not None: + if self.tiktoken_encoding is None: + return 0 + try: return len(self.tiktoken_encoding.encode_ordinary(s)) - return 0 + except Exception: + return 0 def _capture_exception(exc: "Any", manual_span_cleanup: bool = True) -> None: @@ -157,8 +192,8 @@ def _calculate_token_usage( output_tokens += count_tokens(message) elif hasattr(response, "choices"): for choice in response.choices: - if hasattr(choice, "message"): - output_tokens += count_tokens(choice.message) + if hasattr(choice, "message") and hasattr(choice.message, "content"): + output_tokens += count_tokens(choice.message.content) # Do not set token data if it is 0 input_tokens = input_tokens or None @@ -177,12 +212,9 @@ def _calculate_token_usage( ) -def _set_input_data( - span: "Span", +def _get_input_messages( kwargs: "dict[str, Any]", - operation: str, - integration: "OpenAIIntegration", -) -> None: +) -> "Optional[Union[Iterable[Any], list[str]]]": # Input messages (the prompt or data sent to the model) messages = kwargs.get("messages") if messages is None: @@ -191,29 +223,15 @@ def _set_input_data( if isinstance(messages, str): messages = [messages] - if ( - messages is not None - and len(messages) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles(messages) - scope = sentry_sdk.get_current_scope() - messages_data = truncate_and_annotate_messages(normalized_messages, span, scope) - if messages_data is not None: - # Use appropriate field based on operation type - if operation == "embeddings": - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False - ) - else: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) + return messages + +def _commmon_set_input_data( + span: "Span", + kwargs: "dict[str, Any]", +) -> None: # Input attributes: Common set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, "openai") - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation) # Input attributes: Optional kwargs_keys_to_attributes = { @@ -239,11 +257,182 @@ def _set_input_data( ) +def _set_responses_api_input_data( + span: "Span", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + explicit_instructions: "Union[Optional[str], Omit]" = kwargs.get("instructions") + messages: "Optional[Union[str, ResponseInputParam]]" = kwargs.get("input") + + if not should_send_default_pii() or not integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + _commmon_set_input_data(span, kwargs) + return + + if ( + messages is None + and explicit_instructions is not None + and _is_given(explicit_instructions) + ): + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + [ + { + "type": "text", + "content": explicit_instructions, + } + ] + ), + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + _commmon_set_input_data(span, kwargs) + return + + if messages is None: + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + _commmon_set_input_data(span, kwargs) + return + + instructions_text_parts: "list[TextPart]" = [] + if explicit_instructions is not None and _is_given(explicit_instructions): + instructions_text_parts.append( + { + "type": "text", + "content": explicit_instructions, + } + ) + + system_instructions = _get_system_instructions_responses(messages) + # Deliberate use of function accepting completions API type because + # of shared structure FOR THIS PURPOSE ONLY. + instructions_text_parts += _transform_system_instructions(system_instructions) + + if len(instructions_text_parts) > 0: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + + if isinstance(messages, str): + normalized_messages = normalize_message_roles([messages]) # type: ignore + scope = sentry_sdk.get_current_scope() + messages_data = truncate_and_annotate_messages(normalized_messages, span, scope) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + _commmon_set_input_data(span, kwargs) + return + + non_system_messages = [ + message for message in messages if not _is_system_instruction_responses(message) + ] + if len(non_system_messages) > 0: + normalized_messages = normalize_message_roles(non_system_messages) + scope = sentry_sdk.get_current_scope() + messages_data = truncate_and_annotate_messages(normalized_messages, span, scope) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + _commmon_set_input_data(span, kwargs) + + +def _set_completions_api_input_data( + span: "Span", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get( + "messages" + ) + + if ( + not should_send_default_pii() + or not integration.include_prompts + or messages is None + ): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + _commmon_set_input_data(span, kwargs) + return + + system_instructions = _get_system_instructions_completions(messages) + if len(system_instructions) > 0: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + if isinstance(messages, str): + normalized_messages = normalize_message_roles([messages]) # type: ignore + scope = sentry_sdk.get_current_scope() + messages_data = truncate_and_annotate_messages(normalized_messages, span, scope) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + _commmon_set_input_data(span, kwargs) + return + + non_system_messages = [ + message + for message in messages + if not _is_system_instruction_completions(message) + ] + if len(non_system_messages) > 0: + normalized_messages = normalize_message_roles(non_system_messages) # type: ignore + scope = sentry_sdk.get_current_scope() + messages_data = truncate_and_annotate_messages(normalized_messages, span, scope) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + _commmon_set_input_data(span, kwargs) + + +def _set_embeddings_input_data( + span: "Span", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + messages = _get_input_messages(kwargs) + + if ( + messages is not None + and len(messages) > 0 # type: ignore + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles(messages) # type: ignore + scope = sentry_sdk.get_current_scope() + messages_data = truncate_and_annotate_embedding_inputs( + normalized_messages, span, scope + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + _commmon_set_input_data(span, kwargs) + + def _set_output_data( span: "Span", response: "Any", kwargs: "dict[str, Any]", integration: "OpenAIIntegration", + start_time: "Optional[float]" = None, finish_span: bool = True, ) -> None: if hasattr(response, "model"): @@ -258,6 +447,8 @@ def _set_output_data( if messages is not None and isinstance(messages, str): messages = [messages] + ttft: "Optional[float]" = None + if hasattr(response, "choices"): if should_send_default_pii() and integration.include_prompts: response_text = [ @@ -315,6 +506,7 @@ def _set_output_data( old_iterator = response._iterator def new_iterator() -> "Iterator[ChatCompletionChunk]": + nonlocal ttft count_tokens_manually = True for x in old_iterator: with capture_internal_exceptions(): @@ -325,6 +517,8 @@ def new_iterator() -> "Iterator[ChatCompletionChunk]": if hasattr(choice, "delta") and hasattr( choice.delta, "content" ): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time content = choice.delta.content if len(data_buf) <= choice_index: data_buf.append([]) @@ -333,6 +527,8 @@ def new_iterator() -> "Iterator[ChatCompletionChunk]": # OpenAI responses API elif hasattr(x, "delta"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time if len(data_buf) == 0: data_buf.append([]) data_buf[0].append(x.delta or "") @@ -351,6 +547,10 @@ def new_iterator() -> "Iterator[ChatCompletionChunk]": yield x with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) if len(data_buf) > 0: all_responses = ["".join(chunk) for chunk in data_buf] if should_send_default_pii() and integration.include_prompts: @@ -370,6 +570,7 @@ def new_iterator() -> "Iterator[ChatCompletionChunk]": span.__exit__(None, None, None) async def new_iterator_async() -> "AsyncIterator[ChatCompletionChunk]": + nonlocal ttft count_tokens_manually = True async for x in old_iterator: with capture_internal_exceptions(): @@ -380,6 +581,8 @@ async def new_iterator_async() -> "AsyncIterator[ChatCompletionChunk]": if hasattr(choice, "delta") and hasattr( choice.delta, "content" ): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time content = choice.delta.content if len(data_buf) <= choice_index: data_buf.append([]) @@ -388,6 +591,8 @@ async def new_iterator_async() -> "AsyncIterator[ChatCompletionChunk]": # OpenAI responses API elif hasattr(x, "delta"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time if len(data_buf) == 0: data_buf.append([]) data_buf[0].append(x.delta or "") @@ -406,6 +611,10 @@ async def new_iterator_async() -> "AsyncIterator[ChatCompletionChunk]": yield x with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) if len(data_buf) > 0: all_responses = ["".join(chunk) for chunk in data_buf] if should_send_default_pii() and integration.include_prompts: @@ -449,20 +658,20 @@ def _new_chat_completion_common(f: "Any", *args: "Any", **kwargs: "Any") -> "Any return f(*args, **kwargs) model = kwargs.get("model") - operation = "chat" span = sentry_sdk.start_span( op=consts.OP.GEN_AI_CHAT, - name=f"{operation} {model}", + name=f"chat {model}", origin=OpenAIIntegration.origin, ) span.__enter__() - _set_input_data(span, kwargs, operation, integration) + _set_completions_api_input_data(span, kwargs, integration) + start_time = time.perf_counter() response = yield f, args, kwargs - _set_output_data(span, response, kwargs, integration, finish_span=True) + _set_output_data(span, response, kwargs, integration, start_time, finish_span=True) return response @@ -480,8 +689,10 @@ def _execute_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": try: result = f(*args, **kwargs) except Exception as e: - _capture_exception(e) - raise e from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + reraise(*exc_info) return gen.send(result) except StopIteration as e: @@ -512,8 +723,10 @@ async def _execute_async(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": try: result = await f(*args, **kwargs) except Exception as e: - _capture_exception(e) - raise e from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + reraise(*exc_info) return gen.send(result) except StopIteration as e: @@ -537,14 +750,13 @@ def _new_embeddings_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A return f(*args, **kwargs) model = kwargs.get("model") - operation = "embeddings" with sentry_sdk.start_span( op=consts.OP.GEN_AI_EMBEDDINGS, - name=f"{operation} {model}", + name=f"embeddings {model}", origin=OpenAIIntegration.origin, ) as span: - _set_input_data(span, kwargs, operation, integration) + _set_embeddings_input_data(span, kwargs, integration) response = yield f, args, kwargs @@ -566,8 +778,10 @@ def _execute_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": try: result = f(*args, **kwargs) except Exception as e: - _capture_exception(e, manual_span_cleanup=False) - raise e from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e, manual_span_cleanup=False) + reraise(*exc_info) return gen.send(result) except StopIteration as e: @@ -597,8 +811,10 @@ async def _execute_async(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": try: result = await f(*args, **kwargs) except Exception as e: - _capture_exception(e, manual_span_cleanup=False) - raise e from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e, manual_span_cleanup=False) + reraise(*exc_info) return gen.send(result) except StopIteration as e: @@ -621,20 +837,20 @@ def _new_responses_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "An return f(*args, **kwargs) model = kwargs.get("model") - operation = "responses" span = sentry_sdk.start_span( op=consts.OP.GEN_AI_RESPONSES, - name=f"{operation} {model}", + name=f"responses {model}", origin=OpenAIIntegration.origin, ) span.__enter__() - _set_input_data(span, kwargs, operation, integration) + _set_responses_api_input_data(span, kwargs, integration) + start_time = time.perf_counter() response = yield f, args, kwargs - _set_output_data(span, response, kwargs, integration, finish_span=True) + _set_output_data(span, response, kwargs, integration, start_time, finish_span=True) return response @@ -652,8 +868,10 @@ def _execute_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": try: result = f(*args, **kwargs) except Exception as e: - _capture_exception(e) - raise e from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + reraise(*exc_info) return gen.send(result) except StopIteration as e: @@ -683,8 +901,10 @@ async def _execute_async(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": try: result = await f(*args, **kwargs) except Exception as e: - _capture_exception(e) - raise e from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + reraise(*exc_info) return gen.send(result) except StopIteration as e: diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index 3ad7d2ee8d..deb136de01 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -4,13 +4,14 @@ _create_get_model_wrapper, _create_get_all_tools_wrapper, _create_run_wrapper, + _create_run_streamed_wrapper, _patch_agent_run, _patch_error_tracing, ) try: # "agents" is too generic. If someone has an agents.py file in their project - # or another package that's importable via "agents", no ImportError would not + # or another package that's importable via "agents", no ImportError would # be thrown and the integration would enable itself even if openai-agents is # not installed. That's why we're adding the second, more specific import # after it, even if we don't use it. @@ -25,12 +26,16 @@ def _patch_runner() -> None: # Create the root span for one full agent run (including eventual handoffs) # Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around # agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately. - # TODO-anton: Also patch streaming runner: agents.Runner.run_streamed agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper( agents.run.DEFAULT_AGENT_RUNNER.run ) - # Creating the actual spans for each agent run. + # Patch streaming runner + agents.run.DEFAULT_AGENT_RUNNER.run_streamed = _create_run_streamed_wrapper( + agents.run.DEFAULT_AGENT_RUNNER.run_streamed + ) + + # Creating the actual spans for each agent run (works for both streaming and non-streaming). _patch_agent_run() diff --git a/sentry_sdk/integrations/openai_agents/patches/__init__.py b/sentry_sdk/integrations/openai_agents/patches/__init__.py index 33058f01a1..b53ca79e19 100644 --- a/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ b/sentry_sdk/integrations/openai_agents/patches/__init__.py @@ -1,5 +1,5 @@ from .models import _create_get_model_wrapper # noqa: F401 from .tools import _create_get_all_tools_wrapper # noqa: F401 -from .runner import _create_run_wrapper # noqa: F401 +from .runner import _create_run_wrapper, _create_run_streamed_wrapper # noqa: F401 from .agent_run import _patch_agent_run # noqa: F401 from .error_tracing import _patch_error_tracing # noqa: F401 diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index 29649af945..eeb821d42a 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -1,8 +1,9 @@ import sys from functools import wraps +from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.utils import reraise +from sentry_sdk.utils import capture_internal_exceptions, reraise from ..spans import ( invoke_agent_span, end_invoke_agent_span, @@ -31,22 +32,10 @@ def _patch_agent_run() -> None: # Store original methods original_run_single_turn = agents.run.AgentRunner._run_single_turn + original_run_single_turn_streamed = agents.run.AgentRunner._run_single_turn_streamed original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs original_execute_final_output = agents._run_impl.RunImpl.execute_final_output - def _start_invoke_agent_span( - context_wrapper: "agents.RunContextWrapper", - agent: "agents.Agent", - kwargs: "dict[str, Any]", - ) -> "Span": - """Start an agent invocation span""" - # Store the agent on the context wrapper so we can access it later - context_wrapper._sentry_current_agent = agent - span = invoke_agent_span(context_wrapper, agent, kwargs) - context_wrapper._sentry_agent_span = span - - return span - def _has_active_agent_span(context_wrapper: "agents.RunContextWrapper") -> bool: """Check if there's an active agent span for this context""" return getattr(context_wrapper, "_sentry_current_agent", None) is not None @@ -57,6 +46,46 @@ def _get_current_agent( """Get the current agent from context wrapper""" return getattr(context_wrapper, "_sentry_current_agent", None) + def _close_streaming_workflow_span(agent: "Optional[agents.Agent]") -> None: + """Close the workflow span for streaming executions if it exists.""" + if agent and hasattr(agent, "_sentry_workflow_span"): + workflow_span = agent._sentry_workflow_span + workflow_span.__exit__(*sys.exc_info()) + delattr(agent, "_sentry_workflow_span") + + def _maybe_start_agent_span( + context_wrapper: "agents.RunContextWrapper", + agent: "agents.Agent", + should_run_agent_start_hooks: bool, + span_kwargs: "dict[str, Any]", + is_streaming: bool = False, + ) -> "Optional[Span]": + """ + Start an agent invocation span if conditions are met. + Handles ending any existing span for a different agent. + + Returns the new span if started, or the existing span if conditions aren't met. + """ + if not (should_run_agent_start_hooks and agent and context_wrapper): + return getattr(context_wrapper, "_sentry_agent_span", None) + + # End any existing span for a different agent + if _has_active_agent_span(context_wrapper): + current_agent = _get_current_agent(context_wrapper) + if current_agent and current_agent != agent: + end_invoke_agent_span(context_wrapper, current_agent) + + # Store the agent on the context wrapper so we can access it later + context_wrapper._sentry_current_agent = agent + span = invoke_agent_span(context_wrapper, agent, span_kwargs) + context_wrapper._sentry_agent_span = span + agent._sentry_agent_span = span + + if is_streaming: + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + return span + @wraps( original_run_single_turn.__func__ if hasattr(original_run_single_turn, "__func__") @@ -68,28 +97,18 @@ async def patched_run_single_turn( """Patched _run_single_turn that creates agent invocation spans""" agent = kwargs.get("agent") context_wrapper = kwargs.get("context_wrapper") - should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks") - - span = getattr(context_wrapper, "_sentry_agent_span", None) - # Start agent span when agent starts (but only once per agent) - if should_run_agent_start_hooks and agent and context_wrapper: - # End any existing span for a different agent - if _has_active_agent_span(context_wrapper): - current_agent = _get_current_agent(context_wrapper) - if current_agent and current_agent != agent: - end_invoke_agent_span(context_wrapper, current_agent) + should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False) - span = _start_invoke_agent_span(context_wrapper, agent, kwargs) - agent._sentry_agent_span = span + span = _maybe_start_agent_span( + context_wrapper, agent, should_run_agent_start_hooks, kwargs + ) - # Call original method with all the correct parameters try: result = await original_run_single_turn(*args, **kwargs) except Exception as exc: if span is not None and span.timestamp is None: _record_exception_on_span(span, exc) end_invoke_agent_span(context_wrapper, agent) - reraise(*sys.exc_info()) return result @@ -117,7 +136,11 @@ async def patched_execute_handoffs( # Call original method with all parameters try: result = await original_execute_handoffs(*args, **kwargs) - + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _close_streaming_workflow_span(agent) + reraise(*exc_info) finally: # End span for current agent after handoff processing is complete if agent and context_wrapper and _has_active_agent_span(context_wrapper): @@ -139,18 +162,84 @@ async def patched_execute_final_output( context_wrapper = kwargs.get("context_wrapper") final_output = kwargs.get("final_output") - # Call original method with all parameters try: result = await original_execute_final_output(*args, **kwargs) finally: - # End span for current agent after final output processing is complete - if agent and context_wrapper and _has_active_agent_span(context_wrapper): - end_invoke_agent_span(context_wrapper, agent, final_output) + with capture_internal_exceptions(): + if ( + agent + and context_wrapper + and _has_active_agent_span(context_wrapper) + ): + end_invoke_agent_span(context_wrapper, agent, final_output) + # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) + _close_streaming_workflow_span(agent) + + return result + + @wraps( + original_run_single_turn_streamed.__func__ + if hasattr(original_run_single_turn_streamed, "__func__") + else original_run_single_turn_streamed + ) + async def patched_run_single_turn_streamed( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "Any": + """Patched _run_single_turn_streamed that creates agent invocation spans for streaming. + + Note: Unlike _run_single_turn which uses keyword-only arguments (*,), + _run_single_turn_streamed uses positional arguments. The call signature is: + _run_single_turn_streamed( + streamed_result, # args[0] + agent, # args[1] + hooks, # args[2] + context_wrapper, # args[3] + run_config, # args[4] + should_run_agent_start_hooks, # args[5] + tool_use_tracker, # args[6] + all_tools, # args[7] + server_conversation_tracker, # args[8] (optional) + ) + """ + streamed_result = args[0] if len(args) > 0 else kwargs.get("streamed_result") + agent = args[1] if len(args) > 1 else kwargs.get("agent") + context_wrapper = args[3] if len(args) > 3 else kwargs.get("context_wrapper") + should_run_agent_start_hooks = bool( + args[5] + if len(args) > 5 + else kwargs.get("should_run_agent_start_hooks", False) + ) + + span_kwargs: "dict[str, Any]" = {} + if streamed_result and hasattr(streamed_result, "input"): + span_kwargs["original_input"] = streamed_result.input + + span = _maybe_start_agent_span( + context_wrapper, + agent, + should_run_agent_start_hooks, + span_kwargs, + is_streaming=True, + ) + + try: + result = await original_run_single_turn_streamed(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + if span is not None and span.timestamp is None: + _record_exception_on_span(span, exc) + end_invoke_agent_span(context_wrapper, agent) + _close_streaming_workflow_span(agent) + reraise(*exc_info) return result # Apply patches agents.run.AgentRunner._run_single_turn = classmethod(patched_run_single_turn) + agents.run.AgentRunner._run_single_turn_streamed = classmethod( + patched_run_single_turn_streamed + ) agents._run_impl.RunImpl.execute_handoffs = classmethod(patched_execute_handoffs) agents._run_impl.RunImpl.execute_final_output = classmethod( patched_execute_final_output diff --git a/sentry_sdk/integrations/openai_agents/patches/models.py b/sentry_sdk/integrations/openai_agents/patches/models.py index a9b3c16a22..9b57a55f1f 100644 --- a/sentry_sdk/integrations/openai_agents/patches/models.py +++ b/sentry_sdk/integrations/openai_agents/patches/models.py @@ -1,23 +1,71 @@ import copy +import time from functools import wraps from sentry_sdk.integrations import DidNotEnable from ..spans import ai_client_span, update_ai_client_span + +import sentry_sdk from sentry_sdk.consts import SPANDATA +from sentry_sdk.utils import logger +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + should_propagate_trace, + add_sentry_baggage_to_headers, +) from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Callable - + from typing import Any, Callable, Optional + from sentry_sdk.tracing import Span try: import agents + from agents.tool import HostedMCPTool except ImportError: raise DidNotEnable("OpenAI Agents not installed") +def _set_response_model_on_agent_span( + agent: "agents.Agent", response_model: "Optional[str]" +) -> None: + """Set the response model on the agent's invoke_agent span if available.""" + if response_model: + agent_span = getattr(agent, "_sentry_agent_span", None) + if agent_span: + agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + +def _inject_trace_propagation_headers( + hosted_tool: "HostedMCPTool", span: "Span" +) -> None: + headers = hosted_tool.tool_config.get("headers") + if headers is None: + headers = {} + hosted_tool.tool_config["headers"] = headers + + mcp_url = hosted_tool.tool_config.get("server_url") + if not mcp_url: + return + + if should_propagate_trace(sentry_sdk.get_client(), mcp_url): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {mcp_url}.".format( + key=key, value=value, mcp_url=mcp_url + ) + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(headers, value) + else: + headers[key] = value + + def _create_get_model_wrapper( original_get_model: "Callable[..., Any]", ) -> "Callable[..., Any]": @@ -37,15 +85,19 @@ def wrapped_get_model( # because we only patch its direct methods, all underlying data can remain unchanged. model = copy.copy(original_get_model(agent, run_config)) - # Wrap _fetch_response if it exists (for OpenAI models) to capture raw response model + # Capture the request model name for spans (agent.model can be None when using defaults) + request_model_name = model.model if hasattr(model, "model") else str(model) + agent._sentry_request_model = request_model_name + + # Wrap _fetch_response if it exists (for OpenAI models) to capture response model if hasattr(model, "_fetch_response"): original_fetch_response = model._fetch_response @wraps(original_fetch_response) async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": response = await original_fetch_response(*args, **kwargs) - if hasattr(response, "model"): - agent._sentry_raw_response_model = str(response.model) + if hasattr(response, "model") and response.model: + agent._sentry_response_model = str(response.model) return response model._fetch_response = wrapped_fetch_response @@ -54,25 +106,80 @@ async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": @wraps(original_get_response) async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any": + mcp_tools = kwargs.get("tools") + hosted_tools = [] + if mcp_tools is not None: + hosted_tools = [ + tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) + ] + with ai_client_span(agent, kwargs) as span: + for hosted_tool in hosted_tools: + _inject_trace_propagation_headers(hosted_tool, span=span) + result = await original_get_response(*args, **kwargs) - response_model = getattr(agent, "_sentry_raw_response_model", None) + # Get response model captured from _fetch_response and clean up + response_model = getattr(agent, "_sentry_response_model", None) if response_model: - agent_span = getattr(agent, "_sentry_agent_span", None) - if agent_span: - agent_span.set_data( - SPANDATA.GEN_AI_RESPONSE_MODEL, response_model - ) - - delattr(agent, "_sentry_raw_response_model") + delattr(agent, "_sentry_response_model") - update_ai_client_span(span, agent, kwargs, result, response_model) + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span(span, result, response_model) return result model.get_response = wrapped_get_response + # Also wrap stream_response for streaming support + if hasattr(model, "stream_response"): + original_stream_response = model.stream_response + + @wraps(original_stream_response) + async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": + # Uses explicit try/finally instead of context manager to ensure cleanup + # even if the consumer abandons the stream (GeneratorExit). + span_kwargs = dict(kwargs) + if len(args) > 0: + span_kwargs["system_instructions"] = args[0] + if len(args) > 1: + span_kwargs["input"] = args[1] + + with ai_client_span(agent, span_kwargs) as span: + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + streaming_response = None + ttft_recorded = False + # Capture start time locally to avoid race conditions with concurrent requests + start_time = time.perf_counter() + + async for event in original_stream_response(*args, **kwargs): + # Detect first content token (text delta event) + if not ttft_recorded and hasattr(event, "delta"): + ttft = time.perf_counter() - start_time + span.set_data( + SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + ttft_recorded = True + + # Capture the full response from ResponseCompletedEvent + if hasattr(event, "response"): + streaming_response = event.response + yield event + + # Update span with response data (usage, output, model) + if streaming_response: + response_model = ( + str(streaming_response.model) + if hasattr(streaming_response, "model") + and streaming_response.model + else None + ) + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span(span, streaming_response) + + model.stream_response = wrapped_stream_response + return model return wrapped_get_model diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py index 1d3bbc894b..fd3bc284e9 100644 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ b/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -1,7 +1,9 @@ +import sys from functools import wraps import sentry_sdk from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.utils import capture_internal_exceptions, reraise from ..spans import agent_workflow_span, end_invoke_agent_span from ..utils import _capture_exception, _record_exception_on_span @@ -14,7 +16,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Callable + from typing import Any, AsyncIterator, Callable def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": @@ -37,30 +39,110 @@ async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": try: run_result = await original_func(*args, **kwargs) except AgentsException as exc: - _capture_exception(exc) - - context_wrapper = getattr(exc.run_data, "context_wrapper", None) - if context_wrapper is not None: - invoke_agent_span = getattr( - context_wrapper, "_sentry_agent_span", None - ) - - if ( - invoke_agent_span is not None - and invoke_agent_span.timestamp is None - ): - _record_exception_on_span(invoke_agent_span, exc) - end_invoke_agent_span(context_wrapper, agent) - - raise exc from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + + context_wrapper = getattr(exc.run_data, "context_wrapper", None) + if context_wrapper is not None: + invoke_agent_span = getattr( + context_wrapper, "_sentry_agent_span", None + ) + + if ( + invoke_agent_span is not None + and invoke_agent_span.timestamp is None + ): + _record_exception_on_span(invoke_agent_span, exc) + end_invoke_agent_span(context_wrapper, agent) + reraise(*exc_info) except Exception as exc: - # Invoke agent span is not finished in this case. - # This is much less likely to occur than other cases because - # AgentRunner.run() is "just" a while loop around _run_single_turn. - _capture_exception(exc) - raise exc from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Invoke agent span is not finished in this case. + # This is much less likely to occur than other cases because + # AgentRunner.run() is "just" a while loop around _run_single_turn. + _capture_exception(exc) + reraise(*exc_info) end_invoke_agent_span(run_result.context_wrapper, agent) return run_result return wrapper + + +def _create_run_streamed_wrapper( + original_func: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Wraps the agents.Runner.run_streamed method to create a root span for streaming agent workflow runs. + + Unlike run(), run_streamed() returns immediately with a RunResultStreaming object + while execution continues in a background task. The workflow span must stay open + throughout the streaming operation and close when streaming completes or is abandoned. + + Note: We don't use isolation_scope() here because it uses context variables that + cannot span async boundaries (the __enter__ and __exit__ would be called from + different async contexts, causing ValueError). + """ + + @wraps(original_func) + def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + # Clone agent because agent invocation spans are attached per run. + agent = args[0].clone() + + # Start workflow span immediately (before run_streamed returns) + workflow_span = agent_workflow_span(agent) + workflow_span.__enter__() + + # Store span on agent for cleanup + agent._sentry_workflow_span = workflow_span + + args = (agent, *args[1:]) + + try: + # Call original function to get RunResultStreaming + run_result = original_func(*args, **kwargs) + except Exception as exc: + # If run_streamed itself fails (not the background task), clean up immediately + workflow_span.__exit__(*sys.exc_info()) + _capture_exception(exc) + raise + + def _close_workflow_span() -> None: + if hasattr(agent, "_sentry_workflow_span"): + workflow_span.__exit__(*sys.exc_info()) + delattr(agent, "_sentry_workflow_span") + + if hasattr(run_result, "stream_events"): + original_stream_events = run_result.stream_events + + @wraps(original_stream_events) + async def wrapped_stream_events( + *stream_args: "Any", **stream_kwargs: "Any" + ) -> "AsyncIterator[Any]": + try: + async for event in original_stream_events( + *stream_args, **stream_kwargs + ): + yield event + finally: + _close_workflow_span() + + run_result.stream_events = wrapped_stream_events + + if hasattr(run_result, "cancel"): + original_cancel = run_result.cancel + + @wraps(original_cancel) + def wrapped_cancel(*cancel_args: "Any", **cancel_kwargs: "Any") -> "Any": + try: + return original_cancel(*cancel_args, **cancel_kwargs) + finally: + _close_workflow_span() + + run_result.cancel = wrapped_cancel + + return run_result + + return wrapper diff --git a/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/sentry_sdk/integrations/openai_agents/spans/ai_client.py index 1e188aa097..c099f133f4 100644 --- a/sentry_sdk/integrations/openai_agents/spans/ai_client.py +++ b/sentry_sdk/integrations/openai_agents/spans/ai_client.py @@ -21,7 +21,13 @@ def ai_client_span( agent: "Agent", get_response_kwargs: "dict[str, Any]" ) -> "sentry_sdk.tracing.Span": # TODO-anton: implement other types of operations. Now "chat" is hardcoded. - model_name = agent.model.model if hasattr(agent.model, "model") else agent.model + # Get model name from agent.model or fall back to request model (for when agent.model is None/default) + model_name = None + if agent.model: + model_name = agent.model.model if hasattr(agent.model, "model") else agent.model + elif hasattr(agent, "_sentry_request_model"): + model_name = agent._sentry_request_model + span = sentry_sdk.start_span( op=OP.GEN_AI_CHAT, description=f"chat {model_name}", @@ -38,15 +44,18 @@ def ai_client_span( def update_ai_client_span( span: "sentry_sdk.tracing.Span", - agent: "Agent", - get_response_kwargs: "dict[str, Any]", - result: "Any", + response: "Any", response_model: "Optional[str]" = None, ) -> None: - _set_usage_data(span, result.usage) - _set_output_data(span, result) - _create_mcp_execute_tool_spans(span, result) + """Update AI client span with response data (works for streaming and non-streaming).""" + if hasattr(response, "usage") and response.usage: + _set_usage_data(span, response.usage) + + if hasattr(response, "output") and response.output: + _set_output_data(span, response) + _create_mcp_execute_tool_spans(span, response) - # Set response model if captured from raw response if response_model is not None: span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + elif hasattr(response, "model") and response.model: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model)) diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index a24d0e909d..f3873db886 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -1,3 +1,5 @@ +import json + import sentry_sdk from sentry_sdk.ai.utils import ( GEN_AI_ALLOWED_MESSAGE_ROLES, @@ -11,14 +13,20 @@ from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing_utils import set_span_errored from sentry_sdk.utils import event_from_exception, safe_serialize +from sentry_sdk.ai._openai_completions_api import _transform_system_instructions +from sentry_sdk.ai._openai_responses_api import ( + _is_system_instruction, + _get_system_instructions, +) from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any - from agents import Usage + from agents import Usage, TResponseInputItem from sentry_sdk.tracing import Span + from sentry_sdk._types import TextPart try: import agents @@ -63,8 +71,14 @@ def _set_agent_data(span: "sentry_sdk.tracing.Span", agent: "agents.Agent") -> N SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens ) + # Get model name from agent.model or fall back to request model (for when agent.model is None/default) + model_name = None if agent.model: model_name = agent.model.model if hasattr(agent.model, "model") else agent.model + elif hasattr(agent, "_sentry_request_model"): + model_name = agent._sentry_request_model + + if model_name: span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) if agent.model_settings.presence_penalty: @@ -115,19 +129,37 @@ def _set_input_data( return request_messages = [] - system_instructions = get_response_kwargs.get("system_instructions") - if system_instructions: - request_messages.append( + messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", []) + + instructions_text_parts: "list[TextPart]" = [] + explicit_instructions = get_response_kwargs.get("system_instructions") + if explicit_instructions is not None: + instructions_text_parts.append( { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM, - "content": [{"type": "text", "text": system_instructions}], + "type": "text", + "content": explicit_instructions, } ) - for message in get_response_kwargs.get("input", []): + system_instructions = _get_system_instructions(messages) + + # Deliberate use of function accepting completions API type because + # of shared structure FOR THIS PURPOSE ONLY. + instructions_text_parts += _transform_system_instructions(system_instructions) + + if len(instructions_text_parts) > 0: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + + non_system_messages = [ + message for message in messages if not _is_system_instruction(message) + ] + for message in non_system_messages: if "role" in message: - normalized_role = normalize_message_role(message.get("role")) - content = message.get("content") + normalized_role = normalize_message_role(message.get("role")) # type: ignore + content = message.get("content") # type: ignore request_messages.append( { "role": normalized_role, @@ -139,14 +171,14 @@ def _set_input_data( } ) else: - if message.get("type") == "function_call": + if message.get("type") == "function_call": # type: ignore request_messages.append( { "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT, "content": [message], } ) - elif message.get("type") == "function_call_output": + elif message.get("type") == "function_call_output": # type: ignore request_messages.append( { "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, diff --git a/sentry_sdk/integrations/pure_eval.py b/sentry_sdk/integrations/pure_eval.py index 1f3a1f4ea1..f8c7a9fba6 100644 --- a/sentry_sdk/integrations/pure_eval.py +++ b/sentry_sdk/integrations/pure_eval.py @@ -15,12 +15,12 @@ from sentry_sdk._types import Event, Hint try: - import executing + from executing import Source except ImportError: raise DidNotEnable("executing is not installed") try: - import pure_eval + from pure_eval import Evaluator except ImportError: raise DidNotEnable("pure_eval is not installed") @@ -81,7 +81,7 @@ def add_executing_info( def pure_eval_frame(frame: "FrameType") -> "Dict[str, Any]": - source = executing.Source.for_frame(frame) + source = Source.for_frame(frame) if not source.tree: return {} @@ -98,7 +98,7 @@ def pure_eval_frame(frame: "FrameType") -> "Dict[str, Any]": if isinstance(scope, (ast.FunctionDef, ast.ClassDef, ast.Module)): break - evaluator = pure_eval.Evaluator.from_frame(frame) + evaluator = Evaluator.from_frame(frame) expressions = evaluator.interesting_expressions_grouped(scope) def closeness(expression: "Tuple[List[Any], Any]") -> "Tuple[int, int]": diff --git a/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py b/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py index d158d892d5..eaa4385834 100644 --- a/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +++ b/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py @@ -1,7 +1,9 @@ +import sys from functools import wraps import sentry_sdk from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.utils import capture_internal_exceptions, reraise from ..spans import invoke_agent_span, update_invoke_agent_span from ..utils import _capture_exception, pop_agent, push_agent @@ -121,8 +123,10 @@ async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": return result except Exception as exc: - _capture_exception(exc) - raise exc from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) finally: # Pop agent from contextvar stack pop_agent() @@ -177,8 +181,10 @@ async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": async for event in original_func(self, *args, **kwargs): yield event except Exception as exc: - _capture_exception(exc) - raise exc from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) return wrapper diff --git a/sentry_sdk/integrations/pydantic_ai/patches/tools.py b/sentry_sdk/integrations/pydantic_ai/patches/tools.py index b826a543fc..394d44f0f3 100644 --- a/sentry_sdk/integrations/pydantic_ai/patches/tools.py +++ b/sentry_sdk/integrations/pydantic_ai/patches/tools.py @@ -1,7 +1,9 @@ +import sys from functools import wraps from sentry_sdk.integrations import DidNotEnable import sentry_sdk +from sentry_sdk.utils import capture_internal_exceptions, reraise from ..spans import execute_tool_span, update_execute_tool_span from ..utils import _capture_exception, get_current_agent @@ -81,21 +83,22 @@ async def wrapped_call_tool( update_execute_tool_span(span, result) return result except ToolRetryError as exc: - # Avoid circular import due to multi-file integration structure - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) - - integration = sentry_sdk.get_client().get_integration( - PydanticAIIntegration - ) - if ( - integration is None - or not integration.handled_tool_call_exceptions - ): - raise exc from None - _capture_exception(exc, handled=True) - raise exc from None + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Avoid circular import due to multi-file integration structure + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) + + integration = sentry_sdk.get_client().get_integration( + PydanticAIIntegration + ) + if ( + integration is not None + and integration.handled_tool_call_exceptions + ): + _capture_exception(exc, handled=True) + reraise(*exc_info) # No span context - just call original return await original_call_tool( diff --git a/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py b/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py index cb34f36e4f..b2ae15b38f 100644 --- a/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +++ b/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py @@ -1,5 +1,13 @@ +import json + import sentry_sdk -from sentry_sdk.ai.utils import set_data_normalized +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.utils import ( + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, + get_modality_from_mime_type, +) from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.utils import safe_serialize @@ -20,15 +28,18 @@ if TYPE_CHECKING: from typing import Any, List, Dict from pydantic_ai.usage import RequestUsage # type: ignore + from pydantic_ai.messages import ModelMessage, SystemPromptPart # type: ignore + from sentry_sdk._types import TextPart as SentryTextPart try: - from pydantic_ai.messages import ( # type: ignore + from pydantic_ai.messages import ( BaseToolCallPart, BaseToolReturnPart, SystemPromptPart, UserPromptPart, TextPart, ThinkingPart, + BinaryContent, ) except ImportError: # Fallback if these classes are not available @@ -38,6 +49,48 @@ UserPromptPart = None TextPart = None ThinkingPart = None + BinaryContent = None + + +def _transform_system_instructions( + permanent_instructions: "list[SystemPromptPart]", + current_instructions: "list[str]", +) -> "list[SentryTextPart]": + text_parts: "list[SentryTextPart]" = [ + { + "type": "text", + "content": instruction.content, + } + for instruction in permanent_instructions + ] + + text_parts.extend( + { + "type": "text", + "content": instruction, + } + for instruction in current_instructions + ) + + return text_parts + + +def _get_system_instructions( + messages: "list[ModelMessage]", +) -> "tuple[list[SystemPromptPart], list[str]]": + permanent_instructions = [] + current_instructions = [] + + for msg in messages: + if hasattr(msg, "parts"): + for part in msg.parts: + if SystemPromptPart and isinstance(part, SystemPromptPart): + permanent_instructions.append(part) + + if hasattr(msg, "instructions") and msg.instructions is not None: + current_instructions.append(msg.instructions) + + return permanent_instructions, current_instructions def _set_input_messages(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: @@ -48,21 +101,19 @@ def _set_input_messages(span: "sentry_sdk.tracing.Span", messages: "Any") -> Non if not messages: return + permanent_instructions, current_instructions = _get_system_instructions(messages) + if len(permanent_instructions) > 0 or len(current_instructions) > 0: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + _transform_system_instructions( + permanent_instructions, current_instructions + ) + ), + ) + try: formatted_messages = [] - system_prompt = None - - # Extract system prompt from any ModelRequest with instructions - for msg in messages: - if hasattr(msg, "instructions") and msg.instructions: - system_prompt = msg.instructions - break - - # Add system prompt as first message if present - if system_prompt: - formatted_messages.append( - {"role": "system", "content": [{"type": "text", "text": system_prompt}]} - ) for msg in messages: if hasattr(msg, "parts"): @@ -70,7 +121,7 @@ def _set_input_messages(span: "sentry_sdk.tracing.Span", messages: "Any") -> Non role = "user" # Use isinstance checks with proper base classes if SystemPromptPart and isinstance(part, SystemPromptPart): - role = "system" + continue elif ( (TextPart and isinstance(part, TextPart)) or (ThinkingPart and isinstance(part, ThinkingPart)) @@ -107,6 +158,17 @@ def _set_input_messages(span: "sentry_sdk.tracing.Span", messages: "Any") -> Non for item in part.content: if isinstance(item, str): content.append({"type": "text", "text": item}) + elif BinaryContent and isinstance(item, BinaryContent): + content.append( + { + "type": "blob", + "modality": get_modality_from_mime_type( + item.media_type + ), + "mime_type": item.media_type, + "content": BLOB_DATA_SUBSTITUTE, + } + ) else: content.append(safe_serialize(item)) else: @@ -124,8 +186,13 @@ def _set_input_messages(span: "sentry_sdk.tracing.Span", messages: "Any") -> Non formatted_messages.append(message) if formatted_messages: + normalized_messages = normalize_message_roles(formatted_messages) + scope = sentry_sdk.get_current_scope() + messages_data = truncate_and_annotate_messages( + normalized_messages, span, scope + ) set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, formatted_messages, unpack=False + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False ) except Exception: # If we fail to format messages, just skip it diff --git a/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py b/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py index 629b3d1206..b4f8307170 100644 --- a/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +++ b/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py @@ -1,5 +1,12 @@ import sentry_sdk -from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.utils import ( + get_modality_from_mime_type, + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) from sentry_sdk.consts import OP, SPANDATA from ..consts import SPAN_ORIGIN @@ -16,6 +23,11 @@ if TYPE_CHECKING: from typing import Any +try: + from pydantic_ai.messages import BinaryContent # type: ignore +except ImportError: + BinaryContent = None + def invoke_agent_span( user_prompt: "Any", @@ -93,6 +105,17 @@ def invoke_agent_span( for item in user_prompt: if isinstance(item, str): content.append({"text": item, "type": "text"}) + elif BinaryContent and isinstance(item, BinaryContent): + content.append( + { + "type": "blob", + "modality": get_modality_from_mime_type( + item.media_type + ), + "mime_type": item.media_type, + "content": BLOB_DATA_SUBSTITUTE, + } + ) if content: messages.append( { @@ -102,8 +125,13 @@ def invoke_agent_span( ) if messages: + normalized_messages = normalize_message_roles(messages) + scope = sentry_sdk.get_current_scope() + messages_data = truncate_and_annotate_messages( + normalized_messages, span, scope + ) set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages, unpack=False + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False ) return span diff --git a/sentry_sdk/integrations/pydantic_ai/spans/utils.py b/sentry_sdk/integrations/pydantic_ai/spans/utils.py index c70afd5f31..4a8ad4c68c 100644 --- a/sentry_sdk/integrations/pydantic_ai/spans/utils.py +++ b/sentry_sdk/integrations/pydantic_ai/spans/utils.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Union + from typing import Union, Dict, Any, List from pydantic_ai.usage import RequestUsage, RunUsage # type: ignore @@ -28,6 +28,19 @@ def _set_usage_data( if hasattr(usage, "input_tokens") and usage.input_tokens is not None: span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) + # Pydantic AI uses cache_read_tokens (not input_tokens_cached) + if hasattr(usage, "cache_read_tokens") and usage.cache_read_tokens is not None: + span.set_data( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, usage.cache_read_tokens + ) + + # Pydantic AI uses cache_write_tokens (not input_tokens_cache_write) + if hasattr(usage, "cache_write_tokens") and usage.cache_write_tokens is not None: + span.set_data( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, + usage.cache_write_tokens, + ) + if hasattr(usage, "output_tokens") and usage.output_tokens is not None: span.set_data(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) diff --git a/sentry_sdk/integrations/ray.py b/sentry_sdk/integrations/ray.py index 92a35546ab..d1559d5c19 100644 --- a/sentry_sdk/integrations/ray.py +++ b/sentry_sdk/integrations/ray.py @@ -16,6 +16,7 @@ try: import ray # type: ignore[import-not-found] + from ray import remote except ImportError: raise DidNotEnable("Ray not installed.") @@ -36,8 +37,28 @@ def _check_sentry_initialized() -> None: ) +def _insert_sentry_tracing_in_signature(func: "Callable[..., Any]") -> None: + # Patching new_func signature to add the _sentry_tracing parameter to it + # Ray later inspects the signature and finds the unexpected parameter otherwise + signature = inspect.signature(func) + params = list(signature.parameters.values()) + sentry_tracing_param = inspect.Parameter( + "_sentry_tracing", + kind=inspect.Parameter.KEYWORD_ONLY, + default=None, + ) + + # Keyword only arguments are penultimate if function has variadic keyword arguments + if params and params[-1].kind is inspect.Parameter.VAR_KEYWORD: + params.insert(-1, sentry_tracing_param) + else: + params.append(sentry_tracing_param) + + func.__signature__ = signature.replace(parameters=params) # type: ignore[attr-defined] + + def _patch_ray_remote() -> None: - old_remote = ray.remote + old_remote = remote @functools.wraps(old_remote) def new_remote( @@ -86,18 +107,7 @@ def new_func( return result - # Patching new_func signature to add the _sentry_tracing parameter to it - # Ray later inspects the signature and finds the unexpected parameter otherwise - signature = inspect.signature(new_func) - params = list(signature.parameters.values()) - params.append( - inspect.Parameter( - "_sentry_tracing", - kind=inspect.Parameter.KEYWORD_ONLY, - default=None, - ) - ) - new_func.__signature__ = signature.replace(parameters=params) # type: ignore[attr-defined] + _insert_sentry_tracing_in_signature(new_func) if f: rv = old_remote(new_func) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index bf0c626fa8..e3120a3b32 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -69,10 +69,17 @@ def _install_httplib() -> None: def putrequest( self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" ) -> "Any": - host = self.host - port = self.port default_port = self.default_port + # proxies go through set_tunnel + tunnel_host = getattr(self, "_tunnel_host", None) + if tunnel_host: + host = tunnel_host + port = getattr(self, "_tunnel_port", default_port) + else: + host = self.host + port = self.port + client = sentry_sdk.get_client() if client.get_integration(StdlibIntegration) is None or is_sentry_url( client, host @@ -104,6 +111,11 @@ def putrequest( span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + # for proxies, these point to the proxy host/port + if tunnel_host: + span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, self.host) + span.set_data(SPANDATA.NETWORK_PEER_PORT, self.port) + rv = real_putrequest(self, method, url, *args, **kwargs) if should_propagate_trace(client, real_url): diff --git a/sentry_sdk/integrations/threading.py b/sentry_sdk/integrations/threading.py index 6311b935a0..5b5633507f 100644 --- a/sentry_sdk/integrations/threading.py +++ b/sentry_sdk/integrations/threading.py @@ -54,11 +54,14 @@ def setup_once() -> None: try: from django import VERSION as django_version # noqa: N811 + except ImportError: + django_version = None + + try: import channels # type: ignore[import-untyped] channels_version = channels.__version__ - except ImportError: - django_version = None + except (ImportError, AttributeError): channels_version = None is_async_emulated_with_threads = ( diff --git a/sentry_sdk/integrations/typer.py b/sentry_sdk/integrations/typer.py index be3d7d1d68..2865f3ee6f 100644 --- a/sentry_sdk/integrations/typer.py +++ b/sentry_sdk/integrations/typer.py @@ -22,6 +22,7 @@ try: import typer + from typer.main import except_hook except ImportError: raise DidNotEnable("Typer not installed") @@ -31,7 +32,7 @@ class TyperIntegration(Integration): @staticmethod def setup_once() -> None: - typer.main.except_hook = _make_excepthook(typer.main.except_hook) # type: ignore + typer.main.except_hook = _make_excepthook(except_hook) # type: ignore def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": diff --git a/sentry_sdk/metrics.py b/sentry_sdk/metrics.py index 30f8191126..167e49da05 100644 --- a/sentry_sdk/metrics.py +++ b/sentry_sdk/metrics.py @@ -1,8 +1,3 @@ -""" -NOTE: This file contains experimental code that may be changed or removed at any -time without prior notice. -""" - import time from typing import Any, Optional, TYPE_CHECKING, Union diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 1c3fe884e8..3bc51c1af0 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -9,6 +9,7 @@ from functools import wraps from itertools import chain +import sentry_sdk from sentry_sdk._types import AnnotatedValue from sentry_sdk.attachments import Attachment from sentry_sdk.consts import ( @@ -28,9 +29,11 @@ from sentry_sdk.tracing_utils import ( Baggage, has_tracing_enabled, + has_span_streaming_enabled, normalize_incoming_data, PropagationContext, ) +from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, @@ -220,6 +223,7 @@ class Scope: "_breadcrumbs", "_n_breadcrumbs_truncated", "_gen_ai_original_message_count", + "_gen_ai_conversation_id", "_event_processors", "_error_processors", "_should_capture", @@ -302,6 +306,8 @@ def __copy__(self) -> "Scope": rv._attributes = self._attributes.copy() + rv._gen_ai_conversation_id = self._gen_ai_conversation_id + return rv @classmethod @@ -365,6 +371,26 @@ def get_global_scope(cls) -> "Scope": return _global_scope + def set_global_attributes(self) -> None: + from sentry_sdk.client import SDK_INFO + + self.set_attribute("sentry.sdk.name", SDK_INFO["name"]) + self.set_attribute("sentry.sdk.version", SDK_INFO["version"]) + + options = sentry_sdk.get_client().options + + server_name = options.get("server_name") + if server_name: + self.set_attribute(SPANDATA.SERVER_ADDRESS, server_name) + + environment = options.get("environment") + if environment: + self.set_attribute("sentry.environment", environment) + + release = options.get("release") + if release: + self.set_attribute("sentry.release", release) + @classmethod def last_event_id(cls) -> "Optional[str]": """ @@ -467,7 +493,14 @@ def set_client( If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`. """ - self.client = client if client is not None else NonRecordingClient() + if client is not None: + self.client = client + # We need a client to set the initial global attributes on the global + # scope since they mostly come from client options, so populate them + # as soon as a client is set + sentry_sdk.get_global_scope().set_global_attributes() + else: + self.client = NonRecordingClient() def fork(self) -> "Scope": """ @@ -692,6 +725,8 @@ def clear(self) -> None: self._attributes: "Attributes" = {} + self._gen_ai_conversation_id: "Optional[str]" = None + @_attr_setter def level(self, value: "LogLevelStr") -> None: """ @@ -884,6 +919,26 @@ def remove_extra( """Removes a specific extra key.""" self._extras.pop(key, None) + def set_conversation_id(self, conversation_id: str) -> None: + """ + Sets the conversation ID for gen_ai spans. + + :param conversation_id: The conversation ID to set. + """ + self._gen_ai_conversation_id = conversation_id + + def get_conversation_id(self) -> "Optional[str]": + """ + Gets the conversation ID for gen_ai spans. + + :returns: The conversation ID, or None if not set. + """ + return self._gen_ai_conversation_id + + def remove_conversation_id(self) -> None: + """Removes the conversation ID.""" + self._gen_ai_conversation_id = None + def clear_breadcrumbs(self) -> None: """Clears breadcrumb buffer.""" self._breadcrumbs: "Deque[Breadcrumb]" = deque() @@ -1225,6 +1280,17 @@ def _capture_metric(self, metric: "Optional[Metric]") -> None: client._capture_metric(metric, scope=merged_scope) + def _capture_span(self, span: "Optional[StreamedSpan]") -> None: + if span is None: + return + + client = self.get_client() + if not has_span_streaming_enabled(client.options): + return + + merged_scope = self._merge_scopes() + client._capture_span(span, scope=merged_scope) + def capture_message( self, message: str, @@ -1468,44 +1534,26 @@ def _apply_flags_to_event( {"values": flags} ) - def _apply_global_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric]" - ) -> None: - # TODO: Global stuff like this should just be retrieved at init time and - # put onto the global scope's attributes and then applied to events - # from there - from sentry_sdk.client import SDK_INFO - - attributes = telemetry["attributes"] - - attributes["sentry.sdk.name"] = SDK_INFO["name"] - attributes["sentry.sdk.version"] = SDK_INFO["version"] - - options = self.get_client().options - - server_name = options.get("server_name") - if server_name is not None and SPANDATA.SERVER_ADDRESS not in attributes: - attributes[SPANDATA.SERVER_ADDRESS] = server_name - - environment = options.get("environment") - if environment is not None and "sentry.environment" not in attributes: - attributes["sentry.environment"] = environment - - release = options.get("release") - if release is not None and "sentry.release" not in attributes: - attributes["sentry.release"] = release - def _apply_scope_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric]" + self, telemetry: "Union[Log, Metric, StreamedSpan]" ) -> None: + # TODO: turn Logs, Metrics into actual classes + if isinstance(telemetry, dict): + attributes = telemetry["attributes"] + else: + attributes = telemetry._attributes + for attribute, value in self._attributes.items(): - if attribute not in telemetry["attributes"]: - telemetry["attributes"][attribute] = value + if attribute not in attributes: + attributes[attribute] = value def _apply_user_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric]" + self, telemetry: "Union[Log, Metric, StreamedSpan]" ) -> None: - attributes = telemetry["attributes"] + if isinstance(telemetry, dict): + attributes = telemetry["attributes"] + else: + attributes = telemetry._attributes if not should_send_default_pii() or self._user is None: return @@ -1625,20 +1673,22 @@ def apply_to_event( return event @_disable_capture - def apply_to_telemetry(self, telemetry: "Union[Log, Metric]") -> None: + def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> None: # Attributes-based events and telemetry go through here (logs, metrics, # spansV2) - trace_context = self.get_trace_context() - trace_id = trace_context.get("trace_id") - if telemetry.get("trace_id") is None: - telemetry["trace_id"] = trace_id or "00000000-0000-0000-0000-000000000000" - span_id = trace_context.get("span_id") - if telemetry.get("span_id") is None and span_id: - telemetry["span_id"] = span_id + if not isinstance(telemetry, StreamedSpan): + trace_context = self.get_trace_context() + trace_id = trace_context.get("trace_id") + if telemetry.get("trace_id") is None: + telemetry["trace_id"] = ( + trace_id or "00000000-0000-0000-0000-000000000000" + ) + span_id = trace_context.get("span_id") + if telemetry.get("span_id") is None and span_id: + telemetry["span_id"] = span_id self._apply_scope_attributes_to_telemetry(telemetry) self._apply_user_attributes_to_telemetry(telemetry) - self._apply_global_attributes_to_telemetry(telemetry) def update_from_scope(self, scope: "Scope") -> None: """Update the scope with another scope's data.""" @@ -1668,6 +1718,8 @@ def update_from_scope(self, scope: "Scope") -> None: self._gen_ai_original_message_count.update( scope._gen_ai_original_message_count ) + if scope._gen_ai_conversation_id: + self._gen_ai_conversation_id = scope._gen_ai_conversation_id if scope._span: self._span = scope._span if scope._attachments: diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py new file mode 100644 index 0000000000..a63a9bebb0 --- /dev/null +++ b/sentry_sdk/traces.py @@ -0,0 +1,70 @@ +""" +The API in this file is only meant to be used in span streaming mode. + +You can enable span streaming mode via +sentry_sdk.init(_experiments={"trace_lifecycle": "stream"}). +""" + +import uuid +from typing import TYPE_CHECKING + +from sentry_sdk.utils import format_attribute + +if TYPE_CHECKING: + from typing import Optional + from sentry_sdk._types import Attributes, AttributeValue + + +class StreamedSpan: + """ + A span holds timing information of a block of code. + + Spans can have multiple child spans thus forming a span tree. + + This is the Span First span implementation. The original transaction-based + span implementation lives in tracing.Span. + """ + + __slots__ = ( + "name", + "_attributes", + "_trace_id", + ) + + def __init__( + self, + *, + name: str, + attributes: "Optional[Attributes]" = None, + trace_id: "Optional[str]" = None, + ): + self.name: str = name + self._attributes: "Attributes" = {} + if attributes: + for attribute, value in attributes.items(): + self.set_attribute(attribute, value) + + self._trace_id = trace_id + + def get_attributes(self) -> "Attributes": + return self._attributes + + def set_attribute(self, key: str, value: "AttributeValue") -> None: + self._attributes[key] = format_attribute(value) + + def set_attributes(self, attributes: "Attributes") -> None: + for key, value in attributes.items(): + self.set_attribute(key, value) + + def remove_attribute(self, key: str) -> None: + try: + del self._attributes[key] + except KeyError: + pass + + @property + def trace_id(self) -> str: + if not self._trace_id: + self._trace_id = uuid.uuid4().hex + + return self._trace_id diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index c4b38e4528..a778da7361 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -676,6 +676,17 @@ def finish( self.timestamp = datetime.now(timezone.utc) scope = scope or sentry_sdk.get_current_scope() + + # Copy conversation_id from scope to span data if this is an AI span + conversation_id = scope.get_conversation_id() + if conversation_id: + has_ai_op = SPANDATA.GEN_AI_OPERATION_NAME in self._data + is_ai_span_op = self.op is not None and ( + self.op.startswith("ai.") or self.op.startswith("gen_ai.") + ) + if has_ai_op or is_ai_span_op: + self.set_data("gen_ai.conversation.id", conversation_id) + maybe_create_breadcrumbs_from_span(scope, self) return None diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index f45b849499..c1d6c44535 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -4,7 +4,7 @@ import os import re import sys -from collections.abc import Mapping +from collections.abc import Mapping, MutableMapping from datetime import timedelta from random import Random from urllib.parse import quote, unquote @@ -106,6 +106,13 @@ def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: ) +def has_span_streaming_enabled(options: "Optional[dict[str, Any]]") -> bool: + if options is None: + return False + + return (options.get("_experiments") or {}).get("trace_lifecycle") == "stream" + + @contextlib.contextmanager def record_sql_queries( cursor: "Any", @@ -1285,6 +1292,25 @@ def _should_continue_trace(baggage: "Optional[Baggage]") -> bool: return True +def add_sentry_baggage_to_headers( + headers: "MutableMapping[str, str]", sentry_baggage: str +) -> None: + """Add the Sentry baggage to the headers. + + This function directly mutates the provided headers. The provided sentry_baggage + is appended to the existing baggage. If the baggage already contains Sentry items, + they are stripped out first. + """ + existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "") + stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage) + + separator = "," if len(stripped_existing_baggage) > 0 else "" + + headers[BAGGAGE_HEADER_NAME] = ( + stripped_existing_baggage + separator + sentry_baggage + ) + + # Circular imports from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index cee4fa882b..dcfe55406b 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -340,7 +340,21 @@ def record_loss(reason: str) -> None: try: self._update_rate_limits(response) - if response.status == 429: + if response.status == 413: + size_exceeded_message = ( + "HTTP 413: Event dropped due to exceeded envelope size limit" + ) + response_message = getattr( + response, "data", getattr(response, "content", None) + ) + if response_message is not None: + size_exceeded_message += f" (body: {response_message})" + + logger.error(size_exceeded_message) + self.on_dropped_event("status_413") + record_loss("send_error") + + elif response.status == 429: # if we hit a 429. Something was rate limited but we already # acted on this in `self._update_rate_limits`. Note that we # do not want to record event loss here as we will have recorded diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index c99b81a2f5..2fbca486de 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -4,6 +4,7 @@ import logging import math import os +import copy import random import re import subprocess @@ -2061,6 +2062,25 @@ def format_attribute(val: "Any") -> "AttributeValue": if isinstance(val, (bool, int, float, str)): return val + # Only lists of elements of a single type are supported + list_types: 'dict[type, Literal["string[]", "integer[]", "double[]", "boolean[]"]]' = { + str: "string[]", + int: "integer[]", + float: "double[]", + bool: "boolean[]", + } + + if isinstance(val, (list, tuple)) and not val: + return [] + elif isinstance(val, list): + ty = type(val[0]) + if ty in list_types and all(type(v) is ty for v in val): + return copy.deepcopy(val) + elif isinstance(val, tuple): + ty = type(val[0]) + if ty in list_types and all(type(v) is ty for v in val): + return list(val) + return safe_repr(val) @@ -2075,6 +2095,22 @@ def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue": if isinstance(val, str): return {"value": val, "type": "string"} + if isinstance(val, list): + if not val: + return {"value": [], "type": "string[]"} + + # Only lists of elements of a single type are supported + list_types: 'dict[type, Literal["string[]", "integer[]", "double[]", "boolean[]"]]' = { + str: "string[]", + int: "integer[]", + float: "double[]", + bool: "boolean[]", + } + + ty = type(val[0]) + if ty in list_types and all(type(v) is ty for v in val): + return {"value": val, "type": list_types[ty]} + # Coerce to string if we don't know what to do with the value. This should # never happen as we pre-format early in format_attribute, but let's be safe. return {"value": safe_repr(val), "type": "string"} diff --git a/setup.py b/setup.py index a76dbc00d0..ef682c95b3 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.48.0", + version="2.51.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", diff --git a/tests/conftest.py b/tests/conftest.py index dea36f8bda..1cf29bf977 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,7 @@ import json import os +import asyncio +from urllib.parse import urlparse, parse_qs import socket import warnings import brotli @@ -16,6 +18,12 @@ from werkzeug.wrappers import Request, Response import jsonschema +try: + from starlette.testclient import TestClient + # Catch RuntimeError to prevent the following exception in aws_lambda tests. + # RuntimeError: The starlette.testclient module requires the httpx package to be installed. +except (ImportError, RuntimeError): + TestClient = None try: import gevent @@ -45,9 +53,40 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Optional + from typing import Any, Callable, MutableMapping, Optional from collections.abc import Iterator +try: + from anyio import create_memory_object_stream, create_task_group, EndOfStream + from mcp.types import ( + JSONRPCMessage, + JSONRPCNotification, + JSONRPCRequest, + ) + from mcp.shared.message import SessionMessage + from httpx import ( + ASGITransport, + Request as HttpxRequest, + Response as HttpxResponse, + AsyncByteStream, + AsyncClient, + ) +except ImportError: + create_memory_object_stream = None + create_task_group = None + EndOfStream = None + + JSONRPCMessage = None + JSONRPCNotification = None + JSONRPCRequest = None + SessionMessage = None + + ASGITransport = None + HttpxRequest = None + HttpxResponse = None + AsyncByteStream = None + AsyncClient = None + SENTRY_EVENT_SCHEMA = "./checkouts/data-schemas/relay/event.schema.json" @@ -592,6 +631,363 @@ def suppress_deprecation_warnings(): yield +@pytest.fixture +def get_initialization_payload(): + def inner(request_id: str): + return SessionMessage( # type: ignore + message=JSONRPCMessage( # type: ignore + root=JSONRPCRequest( # type: ignore + jsonrpc="2.0", + id=request_id, + method="initialize", + params={ + "protocolVersion": "2025-11-25", + "capabilities": {}, + "clientInfo": {"name": "test-client", "version": "1.0.0"}, + }, + ) + ) + ) + + return inner + + +@pytest.fixture +def get_initialized_notification_payload(): + def inner(): + return SessionMessage( # type: ignore + message=JSONRPCMessage( # type: ignore + root=JSONRPCNotification( # type: ignore + jsonrpc="2.0", + method="notifications/initialized", + ) + ) + ) + + return inner + + +@pytest.fixture +def get_mcp_command_payload(): + def inner(method: str, params, request_id: str): + return SessionMessage( # type: ignore + message=JSONRPCMessage( # type: ignore + root=JSONRPCRequest( # type: ignore + jsonrpc="2.0", + id=request_id, + method=method, + params=params, + ) + ) + ) + + return inner + + +@pytest.fixture +def stdio( + get_initialization_payload, + get_initialized_notification_payload, + get_mcp_command_payload, +): + async def inner(server, method: str, params, request_id: str | None = None): + if request_id is None: + request_id = "1" + + read_stream_writer, read_stream = create_memory_object_stream(0) # type: ignore + write_stream, write_stream_reader = create_memory_object_stream(0) # type: ignore + + result = {} + + async def run_server(): + await server.run( + read_stream, write_stream, server.create_initialization_options() + ) + + async def simulate_client(tg, result): + init_request = get_initialization_payload("1") + await read_stream_writer.send(init_request) + + await write_stream_reader.receive() + + initialized_notification = get_initialized_notification_payload() + await read_stream_writer.send(initialized_notification) + + request = get_mcp_command_payload( + method, params=params, request_id=request_id + ) + await read_stream_writer.send(request) + + result["response"] = await write_stream_reader.receive() + + tg.cancel_scope.cancel() + + async with create_task_group() as tg: # type: ignore + tg.start_soon(run_server) + tg.start_soon(simulate_client, tg, result) + + return result["response"] + + return inner + + +@pytest.fixture() +def json_rpc(): + def inner(app, method: str, params, request_id: str): + with TestClient(app) as client: # type: ignore + init_response = client.post( + "/mcp/", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + }, + json={ + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "clientInfo": {"name": "test-client", "version": "1.0"}, + "protocolVersion": "2025-11-25", + "capabilities": {}, + }, + "id": request_id, + }, + ) + + session_id = init_response.headers["mcp-session-id"] + + # Notification response is mandatory. + # https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle + client.post( + "/mcp/", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + "mcp-session-id": session_id, + }, + json={ + "jsonrpc": "2.0", + "method": "notifications/initialized", + "params": {}, + }, + ) + + response = client.post( + "/mcp/", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + "mcp-session-id": session_id, + }, + json={ + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": request_id, + }, + ) + + return session_id, response + + return inner + + +@pytest.fixture() +def select_mcp_transactions(): + def inner(events): + return [ + event + for event in events + if event["type"] == "transaction" + and event["contexts"]["trace"]["op"] == "mcp.server" + ] + + return inner + + +@pytest.fixture() +def json_rpc_sse(): + class StreamingASGITransport(ASGITransport): + """ + Simple transport whose only purpose is to keep GET request alive in SSE connections, allowing + tests involving SSE interactions to run in-process. + """ + + def __init__( + self, + app: "Callable", + keep_sse_alive: "asyncio.Event", + ) -> None: + self.keep_sse_alive = keep_sse_alive + super().__init__(app) + + async def handle_async_request( + self, request: "HttpxRequest" + ) -> "HttpxResponse": + scope = { + "type": "http", + "method": request.method, + "headers": [(k.lower(), v) for (k, v) in request.headers.raw], + "path": request.url.path, + "query_string": request.url.query, + } + + is_streaming_sse = scope["method"] == "GET" and scope["path"] == "/sse" + if not is_streaming_sse: + return await super().handle_async_request(request) + + request_body = b"" + if request.content: + request_body = await request.aread() + + body_sender, body_receiver = create_memory_object_stream[bytes](0) # type: ignore + + async def receive() -> "dict[str, Any]": + if self.keep_sse_alive.is_set(): + return {"type": "http.disconnect"} + + await self.keep_sse_alive.wait() # Keep alive :) + return { + "type": "http.request", + "body": request_body, + "more_body": False, + } + + async def send(message: "MutableMapping[str, Any]") -> None: + if message["type"] == "http.response.body": + body = message.get("body", b"") + more_body = message.get("more_body", False) + + if body == b"" and not more_body: + return + + if body: + await body_sender.send(body) + + if not more_body: + await body_sender.aclose() + + async def run_app(): + await self.app(scope, receive, send) + + class StreamingBodyStream(AsyncByteStream): # type: ignore + def __init__(self, receiver): + self.receiver = receiver + + async def __aiter__(self): + try: + async for chunk in self.receiver: + yield chunk + except EndOfStream: # type: ignore + pass + + stream = StreamingBodyStream(body_receiver) + response = HttpxResponse(status_code=200, headers=[], stream=stream) # type: ignore + + asyncio.create_task(run_app()) + return response + + def parse_sse_data_package(sse_chunk): + sse_text = sse_chunk.decode("utf-8") + json_str = sse_text.split("data: ")[1] + return json.loads(json_str) + + async def inner( + app, method: str, params, request_id: str, keep_sse_alive: "asyncio.Event" + ): + context = {} + + stream_complete = asyncio.Event() + endpoint_parsed = asyncio.Event() + + # https://github.com/Kludex/starlette/issues/104#issuecomment-729087925 + async with AsyncClient( # type: ignore + transport=StreamingASGITransport(app=app, keep_sse_alive=keep_sse_alive), + base_url="http://test", + ) as client: + + async def parse_stream(): + async with client.stream("GET", "/sse") as stream: + # Read directly from stream.stream instead of aiter_bytes() + async for chunk in stream.stream: + if b"event: endpoint" in chunk: + sse_text = chunk.decode("utf-8") + url = sse_text.split("data: ")[1] + + parsed = urlparse(url) + query_params = parse_qs(parsed.query) + context["session_id"] = query_params["session_id"][0] + endpoint_parsed.set() + continue + + if b"event: message" in chunk and b"structuredContent" in chunk: + context["response"] = parse_sse_data_package(chunk) + break + elif ( + "result" in parse_sse_data_package(chunk) + and "content" in parse_sse_data_package(chunk)["result"] + ): + context["response"] = parse_sse_data_package(chunk) + break + + stream_complete.set() + + task = asyncio.create_task(parse_stream()) + await endpoint_parsed.wait() + + await client.post( + f"/messages/?session_id={context['session_id']}", + headers={ + "Content-Type": "application/json", + }, + json={ + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "clientInfo": {"name": "test-client", "version": "1.0"}, + "protocolVersion": "2025-11-25", + "capabilities": {}, + }, + "id": request_id, + }, + ) + + # Notification response is mandatory. + # https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle + await client.post( + f"/messages/?session_id={context['session_id']}", + headers={ + "Content-Type": "application/json", + "mcp-session-id": context["session_id"], + }, + json={ + "jsonrpc": "2.0", + "method": "notifications/initialized", + "params": {}, + }, + ) + + await client.post( + f"/messages/?session_id={context['session_id']}", + headers={ + "Content-Type": "application/json", + "mcp-session-id": context["session_id"], + }, + json={ + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": request_id, + }, + ) + + await stream_complete.wait() + keep_sse_alive.set() + + return task, context["session_id"], context["response"] + + return inner + + class MockServerRequestHandler(BaseHTTPRequestHandler): def do_GET(self): # noqa: N802 # Process an HTTP GET request and return a response. diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 2204505d47..84d773e129 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -42,12 +42,15 @@ async def __call__(self, *args, **kwargs): from anthropic.types.content_block import ContentBlock as TextBlock from sentry_sdk import start_transaction, start_span +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations.anthropic import ( AnthropicIntegration, _set_output_data, _collect_ai_data, + _transform_anthropic_content_block, ) +from sentry_sdk.ai.utils import transform_content_part, transform_message_content from sentry_sdk.utils import package_version @@ -850,10 +853,11 @@ def test_collect_ai_data_with_input_json_delta(): output_tokens = 20 content_blocks = [] - model, new_input_tokens, new_output_tokens, new_content_blocks = _collect_ai_data( - event, model, input_tokens, output_tokens, content_blocks + model, new_input_tokens, new_output_tokens, _, _, new_content_blocks = ( + _collect_ai_data( + event, model, input_tokens, output_tokens, 0, 0, content_blocks + ) ) - assert model is None assert new_input_tokens == input_tokens assert new_output_tokens == output_tokens @@ -881,6 +885,8 @@ def test_set_output_data_with_input_json_delta(sentry_init): model="", input_tokens=10, output_tokens=20, + cache_read_input_tokens=0, + cache_write_input_tokens=0, content_blocks=[{"text": "".join(json_deltas), "type": "text"}], ) @@ -893,7 +899,25 @@ def test_set_output_data_with_input_json_delta(sentry_init): assert span._data.get(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS) == 30 -def test_anthropic_message_role_mapping(sentry_init, capture_events): +# Test messages with mixed roles including "ai" that should be mapped to "assistant" +@pytest.mark.parametrize( + "test_message,expected_role", + [ + ({"role": "system", "content": "You are helpful."}, "system"), + ({"role": "user", "content": "Hello"}, "user"), + ( + {"role": "ai", "content": "Hi there!"}, + "assistant", + ), # Should be mapped to "assistant" + ( + {"role": "assistant", "content": "How can I help?"}, + "assistant", + ), # Should stay "assistant" + ], +) +def test_anthropic_message_role_mapping( + sentry_init, capture_events, test_message, expected_role +): """Test that Anthropic integration properly maps message roles like 'ai' to 'assistant'""" sentry_init( integrations=[AnthropicIntegration(include_prompts=True)], @@ -918,13 +942,7 @@ def mock_messages_create(*args, **kwargs): client.messages._post = mock.Mock(return_value=mock_messages_create()) - # Test messages with mixed roles including "ai" that should be mapped to "assistant" - test_messages = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - {"role": "ai", "content": "Hi there!"}, # Should be mapped to "assistant" - {"role": "assistant", "content": "How can I help?"}, # Should stay "assistant" - ] + test_messages = [test_message] with start_transaction(name="anthropic tx"): client.messages.create( @@ -942,22 +960,7 @@ def mock_messages_create(*args, **kwargs): # Parse the stored messages stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) - # Verify that "ai" role was mapped to "assistant" - assert len(stored_messages) == 4 - assert stored_messages[0]["role"] == "system" - assert stored_messages[1]["role"] == "user" - assert ( - stored_messages[2]["role"] == "assistant" - ) # "ai" should be mapped to "assistant" - assert stored_messages[3]["role"] == "assistant" # should stay "assistant" - - # Verify content is preserved - assert stored_messages[2]["content"] == "Hi there!" - assert stored_messages[3]["content"] == "How can I help?" - - # Verify no "ai" roles remain - roles = [msg["role"] for msg in stored_messages] - assert "ai" not in roles + assert stored_messages[0]["role"] == expected_role def test_anthropic_message_truncation(sentry_init, capture_events): @@ -1004,9 +1007,62 @@ def test_anthropic_message_truncation(sentry_init, capture_events): parsed_messages = json.loads(messages_data) assert isinstance(parsed_messages, list) - assert len(parsed_messages) == 2 - assert "small message 4" in str(parsed_messages[0]) - assert "small message 5" in str(parsed_messages[1]) + assert len(parsed_messages) == 1 + assert "small message 5" in str(parsed_messages[0]) + + assert chat_span["data"][SPANDATA.META_GEN_AI_ORIGINAL_INPUT_MESSAGES_LENGTH] == 5 + assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5 + + +@pytest.mark.asyncio +async def test_anthropic_message_truncation_async(sentry_init, capture_events): + """Test that large messages are truncated properly in Anthropic integration.""" + sentry_init( + integrations=[AnthropicIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + client = AsyncAnthropic(api_key="z") + client.messages._post = mock.AsyncMock(return_value=EXAMPLE_MESSAGE) + + large_content = ( + "This is a very long message that will exceed our size limits. " * 1000 + ) + messages = [ + {"role": "user", "content": "small message 1"}, + {"role": "assistant", "content": large_content}, + {"role": "user", "content": large_content}, + {"role": "assistant", "content": "small message 4"}, + {"role": "user", "content": "small message 5"}, + ] + + with start_transaction(): + await client.messages.create(max_tokens=1024, messages=messages, model="model") + + assert len(events) > 0 + tx = events[0] + assert tx["type"] == "transaction" + + chat_spans = [ + span for span in tx.get("spans", []) if span.get("op") == OP.GEN_AI_CHAT + ] + assert len(chat_spans) > 0 + + chat_span = chat_spans[0] + assert chat_span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat" + assert SPANDATA.GEN_AI_REQUEST_MESSAGES in chat_span["data"] + + messages_data = chat_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + assert isinstance(messages_data, str) + + parsed_messages = json.loads(messages_data) + assert isinstance(parsed_messages, list) + assert len(parsed_messages) == 1 + assert "small message 5" in str(parsed_messages[0]) + + assert chat_span["data"][SPANDATA.META_GEN_AI_ORIGINAL_INPUT_MESSAGES_LENGTH] == 5 assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5 @@ -1068,17 +1124,22 @@ def test_nonstreaming_create_message_with_system_prompt( assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"] + system_instructions = json.loads( + span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS] + ) + assert system_instructions == [ + {"type": "text", "content": "You are a helpful assistant."} + ] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"] stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) - assert len(stored_messages) == 2 - # System message should be first - assert stored_messages[0]["role"] == "system" - assert stored_messages[0]["content"] == "You are a helpful assistant." - # User message should be second - assert stored_messages[1]["role"] == "user" - assert stored_messages[1]["content"] == "Hello, Claude" + assert len(stored_messages) == 1 + assert stored_messages[0]["role"] == "user" + assert stored_messages[0]["content"] == "Hello, Claude" assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude." else: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] @@ -1147,17 +1208,22 @@ async def test_nonstreaming_create_message_with_system_prompt_async( assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"] + system_instructions = json.loads( + span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS] + ) + assert system_instructions == [ + {"type": "text", "content": "You are a helpful assistant."} + ] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"] stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) - assert len(stored_messages) == 2 - # System message should be first - assert stored_messages[0]["role"] == "system" - assert stored_messages[0]["content"] == "You are a helpful assistant." - # User message should be second - assert stored_messages[1]["role"] == "user" - assert stored_messages[1]["content"] == "Hello, Claude" + assert len(stored_messages) == 1 + assert stored_messages[0]["role"] == "user" + assert stored_messages[0]["content"] == "Hello, Claude" assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude." else: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] @@ -1258,18 +1324,23 @@ def test_streaming_create_message_with_system_prompt( assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"] + system_instructions = json.loads( + span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS] + ) + assert system_instructions == [ + {"type": "text", "content": "You are a helpful assistant."} + ] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"] stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) - assert len(stored_messages) == 2 - # System message should be first - assert stored_messages[0]["role"] == "system" - assert stored_messages[0]["content"] == "You are a helpful assistant." - # User message should be second - assert stored_messages[1]["role"] == "user" - assert stored_messages[1]["content"] == "Hello, Claude" + assert len(stored_messages) == 1 + assert stored_messages[0]["role"] == "user" + assert stored_messages[0]["content"] == "Hello, Claude" assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!" else: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] @@ -1373,18 +1444,23 @@ async def test_streaming_create_message_with_system_prompt_async( assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"] + system_instructions = json.loads( + span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS] + ) + assert system_instructions == [ + {"type": "text", "content": "You are a helpful assistant."} + ] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"] stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) - assert len(stored_messages) == 2 - # System message should be first - assert stored_messages[0]["role"] == "system" - assert stored_messages[0]["content"] == "You are a helpful assistant." - # User message should be second - assert stored_messages[1]["role"] == "user" - assert stored_messages[1]["content"] == "Hello, Claude" + assert len(stored_messages) == 1 + assert stored_messages[0]["role"] == "user" + assert stored_messages[0]["content"] == "Hello, Claude" assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!" else: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] @@ -1431,18 +1507,805 @@ def test_system_prompt_with_complex_structure(sentry_init, capture_events): (span,) = event["spans"] assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat" + + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"] + system_instructions = json.loads(span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) + + # System content should be a list of text blocks + assert isinstance(system_instructions, list) + assert system_instructions == [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"] stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) - # Should have system message first, then user message - assert len(stored_messages) == 2 - assert stored_messages[0]["role"] == "system" - # System content should be a list of text blocks - assert isinstance(stored_messages[0]["content"], list) - assert len(stored_messages[0]["content"]) == 2 - assert stored_messages[0]["content"][0]["type"] == "text" - assert stored_messages[0]["content"][0]["text"] == "You are a helpful assistant." - assert stored_messages[0]["content"][1]["type"] == "text" - assert stored_messages[0]["content"][1]["text"] == "Be concise and clear." - assert stored_messages[1]["role"] == "user" - assert stored_messages[1]["content"] == "Hello" + assert len(stored_messages) == 1 + assert stored_messages[0]["role"] == "user" + assert stored_messages[0]["content"] == "Hello" + + +# Tests for transform_content_part (shared) and _transform_anthropic_content_block helper functions + + +def test_transform_content_part_anthropic_base64_image(): + """Test that base64 encoded images are transformed to blob format.""" + content_block = { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "base64encodeddata...", + }, + } + + result = transform_content_part(content_block) + + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "base64encodeddata...", + } + + +def test_transform_content_part_anthropic_url_image(): + """Test that URL-referenced images are transformed to uri format.""" + content_block = { + "type": "image", + "source": { + "type": "url", + "url": "https://example.com/image.jpg", + }, + } + + result = transform_content_part(content_block) + + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "https://example.com/image.jpg", + } + + +def test_transform_content_part_anthropic_file_image(): + """Test that file_id-referenced images are transformed to file format.""" + content_block = { + "type": "image", + "source": { + "type": "file", + "file_id": "file_abc123", + }, + } + + result = transform_content_part(content_block) + + assert result == { + "type": "file", + "modality": "image", + "mime_type": "", + "file_id": "file_abc123", + } + + +def test_transform_content_part_anthropic_base64_document(): + """Test that base64 encoded PDFs are transformed to blob format.""" + content_block = { + "type": "document", + "source": { + "type": "base64", + "media_type": "application/pdf", + "data": "base64encodedpdfdata...", + }, + } + + result = transform_content_part(content_block) + + assert result == { + "type": "blob", + "modality": "document", + "mime_type": "application/pdf", + "content": "base64encodedpdfdata...", + } + + +def test_transform_content_part_anthropic_url_document(): + """Test that URL-referenced documents are transformed to uri format.""" + content_block = { + "type": "document", + "source": { + "type": "url", + "url": "https://example.com/document.pdf", + }, + } + + result = transform_content_part(content_block) + + assert result == { + "type": "uri", + "modality": "document", + "mime_type": "", + "uri": "https://example.com/document.pdf", + } + + +def test_transform_content_part_anthropic_file_document(): + """Test that file_id-referenced documents are transformed to file format.""" + content_block = { + "type": "document", + "source": { + "type": "file", + "file_id": "file_doc456", + "media_type": "application/pdf", + }, + } + + result = transform_content_part(content_block) + + assert result == { + "type": "file", + "modality": "document", + "mime_type": "application/pdf", + "file_id": "file_doc456", + } + + +def test_transform_anthropic_content_block_text_document(): + """Test that plain text documents are transformed correctly (Anthropic-specific).""" + content_block = { + "type": "document", + "source": { + "type": "text", + "media_type": "text/plain", + "data": "This is plain text content.", + }, + } + + # Use Anthropic-specific helper for text-type documents + result = _transform_anthropic_content_block(content_block) + + assert result == { + "type": "text", + "text": "This is plain text content.", + } + + +def test_transform_content_part_text_block(): + """Test that regular text blocks return None (not transformed).""" + content_block = { + "type": "text", + "text": "Hello, world!", + } + + # Shared transform_content_part returns None for text blocks + result = transform_content_part(content_block) + + assert result is None + + +def test_transform_message_content_string(): + """Test that string content is returned as-is.""" + result = transform_message_content("Hello, world!") + assert result == "Hello, world!" + + +def test_transform_message_content_list_anthropic(): + """Test that list content with Anthropic format is transformed correctly.""" + content = [ + {"type": "text", "text": "Hello!"}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "base64data...", + }, + }, + ] + + result = transform_message_content(content) + + assert len(result) == 2 + # Text block stays as-is (transform returns None, keeps original) + assert result[0] == {"type": "text", "text": "Hello!"} + assert result[1] == { + "type": "blob", + "modality": "image", + "mime_type": "image/png", + "content": "base64data...", + } + + +# Integration tests for binary data in messages + + +def test_message_with_base64_image(sentry_init, capture_events): + """Test that messages with base64 images are properly captured.""" + sentry_init( + integrations=[AnthropicIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + client = Anthropic(api_key="z") + client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE) + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "base64encodeddatahere...", + }, + }, + ], + } + ] + + with start_transaction(name="anthropic"): + client.messages.create(max_tokens=1024, messages=messages, model="model") + + assert len(events) == 1 + (event,) = events + (span,) = event["spans"] + + assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"] + stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + + assert len(stored_messages) == 1 + assert stored_messages[0]["role"] == "user" + content = stored_messages[0]["content"] + assert len(content) == 2 + assert content[0] == {"type": "text", "text": "What's in this image?"} + assert content[1] == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": BLOB_DATA_SUBSTITUTE, + } + + +def test_message_with_url_image(sentry_init, capture_events): + """Test that messages with URL-referenced images are properly captured.""" + sentry_init( + integrations=[AnthropicIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + client = Anthropic(api_key="z") + client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE) + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Describe this image."}, + { + "type": "image", + "source": { + "type": "url", + "url": "https://example.com/photo.png", + }, + }, + ], + } + ] + + with start_transaction(name="anthropic"): + client.messages.create(max_tokens=1024, messages=messages, model="model") + + assert len(events) == 1 + (event,) = events + (span,) = event["spans"] + + stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + content = stored_messages[0]["content"] + assert content[1] == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "https://example.com/photo.png", + } + + +def test_message_with_file_image(sentry_init, capture_events): + """Test that messages with file_id-referenced images are properly captured.""" + sentry_init( + integrations=[AnthropicIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + client = Anthropic(api_key="z") + client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE) + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What do you see?"}, + { + "type": "image", + "source": { + "type": "file", + "file_id": "file_img_12345", + "media_type": "image/webp", + }, + }, + ], + } + ] + + with start_transaction(name="anthropic"): + client.messages.create(max_tokens=1024, messages=messages, model="model") + + assert len(events) == 1 + (event,) = events + (span,) = event["spans"] + + stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + content = stored_messages[0]["content"] + assert content[1] == { + "type": "file", + "modality": "image", + "mime_type": "image/webp", + "file_id": "file_img_12345", + } + + +def test_message_with_base64_pdf(sentry_init, capture_events): + """Test that messages with base64-encoded PDF documents are properly captured.""" + sentry_init( + integrations=[AnthropicIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + client = Anthropic(api_key="z") + client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE) + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Summarize this document."}, + { + "type": "document", + "source": { + "type": "base64", + "media_type": "application/pdf", + "data": "JVBERi0xLjQKJeLj...base64pdfdata", + }, + }, + ], + } + ] + + with start_transaction(name="anthropic"): + client.messages.create(max_tokens=1024, messages=messages, model="model") + + assert len(events) == 1 + (event,) = events + (span,) = event["spans"] + + stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + content = stored_messages[0]["content"] + assert content[1] == { + "type": "blob", + "modality": "document", + "mime_type": "application/pdf", + "content": BLOB_DATA_SUBSTITUTE, + } + + +def test_message_with_url_pdf(sentry_init, capture_events): + """Test that messages with URL-referenced PDF documents are properly captured.""" + sentry_init( + integrations=[AnthropicIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + client = Anthropic(api_key="z") + client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE) + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What is in this PDF?"}, + { + "type": "document", + "source": { + "type": "url", + "url": "https://example.com/report.pdf", + }, + }, + ], + } + ] + + with start_transaction(name="anthropic"): + client.messages.create(max_tokens=1024, messages=messages, model="model") + + assert len(events) == 1 + (event,) = events + (span,) = event["spans"] + + stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + content = stored_messages[0]["content"] + assert content[1] == { + "type": "uri", + "modality": "document", + "mime_type": "", + "uri": "https://example.com/report.pdf", + } + + +def test_message_with_file_document(sentry_init, capture_events): + """Test that messages with file_id-referenced documents are properly captured.""" + sentry_init( + integrations=[AnthropicIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + client = Anthropic(api_key="z") + client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE) + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Analyze this document."}, + { + "type": "document", + "source": { + "type": "file", + "file_id": "file_doc_67890", + "media_type": "application/pdf", + }, + }, + ], + } + ] + + with start_transaction(name="anthropic"): + client.messages.create(max_tokens=1024, messages=messages, model="model") + + assert len(events) == 1 + (event,) = events + (span,) = event["spans"] + + stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + content = stored_messages[0]["content"] + assert content[1] == { + "type": "file", + "modality": "document", + "mime_type": "application/pdf", + "file_id": "file_doc_67890", + } + + +def test_message_with_mixed_content(sentry_init, capture_events): + """Test that messages with mixed content (text, images, documents) are properly captured.""" + sentry_init( + integrations=[AnthropicIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + client = Anthropic(api_key="z") + client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE) + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Compare this image with the document."}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "iVBORw0KGgo...base64imagedata", + }, + }, + { + "type": "image", + "source": { + "type": "url", + "url": "https://example.com/comparison.jpg", + }, + }, + { + "type": "document", + "source": { + "type": "base64", + "media_type": "application/pdf", + "data": "JVBERi0xLjQK...base64pdfdata", + }, + }, + {"type": "text", "text": "Please provide a detailed analysis."}, + ], + } + ] + + with start_transaction(name="anthropic"): + client.messages.create(max_tokens=1024, messages=messages, model="model") + + assert len(events) == 1 + (event,) = events + (span,) = event["spans"] + + stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + content = stored_messages[0]["content"] + + assert len(content) == 5 + assert content[0] == { + "type": "text", + "text": "Compare this image with the document.", + } + assert content[1] == { + "type": "blob", + "modality": "image", + "mime_type": "image/png", + "content": BLOB_DATA_SUBSTITUTE, + } + assert content[2] == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "https://example.com/comparison.jpg", + } + assert content[3] == { + "type": "blob", + "modality": "document", + "mime_type": "application/pdf", + "content": BLOB_DATA_SUBSTITUTE, + } + assert content[4] == { + "type": "text", + "text": "Please provide a detailed analysis.", + } + + +def test_message_with_multiple_images_different_formats(sentry_init, capture_events): + """Test that messages with multiple images of different source types are handled.""" + sentry_init( + integrations=[AnthropicIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + client = Anthropic(api_key="z") + client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE) + + messages = [ + { + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "base64data1...", + }, + }, + { + "type": "image", + "source": { + "type": "url", + "url": "https://example.com/img2.gif", + }, + }, + { + "type": "image", + "source": { + "type": "file", + "file_id": "file_img_789", + "media_type": "image/webp", + }, + }, + {"type": "text", "text": "Compare these three images."}, + ], + } + ] + + with start_transaction(name="anthropic"): + client.messages.create(max_tokens=1024, messages=messages, model="model") + + assert len(events) == 1 + (event,) = events + (span,) = event["spans"] + + stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + content = stored_messages[0]["content"] + + assert len(content) == 4 + assert content[0] == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": BLOB_DATA_SUBSTITUTE, + } + assert content[1] == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "https://example.com/img2.gif", + } + assert content[2] == { + "type": "file", + "modality": "image", + "mime_type": "image/webp", + "file_id": "file_img_789", + } + assert content[3] == {"type": "text", "text": "Compare these three images."} + + +def test_binary_content_not_stored_when_pii_disabled(sentry_init, capture_events): + """Test that binary content is not stored when send_default_pii is False.""" + sentry_init( + integrations=[AnthropicIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=False, + ) + events = capture_events() + client = Anthropic(api_key="z") + client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE) + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "base64encodeddatahere...", + }, + }, + ], + } + ] + + with start_transaction(name="anthropic"): + client.messages.create(max_tokens=1024, messages=messages, model="model") + + assert len(events) == 1 + (event,) = events + (span,) = event["spans"] + + # Messages should not be stored + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + + +def test_binary_content_not_stored_when_prompts_disabled(sentry_init, capture_events): + """Test that binary content is not stored when include_prompts is False.""" + sentry_init( + integrations=[AnthropicIntegration(include_prompts=False)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + client = Anthropic(api_key="z") + client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE) + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "base64encodeddatahere...", + }, + }, + ], + } + ] + + with start_transaction(name="anthropic"): + client.messages.create(max_tokens=1024, messages=messages, model="model") + + assert len(events) == 1 + (event,) = events + (span,) = event["spans"] + + # Messages should not be stored + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + + +def test_cache_tokens_nonstreaming(sentry_init, capture_events): + """Test cache read/write tokens are tracked for non-streaming responses.""" + sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) + events = capture_events() + client = Anthropic(api_key="z") + + client.messages._post = mock.Mock( + return_value=Message( + id="id", + model="claude-3-5-sonnet-20241022", + role="assistant", + content=[TextBlock(type="text", text="Response")], + type="message", + usage=Usage( + input_tokens=100, + output_tokens=50, + cache_read_input_tokens=80, + cache_creation_input_tokens=20, + ), + ) + ) + + with start_transaction(name="anthropic"): + client.messages.create( + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], + model="claude-3-5-sonnet-20241022", + ) + + (span,) = events[0]["spans"] + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80 + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20 + + +def test_cache_tokens_streaming(sentry_init, capture_events): + """Test cache tokens are tracked for streaming responses.""" + client = Anthropic(api_key="z") + returned_stream = Stream(cast_to=None, response=None, client=client) + returned_stream._iterator = [ + MessageStartEvent( + type="message_start", + message=Message( + id="id", + model="claude-3-5-sonnet-20241022", + role="assistant", + content=[], + type="message", + usage=Usage( + input_tokens=100, + output_tokens=0, + cache_read_input_tokens=80, + cache_creation_input_tokens=20, + ), + ), + ), + MessageDeltaEvent( + type="message_delta", + delta=Delta(stop_reason="end_turn"), + usage=MessageDeltaUsage(output_tokens=10), + ), + ] + + sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) + events = capture_events() + client.messages._post = mock.Mock(return_value=returned_stream) + + with start_transaction(name="anthropic"): + for _ in client.messages.create( + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], + model="claude-3-5-sonnet-20241022", + stream=True, + ): + pass + + (span,) = events[0]["spans"] + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80 + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20 diff --git a/tests/integrations/arq/test_arq.py b/tests/integrations/arq/test_arq.py index d8b7e715f2..177f047101 100644 --- a/tests/integrations/arq/test_arq.py +++ b/tests/integrations/arq/test_arq.py @@ -131,9 +131,58 @@ def inner( return inner +@pytest.fixture +def init_arq_with_kwarg_settings(sentry_init): + """Test fixture that passes settings_cls as keyword argument only.""" + + def inner( + cls_functions=None, + cls_cron_jobs=None, + kw_functions=None, + kw_cron_jobs=None, + allow_abort_jobs_=False, + ): + cls_functions = cls_functions or [] + cls_cron_jobs = cls_cron_jobs or [] + + kwargs = {} + if kw_functions is not None: + kwargs["functions"] = kw_functions + if kw_cron_jobs is not None: + kwargs["cron_jobs"] = kw_cron_jobs + + sentry_init( + integrations=[ArqIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + ) + + server = FakeRedis() + pool = ArqRedis(pool_or_conn=server.connection_pool) + + class WorkerSettings: + functions = cls_functions + cron_jobs = cls_cron_jobs + redis_pool = pool + allow_abort_jobs = allow_abort_jobs_ + + if not WorkerSettings.functions: + del WorkerSettings.functions + if not WorkerSettings.cron_jobs: + del WorkerSettings.cron_jobs + + # Pass settings_cls as keyword argument (not positional) + worker = arq.worker.create_worker(settings_cls=WorkerSettings, **kwargs) + + return pool, worker + + return inner + + @pytest.mark.asyncio @pytest.mark.parametrize( - "init_arq_settings", ["init_arq", "init_arq_with_dict_settings"] + "init_arq_settings", + ["init_arq", "init_arq_with_dict_settings", "init_arq_with_kwarg_settings"], ) async def test_job_result(init_arq_settings, request): async def increase(ctx, num): diff --git a/tests/integrations/asyncio/test_asyncio.py b/tests/integrations/asyncio/test_asyncio.py index 11b60fb0e1..b41aa244cb 100644 --- a/tests/integrations/asyncio/test_asyncio.py +++ b/tests/integrations/asyncio/test_asyncio.py @@ -7,7 +7,11 @@ import sentry_sdk from sentry_sdk.consts import OP -from sentry_sdk.integrations.asyncio import AsyncioIntegration, patch_asyncio +from sentry_sdk.integrations.asyncio import ( + AsyncioIntegration, + patch_asyncio, + enable_asyncio_integration, +) try: from contextvars import Context, ContextVar @@ -229,6 +233,7 @@ def test_patch_asyncio(mock_get_running_loop): Test that the patch_asyncio function will patch the task factory. """ mock_loop = mock_get_running_loop.return_value + mock_loop.get_task_factory.return_value._is_sentry_task_factory = False patch_asyncio() @@ -278,6 +283,7 @@ def test_sentry_task_factory_with_factory(mock_get_running_loop): # The original task factory will be mocked out here, let's retrieve the value for later orig_task_factory = mock_loop.get_task_factory.return_value + orig_task_factory._is_sentry_task_factory = False # Retieve sentry task factory (since it is an inner function within patch_asyncio) sentry_task_factory = get_sentry_task_factory(mock_get_running_loop) @@ -340,6 +346,7 @@ def test_sentry_task_factory_context_with_factory(mock_get_running_loop): # The original task factory will be mocked out here, let's retrieve the value for later orig_task_factory = mock_loop.get_task_factory.return_value + orig_task_factory._is_sentry_task_factory = False # Retieve sentry task factory (since it is an inner function within patch_asyncio) sentry_task_factory = get_sentry_task_factory(mock_get_running_loop) @@ -386,3 +393,174 @@ async def test_span_origin( assert event["contexts"]["trace"]["origin"] == "manual" assert event["spans"][0]["origin"] == "auto.function.asyncio" + + +@minimum_python_38 +@pytest.mark.asyncio +async def test_task_spans_false( + sentry_init, + capture_events, + uninstall_integration, +): + uninstall_integration("asyncio") + + sentry_init( + traces_sample_rate=1.0, + integrations=[ + AsyncioIntegration(task_spans=False), + ], + ) + + events = capture_events() + + with sentry_sdk.start_transaction(name="test_no_spans"): + tasks = [asyncio.create_task(foo()), asyncio.create_task(bar())] + await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION) + + sentry_sdk.flush() + + (transaction_event,) = events + + assert not transaction_event["spans"] + + +@minimum_python_38 +@pytest.mark.asyncio +async def test_enable_asyncio_integration_with_task_spans_false( + sentry_init, + capture_events, + uninstall_integration, +): + """ + Test that enable_asyncio_integration() helper works with task_spans=False. + """ + uninstall_integration("asyncio") + + sentry_init(traces_sample_rate=1.0) + + assert "asyncio" not in sentry_sdk.get_client().integrations + + enable_asyncio_integration(task_spans=False) + + assert "asyncio" in sentry_sdk.get_client().integrations + assert sentry_sdk.get_client().integrations["asyncio"].task_spans is False + + events = capture_events() + + with sentry_sdk.start_transaction(name="test"): + await asyncio.create_task(foo()) + + sentry_sdk.flush() + + (transaction,) = events + assert not transaction["spans"] + + +@minimum_python_38 +@pytest.mark.asyncio +async def test_delayed_enable_integration(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + + assert "asyncio" not in sentry_sdk.get_client().integrations + + events = capture_events() + + with sentry_sdk.start_transaction(name="test"): + await asyncio.create_task(foo()) + + assert len(events) == 1 + (transaction,) = events + assert not transaction["spans"] + + enable_asyncio_integration() + + events = capture_events() + + assert "asyncio" in sentry_sdk.get_client().integrations + + with sentry_sdk.start_transaction(name="test"): + await asyncio.create_task(foo()) + + assert len(events) == 1 + (transaction,) = events + assert transaction["spans"] + assert transaction["spans"][0]["origin"] == "auto.function.asyncio" + + +@minimum_python_38 +@pytest.mark.asyncio +async def test_delayed_enable_integration_with_options(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + + assert "asyncio" not in sentry_sdk.get_client().integrations + + mock_init = MagicMock(return_value=None) + mock_setup_once = MagicMock() + with patch( + "sentry_sdk.integrations.asyncio.AsyncioIntegration.__init__", mock_init + ): + with patch( + "sentry_sdk.integrations.asyncio.AsyncioIntegration.setup_once", + mock_setup_once, + ): + enable_asyncio_integration("arg", kwarg="kwarg") + + assert "asyncio" in sentry_sdk.get_client().integrations + mock_init.assert_called_once_with("arg", kwarg="kwarg") + mock_setup_once.assert_called_once() + + +@minimum_python_38 +@pytest.mark.asyncio +async def test_delayed_enable_enabled_integration(sentry_init, uninstall_integration): + # Ensure asyncio integration is not already installed from previous tests + uninstall_integration("asyncio") + + integration = AsyncioIntegration() + sentry_init(integrations=[integration], traces_sample_rate=1.0) + + assert "asyncio" in sentry_sdk.get_client().integrations + + # Get the task factory after initial setup - it should be Sentry's + loop = asyncio.get_running_loop() + task_factory_before = loop.get_task_factory() + assert getattr(task_factory_before, "_is_sentry_task_factory", False) is True + + enable_asyncio_integration() + + assert "asyncio" in sentry_sdk.get_client().integrations + + # The task factory should be the same (loop not re-patched) + task_factory_after = loop.get_task_factory() + assert task_factory_before is task_factory_after + + +@minimum_python_38 +@pytest.mark.asyncio +async def test_delayed_enable_integration_after_disabling(sentry_init, capture_events): + sentry_init(disabled_integrations=[AsyncioIntegration()], traces_sample_rate=1.0) + + assert "asyncio" not in sentry_sdk.get_client().integrations + + events = capture_events() + + with sentry_sdk.start_transaction(name="test"): + await asyncio.create_task(foo()) + + assert len(events) == 1 + (transaction,) = events + assert not transaction["spans"] + + enable_asyncio_integration() + + events = capture_events() + + assert "asyncio" in sentry_sdk.get_client().integrations + + with sentry_sdk.start_transaction(name="test"): + await asyncio.create_task(foo()) + + assert len(events) == 1 + (transaction,) = events + assert transaction["spans"] + assert transaction["spans"][0]["origin"] == "auto.function.asyncio" diff --git a/tests/integrations/dramatiq/test_dramatiq.py b/tests/integrations/dramatiq/test_dramatiq.py index 3860ee61d9..a9d3966839 100644 --- a/tests/integrations/dramatiq/test_dramatiq.py +++ b/tests/integrations/dramatiq/test_dramatiq.py @@ -1,15 +1,16 @@ -import pytest import uuid import dramatiq +import pytest from dramatiq.brokers.stub import StubBroker +from dramatiq.middleware import Middleware, SkipMessage import sentry_sdk -from sentry_sdk.tracing import TransactionSource from sentry_sdk import start_transaction from sentry_sdk.consts import SPANSTATUS from sentry_sdk.integrations.dramatiq import DramatiqIntegration from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.tracing import Transaction, TransactionSource ignore_logger("dramatiq.worker.WorkerThread") @@ -386,3 +387,28 @@ def dummy_actor(): worker.join() assert events == [] + + +@pytest.mark.parametrize("broker", [1.0], indirect=True) +def test_that_skip_message_cleans_up_scope_and_transaction( + broker, worker, capture_events +): + transactions: list[Transaction] = [] + + class SkipMessageMiddleware(Middleware): + def before_process_message(self, broker, message): + transactions.append(sentry_sdk.get_current_scope().transaction) + raise SkipMessage() + + broker.add_middleware(SkipMessageMiddleware()) + + @dramatiq.actor(max_retries=0) + def skipped_actor(): ... + + skipped_actor.send() + + broker.join(skipped_actor.queue_name) + worker.join() + + (transaction,) = transactions + assert transaction.timestamp is not None diff --git a/tests/integrations/fastmcp/test_fastmcp.py b/tests/integrations/fastmcp/test_fastmcp.py index ef2a1f9cb7..d32747855a 100644 --- a/tests/integrations/fastmcp/test_fastmcp.py +++ b/tests/integrations/fastmcp/test_fastmcp.py @@ -21,6 +21,7 @@ accurate testing of the integration's behavior in real MCP Server scenarios. """ +import anyio import asyncio import json import pytest @@ -39,6 +40,19 @@ async def __call__(self, *args, **kwargs): from sentry_sdk.consts import SPANDATA, OP from sentry_sdk.integrations.mcp import MCPIntegration +from mcp.server.sse import SseServerTransport +from mcp.server.streamable_http_manager import StreamableHTTPSessionManager + +try: + from fastmcp.prompts import Message +except ImportError: + Message = None + + +from starlette.responses import Response +from starlette.routing import Mount, Route +from starlette.applications import Starlette + # Try to import both FastMCP implementations try: from mcp.server.fastmcp import FastMCP as MCPFastMCP @@ -71,6 +85,10 @@ async def __call__(self, *args, **kwargs): GetPromptRequest = None ReadResourceRequest = None +try: + from fastmcp import __version__ as FASTMCP_VERSION +except ImportError: + FASTMCP_VERSION = None # Collect available FastMCP implementations for parametrization fastmcp_implementations = [] @@ -245,46 +263,19 @@ def reset_request_ctx(): pass -class MockRequestContext: - """Mock MCP request context""" - - def __init__(self, request_id=None, session_id=None, transport="stdio"): - self.request_id = request_id - if transport in ("http", "sse"): - self.request = MockHTTPRequest(session_id, transport) - else: - self.request = None - - -class MockHTTPRequest: - """Mock HTTP request for SSE/StreamableHTTP transport""" - - def __init__(self, session_id=None, transport="http"): - self.headers = {} - self.query_params = {} - - if transport == "sse": - # SSE transport uses query parameter - if session_id: - self.query_params["session_id"] = session_id - else: - # StreamableHTTP transport uses header - if session_id: - self.headers["mcp-session-id"] = session_id - - # ============================================================================= # Tool Handler Tests - Verifying Sentry Integration # ============================================================================= +@pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) @pytest.mark.parametrize( "send_default_pii, include_prompts", [(True, True), (True, False), (False, True), (False, False)], ) -def test_fastmcp_tool_sync( - sentry_init, capture_events, FastMCP, send_default_pii, include_prompts +async def test_fastmcp_tool_sync( + sentry_init, capture_events, FastMCP, send_default_pii, include_prompts, stdio ): """Test that FastMCP synchronous tool handlers create proper spans""" sentry_init( @@ -296,11 +287,6 @@ def test_fastmcp_tool_sync( mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext(request_id="req-123", transport="stdio") - request_ctx.set(mock_ctx) - @mcp.tool() def add_numbers(a: int, b: int) -> dict: """Add two numbers together""" @@ -308,9 +294,20 @@ def add_numbers(a: int, b: int) -> dict: with start_transaction(name="fastmcp tx"): # Call through MCP protocol to trigger instrumentation - result = call_tool_through_mcp(mcp, "add_numbers", {"a": 10, "b": 5}) + result = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "add_numbers", + "arguments": {"a": 10, "b": 5}, + }, + request_id="req-123", + ) - assert result == {"result": 15, "operation": "addition"} + assert json.loads(result.message.root.result["content"][0]["text"]) == { + "result": 15, + "operation": "addition", + } (tx,) = events assert tx["type"] == "transaction" @@ -340,7 +337,13 @@ def add_numbers(a: int, b: int) -> dict: [(True, True), (True, False), (False, True), (False, False)], ) async def test_fastmcp_tool_async( - sentry_init, capture_events, FastMCP, send_default_pii, include_prompts + sentry_init, + capture_events, + FastMCP, + send_default_pii, + include_prompts, + json_rpc, + select_mcp_transactions, ): """Test that FastMCP async tool handlers create proper spans""" sentry_init( @@ -352,49 +355,61 @@ async def test_fastmcp_tool_async( mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext( - request_id="req-456", session_id="session-789", transport="http" - ) - request_ctx.set(mock_ctx) + session_manager = StreamableHTTPSessionManager( + app=mcp._mcp_server, + json_response=True, + ) + + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), + ) @mcp.tool() async def multiply_numbers(x: int, y: int) -> dict: """Multiply two numbers together""" return {"result": x * y, "operation": "multiplication"} - with start_transaction(name="fastmcp tx"): - result = await call_tool_through_mcp_async( - mcp, "multiply_numbers", {"x": 7, "y": 6} - ) + session_id, result = json_rpc( + app, + method="tools/call", + params={ + "name": "multiply_numbers", + "arguments": {"x": 7, "y": 6}, + }, + request_id="req-456", + ) - assert result == {"result": 42, "operation": "multiplication"} + assert json.loads(result.json()["result"]["content"][0]["text"]) == { + "result": 42, + "operation": "multiplication", + } - (tx,) = events - assert tx["type"] == "transaction" - assert len(tx["spans"]) == 1 + transactions = select_mcp_transactions(events) + assert len(transactions) == 1 + tx = transactions[0] - # Verify span structure - span = tx["spans"][0] - assert span["op"] == OP.MCP_SERVER - assert span["origin"] == "auto.ai.mcp" - assert span["description"] == "tools/call multiply_numbers" - assert span["data"][SPANDATA.MCP_TOOL_NAME] == "multiply_numbers" - assert span["data"][SPANDATA.MCP_METHOD_NAME] == "tools/call" - assert span["data"][SPANDATA.MCP_TRANSPORT] == "http" - assert span["data"][SPANDATA.MCP_REQUEST_ID] == "req-456" - assert span["data"][SPANDATA.MCP_SESSION_ID] == "session-789" + assert tx["contexts"]["trace"]["op"] == OP.MCP_SERVER + assert tx["contexts"]["trace"]["origin"] == "auto.ai.mcp" + assert tx["transaction"] == "tools/call multiply_numbers" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_TOOL_NAME] == "multiply_numbers" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_METHOD_NAME] == "tools/call" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_TRANSPORT] == "http" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_REQUEST_ID] == "req-456" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_SESSION_ID] == session_id # Check PII-sensitive data if send_default_pii and include_prompts: - assert SPANDATA.MCP_TOOL_RESULT_CONTENT in span["data"] + assert SPANDATA.MCP_TOOL_RESULT_CONTENT in tx["contexts"]["trace"]["data"] else: - assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in span["data"] + assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in tx["contexts"]["trace"]["data"] +@pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_tool_with_error(sentry_init, capture_events, FastMCP): +async def test_fastmcp_tool_with_error(sentry_init, capture_events, FastMCP, stdio): """Test that FastMCP tool handler errors are captured properly""" sentry_init( integrations=[MCPIntegration()], @@ -404,26 +419,23 @@ def test_fastmcp_tool_with_error(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext(request_id="req-error", transport="stdio") - request_ctx.set(mock_ctx) - @mcp.tool() def failing_tool(value: int) -> int: """A tool that always fails""" raise ValueError("Tool execution failed") with start_transaction(name="fastmcp tx"): - # MCP protocol may raise the error or return it as an error result - try: - result = call_tool_through_mcp(mcp, "failing_tool", {"value": 42}) - # If no exception raised, check if result indicates error - if hasattr(result, "isError"): - assert result.isError is True - except ValueError: - # Error was raised as expected - pass + result = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "failing_tool", + "arguments": {"value": 42}, + }, + request_id="req-error", + ) + # If no exception raised, check if result indicates error + assert result.message.root.result["isError"] is True # Should have transaction and error events assert len(events) >= 1 @@ -443,8 +455,9 @@ def failing_tool(value: int) -> int: assert tool_spans[0]["data"][SPANDATA.MCP_TOOL_RESULT_IS_ERROR] is True +@pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_multiple_tools(sentry_init, capture_events, FastMCP): +async def test_fastmcp_multiple_tools(sentry_init, capture_events, FastMCP, stdio): """Test that multiple FastMCP tool calls create multiple spans""" sentry_init( integrations=[MCPIntegration()], @@ -454,11 +467,6 @@ def test_fastmcp_multiple_tools(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext(request_id="req-multi", transport="stdio") - request_ctx.set(mock_ctx) - @mcp.tool() def tool_one(x: int) -> int: """First tool""" @@ -475,13 +483,43 @@ def tool_three(z: int) -> int: return z - 5 with start_transaction(name="fastmcp tx"): - result1 = call_tool_through_mcp(mcp, "tool_one", {"x": 5}) - result2 = call_tool_through_mcp(mcp, "tool_two", {"y": result1["result"]}) - result3 = call_tool_through_mcp(mcp, "tool_three", {"z": result2["result"]}) + result1 = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "tool_one", + "arguments": {"x": 5}, + }, + request_id="req-multi", + ) + + result2 = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "tool_two", + "arguments": { + "y": int(result1.message.root.result["content"][0]["text"]) + }, + }, + request_id="req-multi", + ) + + result3 = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "tool_three", + "arguments": { + "z": int(result2.message.root.result["content"][0]["text"]) + }, + }, + request_id="req-multi", + ) - assert result1["result"] == 10 - assert result2["result"] == 20 - assert result3["result"] == 15 + assert result1.message.root.result["content"][0]["text"] == "10" + assert result2.message.root.result["content"][0]["text"] == "20" + assert result3.message.root.result["content"][0]["text"] == "15" (tx,) = events assert tx["type"] == "transaction" @@ -494,8 +532,11 @@ def tool_three(z: int) -> int: assert tool_spans[2]["data"][SPANDATA.MCP_TOOL_NAME] == "tool_three" +@pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_tool_with_complex_return(sentry_init, capture_events, FastMCP): +async def test_fastmcp_tool_with_complex_return( + sentry_init, capture_events, FastMCP, stdio +): """Test FastMCP tool with complex nested return value""" sentry_init( integrations=[MCPIntegration(include_prompts=True)], @@ -506,11 +547,6 @@ def test_fastmcp_tool_with_complex_return(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext(request_id="req-complex", transport="stdio") - request_ctx.set(mock_ctx) - @mcp.tool() def get_user_data(user_id: int) -> dict: """Get complex user data""" @@ -522,11 +558,22 @@ def get_user_data(user_id: int) -> dict: } with start_transaction(name="fastmcp tx"): - result = call_tool_through_mcp(mcp, "get_user_data", {"user_id": 123}) + result = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "get_user_data", + "arguments": {"user_id": 123}, + }, + request_id="req-complex", + ) - assert result["id"] == 123 - assert result["name"] == "Alice" - assert result["nested"]["preferences"]["theme"] == "dark" + assert json.loads(result.message.root.result["content"][0]["text"]) == { + "id": 123, + "name": "Alice", + "nested": {"preferences": {"theme": "dark", "notifications": True}}, + "tags": ["admin", "verified"], + } (tx,) = events assert tx["type"] == "transaction" @@ -545,13 +592,14 @@ def get_user_data(user_id: int) -> dict: # ============================================================================= +@pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) @pytest.mark.parametrize( "send_default_pii, include_prompts", [(True, True), (False, False)], ) -def test_fastmcp_prompt_sync( - sentry_init, capture_events, FastMCP, send_default_pii, include_prompts +async def test_fastmcp_prompt_sync( + sentry_init, capture_events, FastMCP, send_default_pii, include_prompts, stdio ): """Test that FastMCP synchronous prompt handlers create proper spans""" sentry_init( @@ -563,60 +611,69 @@ def test_fastmcp_prompt_sync( mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext(request_id="req-prompt", transport="stdio") - request_ctx.set(mock_ctx) - # Try to register a prompt handler (may not be supported in all versions) - try: - if hasattr(mcp, "prompt"): - - @mcp.prompt() - def code_help_prompt(language: str): - """Get help for a programming language""" - return [ - { - "role": "user", - "content": { - "type": "text", - "text": f"Tell me about {language}", - }, - } - ] - - with start_transaction(name="fastmcp tx"): - result = call_prompt_through_mcp( - mcp, "code_help_prompt", {"language": "python"} - ) + if hasattr(mcp, "prompt"): + + @mcp.prompt() + def code_help_prompt(language: str): + """Get help for a programming language""" + message = { + "role": "user", + "content": { + "type": "text", + "text": f"Tell me about {language}", + }, + } + + if FASTMCP_VERSION is not None and FASTMCP_VERSION.startswith("3"): + message = Message(message) + + return [message] + + with start_transaction(name="fastmcp tx"): + result = await stdio( + mcp._mcp_server, + method="prompts/get", + params={ + "name": "code_help_prompt", + "arguments": {"language": "python"}, + }, + request_id="req-prompt", + ) + + assert result.message.root.result["messages"][0]["role"] == "user" + assert ( + "python" + in result.message.root.result["messages"][0]["content"]["text"].lower() + ) - assert result.messages[0].role == "user" - assert "python" in result.messages[0].content.text.lower() + (tx,) = events + assert tx["type"] == "transaction" - (tx,) = events - assert tx["type"] == "transaction" + # Verify prompt span was created + prompt_spans = [s for s in tx["spans"] if s["op"] == OP.MCP_SERVER] + assert len(prompt_spans) == 1 + span = prompt_spans[0] + assert span["origin"] == "auto.ai.mcp" + assert span["description"] == "prompts/get code_help_prompt" + assert span["data"][SPANDATA.MCP_PROMPT_NAME] == "code_help_prompt" - # Verify prompt span was created - prompt_spans = [s for s in tx["spans"] if s["op"] == OP.MCP_SERVER] - assert len(prompt_spans) == 1 - span = prompt_spans[0] - assert span["origin"] == "auto.ai.mcp" - assert span["description"] == "prompts/get code_help_prompt" - assert span["data"][SPANDATA.MCP_PROMPT_NAME] == "code_help_prompt" - - # Check PII-sensitive data - if send_default_pii and include_prompts: - assert SPANDATA.MCP_PROMPT_CONTENT in span["data"] - else: - assert SPANDATA.MCP_PROMPT_CONTENT not in span["data"] - except AttributeError: - # Prompt handler not supported in this version - pytest.skip("Prompt handlers not supported in this FastMCP version") + # Check PII-sensitive data + if send_default_pii and include_prompts: + assert SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT in span["data"] + else: + assert SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT not in span["data"] @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) @pytest.mark.asyncio -async def test_fastmcp_prompt_async(sentry_init, capture_events, FastMCP): +async def test_fastmcp_prompt_async( + sentry_init, + capture_events, + FastMCP, + json_rpc, + select_mcp_transactions, +): """Test that FastMCP async prompt handlers create proper spans""" sentry_init( integrations=[MCPIntegration()], @@ -626,46 +683,57 @@ async def test_fastmcp_prompt_async(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext( - request_id="req-async-prompt", session_id="session-abc", transport="http" - ) - request_ctx.set(mock_ctx) + session_manager = StreamableHTTPSessionManager( + app=mcp._mcp_server, + json_response=True, + ) - # Try to register an async prompt handler - try: - if hasattr(mcp, "prompt"): - - @mcp.prompt() - async def async_prompt(topic: str): - """Get async prompt for a topic""" - return [ - { - "role": "user", - "content": {"type": "text", "text": f"What is {topic}?"}, - }, - { - "role": "assistant", - "content": { - "type": "text", - "text": "Let me explain that", - }, - }, - ] + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), + ) - with start_transaction(name="fastmcp tx"): - result = await call_prompt_through_mcp_async( - mcp, "async_prompt", {"topic": "MCP"} - ) + # Try to register an async prompt handler + if hasattr(mcp, "prompt"): + + @mcp.prompt() + async def async_prompt(topic: str): + """Get async prompt for a topic""" + message1 = { + "role": "user", + "content": {"type": "text", "text": f"What is {topic}?"}, + } + + message2 = { + "role": "assistant", + "content": { + "type": "text", + "text": "Let me explain that", + }, + } + + if FASTMCP_VERSION is not None and FASTMCP_VERSION.startswith("3"): + message1 = Message(message1) + message2 = Message(message2) + + return [message1, message2] + + _, result = json_rpc( + app, + method="prompts/get", + params={ + "name": "async_prompt", + "arguments": {"topic": "MCP"}, + }, + request_id="req-async-prompt", + ) - assert len(result.messages) == 2 + assert len(result.json()["result"]["messages"]) == 2 - (tx,) = events - assert tx["type"] == "transaction" - except AttributeError: - # Prompt handler not supported in this version - pytest.skip("Prompt handlers not supported in this FastMCP version") + transactions = select_mcp_transactions(events) + assert len(transactions) == 1 # ============================================================================= @@ -673,8 +741,9 @@ async def async_prompt(topic: str): # ============================================================================= +@pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_resource_sync(sentry_init, capture_events, FastMCP): +async def test_fastmcp_resource_sync(sentry_init, capture_events, FastMCP, stdio): """Test that FastMCP synchronous resource handlers create proper spans""" sentry_init( integrations=[MCPIntegration()], @@ -684,11 +753,6 @@ def test_fastmcp_resource_sync(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext(request_id="req-resource", transport="stdio") - request_ctx.set(mock_ctx) - # Try to register a resource handler try: if hasattr(mcp, "resource"): @@ -700,7 +764,14 @@ def read_file(path: str): with start_transaction(name="fastmcp tx"): try: - result = call_resource_through_mcp(mcp, "file:///test.txt") + result = await stdio( + mcp._mcp_server, + method="resources/read", + params={ + "uri": "file:///test.txt", + }, + request_id="req-resource", + ) except ValueError as e: # Older FastMCP versions may not support this URI pattern if "Unknown resource" in str(e): @@ -710,7 +781,7 @@ def read_file(path: str): raise # Resource content is returned as-is - assert "file contents" in result.contents[0].text + assert "file contents" in result.message.root.result["contents"][0]["text"] (tx,) = events assert tx["type"] == "transaction" @@ -729,7 +800,13 @@ def read_file(path: str): @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) @pytest.mark.asyncio -async def test_fastmcp_resource_async(sentry_init, capture_events, FastMCP): +async def test_fastmcp_resource_async( + sentry_init, + capture_events, + FastMCP, + json_rpc, + select_mcp_transactions, +): """Test that FastMCP async resource handlers create proper spans""" sentry_init( integrations=[MCPIntegration()], @@ -739,12 +816,17 @@ async def test_fastmcp_resource_async(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext( - request_id="req-async-resource", session_id="session-res", transport="http" - ) - request_ctx.set(mock_ctx) + session_manager = StreamableHTTPSessionManager( + app=mcp._mcp_server, + json_response=True, + ) + + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), + ) # Try to register an async resource handler try: @@ -755,28 +837,31 @@ async def read_url(resource: str): """Read a URL resource""" return "resource data" - with start_transaction(name="fastmcp tx"): - try: - result = await call_resource_through_mcp_async( - mcp, "https://example.com/resource" - ) - except ValueError as e: - # Older FastMCP versions may not support this URI pattern - if "Unknown resource" in str(e): - pytest.skip( - f"Resource URI not supported in this FastMCP version: {e}" - ) - raise - - assert "resource data" in result.contents[0].text - - (tx,) = events - assert tx["type"] == "transaction" - - # Verify span was created - resource_spans = [s for s in tx["spans"] if s["op"] == OP.MCP_SERVER] - assert len(resource_spans) == 1 - assert resource_spans[0]["data"][SPANDATA.MCP_RESOURCE_PROTOCOL] == "https" + _, result = json_rpc( + app, + method="resources/read", + params={ + "uri": "https://example.com/resource", + }, + request_id="req-async-resource", + ) + # Older FastMCP versions may not support this URI pattern + if ( + "error" in result.json() + and "Unknown resource" in result.json()["error"]["message"] + ): + pytest.skip("Resource URI not supported in this FastMCP version.") + return + + assert "resource data" in result.json()["result"]["contents"][0]["text"] + + transactions = select_mcp_transactions(events) + assert len(transactions) == 1 + tx = transactions[0] + assert ( + tx["contexts"]["trace"]["data"][SPANDATA.MCP_RESOURCE_PROTOCOL] + == "https" + ) except (AttributeError, TypeError): # Resource handler not supported in this version pytest.skip("Resource handlers not supported in this FastMCP version") @@ -787,8 +872,9 @@ async def read_url(resource: str): # ============================================================================= +@pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_span_origin(sentry_init, capture_events, FastMCP): +async def test_fastmcp_span_origin(sentry_init, capture_events, FastMCP, stdio): """Test that FastMCP span origin is set correctly""" sentry_init( integrations=[MCPIntegration()], @@ -798,18 +884,21 @@ def test_fastmcp_span_origin(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext(request_id="req-origin", transport="stdio") - request_ctx.set(mock_ctx) - @mcp.tool() def test_tool(value: int) -> int: """Test tool for origin checking""" return value * 2 with start_transaction(name="fastmcp tx"): - call_tool_through_mcp(mcp, "test_tool", {"value": 21}) + await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "test_tool", + "arguments": {"value": 21}, + }, + request_id="req-origin", + ) (tx,) = events @@ -821,43 +910,16 @@ def test_tool(value: int) -> int: assert mcp_spans[0]["origin"] == "auto.ai.mcp" -@pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_without_request_context(sentry_init, capture_events, FastMCP): - """Test FastMCP handling when no request context is available""" - sentry_init( - integrations=[MCPIntegration()], - traces_sample_rate=1.0, - ) - events = capture_events() - - mcp = FastMCP("Test Server") - - # Clear request context - if request_ctx is not None: - request_ctx.set(None) - - @mcp.tool() - def test_tool_no_ctx(x: int) -> dict: - """Test tool without context""" - return {"result": x + 1} - - with start_transaction(name="fastmcp tx"): - result = call_tool_through_mcp(mcp, "test_tool_no_ctx", {"x": 99}) - - assert result == {"result": 100} - - # Should still create transaction even if context is missing - (tx,) = events - assert tx["type"] == "transaction" - - # ============================================================================= # Transport Detection Tests # ============================================================================= +@pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_sse_transport(sentry_init, capture_events, FastMCP): +async def test_fastmcp_sse_transport( + sentry_init, capture_events, FastMCP, json_rpc_sse +): """Test that FastMCP correctly detects SSE transport""" sentry_init( integrations=[MCPIntegration()], @@ -866,25 +928,66 @@ def test_fastmcp_sse_transport(sentry_init, capture_events, FastMCP): events = capture_events() mcp = FastMCP("Test Server") + sse = SseServerTransport("/messages/") - # Set up mock request context with SSE transport - if request_ctx is not None: - mock_ctx = MockRequestContext( - request_id="req-sse", session_id="session-sse-123", transport="sse" - ) - request_ctx.set(mock_ctx) + sse_connection_closed = asyncio.Event() + + async def handle_sse(request): + async with sse.connect_sse( + request.scope, request.receive, request._send + ) as streams: + async with anyio.create_task_group() as tg: + + async def run_server(): + await mcp._mcp_server.run( + streams[0], + streams[1], + mcp._mcp_server.create_initialization_options(), + ) + + tg.start_soon(run_server) + + sse_connection_closed.set() + return Response() + + app = Starlette( + routes=[ + Route("/sse", endpoint=handle_sse, methods=["GET"]), + Mount("/messages/", app=sse.handle_post_message), + ], + ) @mcp.tool() def sse_tool(value: str) -> dict: """Tool for SSE transport test""" return {"message": f"Received: {value}"} - with start_transaction(name="fastmcp tx"): - result = call_tool_through_mcp(mcp, "sse_tool", {"value": "hello"}) + keep_sse_alive = asyncio.Event() + app_task, _, result = await json_rpc_sse( + app, + method="tools/call", + params={ + "name": "sse_tool", + "arguments": {"value": "hello"}, + }, + request_id="req-sse", + keep_sse_alive=keep_sse_alive, + ) - assert result == {"message": "Received: hello"} + await sse_connection_closed.wait() + await app_task - (tx,) = events + assert json.loads(result["result"]["content"][0]["text"]) == { + "message": "Received: hello" + } + + transactions = [ + event + for event in events + if event["type"] == "transaction" and event["transaction"] == "/sse" + ] + assert len(transactions) == 1 + tx = transactions[0] # Find MCP spans mcp_spans = [s for s in tx["spans"] if s["op"] == OP.MCP_SERVER] @@ -895,7 +998,13 @@ def sse_tool(value: str) -> dict: @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_http_transport(sentry_init, capture_events, FastMCP): +def test_fastmcp_http_transport( + sentry_init, + capture_events, + FastMCP, + json_rpc, + select_mcp_transactions, +): """Test that FastMCP correctly detects HTTP transport""" sentry_init( integrations=[MCPIntegration()], @@ -905,35 +1014,48 @@ def test_fastmcp_http_transport(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context with HTTP transport - if request_ctx is not None: - mock_ctx = MockRequestContext( - request_id="req-http", session_id="session-http-456", transport="http" - ) - request_ctx.set(mock_ctx) + session_manager = StreamableHTTPSessionManager( + app=mcp._mcp_server, + json_response=True, + ) + + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), + ) @mcp.tool() def http_tool(data: str) -> dict: """Tool for HTTP transport test""" return {"processed": data.upper()} - with start_transaction(name="fastmcp tx"): - result = call_tool_through_mcp(mcp, "http_tool", {"data": "test"}) + _, result = json_rpc( + app, + method="tools/call", + params={ + "name": "http_tool", + "arguments": {"data": "test"}, + }, + request_id="req-http", + ) - assert result == {"processed": "TEST"} + assert json.loads(result.json()["result"]["content"][0]["text"]) == { + "processed": "TEST" + } - (tx,) = events + transactions = select_mcp_transactions(events) + assert len(transactions) == 1 + tx = transactions[0] - # Find MCP spans - mcp_spans = [s for s in tx["spans"] if s["op"] == OP.MCP_SERVER] - assert len(mcp_spans) >= 1 - span = mcp_spans[0] # Check that HTTP transport is detected - assert span["data"].get(SPANDATA.MCP_TRANSPORT) == "http" + assert tx["contexts"]["trace"]["data"].get(SPANDATA.MCP_TRANSPORT) == "http" +@pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_stdio_transport(sentry_init, capture_events, FastMCP): +async def test_fastmcp_stdio_transport(sentry_init, capture_events, FastMCP, stdio): """Test that FastMCP correctly detects stdio transport""" sentry_init( integrations=[MCPIntegration()], @@ -943,20 +1065,25 @@ def test_fastmcp_stdio_transport(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context with stdio transport - if request_ctx is not None: - mock_ctx = MockRequestContext(request_id="req-stdio", transport="stdio") - request_ctx.set(mock_ctx) - @mcp.tool() def stdio_tool(n: int) -> dict: """Tool for stdio transport test""" return {"squared": n * n} with start_transaction(name="fastmcp tx"): - result = call_tool_through_mcp(mcp, "stdio_tool", {"n": 7}) + result = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "stdio_tool", + "arguments": {"n": 7}, + }, + request_id="req-stdio", + ) - assert result == {"squared": 49} + assert json.loads(result.message.root.result["content"][0]["text"]) == { + "squared": 49 + } (tx,) = events @@ -1000,10 +1127,11 @@ def package_specific_tool(x: int) -> int: assert tx["type"] == "transaction" +@pytest.mark.asyncio @pytest.mark.skipif( not HAS_STANDALONE_FASTMCP, reason="standalone fastmcp not installed" ) -def test_standalone_fastmcp_specific_features(sentry_init, capture_events): +async def test_standalone_fastmcp_specific_features(sentry_init, capture_events, stdio): """Test features specific to standalone fastmcp package""" sentry_init( integrations=[MCPIntegration()], @@ -1021,12 +1149,19 @@ def standalone_specific_tool(message: str) -> dict: return {"echo": message, "length": len(message)} with start_transaction(name="standalone fastmcp tx"): - result = call_tool_through_mcp( - mcp, "standalone_specific_tool", {"message": "Hello FastMCP"} + result = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "standalone_specific_tool", + "arguments": {"message": "Hello FastMCP"}, + }, ) - assert result["echo"] == "Hello FastMCP" - assert result["length"] == 13 + assert json.loads(result.message.root.result["content"][0]["text"]) == { + "echo": "Hello FastMCP", + "length": 13, + } (tx,) = events assert tx["type"] == "transaction" @@ -1037,8 +1172,11 @@ def standalone_specific_tool(message: str) -> dict: # ============================================================================= +@pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_tool_with_no_arguments(sentry_init, capture_events, FastMCP): +async def test_fastmcp_tool_with_no_arguments( + sentry_init, capture_events, FastMCP, stdio +): """Test FastMCP tool with no arguments""" sentry_init( integrations=[MCPIntegration()], @@ -1054,16 +1192,26 @@ def no_args_tool() -> str: return "success" with start_transaction(name="fastmcp tx"): - result = call_tool_through_mcp(mcp, "no_args_tool", {}) + result = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "no_args_tool", + "arguments": {}, + }, + ) - assert result["result"] == "success" + assert result.message.root.result["content"][0]["text"] == "success" (tx,) = events assert tx["type"] == "transaction" +@pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_tool_with_none_return(sentry_init, capture_events, FastMCP): +async def test_fastmcp_tool_with_none_return( + sentry_init, capture_events, FastMCP, stdio +): """Test FastMCP tool that returns None""" sentry_init( integrations=[MCPIntegration()], @@ -1079,18 +1227,33 @@ def none_return_tool(action: str) -> None: pass with start_transaction(name="fastmcp tx"): - result = call_tool_through_mcp(mcp, "none_return_tool", {"action": "log"}) + result = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "none_return_tool", + "arguments": {"action": "log"}, + }, + ) - # Helper function normalizes to {"result": value} format - assert result["result"] is None + if ( + isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION is not None + ) or isinstance(mcp, MCPFastMCP): + assert len(result.message.root.result["content"]) == 0 + else: + assert result.message.root.result["content"] == [ + {"type": "text", "text": "None"} + ] (tx,) = events assert tx["type"] == "transaction" -@pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) @pytest.mark.asyncio -async def test_fastmcp_mixed_sync_async_tools(sentry_init, capture_events, FastMCP): +@pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) +async def test_fastmcp_mixed_sync_async_tools( + sentry_init, capture_events, FastMCP, stdio +): """Test mixing sync and async tools in FastMCP""" sentry_init( integrations=[MCPIntegration()], @@ -1100,11 +1263,6 @@ async def test_fastmcp_mixed_sync_async_tools(sentry_init, capture_events, FastM mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext(request_id="req-mixed", transport="stdio") - request_ctx.set(mock_ctx) - @mcp.tool() def sync_add(a: int, b: int) -> int: """Sync addition""" @@ -1117,13 +1275,27 @@ async def async_multiply(x: int, y: int) -> int: with start_transaction(name="fastmcp tx"): # Use async version for both since we're in an async context - result1 = await call_tool_through_mcp_async(mcp, "sync_add", {"a": 3, "b": 4}) - result2 = await call_tool_through_mcp_async( - mcp, "async_multiply", {"x": 5, "y": 6} + result1 = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "sync_add", + "arguments": {"a": 3, "b": 4}, + }, + request_id="req-mixed", + ) + result2 = await stdio( + mcp._mcp_server, + method="tools/call", + params={ + "name": "async_multiply", + "arguments": {"x": 5, "y": 6}, + }, + request_id="req-mixed", ) - assert result1["result"] == 7 - assert result2["result"] == 30 + assert result1.message.root.result["content"][0]["text"] == "7" + assert result2.message.root.result["content"][0]["text"] == "30" (tx,) = events assert tx["type"] == "transaction" diff --git a/tests/integrations/google_genai/test_google_genai.py b/tests/integrations/google_genai/test_google_genai.py index a49822f3d4..37ba50420f 100644 --- a/tests/integrations/google_genai/test_google_genai.py +++ b/tests/integrations/google_genai/test_google_genai.py @@ -4,10 +4,13 @@ from google import genai from google.genai import types as genai_types +from google.genai.types import Content, Part from sentry_sdk import start_transaction +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations.google_genai import GoogleGenAIIntegration +from sentry_sdk.integrations.google_genai.utils import extract_contents_messages @pytest.fixture @@ -104,11 +107,6 @@ def create_test_config( if seed is not None: config_dict["seed"] = seed if system_instruction is not None: - # Convert string to Content for system instruction - if isinstance(system_instruction, str): - system_instruction = genai_types.Content( - parts=[genai_types.Part(text=system_instruction)], role="system" - ) config_dict["system_instruction"] = system_instruction if tools is not None: config_dict["tools"] = tools @@ -184,6 +182,7 @@ def test_nonstreaming_generate_content( response_texts = json.loads(response_text) assert response_texts == ["Hello! How can I help you today?"] else: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in invoke_span["data"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in invoke_span["data"] assert SPANDATA.GEN_AI_RESPONSE_TEXT not in chat_span["data"] @@ -200,8 +199,41 @@ def test_nonstreaming_generate_content( assert invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MAX_TOKENS] == 100 +@pytest.mark.parametrize("generate_content_config", (False, True)) +@pytest.mark.parametrize( + "system_instructions,expected_texts", + [ + (None, None), + ({}, []), + (Content(role="system", parts=[]), []), + ({"parts": []}, []), + ("You are a helpful assistant.", ["You are a helpful assistant."]), + (Part(text="You are a helpful assistant."), ["You are a helpful assistant."]), + ( + Content(role="system", parts=[Part(text="You are a helpful assistant.")]), + ["You are a helpful assistant."], + ), + ({"text": "You are a helpful assistant."}, ["You are a helpful assistant."]), + ( + {"parts": [Part(text="You are a helpful assistant.")]}, + ["You are a helpful assistant."], + ), + ( + {"parts": [{"text": "You are a helpful assistant."}]}, + ["You are a helpful assistant."], + ), + (["You are a helpful assistant."], ["You are a helpful assistant."]), + ([Part(text="You are a helpful assistant.")], ["You are a helpful assistant."]), + ([{"text": "You are a helpful assistant."}], ["You are a helpful assistant."]), + ], +) def test_generate_content_with_system_instruction( - sentry_init, capture_events, mock_genai_client + sentry_init, + capture_events, + mock_genai_client, + generate_content_config, + system_instructions, + expected_texts, ): sentry_init( integrations=[GoogleGenAIIntegration(include_prompts=True)], @@ -216,25 +248,35 @@ def test_generate_content_with_system_instruction( mock_genai_client._api_client, "request", return_value=mock_http_response ): with start_transaction(name="google_genai"): - config = create_test_config( - system_instruction="You are a helpful assistant", - temperature=0.5, - ) + config = { + "system_instruction": system_instructions, + "temperature": 0.5, + } + + if generate_content_config: + config = create_test_config(**config) + mock_genai_client.models.generate_content( - model="gemini-1.5-flash", contents="What is 2+2?", config=config + model="gemini-1.5-flash", + contents="What is 2+2?", + config=config, ) (event,) = events invoke_span = event["spans"][0] - # Check that system instruction is included in messages + if expected_texts is None: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in invoke_span["data"] + return + # (PII is enabled and include_prompts is True in this test) - messages_str = invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] - # Parse the JSON string to verify content - messages = json.loads(messages_str) - assert len(messages) == 2 - assert messages[0] == {"role": "system", "content": "You are a helpful assistant"} - assert messages[1] == {"role": "user", "content": "What is 2+2?"} + system_instructions = json.loads( + invoke_span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS] + ) + + assert system_instructions == [ + {"type": "text", "content": text} for text in expected_texts + ] def test_generate_content_with_tools(sentry_init, capture_events, mock_genai_client): @@ -931,10 +973,8 @@ def test_google_genai_message_truncation( with start_transaction(name="google_genai"): mock_genai_client.models.generate_content( model="gemini-1.5-flash", - contents=small_content, - config=create_test_config( - system_instruction=large_content, - ), + contents=[large_content, small_content], + config=create_test_config(), ) (event,) = events @@ -950,6 +990,7 @@ def test_google_genai_message_truncation( assert parsed_messages[0]["role"] == "user" assert small_content in parsed_messages[0]["content"] + assert invoke_span["data"][SPANDATA.META_GEN_AI_ORIGINAL_INPUT_MESSAGES_LENGTH] == 2 assert ( event["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 2 ) @@ -1417,3 +1458,699 @@ async def test_async_embed_content_span_origin( assert event["contexts"]["trace"]["origin"] == "manual" for span in event["spans"]: assert span["origin"] == "auto.ai.google_genai" + + +# Integration tests for generate_content with different input message formats +def test_generate_content_with_content_object( + sentry_init, capture_events, mock_genai_client +): + """Test generate_content with Content object input.""" + sentry_init( + integrations=[GoogleGenAIIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + mock_http_response = create_mock_http_response(EXAMPLE_API_RESPONSE_JSON) + + # Create Content object + content = genai_types.Content( + role="user", parts=[genai_types.Part(text="Hello from Content object")] + ) + + with mock.patch.object( + mock_genai_client._api_client, "request", return_value=mock_http_response + ): + with start_transaction(name="google_genai"): + mock_genai_client.models.generate_content( + model="gemini-1.5-flash", contents=content, config=create_test_config() + ) + + (event,) = events + invoke_span = event["spans"][0] + + messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + assert len(messages) == 1 + assert messages[0]["role"] == "user" + assert messages[0]["content"] == [ + {"text": "Hello from Content object", "type": "text"} + ] + + +def test_generate_content_with_dict_format( + sentry_init, capture_events, mock_genai_client +): + """Test generate_content with dict format input (ContentDict).""" + sentry_init( + integrations=[GoogleGenAIIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + mock_http_response = create_mock_http_response(EXAMPLE_API_RESPONSE_JSON) + + # Dict format content + contents = {"role": "user", "parts": [{"text": "Hello from dict format"}]} + + with mock.patch.object( + mock_genai_client._api_client, "request", return_value=mock_http_response + ): + with start_transaction(name="google_genai"): + mock_genai_client.models.generate_content( + model="gemini-1.5-flash", contents=contents, config=create_test_config() + ) + + (event,) = events + invoke_span = event["spans"][0] + + messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + assert len(messages) == 1 + assert messages[0]["role"] == "user" + assert messages[0]["content"] == [ + {"text": "Hello from dict format", "type": "text"} + ] + + +def test_generate_content_with_file_data( + sentry_init, capture_events, mock_genai_client +): + """Test generate_content with file_data (external file reference).""" + sentry_init( + integrations=[GoogleGenAIIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + mock_http_response = create_mock_http_response(EXAMPLE_API_RESPONSE_JSON) + + # Content with file_data + file_data = genai_types.FileData( + file_uri="gs://bucket/image.jpg", mime_type="image/jpeg" + ) + content = genai_types.Content( + role="user", + parts=[ + genai_types.Part(text="What's in this image?"), + genai_types.Part(file_data=file_data), + ], + ) + + with mock.patch.object( + mock_genai_client._api_client, "request", return_value=mock_http_response + ): + with start_transaction(name="google_genai"): + mock_genai_client.models.generate_content( + model="gemini-1.5-flash", contents=content, config=create_test_config() + ) + + (event,) = events + invoke_span = event["spans"][0] + + messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + assert len(messages) == 1 + assert messages[0]["role"] == "user" + assert len(messages[0]["content"]) == 2 + assert messages[0]["content"][0] == { + "text": "What's in this image?", + "type": "text", + } + assert messages[0]["content"][1]["type"] == "uri" + assert messages[0]["content"][1]["modality"] == "image" + assert messages[0]["content"][1]["mime_type"] == "image/jpeg" + assert messages[0]["content"][1]["uri"] == "gs://bucket/image.jpg" + + +def test_generate_content_with_inline_data( + sentry_init, capture_events, mock_genai_client +): + """Test generate_content with inline_data (binary data).""" + sentry_init( + integrations=[GoogleGenAIIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + mock_http_response = create_mock_http_response(EXAMPLE_API_RESPONSE_JSON) + + # Content with inline binary data + image_bytes = b"fake_image_binary_data" + blob = genai_types.Blob(data=image_bytes, mime_type="image/png") + content = genai_types.Content( + role="user", + parts=[ + genai_types.Part(text="Describe this image"), + genai_types.Part(inline_data=blob), + ], + ) + + with mock.patch.object( + mock_genai_client._api_client, "request", return_value=mock_http_response + ): + with start_transaction(name="google_genai"): + mock_genai_client.models.generate_content( + model="gemini-1.5-flash", contents=content, config=create_test_config() + ) + + (event,) = events + invoke_span = event["spans"][0] + + messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + assert len(messages) == 1 + assert messages[0]["role"] == "user" + assert len(messages[0]["content"]) == 2 + assert messages[0]["content"][0] == {"text": "Describe this image", "type": "text"} + assert messages[0]["content"][1]["type"] == "blob" + assert messages[0]["content"][1]["mime_type"] == "image/png" + # Binary data should be substituted for privacy + assert messages[0]["content"][1]["content"] == BLOB_DATA_SUBSTITUTE + + +def test_generate_content_with_function_response( + sentry_init, capture_events, mock_genai_client +): + """Test generate_content with function_response (tool result).""" + sentry_init( + integrations=[GoogleGenAIIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + mock_http_response = create_mock_http_response(EXAMPLE_API_RESPONSE_JSON) + + # Conversation with function response (tool result) + function_response = genai_types.FunctionResponse( + id="call_123", name="get_weather", response={"output": "Sunny, 72F"} + ) + contents = [ + genai_types.Content( + role="user", parts=[genai_types.Part(text="What's the weather in Paris?")] + ), + genai_types.Content( + role="user", parts=[genai_types.Part(function_response=function_response)] + ), + ] + + with mock.patch.object( + mock_genai_client._api_client, "request", return_value=mock_http_response + ): + with start_transaction(name="google_genai"): + mock_genai_client.models.generate_content( + model="gemini-1.5-flash", contents=contents, config=create_test_config() + ) + + (event,) = events + invoke_span = event["spans"][0] + + messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + assert len(messages) == 1 + # First message is user message + assert messages[0]["role"] == "tool" + assert messages[0]["content"]["toolCallId"] == "call_123" + assert messages[0]["content"]["toolName"] == "get_weather" + assert messages[0]["content"]["output"] == '"Sunny, 72F"' + + +def test_generate_content_with_mixed_string_and_content( + sentry_init, capture_events, mock_genai_client +): + """Test generate_content with mixed string and Content objects in list.""" + sentry_init( + integrations=[GoogleGenAIIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + mock_http_response = create_mock_http_response(EXAMPLE_API_RESPONSE_JSON) + + # Mix of strings and Content objects + contents = [ + "Hello, this is a string message", + genai_types.Content( + role="model", + parts=[genai_types.Part(text="Hi! How can I help you?")], + ), + genai_types.Content( + role="user", + parts=[genai_types.Part(text="Tell me a joke")], + ), + ] + + with mock.patch.object( + mock_genai_client._api_client, "request", return_value=mock_http_response + ): + with start_transaction(name="google_genai"): + mock_genai_client.models.generate_content( + model="gemini-1.5-flash", contents=contents, config=create_test_config() + ) + + (event,) = events + invoke_span = event["spans"][0] + + messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + assert len(messages) == 1 + # User message + assert messages[0]["role"] == "user" + assert messages[0]["content"] == [{"text": "Tell me a joke", "type": "text"}] + + +def test_generate_content_with_part_object_directly( + sentry_init, capture_events, mock_genai_client +): + """Test generate_content with Part object directly (not wrapped in Content).""" + sentry_init( + integrations=[GoogleGenAIIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + mock_http_response = create_mock_http_response(EXAMPLE_API_RESPONSE_JSON) + + # Part object directly + part = genai_types.Part(text="Direct Part object") + + with mock.patch.object( + mock_genai_client._api_client, "request", return_value=mock_http_response + ): + with start_transaction(name="google_genai"): + mock_genai_client.models.generate_content( + model="gemini-1.5-flash", contents=part, config=create_test_config() + ) + + (event,) = events + invoke_span = event["spans"][0] + + messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + assert len(messages) == 1 + assert messages[0]["role"] == "user" + assert messages[0]["content"] == [{"text": "Direct Part object", "type": "text"}] + + +def test_generate_content_with_list_of_dicts( + sentry_init, capture_events, mock_genai_client +): + """Test generate_content with list of dict format inputs.""" + sentry_init( + integrations=[GoogleGenAIIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + mock_http_response = create_mock_http_response(EXAMPLE_API_RESPONSE_JSON) + + # List of dicts (conversation in dict format) + contents = [ + {"role": "user", "parts": [{"text": "First user message"}]}, + {"role": "model", "parts": [{"text": "First model response"}]}, + {"role": "user", "parts": [{"text": "Second user message"}]}, + ] + + with mock.patch.object( + mock_genai_client._api_client, "request", return_value=mock_http_response + ): + with start_transaction(name="google_genai"): + mock_genai_client.models.generate_content( + model="gemini-1.5-flash", contents=contents, config=create_test_config() + ) + + (event,) = events + invoke_span = event["spans"][0] + + messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + assert len(messages) == 1 + assert messages[0]["role"] == "user" + assert messages[0]["content"] == [{"text": "Second user message", "type": "text"}] + + +def test_generate_content_with_dict_inline_data( + sentry_init, capture_events, mock_genai_client +): + """Test generate_content with dict format containing inline_data.""" + sentry_init( + integrations=[GoogleGenAIIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + mock_http_response = create_mock_http_response(EXAMPLE_API_RESPONSE_JSON) + + # Dict with inline_data + contents = { + "role": "user", + "parts": [ + {"text": "What's in this image?"}, + {"inline_data": {"data": b"fake_binary_data", "mime_type": "image/gif"}}, + ], + } + + with mock.patch.object( + mock_genai_client._api_client, "request", return_value=mock_http_response + ): + with start_transaction(name="google_genai"): + mock_genai_client.models.generate_content( + model="gemini-1.5-flash", contents=contents, config=create_test_config() + ) + + (event,) = events + invoke_span = event["spans"][0] + + messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + assert len(messages) == 1 + assert messages[0]["role"] == "user" + assert len(messages[0]["content"]) == 2 + assert messages[0]["content"][0] == { + "text": "What's in this image?", + "type": "text", + } + assert messages[0]["content"][1]["type"] == "blob" + assert messages[0]["content"][1]["mime_type"] == "image/gif" + assert messages[0]["content"][1]["content"] == BLOB_DATA_SUBSTITUTE + + +# Tests for extract_contents_messages function +def test_extract_contents_messages_none(): + """Test extract_contents_messages with None input""" + result = extract_contents_messages(None) + assert result == [] + + +def test_extract_contents_messages_string(): + """Test extract_contents_messages with string input""" + result = extract_contents_messages("Hello world") + assert result == [{"role": "user", "content": "Hello world"}] + + +def test_extract_contents_messages_content_object(): + """Test extract_contents_messages with Content object""" + content = genai_types.Content( + role="user", parts=[genai_types.Part(text="Test message")] + ) + result = extract_contents_messages(content) + assert len(result) == 1 + assert result[0]["role"] == "user" + assert result[0]["content"] == [{"text": "Test message", "type": "text"}] + + +def test_extract_contents_messages_content_object_model_role(): + """Test extract_contents_messages with Content object having model role""" + content = genai_types.Content( + role="model", parts=[genai_types.Part(text="Assistant response")] + ) + result = extract_contents_messages(content) + assert len(result) == 1 + assert result[0]["role"] == "assistant" + assert result[0]["content"] == [{"text": "Assistant response", "type": "text"}] + + +def test_extract_contents_messages_content_object_no_role(): + """Test extract_contents_messages with Content object without role""" + content = genai_types.Content(parts=[genai_types.Part(text="No role message")]) + result = extract_contents_messages(content) + assert len(result) == 1 + assert result[0]["role"] == "user" + assert result[0]["content"] == [{"text": "No role message", "type": "text"}] + + +def test_extract_contents_messages_part_object(): + """Test extract_contents_messages with Part object""" + part = genai_types.Part(text="Direct part") + result = extract_contents_messages(part) + assert len(result) == 1 + assert result[0]["role"] == "user" + assert result[0]["content"] == [{"text": "Direct part", "type": "text"}] + + +def test_extract_contents_messages_file_data(): + """Test extract_contents_messages with file_data""" + file_data = genai_types.FileData( + file_uri="gs://bucket/file.jpg", mime_type="image/jpeg" + ) + part = genai_types.Part(file_data=file_data) + content = genai_types.Content(parts=[part]) + result = extract_contents_messages(content) + + assert len(result) == 1 + assert result[0]["role"] == "user" + assert len(result[0]["content"]) == 1 + blob_part = result[0]["content"][0] + assert blob_part["type"] == "uri" + assert blob_part["modality"] == "image" + assert blob_part["mime_type"] == "image/jpeg" + assert blob_part["uri"] == "gs://bucket/file.jpg" + + +def test_extract_contents_messages_inline_data(): + """Test extract_contents_messages with inline_data (binary)""" + # Create inline data with bytes + image_bytes = b"fake_image_data" + blob = genai_types.Blob(data=image_bytes, mime_type="image/png") + part = genai_types.Part(inline_data=blob) + content = genai_types.Content(parts=[part]) + result = extract_contents_messages(content) + + assert len(result) == 1 + assert result[0]["role"] == "user" + assert len(result[0]["content"]) == 1 + blob_part = result[0]["content"][0] + assert blob_part["type"] == "blob" + assert blob_part["mime_type"] == "image/png" + assert blob_part["content"] == BLOB_DATA_SUBSTITUTE + + +def test_extract_contents_messages_function_response(): + """Test extract_contents_messages with function_response (tool message)""" + function_response = genai_types.FunctionResponse( + id="call_123", name="get_weather", response={"output": "sunny"} + ) + part = genai_types.Part(function_response=function_response) + content = genai_types.Content(parts=[part]) + result = extract_contents_messages(content) + + assert len(result) == 1 + assert result[0]["role"] == "tool" + assert result[0]["content"]["toolCallId"] == "call_123" + assert result[0]["content"]["toolName"] == "get_weather" + assert result[0]["content"]["output"] == '"sunny"' + + +def test_extract_contents_messages_function_response_with_output_key(): + """Test extract_contents_messages with function_response that has output key""" + function_response = genai_types.FunctionResponse( + id="call_456", name="get_time", response={"output": "3:00 PM", "error": None} + ) + part = genai_types.Part(function_response=function_response) + content = genai_types.Content(parts=[part]) + result = extract_contents_messages(content) + + assert len(result) == 1 + assert result[0]["role"] == "tool" + assert result[0]["content"]["toolCallId"] == "call_456" + assert result[0]["content"]["toolName"] == "get_time" + # Should prefer "output" key + assert result[0]["content"]["output"] == '"3:00 PM"' + + +def test_extract_contents_messages_mixed_parts(): + """Test extract_contents_messages with mixed content parts""" + content = genai_types.Content( + role="user", + parts=[ + genai_types.Part(text="Text part"), + genai_types.Part( + file_data=genai_types.FileData( + file_uri="gs://bucket/image.jpg", mime_type="image/jpeg" + ) + ), + ], + ) + result = extract_contents_messages(content) + + assert len(result) == 1 + assert result[0]["role"] == "user" + assert len(result[0]["content"]) == 2 + assert result[0]["content"][0] == {"text": "Text part", "type": "text"} + assert result[0]["content"][1]["type"] == "uri" + assert result[0]["content"][1]["modality"] == "image" + assert result[0]["content"][1]["uri"] == "gs://bucket/image.jpg" + + +def test_extract_contents_messages_list(): + """Test extract_contents_messages with list input""" + contents = [ + "First message", + genai_types.Content( + role="user", parts=[genai_types.Part(text="Second message")] + ), + ] + result = extract_contents_messages(contents) + + assert len(result) == 2 + assert result[0] == {"role": "user", "content": "First message"} + assert result[1]["role"] == "user" + assert result[1]["content"] == [{"text": "Second message", "type": "text"}] + + +def test_extract_contents_messages_dict_content(): + """Test extract_contents_messages with dict (ContentDict)""" + content_dict = {"role": "user", "parts": [{"text": "Dict message"}]} + result = extract_contents_messages(content_dict) + + assert len(result) == 1 + assert result[0]["role"] == "user" + assert result[0]["content"] == [{"text": "Dict message", "type": "text"}] + + +def test_extract_contents_messages_dict_with_text(): + """Test extract_contents_messages with dict containing text key""" + content_dict = {"role": "user", "text": "Simple text"} + result = extract_contents_messages(content_dict) + + assert len(result) == 1 + assert result[0]["role"] == "user" + assert result[0]["content"] == [{"text": "Simple text", "type": "text"}] + + +def test_extract_contents_messages_file_object(): + """Test extract_contents_messages with File object""" + file_obj = genai_types.File( + name="files/123", uri="gs://bucket/file.pdf", mime_type="application/pdf" + ) + result = extract_contents_messages(file_obj) + + assert len(result) == 1 + assert result[0]["role"] == "user" + assert len(result[0]["content"]) == 1 + blob_part = result[0]["content"][0] + assert blob_part["type"] == "uri" + assert blob_part["modality"] == "document" + assert blob_part["mime_type"] == "application/pdf" + assert blob_part["uri"] == "gs://bucket/file.pdf" + + +@pytest.mark.skipif( + not hasattr(genai_types, "PIL_Image") or genai_types.PIL_Image is None, + reason="PIL not available", +) +def test_extract_contents_messages_pil_image(): + """Test extract_contents_messages with PIL.Image.Image""" + try: + from PIL import Image as PILImage + + # Create a simple test image + img = PILImage.new("RGB", (10, 10), color="red") + result = extract_contents_messages(img) + + assert len(result) == 1 + assert result[0]["role"] == "user" + assert len(result[0]["content"]) == 1 + blob_part = result[0]["content"][0] + assert blob_part["type"] == "blob" + assert blob_part["mime_type"].startswith("image/") + assert "content" in blob_part + # Binary content is substituted with placeholder for privacy + assert blob_part["content"] == "[Blob substitute]" + except ImportError: + pytest.skip("PIL not available") + + +def test_extract_contents_messages_tool_and_text(): + """Test extract_contents_messages with both tool message and text""" + content = genai_types.Content( + role="user", + parts=[ + genai_types.Part(text="User question"), + genai_types.Part( + function_response=genai_types.FunctionResponse( + id="call_789", name="search", response={"output": "results"} + ) + ), + ], + ) + result = extract_contents_messages(content) + + # Should have two messages: one user message and one tool message + assert len(result) == 2 + # First should be user message with text + assert result[0]["role"] == "user" + assert result[0]["content"] == [{"text": "User question", "type": "text"}] + # Second should be tool message + assert result[1]["role"] == "tool" + assert result[1]["content"]["toolCallId"] == "call_789" + assert result[1]["content"]["toolName"] == "search" + + +def test_extract_contents_messages_empty_parts(): + """Test extract_contents_messages with Content object with empty parts""" + content = genai_types.Content(role="user", parts=[]) + result = extract_contents_messages(content) + + assert result == [] + + +def test_extract_contents_messages_empty_list(): + """Test extract_contents_messages with empty list""" + result = extract_contents_messages([]) + assert result == [] + + +def test_extract_contents_messages_dict_inline_data(): + """Test extract_contents_messages with dict containing inline_data""" + content_dict = { + "role": "user", + "parts": [{"inline_data": {"data": b"binary_data", "mime_type": "image/gif"}}], + } + result = extract_contents_messages(content_dict) + + assert len(result) == 1 + assert result[0]["role"] == "user" + assert len(result[0]["content"]) == 1 + blob_part = result[0]["content"][0] + assert blob_part["type"] == "blob" + assert blob_part["mime_type"] == "image/gif" + assert blob_part["content"] == BLOB_DATA_SUBSTITUTE + + +def test_extract_contents_messages_dict_function_response(): + """Test extract_contents_messages with dict containing function_response""" + content_dict = { + "role": "user", + "parts": [ + { + "function_response": { + "id": "dict_call_1", + "name": "dict_tool", + "response": {"result": "success"}, + } + } + ], + } + result = extract_contents_messages(content_dict) + + assert len(result) == 1 + assert result[0]["role"] == "tool" + assert result[0]["content"]["toolCallId"] == "dict_call_1" + assert result[0]["content"]["toolName"] == "dict_tool" + assert result[0]["content"]["output"] == '{"result": "success"}' + + +def test_extract_contents_messages_object_with_text_attribute(): + """Test extract_contents_messages with object that has text attribute""" + + class TextObject: + def __init__(self): + self.text = "Object text" + + obj = TextObject() + result = extract_contents_messages(obj) + + assert len(result) == 1 + assert result[0]["role"] == "user" + assert result[0]["content"] == [{"text": "Object text", "type": "text"}] diff --git a/tests/integrations/gql/test_gql.py b/tests/integrations/gql/test_gql.py index 147f7a06a8..2785c63e2c 100644 --- a/tests/integrations/gql/test_gql.py +++ b/tests/integrations/gql/test_gql.py @@ -3,9 +3,13 @@ import responses from gql import gql from gql import Client +from gql import __version__ from gql.transport.exceptions import TransportQueryError from gql.transport.requests import RequestsHTTPTransport from sentry_sdk.integrations.gql import GQLIntegration +from sentry_sdk.utils import parse_version + +GQL_VERSION = parse_version(__version__) @responses.activate @@ -32,7 +36,36 @@ def _execute_mock_query(response_json): return client.execute(query) -def _make_erroneous_query(capture_events): +@responses.activate +def _execute_mock_query_with_keyword_document(response_json): + url = "http://example.com/graphql" + query_string = """ + query Example { + example + } + """ + + # Mock the GraphQL server response + responses.add( + method=responses.POST, + url=url, + json=response_json, + status=200, + ) + + transport = RequestsHTTPTransport(url=url) + client = Client(transport=transport) + query = gql(query_string) + + return client.execute(document=query) + + +_execute_query_funcs = [_execute_mock_query] +if GQL_VERSION < (4,): + _execute_query_funcs.append(_execute_mock_query_with_keyword_document) + + +def _make_erroneous_query(capture_events, execute_query): """ Make an erroneous GraphQL query, and assert that the error was reraised, that exactly one event was recorded, and that the exception recorded was a @@ -42,7 +75,7 @@ def _make_erroneous_query(capture_events): response_json = {"errors": ["something bad happened"]} with pytest.raises(TransportQueryError): - _execute_mock_query(response_json) + execute_query(response_json) assert len(events) == 1, ( "the sdk captured %d events, but 1 event was expected" % len(events) @@ -67,7 +100,8 @@ def test_gql_init(sentry_init): sentry_init(integrations=[GQLIntegration()]) -def test_real_gql_request_no_error(sentry_init, capture_events): +@pytest.mark.parametrize("execute_query", _execute_query_funcs) +def test_real_gql_request_no_error(sentry_init, capture_events, execute_query): """ Integration test verifying that the GQLIntegration works as expected with successful query. """ @@ -77,7 +111,7 @@ def test_real_gql_request_no_error(sentry_init, capture_events): response_data = {"example": "This is the example"} response_json = {"data": response_data} - result = _execute_mock_query(response_json) + result = execute_query(response_json) assert result == response_data, ( "client.execute returned a different value from what it received from the server" @@ -87,27 +121,31 @@ def test_real_gql_request_no_error(sentry_init, capture_events): ) -def test_real_gql_request_with_error_no_pii(sentry_init, capture_events): +@pytest.mark.parametrize("execute_query", _execute_query_funcs) +def test_real_gql_request_with_error_no_pii(sentry_init, capture_events, execute_query): """ Integration test verifying that the GQLIntegration works as expected with query resulting in a GraphQL error, and that PII is not sent. """ sentry_init(integrations=[GQLIntegration()]) - event = _make_erroneous_query(capture_events) + event = _make_erroneous_query(capture_events, execute_query) assert "data" not in event["request"] assert "response" not in event["contexts"] -def test_real_gql_request_with_error_with_pii(sentry_init, capture_events): +@pytest.mark.parametrize("execute_query", _execute_query_funcs) +def test_real_gql_request_with_error_with_pii( + sentry_init, capture_events, execute_query +): """ Integration test verifying that the GQLIntegration works as expected with query resulting in a GraphQL error, and that PII is not sent. """ sentry_init(integrations=[GQLIntegration()], send_default_pii=True) - event = _make_erroneous_query(capture_events) + event = _make_erroneous_query(capture_events, execute_query) assert "data" in event["request"] assert "response" in event["contexts"] diff --git a/tests/integrations/grpc/test_grpc.py b/tests/integrations/grpc/test_grpc.py index 8d2698f411..25436d9feb 100644 --- a/tests/integrations/grpc/test_grpc.py +++ b/tests/integrations/grpc/test_grpc.py @@ -8,6 +8,7 @@ from sentry_sdk import start_span, start_transaction from sentry_sdk.consts import OP from sentry_sdk.integrations.grpc import GRPCIntegration +from sentry_sdk.integrations.grpc.client import ClientInterceptor from tests.conftest import ApproxDict from tests.integrations.grpc.grpc_test_service_pb2 import gRPCTestMessage from tests.integrations.grpc.grpc_test_service_pb2_grpc import ( @@ -269,6 +270,42 @@ def test_grpc_client_other_interceptor(sentry_init, capture_events_forksafe): ) +@pytest.mark.forked +def test_prevent_dual_client_interceptor(sentry_init, capture_events_forksafe): + sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()]) + events = capture_events_forksafe() + + server, channel = _set_up() + + # Intercept the channel + channel = grpc.intercept_channel(channel, ClientInterceptor()) + stub = gRPCTestServiceStub(channel) + + with start_transaction(): + stub.TestServe(gRPCTestMessage(text="test")) + + _tear_down(server=server) + + events.write_file.close() + events.read_event() + local_transaction = events.read_event() + span = local_transaction["spans"][0] + + assert len(local_transaction["spans"]) == 1 + assert span["op"] == OP.GRPC_CLIENT + assert ( + span["description"] + == "unary unary call to /grpc_test_server.gRPCTestService/TestServe" + ) + assert span["data"] == ApproxDict( + { + "type": "unary unary", + "method": "/grpc_test_server.gRPCTestService/TestServe", + "code": "OK", + } + ) + + @pytest.mark.forked def test_grpc_client_and_servers_interceptors_integration( sentry_init, capture_events_forksafe diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 114e819bfb..58cc16cdd7 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -25,6 +25,8 @@ from sentry_sdk.integrations.langchain import ( LangchainIntegration, SentryLangchainCallback, + _transform_langchain_content_block, + _transform_langchain_message_content, ) try: @@ -73,8 +75,26 @@ def _llm_type(self) -> str: (False, False, True), ], ) +@pytest.mark.parametrize( + "system_instructions_content", + [ + "You are very powerful assistant, but don't know current events", + ["You are a helpful assistant.", "Be concise and clear."], + [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + ], + ids=["string", "list", "blocks"], +) def test_langchain_agent( - sentry_init, capture_events, send_default_pii, include_prompts, use_unknown_llm_type + sentry_init, + capture_events, + send_default_pii, + include_prompts, + use_unknown_llm_type, + system_instructions_content, + request, ): global llm_type llm_type = "acme-llm" if use_unknown_llm_type else "openai-chat" @@ -94,7 +114,7 @@ def test_langchain_agent( [ ( "system", - "You are very powerful assistant, but don't know current events", + system_instructions_content, ), ("user", "{input}"), MessagesPlaceholder(variable_name="agent_scratchpad"), @@ -215,17 +235,30 @@ def test_langchain_agent( assert chat_spans[1]["data"]["gen_ai.usage.total_tokens"] == 117 if send_default_pii and include_prompts: - assert ( - "You are very powerful" - in chat_spans[0]["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] - ) assert "5" in chat_spans[0]["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] assert "word" in tool_exec_span["data"][SPANDATA.GEN_AI_TOOL_INPUT] assert 5 == int(tool_exec_span["data"][SPANDATA.GEN_AI_TOOL_OUTPUT]) - assert ( - "You are very powerful" - in chat_spans[1]["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] - ) + + param_id = request.node.callspec.id + if "string" in param_id: + assert [ + { + "type": "text", + "content": "You are very powerful assistant, but don't know current events", + } + ] == json.loads(chat_spans[0]["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) + else: + assert [ + { + "type": "text", + "content": "You are a helpful assistant.", + }, + { + "type": "text", + "content": "Be concise and clear.", + }, + ] == json.loads(chat_spans[0]["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) + assert "5" in chat_spans[1]["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] # Verify tool calls are recorded when PII is enabled @@ -241,8 +274,10 @@ def test_langchain_agent( tool_call_str = str(tool_calls_data) assert "get_word_length" in tool_call_str else: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in chat_spans[0].get("data", {}) assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in chat_spans[0].get("data", {}) assert SPANDATA.GEN_AI_RESPONSE_TEXT not in chat_spans[0].get("data", {}) + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in chat_spans[1].get("data", {}) assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in chat_spans[1].get("data", {}) assert SPANDATA.GEN_AI_RESPONSE_TEXT not in chat_spans[1].get("data", {}) assert SPANDATA.GEN_AI_TOOL_INPUT not in tool_exec_span.get("data", {}) @@ -1033,9 +1068,10 @@ def test_langchain_message_truncation(sentry_init, capture_events): parsed_messages = json.loads(messages_data) assert isinstance(parsed_messages, list) - assert len(parsed_messages) == 2 - assert "small message 4" in str(parsed_messages[0]) - assert "small message 5" in str(parsed_messages[1]) + assert len(parsed_messages) == 1 + assert "small message 5" in str(parsed_messages[0]) + + assert llm_span["data"][SPANDATA.META_GEN_AI_ORIGINAL_INPUT_MESSAGES_LENGTH] == 5 assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5 @@ -1747,3 +1783,330 @@ def test_langchain_response_model_extraction( assert llm_span["data"][SPANDATA.GEN_AI_RESPONSE_MODEL] == expected_model else: assert SPANDATA.GEN_AI_RESPONSE_MODEL not in llm_span.get("data", {}) + + +# Tests for multimodal content transformation functions + + +class TestTransformLangchainContentBlock: + """Tests for _transform_langchain_content_block function.""" + + def test_transform_image_base64(self): + """Test transformation of base64-encoded image content.""" + content_block = { + "type": "image", + "base64": "/9j/4AAQSkZJRgABAQAAAQABAAD...", + "mime_type": "image/jpeg", + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQSkZJRgABAQAAAQABAAD...", + } + + def test_transform_image_url(self): + """Test transformation of URL-referenced image content.""" + content_block = { + "type": "image", + "url": "https://example.com/image.jpg", + "mime_type": "image/jpeg", + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "image/jpeg", + "uri": "https://example.com/image.jpg", + } + + def test_transform_image_file_id(self): + """Test transformation of file_id-referenced image content.""" + content_block = { + "type": "image", + "file_id": "file-abc123", + "mime_type": "image/png", + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "file", + "modality": "image", + "mime_type": "image/png", + "file_id": "file-abc123", + } + + def test_transform_image_url_legacy_with_data_uri(self): + """Test transformation of legacy image_url format with data: URI (base64).""" + content_block = { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD"}, + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQSkZJRgABAQAAAQABAAD", + } + + def test_transform_image_url_legacy_with_http_url(self): + """Test transformation of legacy image_url format with HTTP URL.""" + content_block = { + "type": "image_url", + "image_url": {"url": "https://example.com/image.png"}, + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "https://example.com/image.png", + } + + def test_transform_image_url_legacy_string_url(self): + """Test transformation of legacy image_url format with string URL.""" + content_block = { + "type": "image_url", + "image_url": "https://example.com/image.gif", + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "https://example.com/image.gif", + } + + def test_transform_image_url_legacy_data_uri_png(self): + """Test transformation of legacy image_url format with PNG data URI.""" + content_block = { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + }, + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/png", + "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + } + + def test_transform_missing_mime_type(self): + """Test transformation when mime_type is not provided.""" + content_block = { + "type": "image", + "base64": "/9j/4AAQSkZJRgABAQAAAQABAAD...", + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "", + "content": "/9j/4AAQSkZJRgABAQAAAQABAAD...", + } + + def test_transform_anthropic_source_base64(self): + """Test transformation of Anthropic-style image with base64 source.""" + content_block = { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAAE...", + }, + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/png", + "content": "iVBORw0KGgoAAAANSUhEUgAAAAE...", + } + + def test_transform_anthropic_source_url(self): + """Test transformation of Anthropic-style image with URL source.""" + content_block = { + "type": "image", + "source": { + "type": "url", + "media_type": "image/jpeg", + "url": "https://example.com/image.jpg", + }, + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "image/jpeg", + "uri": "https://example.com/image.jpg", + } + + def test_transform_anthropic_source_without_media_type(self): + """Test transformation of Anthropic-style image without media_type uses empty mime_type.""" + content_block = { + "type": "image", + "mime_type": "image/webp", # Top-level mime_type is ignored by standard Anthropic format + "source": { + "type": "base64", + "data": "UklGRh4AAABXRUJQVlA4IBIAAAAwAQCdASoBAAEAAQAcJYgCdAEO", + }, + } + result = _transform_langchain_content_block(content_block) + # Note: The shared transform_content_part uses media_type from source, not top-level mime_type + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "", + "content": "UklGRh4AAABXRUJQVlA4IBIAAAAwAQCdASoBAAEAAQAcJYgCdAEO", + } + + def test_transform_google_inline_data(self): + """Test transformation of Google-style inline_data format.""" + content_block = { + "inline_data": { + "mime_type": "image/jpeg", + "data": "/9j/4AAQSkZJRgABAQAAAQABAAD...", + } + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQSkZJRgABAQAAAQABAAD...", + } + + def test_transform_google_file_data(self): + """Test transformation of Google-style file_data format.""" + content_block = { + "file_data": { + "mime_type": "image/png", + "file_uri": "gs://bucket/path/to/image.png", + } + } + result = _transform_langchain_content_block(content_block) + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "image/png", + "uri": "gs://bucket/path/to/image.png", + } + + +class TestTransformLangchainMessageContent: + """Tests for _transform_langchain_message_content function.""" + + def test_transform_string_content(self): + """Test that string content is returned unchanged.""" + result = _transform_langchain_message_content("Hello, world!") + assert result == "Hello, world!" + + def test_transform_list_with_text_blocks(self): + """Test transformation of list with text blocks (unchanged).""" + content = [ + {"type": "text", "text": "First message"}, + {"type": "text", "text": "Second message"}, + ] + result = _transform_langchain_message_content(content) + assert result == content + + def test_transform_list_with_image_blocks(self): + """Test transformation of list containing image blocks.""" + content = [ + {"type": "text", "text": "Check out this image:"}, + { + "type": "image", + "base64": "/9j/4AAQSkZJRgABAQAAAQABAAD...", + "mime_type": "image/jpeg", + }, + ] + result = _transform_langchain_message_content(content) + assert len(result) == 2 + assert result[0] == {"type": "text", "text": "Check out this image:"} + assert result[1] == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQSkZJRgABAQAAAQABAAD...", + } + + def test_transform_list_with_mixed_content(self): + """Test transformation of list with mixed content types.""" + content = [ + {"type": "text", "text": "Here are some files:"}, + { + "type": "image", + "url": "https://example.com/image.jpg", + "mime_type": "image/jpeg", + }, + { + "type": "file", + "file_id": "doc-123", + "mime_type": "application/pdf", + }, + {"type": "audio", "base64": "audio_data...", "mime_type": "audio/mp3"}, + ] + result = _transform_langchain_message_content(content) + assert len(result) == 4 + assert result[0] == {"type": "text", "text": "Here are some files:"} + assert result[1] == { + "type": "uri", + "modality": "image", + "mime_type": "image/jpeg", + "uri": "https://example.com/image.jpg", + } + assert result[2] == { + "type": "file", + "modality": "document", + "mime_type": "application/pdf", + "file_id": "doc-123", + } + assert result[3] == { + "type": "blob", + "modality": "audio", + "mime_type": "audio/mp3", + "content": "audio_data...", + } + + def test_transform_list_with_non_dict_items(self): + """Test transformation handles non-dict items in list.""" + content = ["plain string", {"type": "text", "text": "dict text"}] + result = _transform_langchain_message_content(content) + assert result == ["plain string", {"type": "text", "text": "dict text"}] + + def test_transform_tuple_content(self): + """Test transformation of tuple content.""" + content = ( + {"type": "text", "text": "Message"}, + {"type": "image", "base64": "data...", "mime_type": "image/png"}, + ) + result = _transform_langchain_message_content(content) + assert len(result) == 2 + assert result[1] == { + "type": "blob", + "modality": "image", + "mime_type": "image/png", + "content": "data...", + } + + def test_transform_list_with_legacy_image_url(self): + """Test transformation of list containing legacy image_url blocks.""" + content = [ + {"type": "text", "text": "Check this:"}, + { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQ..."}, + }, + ] + result = _transform_langchain_message_content(content) + assert len(result) == 2 + assert result[0] == {"type": "text", "text": "Check this:"} + assert result[1] == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQ...", + } diff --git a/tests/integrations/langgraph/test_langgraph.py b/tests/integrations/langgraph/test_langgraph.py index 99ab216957..9ccd84309f 100644 --- a/tests/integrations/langgraph/test_langgraph.py +++ b/tests/integrations/langgraph/test_langgraph.py @@ -270,9 +270,8 @@ def original_invoke(self, *args, **kwargs): import json request_messages = json.loads(request_messages) - assert len(request_messages) == 2 - assert request_messages[0]["content"] == "Hello, can you help me?" - assert request_messages[1]["content"] == "Of course! How can I assist you?" + assert len(request_messages) == 1 + assert request_messages[0]["content"] == "Of course! How can I assist you?" response_text = invoke_span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] assert response_text == expected_assistant_response @@ -1383,7 +1382,8 @@ def original_invoke(self, *args, **kwargs): parsed_messages = json.loads(messages_data) assert isinstance(parsed_messages, list) - assert len(parsed_messages) == 2 - assert "small message 4" in str(parsed_messages[0]) - assert "small message 5" in str(parsed_messages[1]) + assert len(parsed_messages) == 1 + assert "small message 5" in str(parsed_messages[0]) + + assert invoke_span["data"][SPANDATA.META_GEN_AI_ORIGINAL_INPUT_MESSAGES_LENGTH] == 5 assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5 diff --git a/tests/integrations/litellm/test_litellm.py b/tests/integrations/litellm/test_litellm.py index 1b925fb61f..06772342ab 100644 --- a/tests/integrations/litellm/test_litellm.py +++ b/tests/integrations/litellm/test_litellm.py @@ -1,3 +1,4 @@ +import base64 import json import pytest import time @@ -21,8 +22,10 @@ async def __call__(self, *args, **kwargs): import sentry_sdk from sentry_sdk import start_transaction from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE from sentry_sdk.integrations.litellm import ( LiteLLMIntegration, + _convert_message_parts, _input_callback, _success_callback, _failure_callback, @@ -749,7 +752,246 @@ def test_litellm_message_truncation(sentry_init, capture_events): parsed_messages = json.loads(messages_data) assert isinstance(parsed_messages, list) - assert len(parsed_messages) == 2 - assert "small message 4" in str(parsed_messages[0]) - assert "small message 5" in str(parsed_messages[1]) + assert len(parsed_messages) == 1 + assert "small message 5" in str(parsed_messages[0]) + + assert chat_span["data"][SPANDATA.META_GEN_AI_ORIGINAL_INPUT_MESSAGES_LENGTH] == 5 assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5 + + +IMAGE_DATA = b"fake_image_data_12345" +IMAGE_B64 = base64.b64encode(IMAGE_DATA).decode("utf-8") +IMAGE_DATA_URI = f"data:image/png;base64,{IMAGE_B64}" + + +def test_binary_content_encoding_image_url(sentry_init, capture_events): + sentry_init( + integrations=[LiteLLMIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Look at this image:"}, + { + "type": "image_url", + "image_url": {"url": IMAGE_DATA_URI, "detail": "high"}, + }, + ], + } + ] + mock_response = MockCompletionResponse() + + with start_transaction(name="litellm test"): + kwargs = {"model": "gpt-4-vision-preview", "messages": messages} + _input_callback(kwargs) + _success_callback(kwargs, mock_response, datetime.now(), datetime.now()) + + (event,) = events + (span,) = event["spans"] + messages_data = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + + blob_item = next( + ( + item + for msg in messages_data + if "content" in msg + for item in msg["content"] + if item.get("type") == "blob" + ), + None, + ) + assert blob_item is not None + assert blob_item["modality"] == "image" + assert blob_item["mime_type"] == "image/png" + assert ( + IMAGE_B64 in blob_item["content"] + or blob_item["content"] == BLOB_DATA_SUBSTITUTE + ) + + +def test_binary_content_encoding_mixed_content(sentry_init, capture_events): + sentry_init( + integrations=[LiteLLMIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Here is an image:"}, + { + "type": "image_url", + "image_url": {"url": IMAGE_DATA_URI}, + }, + {"type": "text", "text": "What do you see?"}, + ], + } + ] + mock_response = MockCompletionResponse() + + with start_transaction(name="litellm test"): + kwargs = {"model": "gpt-4-vision-preview", "messages": messages} + _input_callback(kwargs) + _success_callback(kwargs, mock_response, datetime.now(), datetime.now()) + + (event,) = events + (span,) = event["spans"] + messages_data = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + + content_items = [ + item for msg in messages_data if "content" in msg for item in msg["content"] + ] + assert any(item.get("type") == "text" for item in content_items) + assert any(item.get("type") == "blob" for item in content_items) + + +def test_binary_content_encoding_uri_type(sentry_init, capture_events): + sentry_init( + integrations=[LiteLLMIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + messages = [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": "https://example.com/image.jpg"}, + } + ], + } + ] + mock_response = MockCompletionResponse() + + with start_transaction(name="litellm test"): + kwargs = {"model": "gpt-4-vision-preview", "messages": messages} + _input_callback(kwargs) + _success_callback(kwargs, mock_response, datetime.now(), datetime.now()) + + (event,) = events + (span,) = event["spans"] + messages_data = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + + uri_item = next( + ( + item + for msg in messages_data + if "content" in msg + for item in msg["content"] + if item.get("type") == "uri" + ), + None, + ) + assert uri_item is not None + assert uri_item["uri"] == "https://example.com/image.jpg" + + +def test_convert_message_parts_direct(): + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Hello"}, + { + "type": "image_url", + "image_url": {"url": IMAGE_DATA_URI}, + }, + ], + } + ] + converted = _convert_message_parts(messages) + blob_item = next( + item for item in converted[0]["content"] if item.get("type") == "blob" + ) + assert blob_item["modality"] == "image" + assert blob_item["mime_type"] == "image/png" + assert IMAGE_B64 in blob_item["content"] + + +def test_convert_message_parts_does_not_mutate_original(): + """Ensure _convert_message_parts does not mutate the original messages.""" + original_url = IMAGE_DATA_URI + messages = [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": original_url}, + }, + ], + } + ] + _convert_message_parts(messages) + # Original should be unchanged + assert messages[0]["content"][0]["type"] == "image_url" + assert messages[0]["content"][0]["image_url"]["url"] == original_url + + +def test_convert_message_parts_data_url_without_base64(): + """Data URLs without ;base64, marker are still inline data and should be blobs.""" + messages = [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": "data:image/png,rawdata"}, + }, + ], + } + ] + converted = _convert_message_parts(messages) + blob_item = converted[0]["content"][0] + # Data URIs (with or without base64 encoding) contain inline data and should be blobs + assert blob_item["type"] == "blob" + assert blob_item["modality"] == "image" + assert blob_item["mime_type"] == "image/png" + assert blob_item["content"] == "rawdata" + + +def test_convert_message_parts_image_url_none(): + """image_url being None should not crash.""" + messages = [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": None, + }, + ], + } + ] + converted = _convert_message_parts(messages) + # Should return item unchanged + assert converted[0]["content"][0]["type"] == "image_url" + + +def test_convert_message_parts_image_url_missing_url(): + """image_url missing the url key should not crash.""" + messages = [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"detail": "high"}, + }, + ], + } + ] + converted = _convert_message_parts(messages) + # Should return item unchanged + assert converted[0]["content"][0]["type"] == "image_url" diff --git a/tests/integrations/mcp/test_mcp.py b/tests/integrations/mcp/test_mcp.py index 4415467cd7..715f9734ca 100644 --- a/tests/integrations/mcp/test_mcp.py +++ b/tests/integrations/mcp/test_mcp.py @@ -15,6 +15,9 @@ that the integration properly instruments MCP handlers with Sentry spans. """ +import anyio +import asyncio + import pytest import json from unittest import mock @@ -30,6 +33,8 @@ async def __call__(self, *args, **kwargs): from mcp.server.lowlevel import Server from mcp.server.lowlevel.server import request_ctx +from mcp.types import GetPromptResult, PromptMessage, TextContent +from mcp.server.lowlevel.helper_types import ReadResourceContents try: from mcp.server.lowlevel.server import request_ctx @@ -40,6 +45,12 @@ async def __call__(self, *args, **kwargs): from sentry_sdk.consts import SPANDATA, OP from sentry_sdk.integrations.mcp import MCPIntegration +from mcp.server.sse import SseServerTransport +from mcp.server.streamable_http_manager import StreamableHTTPSessionManager +from starlette.routing import Mount, Route +from starlette.applications import Starlette +from starlette.responses import Response + @pytest.fixture(autouse=True) def reset_request_ctx(): @@ -60,47 +71,6 @@ def reset_request_ctx(): pass -# Mock MCP types and structures -class MockURI: - """Mock URI object for resource testing""" - - def __init__(self, uri_string): - self.scheme = uri_string.split("://")[0] if "://" in uri_string else "" - self.path = uri_string.split("://")[1] if "://" in uri_string else uri_string - self._uri_string = uri_string - - def __str__(self): - return self._uri_string - - -class MockRequestContext: - """Mock MCP request context""" - - def __init__(self, request_id=None, session_id=None, transport="stdio"): - self.request_id = request_id - if transport in ("http", "sse"): - self.request = MockHTTPRequest(session_id, transport) - else: - self.request = None - - -class MockHTTPRequest: - """Mock HTTP request for SSE/StreamableHTTP transport""" - - def __init__(self, session_id=None, transport="http"): - self.headers = {} - self.query_params = {} - - if transport == "sse": - # SSE transport uses query parameter - if session_id: - self.query_params["session_id"] = session_id - else: - # StreamableHTTP transport uses header - if session_id: - self.headers["mcp-session-id"] = session_id - - class MockTextContent: """Mock TextContent object""" @@ -108,21 +78,6 @@ def __init__(self, text): self.text = text -class MockPromptMessage: - """Mock PromptMessage object""" - - def __init__(self, role, content_text): - self.role = role - self.content = MockTextContent(content_text) - - -class MockGetPromptResult: - """Mock GetPromptResult object""" - - def __init__(self, messages): - self.messages = messages - - def test_integration_patches_server(sentry_init): """Test that MCPIntegration patches the Server class""" # Get original methods before integration @@ -141,12 +96,13 @@ def test_integration_patches_server(sentry_init): assert Server.read_resource is not original_read_resource +@pytest.mark.asyncio @pytest.mark.parametrize( "send_default_pii, include_prompts", [(True, True), (True, False), (False, True), (False, False)], ) -def test_tool_handler_sync( - sentry_init, capture_events, send_default_pii, include_prompts +async def test_tool_handler_stdio( + sentry_init, capture_events, send_default_pii, include_prompts, stdio ): """Test that synchronous tool handlers create proper spans""" sentry_init( @@ -158,19 +114,25 @@ def test_tool_handler_sync( server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-123", transport="stdio") - request_ctx.set(mock_ctx) - @server.call_tool() - def test_tool(tool_name, arguments): + async def test_tool(tool_name, arguments): return {"result": "success", "value": 42} with start_transaction(name="mcp tx"): - # Call the tool handler - result = test_tool("calculate", {"x": 10, "y": 5}) + result = await stdio( + server, + method="tools/call", + params={ + "name": "calculate", + "arguments": {"x": 10, "y": 5}, + }, + request_id="req-123", + ) - assert result == {"result": "success", "value": 42} + assert result.message.root.result["content"][0]["text"] == json.dumps( + {"result": "success", "value": 42}, + indent=2, + ) (tx,) = events assert tx["type"] == "transaction" @@ -208,8 +170,13 @@ def test_tool(tool_name, arguments): "send_default_pii, include_prompts", [(True, True), (True, False), (False, True), (False, False)], ) -async def test_tool_handler_async( - sentry_init, capture_events, send_default_pii, include_prompts +async def test_tool_handler_streamable_http( + sentry_init, + capture_events, + send_default_pii, + include_prompts, + json_rpc, + select_mcp_transactions, ): """Test that async tool handlers create proper spans""" sentry_init( @@ -221,48 +188,75 @@ async def test_tool_handler_async( server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext( - request_id="req-456", session_id="session-789", transport="http" + session_manager = StreamableHTTPSessionManager( + app=server, + json_response=True, + ) + + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), ) - request_ctx.set(mock_ctx) @server.call_tool() async def test_tool_async(tool_name, arguments): - return {"status": "completed"} - - with start_transaction(name="mcp tx"): - result = await test_tool_async("process", {"data": "test"}) + return [ + TextContent( + type="text", + text=json.dumps({"status": "completed"}), + ) + ] - assert result == {"status": "completed"} + session_id, result = json_rpc( + app, + method="tools/call", + params={ + "name": "process", + "arguments": { + "data": "test", + }, + }, + request_id="req-456", + ) + assert result.json()["result"]["content"][0]["text"] == json.dumps( + {"status": "completed"} + ) - (tx,) = events + transactions = select_mcp_transactions(events) + assert len(transactions) == 1 + tx = transactions[0] assert tx["type"] == "transaction" - assert len(tx["spans"]) == 1 - span = tx["spans"][0] - assert span["op"] == OP.MCP_SERVER - assert span["description"] == "tools/call process" - assert span["origin"] == "auto.ai.mcp" + assert tx["contexts"]["trace"]["op"] == OP.MCP_SERVER + assert tx["transaction"] == "tools/call process" + assert tx["contexts"]["trace"]["origin"] == "auto.ai.mcp" # Check span data - assert span["data"][SPANDATA.MCP_TOOL_NAME] == "process" - assert span["data"][SPANDATA.MCP_METHOD_NAME] == "tools/call" - assert span["data"][SPANDATA.MCP_TRANSPORT] == "http" - assert span["data"][SPANDATA.MCP_REQUEST_ID] == "req-456" - assert span["data"][SPANDATA.MCP_SESSION_ID] == "session-789" - assert span["data"]["mcp.request.argument.data"] == '"test"' + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_TOOL_NAME] == "process" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_METHOD_NAME] == "tools/call" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_TRANSPORT] == "http" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_REQUEST_ID] == "req-456" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_SESSION_ID] == session_id + assert tx["contexts"]["trace"]["data"]["mcp.request.argument.data"] == '"test"' # Check PII-sensitive data if send_default_pii and include_prompts: - assert span["data"][SPANDATA.MCP_TOOL_RESULT_CONTENT] == json.dumps( - {"status": "completed"} + # TODO: Investigate why tool result is double-serialized. + assert tx["contexts"]["trace"]["data"][ + SPANDATA.MCP_TOOL_RESULT_CONTENT + ] == json.dumps( + json.dumps( + {"status": "completed"}, + ) ) else: - assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in span["data"] + assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in tx["contexts"]["trace"]["data"] -def test_tool_handler_with_error(sentry_init, capture_events): +@pytest.mark.asyncio +async def test_tool_handler_with_error(sentry_init, capture_events, stdio): """Test that tool handler errors are captured properly""" sentry_init( integrations=[MCPIntegration()], @@ -272,17 +266,24 @@ def test_tool_handler_with_error(sentry_init, capture_events): server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-error", transport="stdio") - request_ctx.set(mock_ctx) - @server.call_tool() def failing_tool(tool_name, arguments): raise ValueError("Tool execution failed") with start_transaction(name="mcp tx"): - with pytest.raises(ValueError): - failing_tool("bad_tool", {}) + result = await stdio( + server, + method="tools/call", + params={ + "name": "bad_tool", + "arguments": {}, + }, + request_id="req-error", + ) + + assert ( + result.message.root.result["content"][0]["text"] == "Tool execution failed" + ) # Should have error event and transaction assert len(events) == 2 @@ -304,12 +305,13 @@ def failing_tool(tool_name, arguments): assert span["tags"]["status"] == "internal_error" +@pytest.mark.asyncio @pytest.mark.parametrize( "send_default_pii, include_prompts", [(True, True), (True, False), (False, True), (False, False)], ) -def test_prompt_handler_sync( - sentry_init, capture_events, send_default_pii, include_prompts +async def test_prompt_handler_stdio( + sentry_init, capture_events, send_default_pii, include_prompts, stdio ): """Test that synchronous prompt handlers create proper spans""" sentry_init( @@ -321,19 +323,34 @@ def test_prompt_handler_sync( server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-prompt", transport="stdio") - request_ctx.set(mock_ctx) - @server.get_prompt() - def test_prompt(name, arguments): - return MockGetPromptResult([MockPromptMessage("user", "Tell me about Python")]) + async def test_prompt(name, arguments): + return GetPromptResult( + description="A helpful test prompt", + messages=[ + PromptMessage( + role="user", + content=TextContent(type="text", text="Tell me about Python"), + ), + ], + ) with start_transaction(name="mcp tx"): - result = test_prompt("code_help", {"language": "python"}) + result = await stdio( + server, + method="prompts/get", + params={ + "name": "code_help", + "arguments": {"language": "python"}, + }, + request_id="req-prompt", + ) - assert result.messages[0].role == "user" - assert result.messages[0].content.text == "Tell me about Python" + assert result.message.root.result["messages"][0]["role"] == "user" + assert ( + result.message.root.result["messages"][0]["content"]["text"] + == "Tell me about Python" + ) (tx,) = events assert tx["type"] == "transaction" @@ -372,8 +389,13 @@ def test_prompt(name, arguments): "send_default_pii, include_prompts", [(True, True), (True, False), (False, True), (False, False)], ) -async def test_prompt_handler_async( - sentry_init, capture_events, send_default_pii, include_prompts +async def test_prompt_handler_streamable_http( + sentry_init, + capture_events, + send_default_pii, + include_prompts, + json_rpc, + select_mcp_transactions, ): """Test that async prompt handlers create proper spans""" sentry_init( @@ -385,42 +407,70 @@ async def test_prompt_handler_async( server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext( - request_id="req-async-prompt", session_id="session-abc", transport="http" + session_manager = StreamableHTTPSessionManager( + app=server, + json_response=True, + ) + + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), ) - request_ctx.set(mock_ctx) @server.get_prompt() async def test_prompt_async(name, arguments): - return MockGetPromptResult( - [ - MockPromptMessage("system", "You are a helpful assistant"), - MockPromptMessage("user", "What is MCP?"), - ] + return GetPromptResult( + description="A helpful test prompt", + messages=[ + PromptMessage( + role="user", + content=TextContent( + type="text", text="You are a helpful assistant" + ), + ), + PromptMessage( + role="user", content=TextContent(type="text", text="What is MCP?") + ), + ], ) - with start_transaction(name="mcp tx"): - result = await test_prompt_async("mcp_info", {}) - - assert len(result.messages) == 2 + _, result = json_rpc( + app, + method="prompts/get", + params={ + "name": "mcp_info", + "arguments": {}, + }, + request_id="req-async-prompt", + ) + assert len(result.json()["result"]["messages"]) == 2 - (tx,) = events + transactions = select_mcp_transactions(events) + assert len(transactions) == 1 + tx = transactions[0] assert tx["type"] == "transaction" - assert len(tx["spans"]) == 1 - span = tx["spans"][0] - assert span["op"] == OP.MCP_SERVER - assert span["description"] == "prompts/get mcp_info" + assert tx["contexts"]["trace"]["op"] == OP.MCP_SERVER + assert tx["transaction"] == "prompts/get mcp_info" # For multi-message prompts, count is always captured - assert span["data"][SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT] == 2 + assert ( + tx["contexts"]["trace"]["data"][SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT] == 2 + ) # Role/content are never captured for multi-message prompts (even with PII) - assert SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE not in span["data"] - assert SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT not in span["data"] + assert ( + SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE not in tx["contexts"]["trace"]["data"] + ) + assert ( + SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT + not in tx["contexts"]["trace"]["data"] + ) -def test_prompt_handler_with_error(sentry_init, capture_events): +@pytest.mark.asyncio +async def test_prompt_handler_with_error(sentry_init, capture_events, stdio): """Test that prompt handler errors are captured""" sentry_init( integrations=[MCPIntegration()], @@ -430,17 +480,22 @@ def test_prompt_handler_with_error(sentry_init, capture_events): server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-error-prompt", transport="stdio") - request_ctx.set(mock_ctx) - @server.get_prompt() - def failing_prompt(name, arguments): + async def failing_prompt(name, arguments): raise RuntimeError("Prompt not found") with start_transaction(name="mcp tx"): - with pytest.raises(RuntimeError): - failing_prompt("missing_prompt", {}) + response = await stdio( + server, + method="prompts/get", + params={ + "name": "code_help", + "arguments": {"language": "python"}, + }, + request_id="req-error-prompt", + ) + + assert response.message.root.error.message == "Prompt not found" # Should have error event and transaction assert len(events) == 2 @@ -450,7 +505,8 @@ def failing_prompt(name, arguments): assert error_event["exception"]["values"][0]["type"] == "RuntimeError" -def test_resource_handler_sync(sentry_init, capture_events): +@pytest.mark.asyncio +async def test_resource_handler_stdio(sentry_init, capture_events, stdio): """Test that synchronous resource handlers create proper spans""" sentry_init( integrations=[MCPIntegration()], @@ -460,19 +516,27 @@ def test_resource_handler_sync(sentry_init, capture_events): server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-resource", transport="stdio") - request_ctx.set(mock_ctx) - @server.read_resource() - def test_resource(uri): - return {"content": "file contents", "mime_type": "text/plain"} + async def test_resource(uri): + return [ + ReadResourceContents( + content=json.dumps({"content": "file contents"}), mime_type="text/plain" + ) + ] with start_transaction(name="mcp tx"): - uri = MockURI("file:///path/to/file.txt") - result = test_resource(uri) + result = await stdio( + server, + method="resources/read", + params={ + "uri": "file:///path/to/file.txt", + }, + request_id="req-resource", + ) - assert result["content"] == "file contents" + assert result.message.root.result["contents"][0]["text"] == json.dumps( + {"content": "file contents"}, + ) (tx,) = events assert tx["type"] == "transaction" @@ -494,7 +558,12 @@ def test_resource(uri): @pytest.mark.asyncio -async def test_resource_handler_async(sentry_init, capture_events): +async def test_resource_handler_streamble_http( + sentry_init, + capture_events, + json_rpc, + select_mcp_transactions, +): """Test that async resource handlers create proper spans""" sentry_init( integrations=[MCPIntegration()], @@ -504,36 +573,57 @@ async def test_resource_handler_async(sentry_init, capture_events): server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext( - request_id="req-async-resource", session_id="session-res", transport="http" + session_manager = StreamableHTTPSessionManager( + app=server, + json_response=True, + ) + + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), ) - request_ctx.set(mock_ctx) @server.read_resource() async def test_resource_async(uri): - return {"data": "resource data"} + return [ + ReadResourceContents( + content=json.dumps({"data": "resource data"}), mime_type="text/plain" + ) + ] - with start_transaction(name="mcp tx"): - uri = MockURI("https://example.com/resource") - result = await test_resource_async(uri) + session_id, result = json_rpc( + app, + method="resources/read", + params={ + "uri": "https://example.com/resource", + }, + request_id="req-async-resource", + ) - assert result["data"] == "resource data" + assert result.json()["result"]["contents"][0]["text"] == json.dumps( + {"data": "resource data"} + ) - (tx,) = events + transactions = select_mcp_transactions(events) + assert len(transactions) == 1 + tx = transactions[0] assert tx["type"] == "transaction" - assert len(tx["spans"]) == 1 - span = tx["spans"][0] - assert span["op"] == OP.MCP_SERVER - assert span["description"] == "resources/read https://example.com/resource" + assert tx["contexts"]["trace"]["op"] == OP.MCP_SERVER + assert tx["transaction"] == "resources/read https://example.com/resource" - assert span["data"][SPANDATA.MCP_RESOURCE_URI] == "https://example.com/resource" - assert span["data"][SPANDATA.MCP_RESOURCE_PROTOCOL] == "https" - assert span["data"][SPANDATA.MCP_SESSION_ID] == "session-res" + assert ( + tx["contexts"]["trace"]["data"][SPANDATA.MCP_RESOURCE_URI] + == "https://example.com/resource" + ) + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_RESOURCE_PROTOCOL] == "https" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_SESSION_ID] == session_id -def test_resource_handler_with_error(sentry_init, capture_events): +@pytest.mark.asyncio +async def test_resource_handler_with_error(sentry_init, capture_events, stdio): """Test that resource handler errors are captured""" sentry_init( integrations=[MCPIntegration()], @@ -543,18 +633,19 @@ def test_resource_handler_with_error(sentry_init, capture_events): server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-error-resource", transport="stdio") - request_ctx.set(mock_ctx) - @server.read_resource() def failing_resource(uri): raise FileNotFoundError("Resource not found") with start_transaction(name="mcp tx"): - with pytest.raises(FileNotFoundError): - uri = MockURI("file:///missing.txt") - failing_resource(uri) + await stdio( + server, + method="resources/read", + params={ + "uri": "file:///missing.txt", + }, + request_id="req-error-resource", + ) # Should have error event and transaction assert len(events) == 2 @@ -564,12 +655,13 @@ def failing_resource(uri): assert error_event["exception"]["values"][0]["type"] == "FileNotFoundError" +@pytest.mark.asyncio @pytest.mark.parametrize( "send_default_pii, include_prompts", [(True, True), (False, False)], ) -def test_tool_result_extraction_tuple( - sentry_init, capture_events, send_default_pii, include_prompts +async def test_tool_result_extraction_tuple( + sentry_init, capture_events, send_default_pii, include_prompts, stdio ): """Test extraction of tool results from tuple format (UnstructuredContent, StructuredContent)""" sentry_init( @@ -581,10 +673,6 @@ def test_tool_result_extraction_tuple( server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-tuple", transport="stdio") - request_ctx.set(mock_ctx) - @server.call_tool() def test_tool_tuple(tool_name, arguments): # Return CombinationContent: (UnstructuredContent, StructuredContent) @@ -593,7 +681,15 @@ def test_tool_tuple(tool_name, arguments): return (unstructured, structured) with start_transaction(name="mcp tx"): - test_tool_tuple("combo_tool", {}) + await stdio( + server, + method="tools/call", + params={ + "name": "calculate", + "arguments": {}, + }, + request_id="req-tuple", + ) (tx,) = events span = tx["spans"][0] @@ -612,12 +708,13 @@ def test_tool_tuple(tool_name, arguments): assert SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT not in span["data"] +@pytest.mark.asyncio @pytest.mark.parametrize( "send_default_pii, include_prompts", [(True, True), (False, False)], ) -def test_tool_result_extraction_unstructured( - sentry_init, capture_events, send_default_pii, include_prompts +async def test_tool_result_extraction_unstructured( + sentry_init, capture_events, send_default_pii, include_prompts, stdio ): """Test extraction of tool results from UnstructuredContent (list of content blocks)""" sentry_init( @@ -629,10 +726,6 @@ def test_tool_result_extraction_unstructured( server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-unstructured", transport="stdio") - request_ctx.set(mock_ctx) - @server.call_tool() def test_tool_unstructured(tool_name, arguments): # Return UnstructuredContent as list of content blocks @@ -642,7 +735,15 @@ def test_tool_unstructured(tool_name, arguments): ] with start_transaction(name="mcp tx"): - test_tool_unstructured("text_tool", {}) + await stdio( + server, + method="tools/call", + params={ + "name": "text_tool", + "arguments": {}, + }, + request_id="req-unstructured", + ) (tx,) = events span = tx["spans"][0] @@ -656,44 +757,8 @@ def test_tool_unstructured(tool_name, arguments): assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in span["data"] -def test_request_context_no_context(sentry_init, capture_events): - """Test handling when no request context is available""" - sentry_init( - integrations=[MCPIntegration()], - traces_sample_rate=1.0, - ) - events = capture_events() - - server = Server("test-server") - - # Clear request context (simulating no context available) - # This will cause a LookupError when trying to get context - request_ctx.set(None) - - @server.call_tool() - def test_tool_no_ctx(tool_name, arguments): - return {"result": "ok"} - - with start_transaction(name="mcp tx"): - # This should work even without request context - try: - test_tool_no_ctx("tool", {}) - except LookupError: - # If it raises LookupError, that's expected when context is truly missing - pass - - # Should still create span even if context is missing - (tx,) = events - span = tx["spans"][0] - - # Transport defaults to "pipe" when no context - assert span["data"][SPANDATA.MCP_TRANSPORT] == "stdio" - # Request ID and Session ID should not be present - assert SPANDATA.MCP_REQUEST_ID not in span["data"] - assert SPANDATA.MCP_SESSION_ID not in span["data"] - - -def test_span_origin(sentry_init, capture_events): +@pytest.mark.asyncio +async def test_span_origin(sentry_init, capture_events, stdio): """Test that span origin is set correctly""" sentry_init( integrations=[MCPIntegration()], @@ -703,16 +768,20 @@ def test_span_origin(sentry_init, capture_events): server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-origin", transport="stdio") - request_ctx.set(mock_ctx) - @server.call_tool() def test_tool(tool_name, arguments): return {"result": "test"} with start_transaction(name="mcp tx"): - test_tool("origin_test", {}) + await stdio( + server, + method="tools/call", + params={ + "name": "calculate", + "arguments": {"x": 10, "y": 5}, + }, + request_id="req-origin", + ) (tx,) = events @@ -720,7 +789,8 @@ def test_tool(tool_name, arguments): assert tx["spans"][0]["origin"] == "auto.ai.mcp" -def test_multiple_handlers(sentry_init, capture_events): +@pytest.mark.asyncio +async def test_multiple_handlers(sentry_init, capture_events, stdio): """Test that multiple handler calls create multiple spans""" sentry_init( integrations=[MCPIntegration()], @@ -730,10 +800,6 @@ def test_multiple_handlers(sentry_init, capture_events): server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-multi", transport="stdio") - request_ctx.set(mock_ctx) - @server.call_tool() def tool1(tool_name, arguments): return {"result": "tool1"} @@ -744,12 +810,45 @@ def tool2(tool_name, arguments): @server.get_prompt() def prompt1(name, arguments): - return MockGetPromptResult([MockPromptMessage("user", "Test prompt")]) + return GetPromptResult( + description="A test prompt", + messages=[ + PromptMessage( + role="user", content=TextContent(type="text", text="Test prompt") + ) + ], + ) with start_transaction(name="mcp tx"): - tool1("tool_a", {}) - tool2("tool_b", {}) - prompt1("prompt_a", {}) + await stdio( + server, + method="tools/call", + params={ + "name": "tool_a", + "arguments": {}, + }, + request_id="req-multi", + ) + + await stdio( + server, + method="tools/call", + params={ + "name": "tool_b", + "arguments": {}, + }, + request_id="req-multi", + ) + + await stdio( + server, + method="prompts/get", + params={ + "name": "prompt_a", + "arguments": {}, + }, + request_id="req-multi", + ) (tx,) = events assert tx["type"] == "transaction" @@ -765,12 +864,13 @@ def prompt1(name, arguments): assert "prompts/get prompt_a" in span_descriptions +@pytest.mark.asyncio @pytest.mark.parametrize( "send_default_pii, include_prompts", [(True, True), (False, False)], ) -def test_prompt_with_dict_result( - sentry_init, capture_events, send_default_pii, include_prompts +async def test_prompt_with_dict_result( + sentry_init, capture_events, send_default_pii, include_prompts, stdio ): """Test prompt handler with dict result instead of GetPromptResult object""" sentry_init( @@ -782,10 +882,6 @@ def test_prompt_with_dict_result( server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-dict-prompt", transport="stdio") - request_ctx.set(mock_ctx) - @server.get_prompt() def test_prompt_dict(name, arguments): # Return dict format instead of GetPromptResult object @@ -796,7 +892,15 @@ def test_prompt_dict(name, arguments): } with start_transaction(name="mcp tx"): - test_prompt_dict("dict_prompt", {}) + await stdio( + server, + method="prompts/get", + params={ + "name": "dict_prompt", + "arguments": {}, + }, + request_id="req-dict-prompt", + ) (tx,) = events span = tx["spans"][0] @@ -816,37 +920,8 @@ def test_prompt_dict(name, arguments): assert SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT not in span["data"] -def test_resource_without_protocol(sentry_init, capture_events): - """Test resource handler with URI without protocol scheme""" - sentry_init( - integrations=[MCPIntegration()], - traces_sample_rate=1.0, - ) - events = capture_events() - - server = Server("test-server") - - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-no-proto", transport="stdio") - request_ctx.set(mock_ctx) - - @server.read_resource() - def test_resource(uri): - return {"data": "test"} - - with start_transaction(name="mcp tx"): - # URI without protocol - test_resource("simple-path") - - (tx,) = events - span = tx["spans"][0] - - assert span["data"][SPANDATA.MCP_RESOURCE_URI] == "simple-path" - # No protocol should be set - assert SPANDATA.MCP_RESOURCE_PROTOCOL not in span["data"] - - -def test_tool_with_complex_arguments(sentry_init, capture_events): +@pytest.mark.asyncio +async def test_tool_with_complex_arguments(sentry_init, capture_events, stdio): """Test tool handler with complex nested arguments""" sentry_init( integrations=[MCPIntegration()], @@ -856,10 +931,6 @@ def test_tool_with_complex_arguments(sentry_init, capture_events): server = Server("test-server") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-complex", transport="stdio") - request_ctx.set(mock_ctx) - @server.call_tool() def test_tool_complex(tool_name, arguments): return {"processed": True} @@ -870,7 +941,15 @@ def test_tool_complex(tool_name, arguments): "string": "test", "number": 42, } - test_tool_complex("complex_tool", complex_args) + await stdio( + server, + method="tools/call", + params={ + "name": "complex_tool", + "arguments": complex_args, + }, + request_id="req-complex", + ) (tx,) = events span = tx["spans"][0] @@ -884,8 +963,8 @@ def test_tool_complex(tool_name, arguments): @pytest.mark.asyncio -async def test_async_handlers_mixed(sentry_init, capture_events): - """Test mixing sync and async handlers in the same transaction""" +async def test_sse_transport_detection(sentry_init, capture_events, json_rpc_sse): + """Test that SSE transport is correctly detected via query parameter""" sentry_init( integrations=[MCPIntegration()], traces_sample_rate=1.0, @@ -893,68 +972,75 @@ async def test_async_handlers_mixed(sentry_init, capture_events): events = capture_events() server = Server("test-server") + sse = SseServerTransport("/messages/") - # Set up mock request context - mock_ctx = MockRequestContext(request_id="req-mixed", transport="stdio") - request_ctx.set(mock_ctx) - - @server.call_tool() - def sync_tool(tool_name, arguments): - return {"type": "sync"} - - @server.call_tool() - async def async_tool(tool_name, arguments): - return {"type": "async"} - - with start_transaction(name="mcp tx"): - sync_result = sync_tool("sync", {}) - async_result = await async_tool("async", {}) - - assert sync_result["type"] == "sync" - assert async_result["type"] == "async" - - (tx,) = events - assert len(tx["spans"]) == 2 + sse_connection_closed = asyncio.Event() - # Both should be instrumented correctly - assert all(span["op"] == OP.MCP_SERVER for span in tx["spans"]) + async def handle_sse(request): + async with sse.connect_sse( + request.scope, request.receive, request._send + ) as streams: + async with anyio.create_task_group() as tg: + async def run_server(): + await server.run( + streams[0], streams[1], server.create_initialization_options() + ) -def test_sse_transport_detection(sentry_init, capture_events): - """Test that SSE transport is correctly detected via query parameter""" - sentry_init( - integrations=[MCPIntegration()], - traces_sample_rate=1.0, - ) - events = capture_events() + tg.start_soon(run_server) - server = Server("test-server") + sse_connection_closed.set() + return Response() - # Set up mock request context with SSE transport - mock_ctx = MockRequestContext( - request_id="req-sse", session_id="session-sse-123", transport="sse" + app = Starlette( + routes=[ + Route("/sse", endpoint=handle_sse, methods=["GET"]), + Mount("/messages/", app=sse.handle_post_message), + ], ) - request_ctx.set(mock_ctx) @server.call_tool() - def test_tool(tool_name, arguments): + async def test_tool(tool_name, arguments): return {"result": "success"} - with start_transaction(name="mcp tx"): - result = test_tool("sse_tool", {}) + keep_sse_alive = asyncio.Event() + app_task, session_id, result = await json_rpc_sse( + app, + method="tools/call", + params={ + "name": "sse_tool", + "arguments": {}, + }, + request_id="req-sse", + keep_sse_alive=keep_sse_alive, + ) - assert result == {"result": "success"} + await sse_connection_closed.wait() + await app_task - (tx,) = events + assert result["result"]["structuredContent"] == {"result": "success"} + + transactions = [ + event + for event in events + if event["type"] == "transaction" and event["transaction"] == "/sse" + ] + assert len(transactions) == 1 + tx = transactions[0] span = tx["spans"][0] # Check that SSE transport is detected assert span["data"][SPANDATA.MCP_TRANSPORT] == "sse" assert span["data"][SPANDATA.NETWORK_TRANSPORT] == "tcp" - assert span["data"][SPANDATA.MCP_SESSION_ID] == "session-sse-123" + assert span["data"][SPANDATA.MCP_SESSION_ID] == session_id -def test_streamable_http_transport_detection(sentry_init, capture_events): +def test_streamable_http_transport_detection( + sentry_init, + capture_events, + json_rpc, + select_mcp_transactions, +): """Test that StreamableHTTP transport is correctly detected via header""" sentry_init( integrations=[MCPIntegration()], @@ -964,31 +1050,52 @@ def test_streamable_http_transport_detection(sentry_init, capture_events): server = Server("test-server") - # Set up mock request context with StreamableHTTP transport - mock_ctx = MockRequestContext( - request_id="req-http", session_id="session-http-456", transport="http" + session_manager = StreamableHTTPSessionManager( + app=server, + json_response=True, ) - request_ctx.set(mock_ctx) - @server.call_tool() - def test_tool(tool_name, arguments): - return {"result": "success"} + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), + ) - with start_transaction(name="mcp tx"): - result = test_tool("http_tool", {}) + @server.call_tool() + async def test_tool(tool_name, arguments): + return [ + TextContent( + type="text", + text=json.dumps({"status": "success"}), + ) + ] - assert result == {"result": "success"} + session_id, result = json_rpc( + app, + method="tools/call", + params={ + "name": "http_tool", + "arguments": {}, + }, + request_id="req-http", + ) + assert result.json()["result"]["content"][0]["text"] == json.dumps( + {"status": "success"} + ) - (tx,) = events - span = tx["spans"][0] + transactions = select_mcp_transactions(events) + assert len(transactions) == 1 + tx = transactions[0] # Check that HTTP transport is detected - assert span["data"][SPANDATA.MCP_TRANSPORT] == "http" - assert span["data"][SPANDATA.NETWORK_TRANSPORT] == "tcp" - assert span["data"][SPANDATA.MCP_SESSION_ID] == "session-http-456" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_TRANSPORT] == "http" + assert tx["contexts"]["trace"]["data"][SPANDATA.NETWORK_TRANSPORT] == "tcp" + assert tx["contexts"]["trace"]["data"][SPANDATA.MCP_SESSION_ID] == session_id -def test_stdio_transport_detection(sentry_init, capture_events): +@pytest.mark.asyncio +async def test_stdio_transport_detection(sentry_init, capture_events, stdio): """Test that stdio transport is correctly detected when no HTTP request""" sentry_init( integrations=[MCPIntegration()], @@ -998,18 +1105,22 @@ def test_stdio_transport_detection(sentry_init, capture_events): server = Server("test-server") - # Set up mock request context with stdio transport (no HTTP request) - mock_ctx = MockRequestContext(request_id="req-stdio", transport="stdio") - request_ctx.set(mock_ctx) - @server.call_tool() - def test_tool(tool_name, arguments): + async def test_tool(tool_name, arguments): return {"result": "success"} with start_transaction(name="mcp tx"): - result = test_tool("stdio_tool", {}) + result = await stdio( + server, + method="tools/call", + params={ + "name": "stdio_tool", + "arguments": {}, + }, + request_id="req-stdio", + ) - assert result == {"result": "success"} + assert result.message.root.result["structuredContent"] == {"result": "success"} (tx,) = events span = tx["spans"][0] diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 814289c887..3581a14bd7 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -9,8 +9,10 @@ NOT_GIVEN = None try: from openai import omit + from openai import Omit except ImportError: omit = None + Omit = None from openai import AsyncOpenAI, OpenAI, AsyncStream, Stream, OpenAIError from openai.types import CompletionUsage, CreateEmbeddingResponse, Embedding @@ -44,9 +46,9 @@ OpenAIIntegration, _calculate_token_usage, ) -from sentry_sdk.ai.utils import MAX_GEN_AI_MESSAGE_BYTES from sentry_sdk._types import AnnotatedValue from sentry_sdk.serializer import serialize +from sentry_sdk.utils import safe_serialize from unittest import mock # python 3.3 and above @@ -129,9 +131,13 @@ async def async_iterator(values): @pytest.mark.parametrize( "send_default_pii, include_prompts", - [(True, True), (True, False), (False, True), (False, False)], + [ + (True, False), + (False, True), + (False, False), + ], ) -def test_nonstreaming_chat_completion( +def test_nonstreaming_chat_completion_no_prompts( sentry_init, capture_events, send_default_pii, include_prompts ): sentry_init( @@ -147,7 +153,11 @@ def test_nonstreaming_chat_completion( with start_transaction(name="openai tx"): response = ( client.chat.completions.create( - model="some-model", messages=[{"role": "system", "content": "hello"}] + model="some-model", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "hello"}, + ], ) .choices[0] .message.content @@ -159,12 +169,92 @@ def test_nonstreaming_chat_completion( span = tx["spans"][0] assert span["op"] == "gen_ai.chat" - if send_default_pii and include_prompts: - assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] - assert "the model response" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] + + assert span["data"]["gen_ai.usage.output_tokens"] == 10 + assert span["data"]["gen_ai.usage.input_tokens"] == 20 + assert span["data"]["gen_ai.usage.total_tokens"] == 30 + + +@pytest.mark.parametrize( + "messages", + [ + pytest.param( + [ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + {"role": "user", "content": "hello"}, + ], + id="blocks", + ), + pytest.param( + [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + {"role": "user", "content": "hello"}, + ], + id="parts", + ), + ], +) +def test_nonstreaming_chat_completion(sentry_init, capture_events, messages, request): + sentry_init( + integrations=[OpenAIIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + client = OpenAI(api_key="z") + client.chat.completions._post = mock.Mock(return_value=EXAMPLE_CHAT_COMPLETION) + + with start_transaction(name="openai tx"): + response = ( + client.chat.completions.create( + model="some-model", + messages=messages, + ) + .choices[0] + .message.content + ) + + assert response == "the model response" + tx = events[0] + assert tx["type"] == "transaction" + span = tx["spans"][0] + assert span["op"] == "gen_ai.chat" + + param_id = request.node.callspec.id + if "blocks" in param_id: + assert json.loads(span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [ + { + "type": "text", + "content": "You are a helpful assistant.", + } + ] else: - assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] + assert json.loads(span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [ + { + "type": "text", + "content": "You are a helpful assistant.", + }, + { + "type": "text", + "content": "Be concise and clear.", + }, + ] + + assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + assert "the model response" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] assert span["data"]["gen_ai.usage.output_tokens"] == 10 assert span["data"]["gen_ai.usage.input_tokens"] == 20 @@ -174,9 +264,13 @@ def test_nonstreaming_chat_completion( @pytest.mark.asyncio @pytest.mark.parametrize( "send_default_pii, include_prompts", - [(True, True), (True, False), (False, True), (False, False)], + [ + (True, False), + (False, True), + (False, False), + ], ) -async def test_nonstreaming_chat_completion_async( +async def test_nonstreaming_chat_completion_async_no_prompts( sentry_init, capture_events, send_default_pii, include_prompts ): sentry_init( @@ -186,12 +280,80 @@ async def test_nonstreaming_chat_completion_async( ) events = capture_events() + client = AsyncOpenAI(api_key="z") + client.chat.completions._post = mock.AsyncMock(return_value=EXAMPLE_CHAT_COMPLETION) + + with start_transaction(name="openai tx"): + response = await client.chat.completions.create( + model="some-model", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "hello"}, + ], + ) + response = response.choices[0].message.content + + assert response == "the model response" + tx = events[0] + assert tx["type"] == "transaction" + span = tx["spans"][0] + assert span["op"] == "gen_ai.chat" + + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] + + assert span["data"]["gen_ai.usage.output_tokens"] == 10 + assert span["data"]["gen_ai.usage.input_tokens"] == 20 + assert span["data"]["gen_ai.usage.total_tokens"] == 30 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "messages", + [ + pytest.param( + [ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + {"role": "user", "content": "hello"}, + ], + id="blocks", + ), + pytest.param( + [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + {"role": "user", "content": "hello"}, + ], + id="parts", + ), + ], +) +async def test_nonstreaming_chat_completion_async( + sentry_init, capture_events, messages, request +): + sentry_init( + integrations=[OpenAIIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + client = AsyncOpenAI(api_key="z") client.chat.completions._post = AsyncMock(return_value=EXAMPLE_CHAT_COMPLETION) with start_transaction(name="openai tx"): response = await client.chat.completions.create( - model="some-model", messages=[{"role": "system", "content": "hello"}] + model="some-model", + messages=messages, ) response = response.choices[0].message.content @@ -201,12 +363,28 @@ async def test_nonstreaming_chat_completion_async( span = tx["spans"][0] assert span["op"] == "gen_ai.chat" - if send_default_pii and include_prompts: - assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] - assert "the model response" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + param_id = request.node.callspec.id + if "blocks" in param_id: + assert json.loads(span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [ + { + "type": "text", + "content": "You are a helpful assistant.", + } + ] else: - assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] + assert json.loads(span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [ + { + "type": "text", + "content": "You are a helpful assistant.", + }, + { + "type": "text", + "content": "Be concise and clear.", + }, + ] + + assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + assert "the model response" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] assert span["data"]["gen_ai.usage.output_tokens"] == 10 assert span["data"]["gen_ai.usage.input_tokens"] == 20 @@ -225,9 +403,13 @@ def tiktoken_encoding_if_installed(): # noinspection PyTypeChecker @pytest.mark.parametrize( "send_default_pii, include_prompts", - [(True, True), (True, False), (False, True), (False, False)], + [ + (True, False), + (False, True), + (False, False), + ], ) -def test_streaming_chat_completion( +def test_streaming_chat_completion_no_prompts( sentry_init, capture_events, send_default_pii, include_prompts ): sentry_init( @@ -283,7 +465,11 @@ def test_streaming_chat_completion( client.chat.completions._post = mock.Mock(return_value=returned_stream) with start_transaction(name="openai tx"): response_stream = client.chat.completions.create( - model="some-model", messages=[{"role": "system", "content": "hello"}] + model="some-model", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "hello"}, + ], ) response_string = "".join( map(lambda x: x.choices[0].delta.content, response_stream) @@ -294,19 +480,149 @@ def test_streaming_chat_completion( span = tx["spans"][0] assert span["op"] == "gen_ai.chat" - if send_default_pii and include_prompts: - assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] - assert "hello world" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - else: - assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] try: import tiktoken # type: ignore # noqa # pylint: disable=unused-import assert span["data"]["gen_ai.usage.output_tokens"] == 2 - assert span["data"]["gen_ai.usage.input_tokens"] == 1 - assert span["data"]["gen_ai.usage.total_tokens"] == 3 + assert span["data"]["gen_ai.usage.input_tokens"] == 7 + assert span["data"]["gen_ai.usage.total_tokens"] == 9 + except ImportError: + pass # if tiktoken is not installed, we can't guarantee token usage will be calculated properly + + +# noinspection PyTypeChecker +@pytest.mark.parametrize( + "messages", + [ + pytest.param( + [ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + {"role": "user", "content": "hello"}, + ], + id="blocks", + ), + pytest.param( + [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + {"role": "user", "content": "hello"}, + ], + id="parts", + ), + ], +) +def test_streaming_chat_completion(sentry_init, capture_events, messages, request): + sentry_init( + integrations=[ + OpenAIIntegration( + include_prompts=True, + tiktoken_encoding_name=tiktoken_encoding_if_installed(), + ) + ], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + client = OpenAI(api_key="z") + returned_stream = Stream(cast_to=None, response=None, client=client) + returned_stream._iterator = [ + ChatCompletionChunk( + id="1", + choices=[ + DeltaChoice( + index=0, delta=ChoiceDelta(content="hel"), finish_reason=None + ) + ], + created=100000, + model="model-id", + object="chat.completion.chunk", + ), + ChatCompletionChunk( + id="1", + choices=[ + DeltaChoice( + index=1, delta=ChoiceDelta(content="lo "), finish_reason=None + ) + ], + created=100000, + model="model-id", + object="chat.completion.chunk", + ), + ChatCompletionChunk( + id="1", + choices=[ + DeltaChoice( + index=2, delta=ChoiceDelta(content="world"), finish_reason="stop" + ) + ], + created=100000, + model="model-id", + object="chat.completion.chunk", + ), + ] + + client.chat.completions._post = mock.Mock(return_value=returned_stream) + with start_transaction(name="openai tx"): + response_stream = client.chat.completions.create( + model="some-model", + messages=messages, + ) + response_string = "".join( + map(lambda x: x.choices[0].delta.content, response_stream) + ) + assert response_string == "hello world" + tx = events[0] + assert tx["type"] == "transaction" + span = tx["spans"][0] + assert span["op"] == "gen_ai.chat" + + param_id = request.node.callspec.id + if "blocks" in param_id: + assert json.loads(span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [ + { + "type": "text", + "content": "You are a helpful assistant.", + } + ] + else: + assert json.loads(span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [ + { + "type": "text", + "content": "You are a helpful assistant.", + }, + { + "type": "text", + "content": "Be concise and clear.", + }, + ] + + assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + assert "hello world" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + + try: + import tiktoken # type: ignore # noqa # pylint: disable=unused-import + + if "blocks" in param_id: + assert span["data"]["gen_ai.usage.output_tokens"] == 2 + assert span["data"]["gen_ai.usage.input_tokens"] == 7 + assert span["data"]["gen_ai.usage.total_tokens"] == 9 + else: + assert span["data"]["gen_ai.usage.output_tokens"] == 2 + assert span["data"]["gen_ai.usage.input_tokens"] == 1 + assert span["data"]["gen_ai.usage.total_tokens"] == 3 except ImportError: pass # if tiktoken is not installed, we can't guarantee token usage will be calculated properly @@ -315,9 +631,13 @@ def test_streaming_chat_completion( @pytest.mark.asyncio @pytest.mark.parametrize( "send_default_pii, include_prompts", - [(True, True), (True, False), (False, True), (False, False)], + [ + (True, False), + (False, True), + (False, False), + ], ) -async def test_streaming_chat_completion_async( +async def test_streaming_chat_completion_async_no_prompts( sentry_init, capture_events, send_default_pii, include_prompts ): sentry_init( @@ -377,7 +697,11 @@ async def test_streaming_chat_completion_async( client.chat.completions._post = AsyncMock(return_value=returned_stream) with start_transaction(name="openai tx"): response_stream = await client.chat.completions.create( - model="some-model", messages=[{"role": "system", "content": "hello"}] + model="some-model", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "hello"}, + ], ) response_string = "" @@ -390,19 +714,160 @@ async def test_streaming_chat_completion_async( span = tx["spans"][0] assert span["op"] == "gen_ai.chat" - if send_default_pii and include_prompts: - assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] - assert "hello world" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - else: - assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] try: import tiktoken # type: ignore # noqa # pylint: disable=unused-import assert span["data"]["gen_ai.usage.output_tokens"] == 2 - assert span["data"]["gen_ai.usage.input_tokens"] == 1 - assert span["data"]["gen_ai.usage.total_tokens"] == 3 + assert span["data"]["gen_ai.usage.input_tokens"] == 7 + assert span["data"]["gen_ai.usage.total_tokens"] == 9 + + except ImportError: + pass # if tiktoken is not installed, we can't guarantee token usage will be calculated properly + + +# noinspection PyTypeChecker +@pytest.mark.asyncio +@pytest.mark.parametrize( + "messages", + [ + pytest.param( + [ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + {"role": "user", "content": "hello"}, + ], + id="blocks", + ), + pytest.param( + [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + {"role": "user", "content": "hello"}, + ], + id="parts", + ), + ], +) +async def test_streaming_chat_completion_async( + sentry_init, capture_events, messages, request +): + sentry_init( + integrations=[ + OpenAIIntegration( + include_prompts=True, + tiktoken_encoding_name=tiktoken_encoding_if_installed(), + ) + ], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + client = AsyncOpenAI(api_key="z") + returned_stream = AsyncStream(cast_to=None, response=None, client=client) + returned_stream._iterator = async_iterator( + [ + ChatCompletionChunk( + id="1", + choices=[ + DeltaChoice( + index=0, delta=ChoiceDelta(content="hel"), finish_reason=None + ) + ], + created=100000, + model="model-id", + object="chat.completion.chunk", + ), + ChatCompletionChunk( + id="1", + choices=[ + DeltaChoice( + index=1, delta=ChoiceDelta(content="lo "), finish_reason=None + ) + ], + created=100000, + model="model-id", + object="chat.completion.chunk", + ), + ChatCompletionChunk( + id="1", + choices=[ + DeltaChoice( + index=2, + delta=ChoiceDelta(content="world"), + finish_reason="stop", + ) + ], + created=100000, + model="model-id", + object="chat.completion.chunk", + ), + ] + ) + + client.chat.completions._post = AsyncMock(return_value=returned_stream) + with start_transaction(name="openai tx"): + response_stream = await client.chat.completions.create( + model="some-model", + messages=messages, + ) + + response_string = "" + async for x in response_stream: + response_string += x.choices[0].delta.content + + assert response_string == "hello world" + tx = events[0] + assert tx["type"] == "transaction" + span = tx["spans"][0] + assert span["op"] == "gen_ai.chat" + + param_id = request.node.callspec.id + if "blocks" in param_id: + assert json.loads(span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [ + { + "type": "text", + "content": "You are a helpful assistant.", + } + ] + else: + assert json.loads(span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) == [ + { + "type": "text", + "content": "You are a helpful assistant.", + }, + { + "type": "text", + "content": "Be concise and clear.", + }, + ] + + assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + assert "hello world" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + + try: + import tiktoken # type: ignore # noqa # pylint: disable=unused-import + + if "blocks" in param_id: + assert span["data"]["gen_ai.usage.output_tokens"] == 2 + assert span["data"]["gen_ai.usage.input_tokens"] == 7 + assert span["data"]["gen_ai.usage.total_tokens"] == 9 + else: + assert span["data"]["gen_ai.usage.output_tokens"] == 2 + assert span["data"]["gen_ai.usage.input_tokens"] == 1 + assert span["data"]["gen_ai.usage.total_tokens"] == 3 + except ImportError: pass # if tiktoken is not installed, we can't guarantee token usage will be calculated properly @@ -1034,12 +1499,79 @@ def test_ai_client_span_responses_api_no_pii(sentry_init, capture_events): "thread.name": mock.ANY, } + assert "gen_ai.system_instructions" not in spans[0]["data"] assert "gen_ai.request.messages" not in spans[0]["data"] assert "gen_ai.response.text" not in spans[0]["data"] +@pytest.mark.parametrize( + "instructions", + ( + omit, + None, + "You are a coding assistant that talks like a pirate.", + ), +) +@pytest.mark.parametrize( + "input", + [ + pytest.param( + "How do I check if a Python object is an instance of a class?", id="string" + ), + pytest.param( + [ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + {"role": "user", "content": "hello"}, + ], + id="blocks_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": "You are a helpful assistant.", + }, + {"type": "message", "role": "user", "content": "hello"}, + ], + id="blocks", + ), + pytest.param( + [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + {"role": "user", "content": "hello"}, + ], + id="parts_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + {"type": "message", "role": "user", "content": "hello"}, + ], + id="parts", + ), + ], +) @pytest.mark.skipif(SKIP_RESPONSES_TESTS, reason="Responses API not available") -def test_ai_client_span_responses_api(sentry_init, capture_events): +def test_ai_client_span_responses_api( + sentry_init, capture_events, instructions, input, request +): sentry_init( integrations=[OpenAIIntegration(include_prompts=True)], traces_sample_rate=1.0, @@ -1053,8 +1585,8 @@ def test_ai_client_span_responses_api(sentry_init, capture_events): with start_transaction(name="openai tx"): client.responses.create( model="gpt-4o", - instructions="You are a coding assistant that talks like a pirate.", - input="How do I check if a Python object is an instance of a class?", + instructions=instructions, + input=input, ) (transaction,) = events @@ -1063,10 +1595,9 @@ def test_ai_client_span_responses_api(sentry_init, capture_events): assert len(spans) == 1 assert spans[0]["op"] == "gen_ai.responses" assert spans[0]["origin"] == "auto.ai.openai" - assert spans[0]["data"] == { + + expected_data = { "gen_ai.operation.name": "responses", - "gen_ai.request.messages": '["How do I check if a Python object is an instance of a class?"]', - "gen_ai.request.model": "gpt-4o", "gen_ai.system": "openai", "gen_ai.response.model": "response-model-id", "gen_ai.usage.input_tokens": 20, @@ -1074,11 +1605,169 @@ def test_ai_client_span_responses_api(sentry_init, capture_events): "gen_ai.usage.output_tokens": 10, "gen_ai.usage.output_tokens.reasoning": 8, "gen_ai.usage.total_tokens": 30, + "gen_ai.request.model": "gpt-4o", "gen_ai.response.text": "the model response", + "sentry.sdk_meta.gen_ai.input.messages.original_length": 1, "thread.id": mock.ANY, "thread.name": mock.ANY, } + param_id = request.node.callspec.id + if "string" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.request.messages": safe_serialize( + ["How do I check if a Python object is an instance of a class?"] + ), + } + ) + elif "string" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + } + ] + ), + "gen_ai.request.messages": safe_serialize( + ["How do I check if a Python object is an instance of a class?"] + ), + } + ) + elif "blocks_no_type" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [{"type": "text", "content": "You are a helpful assistant."}] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif "blocks_no_type" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif "blocks" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [{"type": "text", "content": "You are a helpful assistant."}] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + elif "blocks" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + elif "parts_no_type" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif "parts_no_type" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif instructions is None or isinstance(instructions, Omit): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + else: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + + assert spans[0]["data"] == expected_data + @pytest.mark.skipif(SKIP_RESPONSES_TESTS, reason="Responses API not available") def test_error_in_responses_api(sentry_init, capture_events): @@ -1119,7 +1808,73 @@ def test_error_in_responses_api(sentry_init, capture_events): @pytest.mark.asyncio @pytest.mark.skipif(SKIP_RESPONSES_TESTS, reason="Responses API not available") -async def test_ai_client_span_responses_async_api(sentry_init, capture_events): +@pytest.mark.parametrize( + "instructions", + ( + omit, + None, + "You are a coding assistant that talks like a pirate.", + ), +) +@pytest.mark.parametrize( + "input", + [ + pytest.param( + "How do I check if a Python object is an instance of a class?", id="string" + ), + pytest.param( + [ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + {"role": "user", "content": "hello"}, + ], + id="blocks_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": "You are a helpful assistant.", + }, + {"type": "message", "role": "user", "content": "hello"}, + ], + id="blocks", + ), + pytest.param( + [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + {"role": "user", "content": "hello"}, + ], + id="parts_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + {"type": "message", "role": "user", "content": "hello"}, + ], + id="parts", + ), + ], +) +async def test_ai_client_span_responses_async_api( + sentry_init, capture_events, instructions, input, request +): sentry_init( integrations=[OpenAIIntegration(include_prompts=True)], traces_sample_rate=1.0, @@ -1133,8 +1888,8 @@ async def test_ai_client_span_responses_async_api(sentry_init, capture_events): with start_transaction(name="openai tx"): await client.responses.create( model="gpt-4o", - instructions="You are a coding assistant that talks like a pirate.", - input="How do I check if a Python object is an instance of a class?", + instructions=instructions, + input=input, ) (transaction,) = events @@ -1143,7 +1898,8 @@ async def test_ai_client_span_responses_async_api(sentry_init, capture_events): assert len(spans) == 1 assert spans[0]["op"] == "gen_ai.responses" assert spans[0]["origin"] == "auto.ai.openai" - assert spans[0]["data"] == { + + expected_data = { "gen_ai.operation.name": "responses", "gen_ai.request.messages": '["How do I check if a Python object is an instance of a class?"]', "gen_ai.request.model": "gpt-4o", @@ -1155,15 +1911,236 @@ async def test_ai_client_span_responses_async_api(sentry_init, capture_events): "gen_ai.usage.output_tokens.reasoning": 8, "gen_ai.usage.total_tokens": 30, "gen_ai.response.text": "the model response", + "sentry.sdk_meta.gen_ai.input.messages.original_length": 1, "thread.id": mock.ANY, "thread.name": mock.ANY, } + param_id = request.node.callspec.id + if "string" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.request.messages": safe_serialize( + ["How do I check if a Python object is an instance of a class?"] + ), + } + ) + elif "string" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + } + ] + ), + "gen_ai.request.messages": safe_serialize( + ["How do I check if a Python object is an instance of a class?"] + ), + } + ) + elif "blocks_no_type" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [{"type": "text", "content": "You are a helpful assistant."}] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif "blocks_no_type" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif "blocks" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [{"type": "text", "content": "You are a helpful assistant."}] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + elif "blocks" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + elif "parts_no_type" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif "parts_no_type" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif instructions is None or isinstance(instructions, Omit): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + else: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + + assert spans[0]["data"] == expected_data + @pytest.mark.asyncio +@pytest.mark.parametrize( + "instructions", + ( + omit, + None, + "You are a coding assistant that talks like a pirate.", + ), +) +@pytest.mark.parametrize( + "input", + [ + pytest.param( + "How do I check if a Python object is an instance of a class?", id="string" + ), + pytest.param( + [ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + {"role": "user", "content": "hello"}, + ], + id="blocks_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": "You are a helpful assistant.", + }, + {"type": "message", "role": "user", "content": "hello"}, + ], + id="blocks", + ), + pytest.param( + [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + {"role": "user", "content": "hello"}, + ], + id="parts_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + {"type": "message", "role": "user", "content": "hello"}, + ], + id="parts", + ), + ], +) @pytest.mark.skipif(SKIP_RESPONSES_TESTS, reason="Responses API not available") async def test_ai_client_span_streaming_responses_async_api( - sentry_init, capture_events + sentry_init, capture_events, instructions, input, request ): sentry_init( integrations=[OpenAIIntegration(include_prompts=True)], @@ -1178,8 +2155,8 @@ async def test_ai_client_span_streaming_responses_async_api( with start_transaction(name="openai tx"): await client.responses.create( model="gpt-4o", - instructions="You are a coding assistant that talks like a pirate.", - input="How do I check if a Python object is an instance of a class?", + instructions=instructions, + input=input, stream=True, ) @@ -1189,23 +2166,180 @@ async def test_ai_client_span_streaming_responses_async_api( assert len(spans) == 1 assert spans[0]["op"] == "gen_ai.responses" assert spans[0]["origin"] == "auto.ai.openai" - assert spans[0]["data"] == { + + expected_data = { "gen_ai.operation.name": "responses", - "gen_ai.request.messages": '["How do I check if a Python object is an instance of a class?"]', - "gen_ai.request.model": "gpt-4o", - "gen_ai.response.model": "response-model-id", "gen_ai.response.streaming": True, "gen_ai.system": "openai", + "gen_ai.response.model": "response-model-id", "gen_ai.usage.input_tokens": 20, "gen_ai.usage.input_tokens.cached": 5, "gen_ai.usage.output_tokens": 10, "gen_ai.usage.output_tokens.reasoning": 8, "gen_ai.usage.total_tokens": 30, + "gen_ai.request.model": "gpt-4o", "gen_ai.response.text": "the model response", + "sentry.sdk_meta.gen_ai.input.messages.original_length": 1, "thread.id": mock.ANY, "thread.name": mock.ANY, } + param_id = request.node.callspec.id + if "string" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.request.messages": safe_serialize( + ["How do I check if a Python object is an instance of a class?"] + ), + } + ) + elif "string" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + } + ] + ), + "gen_ai.request.messages": safe_serialize( + ["How do I check if a Python object is an instance of a class?"] + ), + } + ) + elif "blocks_no_type" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [{"type": "text", "content": "You are a helpful assistant."}] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif "blocks_no_type" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif "blocks" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [{"type": "text", "content": "You are a helpful assistant."}] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + elif "blocks" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + elif "parts_no_type" in param_id and ( + instructions is None or isinstance(instructions, Omit) + ): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif "parts_no_type" in param_id: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"role": "user", "content": "hello"}] + ), + } + ) + elif instructions is None or isinstance(instructions, Omit): # type: ignore + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + else: + expected_data.update( + { + "gen_ai.system_instructions": safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ), + "gen_ai.request.messages": safe_serialize( + [{"type": "message", "role": "user", "content": "hello"}] + ), + } + ) + + assert spans[0]["data"] == expected_data + @pytest.mark.asyncio @pytest.mark.skipif(SKIP_RESPONSES_TESTS, reason="Responses API not available") @@ -1458,7 +2592,24 @@ def test_empty_tools_in_chat_completion(sentry_init, capture_events, tools): assert "gen_ai.request.available_tools" not in span["data"] -def test_openai_message_role_mapping(sentry_init, capture_events): +# Test messages with mixed roles including "ai" that should be mapped to "assistant" +@pytest.mark.parametrize( + "test_message,expected_role", + [ + ({"role": "user", "content": "Hello"}, "user"), + ( + {"role": "ai", "content": "Hi there!"}, + "assistant", + ), # Should be mapped to "assistant" + ( + {"role": "assistant", "content": "How can I help?"}, + "assistant", + ), # Should stay "assistant" + ], +) +def test_openai_message_role_mapping( + sentry_init, capture_events, test_message, expected_role +): """Test that OpenAI integration properly maps message roles like 'ai' to 'assistant'""" sentry_init( @@ -1470,13 +2621,8 @@ def test_openai_message_role_mapping(sentry_init, capture_events): client = OpenAI(api_key="z") client.chat.completions._post = mock.Mock(return_value=EXAMPLE_CHAT_COMPLETION) - # Test messages with mixed roles including "ai" that should be mapped to "assistant" - test_messages = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - {"role": "ai", "content": "Hi there!"}, # Should be mapped to "assistant" - {"role": "assistant", "content": "How can I help?"}, # Should stay "assistant" - ] + + test_messages = [test_message] with start_transaction(name="openai tx"): client.chat.completions.create(model="test-model", messages=test_messages) @@ -1491,22 +2637,8 @@ def test_openai_message_role_mapping(sentry_init, capture_events): stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) - # Verify that "ai" role was mapped to "assistant" - assert len(stored_messages) == 4 - assert stored_messages[0]["role"] == "system" - assert stored_messages[1]["role"] == "user" - assert ( - stored_messages[2]["role"] == "assistant" - ) # "ai" should be mapped to "assistant" - assert stored_messages[3]["role"] == "assistant" # should stay "assistant" - - # Verify content is preserved - assert stored_messages[2]["content"] == "Hi there!" - assert stored_messages[3]["content"] == "How can I help?" - - # Verify no "ai" roles remain - roles = [msg["role"] for msg in stored_messages] - assert "ai" not in roles + assert len(stored_messages) == 1 + assert stored_messages[0]["role"] == expected_role def test_openai_message_truncation(sentry_init, capture_events): @@ -1548,14 +2680,207 @@ def test_openai_message_truncation(sentry_init, capture_events): assert isinstance(parsed_messages, list) assert len(parsed_messages) <= len(large_messages) - if "_meta" in event and len(parsed_messages) < len(large_messages): - meta_path = event["_meta"] - if ( - "spans" in meta_path - and "0" in meta_path["spans"] - and "data" in meta_path["spans"]["0"] - ): - span_meta = meta_path["spans"]["0"]["data"] - if SPANDATA.GEN_AI_REQUEST_MESSAGES in span_meta: - messages_meta = span_meta[SPANDATA.GEN_AI_REQUEST_MESSAGES] - assert "len" in messages_meta.get("", {}) + meta_path = event["_meta"] + span_meta = meta_path["spans"]["0"]["data"] + messages_meta = span_meta[SPANDATA.GEN_AI_REQUEST_MESSAGES] + assert "len" in messages_meta.get("", {}) + + +# noinspection PyTypeChecker +def test_streaming_chat_completion_ttft(sentry_init, capture_events): + """ + Test that streaming chat completions capture time-to-first-token (TTFT). + """ + sentry_init( + integrations=[OpenAIIntegration()], + traces_sample_rate=1.0, + ) + events = capture_events() + + client = OpenAI(api_key="z") + returned_stream = Stream(cast_to=None, response=None, client=client) + returned_stream._iterator = [ + ChatCompletionChunk( + id="1", + choices=[ + DeltaChoice( + index=0, delta=ChoiceDelta(content="Hello"), finish_reason=None + ) + ], + created=100000, + model="model-id", + object="chat.completion.chunk", + ), + ChatCompletionChunk( + id="1", + choices=[ + DeltaChoice( + index=0, delta=ChoiceDelta(content=" world"), finish_reason="stop" + ) + ], + created=100000, + model="model-id", + object="chat.completion.chunk", + ), + ] + + client.chat.completions._post = mock.Mock(return_value=returned_stream) + + with start_transaction(name="openai tx"): + response_stream = client.chat.completions.create( + model="some-model", messages=[{"role": "user", "content": "Say hello"}] + ) + # Consume the stream + for _ in response_stream: + pass + + (tx,) = events + span = tx["spans"][0] + assert span["op"] == "gen_ai.chat" + + # Verify TTFT is captured + assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN in span["data"] + ttft = span["data"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN] + assert isinstance(ttft, float) + assert ttft > 0 + + +# noinspection PyTypeChecker +@pytest.mark.asyncio +async def test_streaming_chat_completion_ttft_async(sentry_init, capture_events): + """ + Test that async streaming chat completions capture time-to-first-token (TTFT). + """ + sentry_init( + integrations=[OpenAIIntegration()], + traces_sample_rate=1.0, + ) + events = capture_events() + + client = AsyncOpenAI(api_key="z") + returned_stream = AsyncStream(cast_to=None, response=None, client=client) + returned_stream._iterator = async_iterator( + [ + ChatCompletionChunk( + id="1", + choices=[ + DeltaChoice( + index=0, delta=ChoiceDelta(content="Hello"), finish_reason=None + ) + ], + created=100000, + model="model-id", + object="chat.completion.chunk", + ), + ChatCompletionChunk( + id="1", + choices=[ + DeltaChoice( + index=0, + delta=ChoiceDelta(content=" world"), + finish_reason="stop", + ) + ], + created=100000, + model="model-id", + object="chat.completion.chunk", + ), + ] + ) + + client.chat.completions._post = AsyncMock(return_value=returned_stream) + + with start_transaction(name="openai tx"): + response_stream = await client.chat.completions.create( + model="some-model", messages=[{"role": "user", "content": "Say hello"}] + ) + # Consume the stream + async for _ in response_stream: + pass + + (tx,) = events + span = tx["spans"][0] + assert span["op"] == "gen_ai.chat" + + # Verify TTFT is captured + assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN in span["data"] + ttft = span["data"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN] + assert isinstance(ttft, float) + assert ttft > 0 + + +# noinspection PyTypeChecker +@pytest.mark.skipif(SKIP_RESPONSES_TESTS, reason="Responses API not available") +def test_streaming_responses_api_ttft(sentry_init, capture_events): + """ + Test that streaming responses API captures time-to-first-token (TTFT). + """ + sentry_init( + integrations=[OpenAIIntegration()], + traces_sample_rate=1.0, + ) + events = capture_events() + + client = OpenAI(api_key="z") + returned_stream = Stream(cast_to=None, response=None, client=client) + returned_stream._iterator = EXAMPLE_RESPONSES_STREAM + client.responses._post = mock.Mock(return_value=returned_stream) + + with start_transaction(name="openai tx"): + response_stream = client.responses.create( + model="some-model", + input="hello", + stream=True, + ) + # Consume the stream + for _ in response_stream: + pass + + (tx,) = events + span = tx["spans"][0] + assert span["op"] == "gen_ai.responses" + + # Verify TTFT is captured + assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN in span["data"] + ttft = span["data"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN] + assert isinstance(ttft, float) + assert ttft > 0 + + +# noinspection PyTypeChecker +@pytest.mark.asyncio +@pytest.mark.skipif(SKIP_RESPONSES_TESTS, reason="Responses API not available") +async def test_streaming_responses_api_ttft_async(sentry_init, capture_events): + """ + Test that async streaming responses API captures time-to-first-token (TTFT). + """ + sentry_init( + integrations=[OpenAIIntegration()], + traces_sample_rate=1.0, + ) + events = capture_events() + + client = AsyncOpenAI(api_key="z") + returned_stream = AsyncStream(cast_to=None, response=None, client=client) + returned_stream._iterator = async_iterator(EXAMPLE_RESPONSES_STREAM) + client.responses._post = AsyncMock(return_value=returned_stream) + + with start_transaction(name="openai tx"): + response_stream = await client.responses.create( + model="some-model", + input="hello", + stream=True, + ) + # Consume the stream + async for _ in response_stream: + pass + + (tx,) = events + span = tx["spans"][0] + assert span["op"] == "gen_ai.responses" + + # Verify TTFT is captured + assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN in span["data"] + ttft = span["data"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN] + assert isinstance(ttft, float) + assert ttft > 0 diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index c5cb25dfee..4bf212b8f3 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -12,6 +12,12 @@ from sentry_sdk.integrations.openai_agents.utils import _set_input_data, safe_serialize from sentry_sdk.utils import parse_version +from openai import AsyncOpenAI +from agents.models.openai_responses import OpenAIResponsesModel + +from unittest import mock +from unittest.mock import AsyncMock + import agents from agents import ( Agent, @@ -25,9 +31,11 @@ ResponseOutputText, ResponseFunctionToolCall, ) +from agents.tool import HostedMCPTool from agents.exceptions import MaxTurnsExceeded, ModelBehaviorError from agents.version import __version__ as OPENAI_AGENTS_VERSION +from openai.types.responses import Response, ResponseUsage from openai.types.responses.response_usage import ( InputTokensDetails, OutputTokensDetails, @@ -35,6 +43,42 @@ test_run_config = agents.RunConfig(tracing_disabled=True) +EXAMPLE_RESPONSE = Response( + id="chat-id", + output=[ + ResponseOutputMessage( + id="message-id", + content=[ + ResponseOutputText( + annotations=[], + text="the model response", + type="output_text", + ), + ], + role="assistant", + status="completed", + type="message", + ), + ], + parallel_tool_calls=False, + tool_choice="none", + tools=[], + created_at=10000000, + model="response-model-id", + object="response", + usage=ResponseUsage( + input_tokens=20, + input_tokens_details=InputTokensDetails( + cached_tokens=5, + ), + output_tokens=10, + output_tokens_details=OutputTokensDetails( + reasoning_tokens=8, + ), + total_tokens=30, + ), +) + @pytest.fixture def mock_usage(): @@ -88,6 +132,26 @@ def test_agent(): ) +@pytest.fixture +def test_agent_with_instructions(): + def inner(instructions): + """Create a real Agent instance for testing.""" + return Agent( + name="test_agent", + instructions=instructions, + model="gpt-4", + model_settings=ModelSettings( + max_tokens=100, + temperature=0.7, + top_p=1.0, + presence_penalty=0.0, + frequency_penalty=0.0, + ), + ) + + return inner + + @pytest.fixture def test_agent_custom_model(): """Create a real Agent instance for testing.""" @@ -107,8 +171,145 @@ def test_agent_custom_model(): @pytest.mark.asyncio -async def test_agent_invocation_span( +async def test_agent_invocation_span_no_pii( sentry_init, capture_events, test_agent, mock_model_response +): + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + with patch( + "agents.models.openai_responses.OpenAIResponsesModel.get_response" + ) as mock_get_response: + mock_get_response.return_value = mock_model_response + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + send_default_pii=False, + ) + + events = capture_events() + + result = await agents.Runner.run( + test_agent, "Test input", run_config=test_run_config + ) + + assert result is not None + assert result.final_output == "Hello, how can I help you?" + + (transaction,) = events + spans = transaction["spans"] + invoke_agent_span, ai_client_span = spans + + assert transaction["transaction"] == "test_agent workflow" + assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" + + assert invoke_agent_span["description"] == "invoke_agent test_agent" + + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in invoke_agent_span["data"] + assert "gen_ai.request.messages" not in invoke_agent_span["data"] + assert "gen_ai.response.text" not in invoke_agent_span["data"] + + assert invoke_agent_span["data"]["gen_ai.operation.name"] == "invoke_agent" + assert invoke_agent_span["data"]["gen_ai.system"] == "openai" + assert invoke_agent_span["data"]["gen_ai.agent.name"] == "test_agent" + assert invoke_agent_span["data"]["gen_ai.request.max_tokens"] == 100 + assert invoke_agent_span["data"]["gen_ai.request.model"] == "gpt-4" + assert invoke_agent_span["data"]["gen_ai.request.temperature"] == 0.7 + assert invoke_agent_span["data"]["gen_ai.request.top_p"] == 1.0 + + assert ai_client_span["description"] == "chat gpt-4" + assert ai_client_span["data"]["gen_ai.operation.name"] == "chat" + assert ai_client_span["data"]["gen_ai.system"] == "openai" + assert ai_client_span["data"]["gen_ai.agent.name"] == "test_agent" + assert ai_client_span["data"]["gen_ai.request.max_tokens"] == 100 + assert ai_client_span["data"]["gen_ai.request.model"] == "gpt-4" + assert ai_client_span["data"]["gen_ai.request.temperature"] == 0.7 + assert ai_client_span["data"]["gen_ai.request.top_p"] == 1.0 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "instructions", + ( + None, + "You are a coding assistant that talks like a pirate.", + ), +) +@pytest.mark.parametrize( + "input", + [ + pytest.param("Test input", id="string"), + pytest.param( + [ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + { + "role": "user", + "content": "Test input", + }, + ], + id="blocks_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": "You are a helpful assistant.", + }, + { + "type": "message", + "role": "user", + "content": "Test input", + }, + ], + id="blocks", + ), + pytest.param( + [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + { + "role": "user", + "content": "Test input", + }, + ], + id="parts_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + { + "type": "message", + "role": "user", + "content": "Test input", + }, + ], + id="parts", + ), + ], +) +async def test_agent_invocation_span( + sentry_init, + capture_events, + test_agent_with_instructions, + mock_model_response, + instructions, + input, + request, ): """ Test that the integration creates spans for agent invocations. @@ -129,7 +330,9 @@ async def test_agent_invocation_span( events = capture_events() result = await agents.Runner.run( - test_agent, "Test input", run_config=test_run_config + test_agent_with_instructions(instructions), + input, + run_config=test_run_config, ) assert result is not None @@ -143,21 +346,101 @@ async def test_agent_invocation_span( assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" assert invoke_agent_span["description"] == "invoke_agent test_agent" - assert invoke_agent_span["data"]["gen_ai.request.messages"] == safe_serialize( - [ - { - "content": [ - {"text": "You are a helpful test assistant.", "type": "text"} - ], - "role": "system", - }, - {"content": [{"text": "Test input", "type": "text"}], "role": "user"}, - ] - ) + + # Only first case checks "gen_ai.request.messages" until further input handling work. + param_id = request.node.callspec.id + if "string" in param_id and instructions is None: # type: ignore + assert "gen_ai.system_instructions" not in ai_client_span["data"] + + assert invoke_agent_span["data"]["gen_ai.request.messages"] == safe_serialize( + [ + {"content": [{"text": "Test input", "type": "text"}], "role": "user"}, + ] + ) + + elif "string" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + ] + ) + elif "blocks_no_type" in param_id and instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks_no_type" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks" in param_id and instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "parts_no_type" in param_id and instructions is None: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + elif "parts_no_type" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + elif instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + else: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + assert ( invoke_agent_span["data"]["gen_ai.response.text"] == "Hello, how can I help you?" ) + assert invoke_agent_span["data"]["gen_ai.operation.name"] == "invoke_agent" assert invoke_agent_span["data"]["gen_ai.system"] == "openai" assert invoke_agent_span["data"]["gen_ai.agent.name"] == "test_agent" @@ -212,8 +495,11 @@ async def test_client_span_custom_model( assert ai_client_span["data"]["gen_ai.request.model"] == "my-custom-model" -def test_agent_invocation_span_sync( - sentry_init, capture_events, test_agent, mock_model_response +def test_agent_invocation_span_sync_no_pii( + sentry_init, + capture_events, + test_agent, + mock_model_response, ): """ Test that the integration creates spans for agent invocations. @@ -228,6 +514,7 @@ def test_agent_invocation_span_sync( sentry_init( integrations=[OpenAIAgentsIntegration()], traces_sample_rate=1.0, + send_default_pii=False, ) events = capture_events() @@ -264,6 +551,226 @@ def test_agent_invocation_span_sync( assert ai_client_span["data"]["gen_ai.request.temperature"] == 0.7 assert ai_client_span["data"]["gen_ai.request.top_p"] == 1.0 + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in invoke_agent_span["data"] + + +@pytest.mark.parametrize( + "instructions", + ( + None, + "You are a coding assistant that talks like a pirate.", + ), +) +@pytest.mark.parametrize( + "input", + [ + pytest.param("Test input", id="string"), + pytest.param( + [ + { + "role": "system", + "content": "You are a helpful assistant.", + }, + { + "role": "user", + "content": "Test input", + }, + ], + id="blocks_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": "You are a helpful assistant.", + }, + { + "type": "message", + "role": "user", + "content": "Test input", + }, + ], + id="blocks", + ), + pytest.param( + [ + { + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + { + "role": "user", + "content": "Test input", + }, + ], + id="parts_no_type", + ), + pytest.param( + [ + { + "type": "message", + "role": "system", + "content": [ + {"type": "text", "text": "You are a helpful assistant."}, + {"type": "text", "text": "Be concise and clear."}, + ], + }, + { + "type": "message", + "role": "user", + "content": "Test input", + }, + ], + id="parts", + ), + ], +) +def test_agent_invocation_span_sync( + sentry_init, + capture_events, + test_agent_with_instructions, + mock_model_response, + instructions, + input, + request, +): + """ + Test that the integration creates spans for agent invocations. + """ + + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + with patch( + "agents.models.openai_responses.OpenAIResponsesModel.get_response" + ) as mock_get_response: + mock_get_response.return_value = mock_model_response + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + ) + + events = capture_events() + + result = agents.Runner.run_sync( + test_agent_with_instructions(instructions), + input, + run_config=test_run_config, + ) + + assert result is not None + assert result.final_output == "Hello, how can I help you?" + + (transaction,) = events + spans = transaction["spans"] + invoke_agent_span, ai_client_span = spans + + assert transaction["transaction"] == "test_agent workflow" + assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" + + assert invoke_agent_span["description"] == "invoke_agent test_agent" + assert invoke_agent_span["data"]["gen_ai.operation.name"] == "invoke_agent" + assert invoke_agent_span["data"]["gen_ai.system"] == "openai" + assert invoke_agent_span["data"]["gen_ai.agent.name"] == "test_agent" + assert invoke_agent_span["data"]["gen_ai.request.max_tokens"] == 100 + assert invoke_agent_span["data"]["gen_ai.request.model"] == "gpt-4" + assert invoke_agent_span["data"]["gen_ai.request.temperature"] == 0.7 + assert invoke_agent_span["data"]["gen_ai.request.top_p"] == 1.0 + + assert ai_client_span["description"] == "chat gpt-4" + assert ai_client_span["data"]["gen_ai.operation.name"] == "chat" + assert ai_client_span["data"]["gen_ai.system"] == "openai" + assert ai_client_span["data"]["gen_ai.agent.name"] == "test_agent" + assert ai_client_span["data"]["gen_ai.request.max_tokens"] == 100 + assert ai_client_span["data"]["gen_ai.request.model"] == "gpt-4" + assert ai_client_span["data"]["gen_ai.request.temperature"] == 0.7 + assert ai_client_span["data"]["gen_ai.request.top_p"] == 1.0 + + param_id = request.node.callspec.id + if "string" in param_id and instructions is None: # type: ignore + assert "gen_ai.system_instructions" not in ai_client_span["data"] + elif "string" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + ] + ) + elif "blocks_no_type" in param_id and instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks_no_type" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks" in param_id and instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "blocks" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + ] + ) + elif "parts_no_type" in param_id and instructions is None: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + elif "parts_no_type" in param_id: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + elif instructions is None: # type: ignore + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + else: + assert ai_client_span["data"]["gen_ai.system_instructions"] == safe_serialize( + [ + { + "type": "text", + "content": "You are a coding assistant that talks like a pirate.", + }, + {"type": "text", "content": "You are a helpful assistant."}, + {"type": "text", "content": "Be concise and clear."}, + ] + ) + @pytest.mark.asyncio async def test_handoff_span(sentry_init, capture_events, mock_usage): @@ -573,12 +1080,6 @@ def simple_test_tool(message: str) -> str: assert ai_client_span1["data"]["gen_ai.request.max_tokens"] == 100 assert ai_client_span1["data"]["gen_ai.request.messages"] == safe_serialize( [ - { - "role": "system", - "content": [ - {"type": "text", "text": "You are a helpful test assistant."} - ], - }, { "role": "user", "content": [ @@ -644,30 +1145,6 @@ def simple_test_tool(message: str) -> str: assert ai_client_span2["data"]["gen_ai.request.max_tokens"] == 100 assert ai_client_span2["data"]["gen_ai.request.messages"] == safe_serialize( [ - { - "role": "system", - "content": [ - {"type": "text", "text": "You are a helpful test assistant."} - ], - }, - { - "role": "user", - "content": [ - {"type": "text", "text": "Please use the simple test tool"} - ], - }, - { - "role": "assistant", - "content": [ - { - "arguments": '{"message": "hello"}', - "call_id": "call_123", - "name": "simple_test_tool", - "type": "function_call", - "id": "call_123", - } - ], - }, { "role": "tool", "content": [ @@ -695,6 +1172,87 @@ def simple_test_tool(message: str) -> str: assert ai_client_span2["data"]["gen_ai.usage.total_tokens"] == 25 +@pytest.mark.asyncio +async def test_hosted_mcp_tool_propagation_headers(sentry_init, test_agent): + """ + Test responses API is given trace propagation headers with HostedMCPTool. + """ + + hosted_tool = HostedMCPTool( + tool_config={ + "type": "mcp", + "server_label": "test_server", + "server_url": "http://example.com/", + "headers": { + "baggage": "custom=data", + }, + }, + ) + + client = AsyncOpenAI(api_key="z") + client.responses._post = AsyncMock(return_value=EXAMPLE_RESPONSE) + + model = OpenAIResponsesModel(model="gpt-4", openai_client=client) + + agent_with_tool = test_agent.clone( + tools=[hosted_tool], + model=model, + ) + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + release="d08ebdb9309e1b004c6f52202de58a09c2268e42", + ) + + with patch.object( + model._client.responses, + "create", + wraps=model._client.responses.create, + ) as create, mock.patch( + "sentry_sdk.tracing_utils.Random.randrange", return_value=500000 + ): + with sentry_sdk.start_transaction( + name="/interactions/other-dogs/new-dog", + op="greeting.sniff", + trace_id="01234567890123456789012345678901", + ) as transaction: + await agents.Runner.run( + agent_with_tool, + "Please use the simple test tool", + run_config=test_run_config, + ) + + ai_client_span = transaction._span_recorder.spans[-1] + + args, kwargs = create.call_args + + assert "tools" in kwargs + assert len(kwargs["tools"]) == 1 + hosted_mcp_tool = kwargs["tools"][0] + + assert hosted_mcp_tool["headers"][ + "sentry-trace" + ] == "{trace_id}-{parent_span_id}-{sampled}".format( + trace_id=transaction.trace_id, + parent_span_id=ai_client_span.span_id, + sampled=1, + ) + + expected_outgoing_baggage = ( + "custom=data," + "sentry-trace_id=01234567890123456789012345678901," + "sentry-sample_rand=0.500000," + "sentry-environment=production," + "sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42," + "sentry-transaction=/interactions/other-dogs/new-dog," + "sentry-sample_rate=1.0," + "sentry-sampled=true" + ) + + assert hosted_mcp_tool["headers"]["baggage"] == expected_outgoing_baggage + + @pytest.mark.asyncio async def test_model_behavior_error(sentry_init, capture_events, test_agent): """ @@ -857,12 +1415,6 @@ async def test_error_captures_input_data(sentry_init, capture_events, test_agent assert "gen_ai.request.messages" in ai_client_span["data"] request_messages = safe_serialize( [ - { - "role": "system", - "content": [ - {"type": "text", "text": "You are a helpful test assistant."} - ], - }, {"role": "user", "content": [{"type": "text", "text": "Test input"}]}, ] ) @@ -1241,7 +1793,24 @@ async def run(): assert txn3["transaction"] == "test_agent workflow" -def test_openai_agents_message_role_mapping(sentry_init, capture_events): +# Test input messages with mixed roles including "ai" +@pytest.mark.parametrize( + "test_message,expected_role", + [ + ({"role": "user", "content": "Hello"}, "user"), + ( + {"role": "ai", "content": "Hi there!"}, + "assistant", + ), # Should be mapped to "assistant" + ( + {"role": "assistant", "content": "How can I help?"}, + "assistant", + ), # Should stay "assistant" + ], +) +def test_openai_agents_message_role_mapping( + sentry_init, capture_events, test_message, expected_role +): """Test that OpenAI Agents integration properly maps message roles like 'ai' to 'assistant'""" sentry_init( integrations=[OpenAIAgentsIntegration()], @@ -1249,15 +1818,7 @@ def test_openai_agents_message_role_mapping(sentry_init, capture_events): send_default_pii=True, ) - # Test input messages with mixed roles including "ai" - test_input = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - {"role": "ai", "content": "Hi there!"}, # Should be mapped to "assistant" - {"role": "assistant", "content": "How can I help?"}, # Should stay "assistant" - ] - - get_response_kwargs = {"input": test_input} + get_response_kwargs = {"input": [test_message]} from sentry_sdk.integrations.openai_agents.utils import _set_input_data from sentry_sdk import start_span @@ -1268,23 +1829,10 @@ def test_openai_agents_message_role_mapping(sentry_init, capture_events): # Verify that messages were processed and roles were mapped from sentry_sdk.consts import SPANDATA - if SPANDATA.GEN_AI_REQUEST_MESSAGES in span._data: - import json - - stored_messages = json.loads(span._data[SPANDATA.GEN_AI_REQUEST_MESSAGES]) - - # Verify roles were properly mapped - found_assistant_roles = 0 - for message in stored_messages: - if message["role"] == "assistant": - found_assistant_roles += 1 - - # Should have 2 assistant roles (1 from original "assistant", 1 from mapped "ai") - assert found_assistant_roles == 2 + stored_messages = json.loads(span._data[SPANDATA.GEN_AI_REQUEST_MESSAGES]) - # Verify no "ai" roles remain in any message - for message in stored_messages: - assert message["role"] != "ai" + # Verify roles were properly mapped + assert stored_messages[0]["role"] == expected_role @pytest.mark.asyncio @@ -1465,7 +2013,7 @@ async def test_ai_client_span_includes_response_model( ): """ Test that ai_client spans (gen_ai.chat) include the response model from the actual API response. - This verifies the new functionality to capture the model used in the response. + This verifies we capture the actual model used (which may differ from the requested model). """ with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): @@ -1473,7 +2021,7 @@ async def test_ai_client_span_includes_response_model( with patch( "agents.models.openai_responses.OpenAIResponsesModel._fetch_response" ) as mock_fetch_response: - # Create a mock OpenAI Response object with a model field + # Create a mock OpenAI Response object with a specific model version mock_response = MagicMock() mock_response.model = "gpt-4.1-2025-04-14" # The actual response model mock_response.id = "resp_123" @@ -1523,7 +2071,7 @@ async def test_ai_client_span_includes_response_model( spans = transaction["spans"] _, ai_client_span = spans - # Verify ai_client span has response model + # Verify ai_client span has response model from API response assert ai_client_span["description"] == "chat gpt-4" assert "gen_ai.response.model" in ai_client_span["data"] assert ai_client_span["data"]["gen_ai.response.model"] == "gpt-4.1-2025-04-14" @@ -1545,13 +2093,13 @@ async def test_ai_client_span_response_model_with_chat_completions( ) with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): - # Mock the get_response method directly since ChatCompletions may use Responses API anyway + # Mock the _fetch_response method with patch( "agents.models.openai_responses.OpenAIResponsesModel._fetch_response" ) as mock_fetch_response: - # Create a mock Response object with a model field + # Create a mock Response object mock_response = MagicMock() - mock_response.model = "gpt-4o-mini-2024-07-18" # Actual response model + mock_response.model = "gpt-4o-mini-2024-07-18" mock_response.id = "resp_123" mock_response.output = [ ResponseOutputMessage( @@ -1598,7 +2146,7 @@ async def test_ai_client_span_response_model_with_chat_completions( spans = transaction["spans"] _, ai_client_span = spans - # Verify response model from Response is captured + # Verify response model from API response is captured assert "gen_ai.response.model" in ai_client_span["data"] assert ai_client_span["data"]["gen_ai.response.model"] == "gpt-4o-mini-2024-07-18" @@ -1711,41 +2259,45 @@ async def test_response_model_not_set_when_unavailable( sentry_init, capture_events, test_agent ): """ - Test that response model is not set if the raw response doesn't have a model field. - This can happen with custom model implementations. + Test that response model is not set if the API response doesn't have a model field. + The request model should still be set correctly. """ with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): - # Mock without _fetch_response (simulating custom model without this method) with patch( - "agents.models.openai_responses.OpenAIResponsesModel.get_response" - ) as mock_get_response: - response = ModelResponse( - output=[ - ResponseOutputMessage( - id="msg_123", - type="message", - status="completed", - content=[ - ResponseOutputText( - text="Response without model field", - type="output_text", - annotations=[], - ) - ], - role="assistant", - ) - ], - usage=Usage( - requests=1, - input_tokens=10, - output_tokens=20, - total_tokens=30, - ), - response_id="resp_123", + "agents.models.openai_responses.OpenAIResponsesModel._fetch_response" + ) as mock_fetch_response: + # Create a mock response without a model field + mock_response = MagicMock() + mock_response.model = None # No model in response + mock_response.id = "resp_123" + mock_response.output = [ + ResponseOutputMessage( + id="msg_123", + type="message", + status="completed", + content=[ + ResponseOutputText( + text="Response without model field", + type="output_text", + annotations=[], + ) + ], + role="assistant", + ) + ] + mock_response.usage = MagicMock() + mock_response.usage.input_tokens = 10 + mock_response.usage.output_tokens = 20 + mock_response.usage.total_tokens = 30 + mock_response.usage.input_tokens_details = InputTokensDetails( + cached_tokens=0 + ) + mock_response.usage.output_tokens_details = OutputTokensDetails( + reasoning_tokens=0 ) - # Don't set _sentry_response_model attribute - mock_get_response.return_value = response + + mock_fetch_response.return_value = mock_response sentry_init( integrations=[OpenAIAgentsIntegration()], @@ -1754,25 +2306,21 @@ async def test_response_model_not_set_when_unavailable( events = capture_events() - # Remove the _fetch_response method to simulate custom model - with patch.object( - agents.models.openai_responses.OpenAIResponsesModel, - "_fetch_response", - None, - ): - result = await agents.Runner.run( - test_agent, "Test input", run_config=test_run_config - ) + result = await agents.Runner.run( + test_agent, "Test input", run_config=test_run_config + ) - assert result is not None + assert result is not None (transaction,) = events spans = transaction["spans"] _, ai_client_span = spans - # When response model can't be captured, it shouldn't be in the span data - # (we only set it when we can accurately capture it) + # Response model should NOT be set when API doesn't return it assert "gen_ai.response.model" not in ai_client_span["data"] + # But request model should still be set + assert "gen_ai.request.model" in ai_client_span["data"] + assert ai_client_span["data"]["gen_ai.request.model"] == "gpt-4" @pytest.mark.asyncio @@ -1780,15 +2328,14 @@ async def test_invoke_agent_span_includes_response_model( sentry_init, capture_events, test_agent ): """ - Test that invoke_agent spans include the response model. - When an agent makes multiple LLM calls, it should report the last model used. + Test that invoke_agent spans include the response model from the API response. """ with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): with patch( "agents.models.openai_responses.OpenAIResponsesModel._fetch_response" ) as mock_fetch_response: - # Create a mock OpenAI Response object with a model field + # Create a mock OpenAI Response object with a specific model version mock_response = MagicMock() mock_response.model = "gpt-4.1-2025-04-14" # The actual response model mock_response.id = "resp_123" @@ -1838,7 +2385,7 @@ async def test_invoke_agent_span_includes_response_model( spans = transaction["spans"] invoke_agent_span, ai_client_span = spans - # Verify invoke_agent span has response model + # Verify invoke_agent span has response model from API assert invoke_agent_span["description"] == "invoke_agent test_agent" assert "gen_ai.response.model" in invoke_agent_span["data"] assert invoke_agent_span["data"]["gen_ai.response.model"] == "gpt-4.1-2025-04-14" @@ -1868,7 +2415,7 @@ def calculator(a: int, b: int) -> int: with patch( "agents.models.openai_responses.OpenAIResponsesModel._fetch_response" ) as mock_fetch_response: - # First call: gpt-4 model + # First call: gpt-4 model returns tool call first_response = MagicMock() first_response.model = "gpt-4-0613" first_response.id = "resp_1" @@ -1892,9 +2439,9 @@ def calculator(a: int, b: int) -> int: reasoning_tokens=0 ) - # Second call: different model (e.g., after tool execution) + # Second call: different model version returns final message second_response = MagicMock() - second_response.model = "gpt-4.1-2025-04-14" # Different model + second_response.model = "gpt-4.1-2025-04-14" second_response.id = "resp_2" second_response.output = [ ResponseOutputMessage( @@ -1946,11 +2493,11 @@ def calculator(a: int, b: int) -> int: first_ai_client_span = spans[1] second_ai_client_span = spans[3] # After tool span - # Verify invoke_agent span uses the LAST response model + # Invoke_agent span uses the LAST response model assert "gen_ai.response.model" in invoke_agent_span["data"] assert invoke_agent_span["data"]["gen_ai.response.model"] == "gpt-4.1-2025-04-14" - # Verify each ai_client span has its own response model + # Each ai_client span has its own response model from the API assert first_ai_client_span["data"]["gen_ai.response.model"] == "gpt-4-0613" assert ( second_ai_client_span["data"]["gen_ai.response.model"] == "gpt-4.1-2025-04-14" @@ -1971,7 +2518,6 @@ def test_openai_agents_message_truncation(sentry_init, capture_events): ) test_messages = [ - {"role": "system", "content": "small message 1"}, {"role": "user", "content": large_content}, {"role": "assistant", "content": large_content}, {"role": "user", "content": "small message 4"}, @@ -1985,8 +2531,8 @@ def test_openai_agents_message_truncation(sentry_init, capture_events): _set_input_data(span, get_response_kwargs) if hasattr(scope, "_gen_ai_original_message_count"): truncated_count = scope._gen_ai_original_message_count.get(span.span_id) - assert truncated_count == 5, ( - f"Expected 5 original messages, got {truncated_count}" + assert truncated_count == 4, ( + f"Expected 4 original messages, got {truncated_count}" ) assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span._data @@ -1995,6 +2541,172 @@ def test_openai_agents_message_truncation(sentry_init, capture_events): parsed_messages = json.loads(messages_data) assert isinstance(parsed_messages, list) - assert len(parsed_messages) == 2 - assert "small message 4" in str(parsed_messages[0]) - assert "small message 5" in str(parsed_messages[1]) + assert len(parsed_messages) == 1 + assert "small message 5" in str(parsed_messages[0]) + + +def test_streaming_patches_applied(sentry_init): + """ + Test that the streaming patches are applied correctly. + """ + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + ) + + # Verify that run_streamed is patched (will have __wrapped__ attribute if patched) + import agents + + # Check that the method exists and has been modified + assert hasattr(agents.run.DEFAULT_AGENT_RUNNER, "run_streamed") + assert hasattr(agents.run.AgentRunner, "_run_single_turn_streamed") + + # Verify the patches were applied by checking for our wrapper + run_streamed_func = agents.run.DEFAULT_AGENT_RUNNER.run_streamed + assert run_streamed_func is not None + + +@pytest.mark.asyncio +async def test_streaming_span_update_captures_response_data( + sentry_init, test_agent, mock_usage +): + """ + Test that update_ai_client_span correctly captures response text, + usage data, and response model from a streaming response. + """ + from sentry_sdk.integrations.openai_agents.spans.ai_client import ( + update_ai_client_span, + ) + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + ) + + # Create a mock streaming response object (similar to what we'd get from ResponseCompletedEvent) + mock_streaming_response = MagicMock() + mock_streaming_response.model = "gpt-4-streaming" + mock_streaming_response.usage = mock_usage + mock_streaming_response.output = [ + ResponseOutputMessage( + id="msg_streaming_123", + type="message", + status="completed", + content=[ + ResponseOutputText( + text="Hello from streaming!", + type="output_text", + annotations=[], + ) + ], + role="assistant", + ) + ] + + # Test the unified update function (works for both streaming and non-streaming) + with start_span(op="gen_ai.chat", description="test chat") as span: + update_ai_client_span(span, mock_streaming_response) + + # Verify the span data was set correctly + assert span._data["gen_ai.response.text"] == "Hello from streaming!" + assert span._data["gen_ai.usage.input_tokens"] == 10 + assert span._data["gen_ai.usage.output_tokens"] == 20 + assert span._data["gen_ai.response.model"] == "gpt-4-streaming" + + +@pytest.mark.asyncio +async def test_streaming_ttft_on_chat_span(sentry_init, test_agent): + """ + Test that time-to-first-token (TTFT) is recorded on chat spans during streaming. + + TTFT is triggered by events with a `delta` attribute, which includes: + - ResponseTextDeltaEvent (text output) + - ResponseAudioDeltaEvent (audio output) + - ResponseReasoningTextDeltaEvent (reasoning/thinking) + - ResponseFunctionCallArgumentsDeltaEvent (function call args) + - and other delta events... + + Events WITHOUT delta (like ResponseCompletedEvent, ResponseCreatedEvent, etc.) + should NOT trigger TTFT. + """ + from sentry_sdk.integrations.openai_agents.patches.models import ( + _create_get_model_wrapper, + ) + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + ) + + # Create a mock model with stream_response and get_response + class MockModel: + model = "gpt-4" + + async def get_response(self, *args, **kwargs): + # Not used in this test, but required by the wrapper + pass + + async def stream_response(self, *args, **kwargs): + # First event: ResponseCreatedEvent (no delta - should NOT trigger TTFT) + created_event = MagicMock(spec=["type", "sequence_number"]) + created_event.type = "response.created" + yield created_event + + # Simulate server-side processing delay before first token + await asyncio.sleep(0.05) # 50ms delay + + # Second event: ResponseTextDeltaEvent (HAS delta - triggers TTFT) + text_delta_event = MagicMock(spec=["delta", "type", "content_index"]) + text_delta_event.delta = "Hello" + text_delta_event.type = "response.output_text.delta" + yield text_delta_event + + # Third event: more text content (also has delta, but TTFT already recorded) + text_delta_event2 = MagicMock(spec=["delta", "type", "content_index"]) + text_delta_event2.delta = " world!" + text_delta_event2.type = "response.output_text.delta" + yield text_delta_event2 + + # Final event: ResponseCompletedEvent (has response, no delta) + completed_event = MagicMock(spec=["response", "type", "sequence_number"]) + completed_event.response = MagicMock() + completed_event.response.model = "gpt-4" + completed_event.response.usage = Usage( + requests=1, + input_tokens=10, + output_tokens=5, + total_tokens=15, + ) + completed_event.response.output = [] + yield completed_event + + # Create a mock original _get_model that returns our mock model + def mock_get_model(agent, run_config): + return MockModel() + + # Wrap it with our integration wrapper + wrapped_get_model = _create_get_model_wrapper(mock_get_model) + + with sentry_sdk.start_transaction(name="test_ttft", sampled=True) as transaction: + # Get the wrapped model (this applies the stream_response wrapper) + wrapped_model = wrapped_get_model(None, test_agent, MagicMock()) + + # Call the wrapped stream_response and consume all events + async for _event in wrapped_model.stream_response(): + pass + + # Verify TTFT is recorded on the chat span (must be inside transaction context) + chat_spans = [ + s for s in transaction._span_recorder.spans if s.op == "gen_ai.chat" + ] + assert len(chat_spans) >= 1 + chat_span = chat_spans[0] + + assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN in chat_span._data + ttft_value = chat_span._data[SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN] + # TTFT should be at least 40ms (our simulated delay minus some variance) but reasonable + assert 0.04 < ttft_value < 1.0, f"TTFT {ttft_value} should be around 50ms" + + # Verify streaming flag is set + assert chat_span._data.get(SPANDATA.GEN_AI_RESPONSE_STREAMING) is True diff --git a/tests/integrations/pydantic_ai/test_pydantic_ai.py b/tests/integrations/pydantic_ai/test_pydantic_ai.py index 049bcde39c..f315909ea1 100644 --- a/tests/integrations/pydantic_ai/test_pydantic_ai.py +++ b/tests/integrations/pydantic_ai/test_pydantic_ai.py @@ -1,12 +1,21 @@ import asyncio +import json import pytest +from unittest.mock import MagicMock from typing import Annotated from pydantic import Field +import sentry_sdk +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations.pydantic_ai import PydanticAIIntegration +from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages +from sentry_sdk.integrations.pydantic_ai.spans.utils import _set_usage_data from pydantic_ai import Agent +from pydantic_ai.messages import BinaryContent, UserPromptPart +from pydantic_ai.usage import RequestUsage from pydantic_ai.models.test import TestModel from pydantic_ai.exceptions import ModelRetry, UnexpectedModelBehavior @@ -505,7 +514,18 @@ async def test_model_settings(sentry_init, capture_events, test_agent_with_setti @pytest.mark.asyncio -async def test_system_prompt_in_messages(sentry_init, capture_events): +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +async def test_system_prompt_attribute( + sentry_init, capture_events, send_default_pii, include_prompts +): """ Test that system prompts are included as the first message. """ @@ -516,9 +536,9 @@ async def test_system_prompt_in_messages(sentry_init, capture_events): ) sentry_init( - integrations=[PydanticAIIntegration()], + integrations=[PydanticAIIntegration(include_prompts=include_prompts)], traces_sample_rate=1.0, - send_default_pii=True, + send_default_pii=send_default_pii, ) events = capture_events() @@ -533,12 +553,17 @@ async def test_system_prompt_in_messages(sentry_init, capture_events): assert len(chat_spans) >= 1 chat_span = chat_spans[0] - messages_str = chat_span["data"]["gen_ai.request.messages"] - # Messages is serialized as a string - # Should contain system role and helpful assistant text - assert "system" in messages_str - assert "helpful assistant" in messages_str + if send_default_pii and include_prompts: + system_instructions = chat_span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS] + assert json.loads(system_instructions) == [ + { + "type": "text", + "content": "You are a helpful assistant specialized in testing.", + } + ] + else: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in chat_span["data"] @pytest.mark.asyncio @@ -1175,7 +1200,18 @@ async def test_invoke_agent_with_list_user_prompt(sentry_init, capture_events): @pytest.mark.asyncio -async def test_invoke_agent_with_instructions(sentry_init, capture_events): +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +async def test_invoke_agent_with_instructions( + sentry_init, capture_events, send_default_pii, include_prompts +): """ Test that invoke_agent span handles instructions correctly. """ @@ -1192,9 +1228,9 @@ async def test_invoke_agent_with_instructions(sentry_init, capture_events): agent._system_prompts = ["System prompt"] sentry_init( - integrations=[PydanticAIIntegration()], + integrations=[PydanticAIIntegration(include_prompts=include_prompts)], traces_sample_rate=1.0, - send_default_pii=True, + send_default_pii=send_default_pii, ) events = capture_events() @@ -1202,14 +1238,22 @@ async def test_invoke_agent_with_instructions(sentry_init, capture_events): await agent.run("Test input") (transaction,) = events + spans = transaction["spans"] - # Check that the invoke_agent transaction has messages data - if "gen_ai.request.messages" in transaction["contexts"]["trace"]["data"]: - messages_str = transaction["contexts"]["trace"]["data"][ - "gen_ai.request.messages" + # The transaction IS the invoke_agent span, check for messages in chat spans instead + chat_spans = [s for s in spans if s["op"] == "gen_ai.chat"] + assert len(chat_spans) >= 1 + + chat_span = chat_spans[0] + + if send_default_pii and include_prompts: + system_instructions = chat_span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS] + assert json.loads(system_instructions) == [ + {"type": "text", "content": "System prompt"}, + {"type": "text", "content": "Instruction 1\nInstruction 2"}, ] - # Should contain both instructions and system prompts - assert "Instruction" in messages_str or "System prompt" in messages_str + else: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in chat_span["data"] @pytest.mark.asyncio @@ -2604,3 +2648,147 @@ async def test_ai_client_span_gets_agent_from_scope(sentry_init, capture_events) # Should not crash assert transaction is not None + + +def _get_messages_from_span(span_data): + """Helper to extract and parse messages from span data.""" + messages_data = span_data["gen_ai.request.messages"] + return ( + json.loads(messages_data) if isinstance(messages_data, str) else messages_data + ) + + +def _find_binary_content(messages_data, expected_modality, expected_mime_type): + """Helper to find and verify binary content in messages.""" + for msg in messages_data: + if "content" not in msg: + continue + for content_item in msg["content"]: + if content_item.get("type") == "blob": + assert content_item["modality"] == expected_modality + assert content_item["mime_type"] == expected_mime_type + assert content_item["content"] == BLOB_DATA_SUBSTITUTE + return True + return False + + +@pytest.mark.asyncio +async def test_binary_content_encoding_image(sentry_init, capture_events): + """Test that BinaryContent with image data is properly encoded in messages.""" + sentry_init( + integrations=[PydanticAIIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + ) + + events = capture_events() + + with sentry_sdk.start_transaction(op="test", name="test"): + span = sentry_sdk.start_span(op="test_span") + binary_content = BinaryContent( + data=b"fake_image_data_12345", media_type="image/png" + ) + user_part = UserPromptPart(content=["Look at this image:", binary_content]) + mock_msg = MagicMock() + mock_msg.parts = [user_part] + mock_msg.instructions = None + + _set_input_messages(span, [mock_msg]) + span.finish() + + (event,) = events + span_data = event["spans"][0]["data"] + messages_data = _get_messages_from_span(span_data) + assert _find_binary_content(messages_data, "image", "image/png") + + +@pytest.mark.asyncio +async def test_binary_content_encoding_mixed_content(sentry_init, capture_events): + """Test that BinaryContent mixed with text content is properly handled.""" + sentry_init( + integrations=[PydanticAIIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + ) + + events = capture_events() + + with sentry_sdk.start_transaction(op="test", name="test"): + span = sentry_sdk.start_span(op="test_span") + binary_content = BinaryContent( + data=b"fake_image_bytes", media_type="image/jpeg" + ) + user_part = UserPromptPart( + content=["Here is an image:", binary_content, "What do you see?"] + ) + mock_msg = MagicMock() + mock_msg.parts = [user_part] + mock_msg.instructions = None + + _set_input_messages(span, [mock_msg]) + span.finish() + + (event,) = events + span_data = event["spans"][0]["data"] + messages_data = _get_messages_from_span(span_data) + + # Verify both text and binary content are present + found_text = any( + content_item.get("type") == "text" + for msg in messages_data + if "content" in msg + for content_item in msg["content"] + ) + assert found_text, "Text content should be found" + assert _find_binary_content(messages_data, "image", "image/jpeg") + + +@pytest.mark.asyncio +async def test_binary_content_in_agent_run(sentry_init, capture_events): + """Test that BinaryContent in actual agent run is properly captured in spans.""" + agent = Agent("test", name="test_binary_agent") + + sentry_init( + integrations=[PydanticAIIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + ) + + events = capture_events() + binary_content = BinaryContent( + data=b"fake_image_data_for_testing", media_type="image/png" + ) + await agent.run(["Analyze this image:", binary_content]) + + (transaction,) = events + chat_spans = [s for s in transaction["spans"] if s["op"] == "gen_ai.chat"] + assert len(chat_spans) >= 1 + + chat_span = chat_spans[0] + if "gen_ai.request.messages" in chat_span["data"]: + messages_str = str(chat_span["data"]["gen_ai.request.messages"]) + assert any(keyword in messages_str for keyword in ["blob", "image", "base64"]) + + +@pytest.mark.asyncio +async def test_set_usage_data_with_cache_tokens(sentry_init, capture_events): + """Test that cache_read_tokens and cache_write_tokens are tracked.""" + sentry_init(integrations=[PydanticAIIntegration()], traces_sample_rate=1.0) + + events = capture_events() + + with sentry_sdk.start_transaction(op="test", name="test"): + span = sentry_sdk.start_span(op="test_span") + usage = RequestUsage( + input_tokens=100, + output_tokens=50, + cache_read_tokens=80, + cache_write_tokens=20, + ) + _set_usage_data(span, usage) + span.finish() + + (event,) = events + (span_data,) = event["spans"] + assert span_data["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80 + assert span_data["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20 diff --git a/tests/integrations/ray/test_ray.py b/tests/integrations/ray/test_ray.py index dcbf8f456b..be7ebc9d05 100644 --- a/tests/integrations/ray/test_ray.py +++ b/tests/integrations/ray/test_ray.py @@ -74,10 +74,29 @@ def read_error_from_log(job_id, ray_temp_dir): return error +def example_task(): + with sentry_sdk.start_span(op="task", name="example task step"): + ... + + return sentry_sdk.get_client().transport.envelopes + + +# RayIntegration must leave variadic keyword arguments at the end +def example_task_with_kwargs(**kwargs): + with sentry_sdk.start_span(op="task", name="example task step"): + ... + + return sentry_sdk.get_client().transport.envelopes + + @pytest.mark.parametrize( "task_options", [{}, {"num_cpus": 0, "memory": 1024 * 1024 * 10}] ) -def test_tracing_in_ray_tasks(task_options): +@pytest.mark.parametrize( + "task", + [example_task, example_task_with_kwargs], +) +def test_tracing_in_ray_tasks(task_options, task): setup_sentry() ray.init( @@ -87,21 +106,18 @@ def test_tracing_in_ray_tasks(task_options): } ) - def example_task(): - with sentry_sdk.start_span(op="task", name="example task step"): - ... - - return sentry_sdk.get_client().transport.envelopes - # Setup ray task, calling decorator directly instead of @, # to accommodate for test parametrization if task_options: - example_task = ray.remote(**task_options)(example_task) + example_task = ray.remote(**task_options)(task) else: - example_task = ray.remote(example_task) + example_task = ray.remote(task) # Function name shouldn't be overwritten by Sentry wrapper - assert example_task._function_name == "tests.integrations.ray.test_ray.example_task" + assert ( + example_task._function_name + == f"tests.integrations.ray.test_ray.{task.__name__}" + ) with sentry_sdk.start_transaction(op="task", name="ray test transaction"): worker_envelopes = ray.get(example_task.remote()) @@ -115,17 +131,14 @@ def example_task(): worker_transaction = worker_envelope.get_transaction_event() assert ( worker_transaction["transaction"] - == "tests.integrations.ray.test_ray.test_tracing_in_ray_tasks..example_task" + == f"tests.integrations.ray.test_ray.{task.__name__}" ) assert worker_transaction["transaction_info"] == {"source": "task"} (span,) = client_transaction["spans"] assert span["op"] == "queue.submit.ray" assert span["origin"] == "auto.queue.ray" - assert ( - span["description"] - == "tests.integrations.ray.test_ray.test_tracing_in_ray_tasks..example_task" - ) + assert span["description"] == f"tests.integrations.ray.test_ray.{task.__name__}" assert span["parent_span_id"] == client_transaction["contexts"]["trace"]["span_id"] assert span["trace_id"] == client_transaction["contexts"]["trace"]["trace_id"] diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 588c3b34f4..ad6b0688b9 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -1,7 +1,10 @@ import os import datetime +import socket from http.client import HTTPConnection, HTTPSConnection +from http.server import BaseHTTPRequestHandler, HTTPServer from socket import SocketIO +from threading import Thread from urllib.error import HTTPError from urllib.request import urlopen from unittest import mock @@ -12,11 +15,35 @@ from sentry_sdk.consts import MATCH_ALL, SPANDATA from sentry_sdk.integrations.stdlib import StdlibIntegration -from tests.conftest import ApproxDict, create_mock_http_server +from tests.conftest import ApproxDict, create_mock_http_server, get_free_port PORT = create_mock_http_server() +class MockProxyRequestHandler(BaseHTTPRequestHandler): + def do_CONNECT(self): + self.send_response(200, "Connection Established") + self.end_headers() + + self.rfile.readline() + + response = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n" + self.wfile.write(response) + self.wfile.flush() + + +def create_mock_proxy_server(): + proxy_port = get_free_port() + proxy_server = HTTPServer(("localhost", proxy_port), MockProxyRequestHandler) + proxy_thread = Thread(target=proxy_server.serve_forever) + proxy_thread.daemon = True + proxy_thread.start() + return proxy_port + + +PROXY_PORT = create_mock_proxy_server() + + def test_crumb_capture(sentry_init, capture_events): sentry_init(integrations=[StdlibIntegration()]) events = capture_events() @@ -642,3 +669,25 @@ def test_http_timeout(monkeypatch, sentry_init, capture_envelopes): span = transaction["spans"][0] assert span["op"] == "http.client" assert span["description"] == f"GET http://localhost:{PORT}/bla" # noqa: E231 + + +@pytest.mark.parametrize("tunnel_port", [8080, None]) +def test_proxy_http_tunnel(sentry_init, capture_events, tunnel_port): + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + with start_transaction(name="test_transaction"): + conn = HTTPConnection("localhost", PROXY_PORT) + conn.set_tunnel("api.example.com", tunnel_port) + conn.request("GET", "/foo") + conn.getresponse() + + (event,) = events + (span,) = event["spans"] + + port_modifier = f":{tunnel_port}" if tunnel_port else "" + assert span["description"] == f"GET http://api.example.com{port_modifier}/foo" + assert span["data"]["url"] == f"http://api.example.com{port_modifier}/foo" + assert span["data"][SPANDATA.HTTP_METHOD] == "GET" + assert span["data"][SPANDATA.NETWORK_PEER_ADDRESS] == "localhost" + assert span["data"][SPANDATA.NETWORK_PEER_PORT] == PROXY_PORT diff --git a/tests/test_ai_integration_deactivation.py b/tests/test_ai_integration_deactivation.py index dc8aef6be8..b02b64c6ee 100644 --- a/tests/test_ai_integration_deactivation.py +++ b/tests/test_ai_integration_deactivation.py @@ -57,6 +57,7 @@ def test_integration_deactivates_map_exists(): assert "langchain" in _INTEGRATION_DEACTIVATES assert "openai" in _INTEGRATION_DEACTIVATES["langchain"] assert "anthropic" in _INTEGRATION_DEACTIVATES["langchain"] + assert "google_genai" in _INTEGRATION_DEACTIVATES["langchain"] assert "openai_agents" in _INTEGRATION_DEACTIVATES assert "openai" in _INTEGRATION_DEACTIVATES["openai_agents"] assert "pydantic_ai" in _INTEGRATION_DEACTIVATES diff --git a/tests/test_ai_monitoring.py b/tests/test_ai_monitoring.py index 8d3d4ba204..969d14658d 100644 --- a/tests/test_ai_monitoring.py +++ b/tests/test_ai_monitoring.py @@ -4,7 +4,11 @@ import pytest import sentry_sdk -from sentry_sdk._types import AnnotatedValue +from sentry_sdk._types import ( + AnnotatedValue, + SENSITIVE_DATA_SUBSTITUTE, + BLOB_DATA_SUBSTITUTE, +) from sentry_sdk.ai.monitoring import ai_track from sentry_sdk.ai.utils import ( MAX_GEN_AI_MESSAGE_BYTES, @@ -13,6 +17,15 @@ truncate_and_annotate_messages, truncate_messages_by_size, _find_truncation_index, + parse_data_uri, + redact_blob_message_parts, + get_modality_from_mime_type, + transform_openai_content_part, + transform_anthropic_content_part, + transform_google_content_part, + transform_generic_content_part, + transform_content_part, + transform_message_content, ) from sentry_sdk.serializer import serialize from sentry_sdk.utils import safe_serialize @@ -307,7 +320,7 @@ def test_single_message_truncation(self): class TestTruncateAndAnnotateMessages: - def test_no_truncation_returns_list(self, sample_messages): + def test_only_keeps_last_message(self, sample_messages): class MockSpan: def __init__(self): self.span_id = "test_span_id" @@ -326,9 +339,8 @@ def __init__(self): assert isinstance(result, list) assert not isinstance(result, AnnotatedValue) - assert len(result) == len(sample_messages) - assert result == sample_messages - assert span.span_id not in scope._gen_ai_original_message_count + assert len(result) == 1 + assert result[0] == sample_messages[-1] def test_truncation_sets_metadata_on_scope(self, large_messages): class MockSpan: @@ -348,7 +360,7 @@ def __init__(self): scope = MockScope() original_count = len(large_messages) result = truncate_and_annotate_messages( - large_messages, span, scope, max_bytes=small_limit + large_messages, span, scope, max_single_message_chars=small_limit ) assert isinstance(result, list) @@ -375,7 +387,7 @@ def __init__(self): scope = MockScope() result = truncate_and_annotate_messages( - large_messages, span, scope, max_bytes=small_limit + large_messages, span, scope, max_single_message_chars=small_limit ) assert scope._gen_ai_original_message_count[span.span_id] == original_count @@ -419,12 +431,55 @@ def __init__(self): span = MockSpan() scope = MockScope() result = truncate_and_annotate_messages( - large_messages, span, scope, max_bytes=small_limit + large_messages, span, scope, max_single_message_chars=small_limit ) assert isinstance(result, list) assert result[0] == large_messages[-len(result)] + def test_preserves_original_messages_with_blobs(self): + """Test that truncate_and_annotate_messages doesn't mutate the original messages""" + + class MockSpan: + def __init__(self): + self.span_id = "test_span_id" + self.data = {} + + def set_data(self, key, value): + self.data[key] = value + + class MockScope: + def __init__(self): + self._gen_ai_original_message_count = {} + + messages = [ + { + "role": "user", + "content": [ + {"text": "What's in this image?", "type": "text"}, + { + "type": "blob", + "modality": "image", + "content": "data:image/jpeg;base64,original_content", + }, + ], + } + ] + + original_blob_content = messages[0]["content"][1]["content"] + + span = MockSpan() + scope = MockScope() + + # This should NOT mutate the original messages + result = truncate_and_annotate_messages(messages, span, scope) + + # Verify original is unchanged + assert messages[0]["content"][1]["content"] == original_blob_content + + # Verify result has redacted content + assert result[0]["content"][1]["content"] == BLOB_DATA_SUBSTITUTE + class TestClientAnnotation: def test_client_wraps_truncated_messages_in_annotated_value(self, large_messages): @@ -451,7 +506,7 @@ def __init__(self): # Simulate what integrations do truncated_messages = truncate_and_annotate_messages( - large_messages, span, scope, max_bytes=small_limit + large_messages, span, scope, max_single_message_chars=small_limit ) span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, truncated_messages) @@ -507,7 +562,7 @@ def __init__(self): original_message_count = len(large_messages) truncated_messages = truncate_and_annotate_messages( - large_messages, span, scope, max_bytes=small_limit + large_messages, span, scope, max_single_message_chars=small_limit ) assert len(truncated_messages) < original_message_count @@ -542,3 +597,1157 @@ def __init__(self): assert isinstance(messages_value, AnnotatedValue) assert messages_value.metadata["len"] == stored_original_length assert len(messages_value.value) == len(truncated_messages) + + +class TestRedactBlobMessageParts: + def test_redacts_single_blob_content(self): + """Test that blob content is redacted without mutating original messages""" + messages = [ + { + "role": "user", + "content": [ + { + "text": "How many ponies do you see in the image?", + "type": "text", + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "data:image/jpeg;base64,/9j/4AAQSkZJRg==", + }, + ], + } + ] + + # Save original blob content for comparison + original_blob_content = messages[0]["content"][1]["content"] + + result = redact_blob_message_parts(messages) + + # Original messages should be UNCHANGED + assert messages[0]["content"][1]["content"] == original_blob_content + + # Result should have redacted content + assert ( + result[0]["content"][0]["text"] + == "How many ponies do you see in the image?" + ) + assert result[0]["content"][0]["type"] == "text" + assert result[0]["content"][1]["type"] == "blob" + assert result[0]["content"][1]["modality"] == "image" + assert result[0]["content"][1]["mime_type"] == "image/jpeg" + assert result[0]["content"][1]["content"] == BLOB_DATA_SUBSTITUTE + + def test_redacts_multiple_blob_parts(self): + """Test that multiple blob parts are redacted without mutation""" + messages = [ + { + "role": "user", + "content": [ + {"text": "Compare these images", "type": "text"}, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "data:image/jpeg;base64,first_image", + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/png", + "content": "data:image/png;base64,second_image", + }, + ], + } + ] + + original_first = messages[0]["content"][1]["content"] + original_second = messages[0]["content"][2]["content"] + + result = redact_blob_message_parts(messages) + + # Original should be unchanged + assert messages[0]["content"][1]["content"] == original_first + assert messages[0]["content"][2]["content"] == original_second + + # Result should be redacted + assert result[0]["content"][0]["text"] == "Compare these images" + assert result[0]["content"][1]["content"] == BLOB_DATA_SUBSTITUTE + assert result[0]["content"][2]["content"] == BLOB_DATA_SUBSTITUTE + + def test_redacts_blobs_in_multiple_messages(self): + """Test that blob parts are redacted across multiple messages without mutation""" + messages = [ + { + "role": "user", + "content": [ + {"text": "First message", "type": "text"}, + { + "type": "blob", + "modality": "image", + "content": "data:image/jpeg;base64,first", + }, + ], + }, + { + "role": "assistant", + "content": "I see the image.", + }, + { + "role": "user", + "content": [ + {"text": "Second message", "type": "text"}, + { + "type": "blob", + "modality": "image", + "content": "data:image/jpeg;base64,second", + }, + ], + }, + ] + + original_first = messages[0]["content"][1]["content"] + original_second = messages[2]["content"][1]["content"] + + result = redact_blob_message_parts(messages) + + # Original should be unchanged + assert messages[0]["content"][1]["content"] == original_first + assert messages[2]["content"][1]["content"] == original_second + + # Result should be redacted + assert result[0]["content"][1]["content"] == BLOB_DATA_SUBSTITUTE + assert result[1]["content"] == "I see the image." # Unchanged + assert result[2]["content"][1]["content"] == BLOB_DATA_SUBSTITUTE + + def test_no_blobs_returns_original_list(self): + """Test that messages without blobs are returned as-is (performance optimization)""" + messages = [ + {"role": "user", "content": "Simple text message"}, + {"role": "assistant", "content": "Simple response"}, + ] + + result = redact_blob_message_parts(messages) + + # Should return the same list object when no blobs present + assert result is messages + + def test_handles_non_dict_messages(self): + """Test that non-dict messages are handled gracefully""" + messages = [ + "string message", + {"role": "user", "content": "text"}, + None, + 123, + ] + + result = redact_blob_message_parts(messages) + + # Should return same list since no blobs + assert result is messages + + def test_handles_non_dict_content_items(self): + """Test that non-dict content items in arrays are handled""" + messages = [ + { + "role": "user", + "content": [ + "string item", + {"text": "text item", "type": "text"}, + None, + ], + } + ] + + result = redact_blob_message_parts(messages) + + # Should return same list since no blobs + assert result is messages + + +class TestParseDataUri: + def test_parses_base64_image_data_uri(self): + """Test parsing a standard base64-encoded image data URI""" + uri = "data:image/jpeg;base64,/9j/4AAQSkZJRg==" + mime_type, content = parse_data_uri(uri) + + assert mime_type == "image/jpeg" + assert content == "/9j/4AAQSkZJRg==" + + def test_parses_png_data_uri(self): + """Test parsing a PNG image data URI""" + uri = "data:image/png;base64,iVBORw0KGgo=" + mime_type, content = parse_data_uri(uri) + + assert mime_type == "image/png" + assert content == "iVBORw0KGgo=" + + def test_parses_plain_text_data_uri(self): + """Test parsing a plain text data URI without base64 encoding""" + uri = "data:text/plain,Hello World" + mime_type, content = parse_data_uri(uri) + + assert mime_type == "text/plain" + assert content == "Hello World" + + def test_parses_data_uri_with_empty_mime_type(self): + """Test parsing a data URI with empty mime type""" + uri = "data:;base64,SGVsbG8=" + mime_type, content = parse_data_uri(uri) + + assert mime_type == "" + assert content == "SGVsbG8=" + + def test_parses_data_uri_with_only_data_prefix(self): + """Test parsing a data URI with only the data: prefix and content""" + uri = "data:,Hello" + mime_type, content = parse_data_uri(uri) + + assert mime_type == "" + assert content == "Hello" + + def test_raises_on_missing_comma(self): + """Test that ValueError is raised when comma separator is missing""" + with pytest.raises(ValueError, match="missing comma separator"): + parse_data_uri("data:image/jpeg;base64") + + def test_raises_on_empty_string(self): + """Test that ValueError is raised for empty string""" + with pytest.raises(ValueError, match="missing comma separator"): + parse_data_uri("") + + def test_handles_content_with_commas(self): + """Test that only the first comma is used as separator""" + uri = "data:text/plain,Hello,World,With,Commas" + mime_type, content = parse_data_uri(uri) + + assert mime_type == "text/plain" + assert content == "Hello,World,With,Commas" + + def test_parses_data_uri_with_multiple_parameters(self): + """Test parsing a data URI with multiple parameters in header""" + uri = "data:text/plain;charset=utf-8;base64,SGVsbG8=" + mime_type, content = parse_data_uri(uri) + + assert mime_type == "text/plain" + assert content == "SGVsbG8=" + + def test_parses_audio_data_uri(self): + """Test parsing an audio data URI""" + uri = "data:audio/wav;base64,UklGRiQA" + mime_type, content = parse_data_uri(uri) + + assert mime_type == "audio/wav" + assert content == "UklGRiQA" + + def test_handles_uri_without_data_prefix(self): + """Test parsing a URI that doesn't have the data: prefix""" + uri = "image/jpeg;base64,/9j/4AAQ" + mime_type, content = parse_data_uri(uri) + + assert mime_type == "image/jpeg" + assert content == "/9j/4AAQ" + + +class TestGetModalityFromMimeType: + def test_image_mime_types(self): + """Test that image MIME types return 'image' modality""" + assert get_modality_from_mime_type("image/jpeg") == "image" + assert get_modality_from_mime_type("image/png") == "image" + assert get_modality_from_mime_type("image/gif") == "image" + assert get_modality_from_mime_type("image/webp") == "image" + assert get_modality_from_mime_type("IMAGE/JPEG") == "image" # case insensitive + + def test_audio_mime_types(self): + """Test that audio MIME types return 'audio' modality""" + assert get_modality_from_mime_type("audio/mp3") == "audio" + assert get_modality_from_mime_type("audio/wav") == "audio" + assert get_modality_from_mime_type("audio/ogg") == "audio" + assert get_modality_from_mime_type("AUDIO/MP3") == "audio" # case insensitive + + def test_video_mime_types(self): + """Test that video MIME types return 'video' modality""" + assert get_modality_from_mime_type("video/mp4") == "video" + assert get_modality_from_mime_type("video/webm") == "video" + assert get_modality_from_mime_type("video/quicktime") == "video" + assert get_modality_from_mime_type("VIDEO/MP4") == "video" # case insensitive + + def test_document_mime_types(self): + """Test that application and text MIME types return 'document' modality""" + assert get_modality_from_mime_type("application/pdf") == "document" + assert get_modality_from_mime_type("application/json") == "document" + assert get_modality_from_mime_type("text/plain") == "document" + assert get_modality_from_mime_type("text/html") == "document" + + def test_empty_mime_type_returns_image(self): + """Test that empty MIME type defaults to 'image'""" + assert get_modality_from_mime_type("") == "image" + + def test_none_mime_type_returns_image(self): + """Test that None-like values default to 'image'""" + assert get_modality_from_mime_type(None) == "image" + + def test_unknown_mime_type_returns_image(self): + """Test that unknown MIME types default to 'image'""" + assert get_modality_from_mime_type("unknown/type") == "image" + assert get_modality_from_mime_type("custom/format") == "image" + + +class TestTransformOpenAIContentPart: + """Tests for the OpenAI-specific transform function.""" + + def test_image_url_with_data_uri(self): + """Test transforming OpenAI image_url with base64 data URI""" + content_part = { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg=="}, + } + result = transform_openai_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQSkZJRg==", + } + + def test_image_url_with_regular_url(self): + """Test transforming OpenAI image_url with regular URL""" + content_part = { + "type": "image_url", + "image_url": {"url": "https://example.com/image.jpg"}, + } + result = transform_openai_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "https://example.com/image.jpg", + } + + def test_image_url_string_format(self): + """Test transforming OpenAI image_url where image_url is a string""" + content_part = { + "type": "image_url", + "image_url": "https://example.com/image.jpg", + } + result = transform_openai_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "https://example.com/image.jpg", + } + + def test_image_url_invalid_data_uri(self): + """Test transforming OpenAI image_url with invalid data URI falls back to URI""" + content_part = { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64"}, # Missing comma + } + result = transform_openai_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "data:image/jpeg;base64", + } + + def test_empty_url_returns_none(self): + """Test that image_url with empty URL returns None""" + content_part = {"type": "image_url", "image_url": {"url": ""}} + assert transform_openai_content_part(content_part) is None + + def test_non_image_url_type_returns_none(self): + """Test that non-image_url types return None""" + content_part = {"type": "text", "text": "Hello"} + assert transform_openai_content_part(content_part) is None + + def test_anthropic_format_returns_none(self): + """Test that Anthropic format returns None (not handled)""" + content_part = { + "type": "image", + "source": {"type": "base64", "media_type": "image/png", "data": "abc"}, + } + assert transform_openai_content_part(content_part) is None + + def test_google_format_returns_none(self): + """Test that Google format returns None (not handled)""" + content_part = {"inline_data": {"mime_type": "image/jpeg", "data": "abc"}} + assert transform_openai_content_part(content_part) is None + + def test_non_dict_returns_none(self): + """Test that non-dict input returns None""" + assert transform_openai_content_part("string") is None + assert transform_openai_content_part(123) is None + assert transform_openai_content_part(None) is None + + +class TestTransformAnthropicContentPart: + """Tests for the Anthropic-specific transform function.""" + + def test_image_base64(self): + """Test transforming Anthropic image with base64 source""" + content_part = { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "iVBORw0KGgo=", + }, + } + result = transform_anthropic_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/png", + "content": "iVBORw0KGgo=", + } + + def test_image_url(self): + """Test transforming Anthropic image with URL source""" + content_part = { + "type": "image", + "source": { + "type": "url", + "media_type": "image/jpeg", + "url": "https://example.com/image.jpg", + }, + } + result = transform_anthropic_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "image/jpeg", + "uri": "https://example.com/image.jpg", + } + + def test_image_file(self): + """Test transforming Anthropic image with file source""" + content_part = { + "type": "image", + "source": { + "type": "file", + "media_type": "image/jpeg", + "file_id": "file_123", + }, + } + result = transform_anthropic_content_part(content_part) + + assert result == { + "type": "file", + "modality": "image", + "mime_type": "image/jpeg", + "file_id": "file_123", + } + + def test_document_base64(self): + """Test transforming Anthropic document with base64 source""" + content_part = { + "type": "document", + "source": { + "type": "base64", + "media_type": "application/pdf", + "data": "JVBERi0xLjQ=", + }, + } + result = transform_anthropic_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "document", + "mime_type": "application/pdf", + "content": "JVBERi0xLjQ=", + } + + def test_document_url(self): + """Test transforming Anthropic document with URL source""" + content_part = { + "type": "document", + "source": { + "type": "url", + "media_type": "application/pdf", + "url": "https://example.com/doc.pdf", + }, + } + result = transform_anthropic_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "document", + "mime_type": "application/pdf", + "uri": "https://example.com/doc.pdf", + } + + def test_invalid_source_returns_none(self): + """Test that Anthropic format with invalid source returns None""" + content_part = {"type": "image", "source": "not_a_dict"} + assert transform_anthropic_content_part(content_part) is None + + def test_unknown_source_type_returns_none(self): + """Test that Anthropic format with unknown source type returns None""" + content_part = { + "type": "image", + "source": {"type": "unknown", "data": "something"}, + } + assert transform_anthropic_content_part(content_part) is None + + def test_missing_source_returns_none(self): + """Test that Anthropic format without source returns None""" + content_part = {"type": "image", "data": "something"} + assert transform_anthropic_content_part(content_part) is None + + def test_openai_format_returns_none(self): + """Test that OpenAI format returns None (not handled)""" + content_part = { + "type": "image_url", + "image_url": {"url": "https://example.com"}, + } + assert transform_anthropic_content_part(content_part) is None + + def test_google_format_returns_none(self): + """Test that Google format returns None (not handled)""" + content_part = {"inline_data": {"mime_type": "image/jpeg", "data": "abc"}} + assert transform_anthropic_content_part(content_part) is None + + def test_non_dict_returns_none(self): + """Test that non-dict input returns None""" + assert transform_anthropic_content_part("string") is None + assert transform_anthropic_content_part(123) is None + assert transform_anthropic_content_part(None) is None + + +class TestTransformGoogleContentPart: + """Tests for the Google GenAI-specific transform function.""" + + def test_inline_data(self): + """Test transforming Google inline_data format""" + content_part = { + "inline_data": { + "mime_type": "image/jpeg", + "data": "/9j/4AAQSkZJRg==", + } + } + result = transform_google_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQSkZJRg==", + } + + def test_file_data(self): + """Test transforming Google file_data format""" + content_part = { + "file_data": { + "mime_type": "video/mp4", + "file_uri": "gs://bucket/video.mp4", + } + } + result = transform_google_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "video", + "mime_type": "video/mp4", + "uri": "gs://bucket/video.mp4", + } + + def test_inline_data_audio(self): + """Test transforming Google inline_data with audio""" + content_part = { + "inline_data": { + "mime_type": "audio/wav", + "data": "UklGRiQA", + } + } + result = transform_google_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "audio", + "mime_type": "audio/wav", + "content": "UklGRiQA", + } + + def test_inline_data_not_dict_returns_none(self): + """Test that Google inline_data with non-dict value returns None""" + content_part = {"inline_data": "not_a_dict"} + assert transform_google_content_part(content_part) is None + + def test_file_data_not_dict_returns_none(self): + """Test that Google file_data with non-dict value returns None""" + content_part = {"file_data": "not_a_dict"} + assert transform_google_content_part(content_part) is None + + def test_openai_format_returns_none(self): + """Test that OpenAI format returns None (not handled)""" + content_part = { + "type": "image_url", + "image_url": {"url": "https://example.com"}, + } + assert transform_google_content_part(content_part) is None + + def test_anthropic_format_returns_none(self): + """Test that Anthropic format returns None (not handled)""" + content_part = { + "type": "image", + "source": {"type": "base64", "media_type": "image/png", "data": "abc"}, + } + assert transform_google_content_part(content_part) is None + + def test_non_dict_returns_none(self): + """Test that non-dict input returns None""" + assert transform_google_content_part("string") is None + assert transform_google_content_part(123) is None + assert transform_google_content_part(None) is None + + +class TestTransformGenericContentPart: + """Tests for the generic/LangChain-style transform function.""" + + def test_image_base64(self): + """Test transforming generic format with base64""" + content_part = { + "type": "image", + "base64": "/9j/4AAQSkZJRg==", + "mime_type": "image/jpeg", + } + result = transform_generic_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQSkZJRg==", + } + + def test_audio_url(self): + """Test transforming generic format with URL""" + content_part = { + "type": "audio", + "url": "https://example.com/audio.mp3", + "mime_type": "audio/mp3", + } + result = transform_generic_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "audio", + "mime_type": "audio/mp3", + "uri": "https://example.com/audio.mp3", + } + + def test_file_with_file_id(self): + """Test transforming generic format with file_id""" + content_part = { + "type": "file", + "file_id": "file_456", + "mime_type": "application/pdf", + } + result = transform_generic_content_part(content_part) + + assert result == { + "type": "file", + "modality": "document", + "mime_type": "application/pdf", + "file_id": "file_456", + } + + def test_video_base64(self): + """Test transforming generic video format""" + content_part = { + "type": "video", + "base64": "AAAA", + "mime_type": "video/mp4", + } + result = transform_generic_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "video", + "mime_type": "video/mp4", + "content": "AAAA", + } + + def test_image_with_source_returns_none(self): + """Test that image with source key (Anthropic style) returns None""" + # This is Anthropic format, should NOT be handled by generic + content_part = { + "type": "image", + "source": {"type": "base64", "data": "abc"}, + } + assert transform_generic_content_part(content_part) is None + + def test_text_type_returns_none(self): + """Test that text type returns None""" + content_part = {"type": "text", "text": "Hello"} + assert transform_generic_content_part(content_part) is None + + def test_openai_format_returns_none(self): + """Test that OpenAI format returns None (not handled)""" + content_part = { + "type": "image_url", + "image_url": {"url": "https://example.com"}, + } + assert transform_generic_content_part(content_part) is None + + def test_google_format_returns_none(self): + """Test that Google format returns None (not handled)""" + content_part = {"inline_data": {"mime_type": "image/jpeg", "data": "abc"}} + assert transform_generic_content_part(content_part) is None + + def test_non_dict_returns_none(self): + """Test that non-dict input returns None""" + assert transform_generic_content_part("string") is None + assert transform_generic_content_part(123) is None + assert transform_generic_content_part(None) is None + + def test_missing_data_key_returns_none(self): + """Test that missing data key (base64/url/file_id) returns None""" + content_part = {"type": "image", "mime_type": "image/jpeg"} + assert transform_generic_content_part(content_part) is None + + +class TestTransformContentPart: + # OpenAI/LiteLLM format tests + def test_openai_image_url_with_data_uri(self): + """Test transforming OpenAI image_url with base64 data URI""" + content_part = { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg=="}, + } + result = transform_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQSkZJRg==", + } + + def test_openai_image_url_with_regular_url(self): + """Test transforming OpenAI image_url with regular URL""" + content_part = { + "type": "image_url", + "image_url": {"url": "https://example.com/image.jpg"}, + } + result = transform_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "https://example.com/image.jpg", + } + + def test_openai_image_url_string_format(self): + """Test transforming OpenAI image_url where image_url is a string""" + content_part = { + "type": "image_url", + "image_url": "https://example.com/image.jpg", + } + result = transform_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "https://example.com/image.jpg", + } + + def test_openai_image_url_invalid_data_uri(self): + """Test transforming OpenAI image_url with invalid data URI falls back to URI""" + content_part = { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64"}, # Missing comma + } + result = transform_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "data:image/jpeg;base64", + } + + # Anthropic format tests + def test_anthropic_image_base64(self): + """Test transforming Anthropic image with base64 source""" + content_part = { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "iVBORw0KGgo=", + }, + } + result = transform_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/png", + "content": "iVBORw0KGgo=", + } + + def test_anthropic_image_url(self): + """Test transforming Anthropic image with URL source""" + content_part = { + "type": "image", + "source": { + "type": "url", + "media_type": "image/jpeg", + "url": "https://example.com/image.jpg", + }, + } + result = transform_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "image", + "mime_type": "image/jpeg", + "uri": "https://example.com/image.jpg", + } + + def test_anthropic_image_file(self): + """Test transforming Anthropic image with file source""" + content_part = { + "type": "image", + "source": { + "type": "file", + "media_type": "image/jpeg", + "file_id": "file_123", + }, + } + result = transform_content_part(content_part) + + assert result == { + "type": "file", + "modality": "image", + "mime_type": "image/jpeg", + "file_id": "file_123", + } + + def test_anthropic_document_base64(self): + """Test transforming Anthropic document with base64 source""" + content_part = { + "type": "document", + "source": { + "type": "base64", + "media_type": "application/pdf", + "data": "JVBERi0xLjQ=", + }, + } + result = transform_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "document", + "mime_type": "application/pdf", + "content": "JVBERi0xLjQ=", + } + + def test_anthropic_document_url(self): + """Test transforming Anthropic document with URL source""" + content_part = { + "type": "document", + "source": { + "type": "url", + "media_type": "application/pdf", + "url": "https://example.com/doc.pdf", + }, + } + result = transform_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "document", + "mime_type": "application/pdf", + "uri": "https://example.com/doc.pdf", + } + + # Google format tests + def test_google_inline_data(self): + """Test transforming Google inline_data format""" + content_part = { + "inline_data": { + "mime_type": "image/jpeg", + "data": "/9j/4AAQSkZJRg==", + } + } + result = transform_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQSkZJRg==", + } + + def test_google_file_data(self): + """Test transforming Google file_data format""" + content_part = { + "file_data": { + "mime_type": "video/mp4", + "file_uri": "gs://bucket/video.mp4", + } + } + result = transform_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "video", + "mime_type": "video/mp4", + "uri": "gs://bucket/video.mp4", + } + + def test_google_inline_data_audio(self): + """Test transforming Google inline_data with audio""" + content_part = { + "inline_data": { + "mime_type": "audio/wav", + "data": "UklGRiQA", + } + } + result = transform_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "audio", + "mime_type": "audio/wav", + "content": "UklGRiQA", + } + + # Generic format tests (LangChain style) + def test_generic_image_base64(self): + """Test transforming generic format with base64""" + content_part = { + "type": "image", + "base64": "/9j/4AAQSkZJRg==", + "mime_type": "image/jpeg", + } + result = transform_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQSkZJRg==", + } + + def test_generic_audio_url(self): + """Test transforming generic format with URL""" + content_part = { + "type": "audio", + "url": "https://example.com/audio.mp3", + "mime_type": "audio/mp3", + } + result = transform_content_part(content_part) + + assert result == { + "type": "uri", + "modality": "audio", + "mime_type": "audio/mp3", + "uri": "https://example.com/audio.mp3", + } + + def test_generic_file_with_file_id(self): + """Test transforming generic format with file_id""" + content_part = { + "type": "file", + "file_id": "file_456", + "mime_type": "application/pdf", + } + result = transform_content_part(content_part) + + assert result == { + "type": "file", + "modality": "document", + "mime_type": "application/pdf", + "file_id": "file_456", + } + + def test_generic_video_base64(self): + """Test transforming generic video format""" + content_part = { + "type": "video", + "base64": "AAAA", + "mime_type": "video/mp4", + } + result = transform_content_part(content_part) + + assert result == { + "type": "blob", + "modality": "video", + "mime_type": "video/mp4", + "content": "AAAA", + } + + # Edge cases and error handling + def test_text_block_returns_none(self): + """Test that text blocks return None (not transformed)""" + content_part = {"type": "text", "text": "Hello world"} + result = transform_content_part(content_part) + + assert result is None + + def test_non_dict_returns_none(self): + """Test that non-dict input returns None""" + assert transform_content_part("string") is None + assert transform_content_part(123) is None + assert transform_content_part(None) is None + assert transform_content_part([1, 2, 3]) is None + + def test_empty_dict_returns_none(self): + """Test that empty dict returns None""" + assert transform_content_part({}) is None + + def test_unknown_type_returns_none(self): + """Test that unknown type returns None""" + content_part = {"type": "unknown", "data": "something"} + assert transform_content_part(content_part) is None + + def test_openai_image_url_empty_url_returns_none(self): + """Test that image_url with empty URL returns None""" + content_part = {"type": "image_url", "image_url": {"url": ""}} + assert transform_content_part(content_part) is None + + def test_anthropic_invalid_source_returns_none(self): + """Test that Anthropic format with invalid source returns None""" + content_part = {"type": "image", "source": "not_a_dict"} + assert transform_content_part(content_part) is None + + def test_anthropic_unknown_source_type_returns_none(self): + """Test that Anthropic format with unknown source type returns None""" + content_part = { + "type": "image", + "source": {"type": "unknown", "data": "something"}, + } + assert transform_content_part(content_part) is None + + def test_google_inline_data_not_dict_returns_none(self): + """Test that Google inline_data with non-dict value returns None""" + content_part = {"inline_data": "not_a_dict"} + assert transform_content_part(content_part) is None + + def test_google_file_data_not_dict_returns_none(self): + """Test that Google file_data with non-dict value returns None""" + content_part = {"file_data": "not_a_dict"} + assert transform_content_part(content_part) is None + + +class TestTransformMessageContent: + def test_string_content_returned_as_is(self): + """Test that string content is returned unchanged""" + content = "Hello, world!" + result = transform_message_content(content) + + assert result == "Hello, world!" + + def test_list_with_transformable_items(self): + """Test transforming a list with transformable content parts""" + content = [ + {"type": "text", "text": "What's in this image?"}, + { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQ"}, + }, + ] + result = transform_message_content(content) + + assert len(result) == 2 + # Text block should be unchanged (transform returns None, so original kept) + assert result[0] == {"type": "text", "text": "What's in this image?"} + # Image should be transformed + assert result[1] == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQ", + } + + def test_list_with_non_dict_items(self): + """Test that non-dict items in list are kept as-is""" + content = ["text string", 123, {"type": "text", "text": "hi"}] + result = transform_message_content(content) + + assert result == ["text string", 123, {"type": "text", "text": "hi"}] + + def test_tuple_content(self): + """Test that tuple content is also handled""" + content = ( + {"type": "text", "text": "Hello"}, + { + "type": "image_url", + "image_url": {"url": "https://example.com/img.jpg"}, + }, + ) + result = transform_message_content(content) + + assert len(result) == 2 + assert result[0] == {"type": "text", "text": "Hello"} + assert result[1] == { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": "https://example.com/img.jpg", + } + + def test_other_types_returned_as_is(self): + """Test that other types are returned unchanged""" + assert transform_message_content(123) == 123 + assert transform_message_content(None) is None + assert transform_message_content({"key": "value"}) == {"key": "value"} + + def test_mixed_content_types(self): + """Test transforming mixed content with multiple formats""" + content = [ + {"type": "text", "text": "Look at these:"}, + { + "type": "image_url", + "image_url": {"url": "data:image/png;base64,iVBORw0"}, + }, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "/9j/4AAQ", + }, + }, + {"inline_data": {"mime_type": "audio/wav", "data": "UklGRiQA"}}, + ] + result = transform_message_content(content) + + assert len(result) == 4 + assert result[0] == {"type": "text", "text": "Look at these:"} + assert result[1] == { + "type": "blob", + "modality": "image", + "mime_type": "image/png", + "content": "iVBORw0", + } + assert result[2] == { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "/9j/4AAQ", + } + assert result[3] == { + "type": "blob", + "modality": "audio", + "mime_type": "audio/wav", + "content": "UklGRiQA", + } + + def test_empty_list(self): + """Test that empty list is returned as empty list""" + assert transform_message_content([]) == [] diff --git a/tests/test_logs.py b/tests/test_logs.py index 11c7b8e4d5..386e22658e 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -471,6 +471,69 @@ def test_logs_with_literal_braces( assert "sentry.message.template" not in logs[0]["attributes"] +@minimum_python_37 +def test_transport_format(sentry_init, capture_envelopes): + sentry_init(enable_logs=True, server_name="test-server", release="1.0.0") + + envelopes = capture_envelopes() + + sentry_sdk.logger.warning("This is a log...") + + sentry_sdk.get_client().flush() + + assert len(envelopes) == 1 + assert len(envelopes[0].items) == 1 + item = envelopes[0].items[0] + + assert item.type == "log" + assert item.headers == { + "type": "log", + "item_count": 1, + "content_type": "application/vnd.sentry.items.log+json", + } + assert item.payload.json == { + "items": [ + { + "body": "This is a log...", + "level": "warn", + "timestamp": mock.ANY, + "trace_id": mock.ANY, + "span_id": mock.ANY, + "attributes": { + "sentry.environment": { + "type": "string", + "value": "production", + }, + "sentry.release": { + "type": "string", + "value": "1.0.0", + }, + "sentry.sdk.name": { + "type": "string", + "value": mock.ANY, + }, + "sentry.sdk.version": { + "type": "string", + "value": VERSION, + }, + "sentry.severity_number": { + "type": "integer", + "value": 13, + }, + "sentry.severity_text": { + "type": "string", + "value": "warn", + }, + "server.address": { + "type": "string", + "value": "test-server", + }, + }, + } + ] + } + + @minimum_python_37 def test_batcher_drops_logs(sentry_init, monkeypatch): sentry_init(enable_logs=True, server_name="test-server", release="1.0.0") @@ -598,13 +661,71 @@ def test_log_attributes_override_scope_attributes(sentry_init, capture_envelopes assert log["attributes"]["temp.attribute"] == "value2" +@minimum_python_37 +def test_log_array_attributes(sentry_init, capture_envelopes): + """Test homogeneous list and tuple attributes, and fallback for inhomogeneous collections.""" + + sentry_init(enable_logs=True) + + envelopes = capture_envelopes() + + with sentry_sdk.new_scope() as scope: + scope.set_attribute("string_list", ["value1", "value2"]) + scope.set_attribute("int_tuple", (3, 2, 1, 4)) + scope.set_attribute("inhomogeneous_tuple", (3, 2.0, 1, 4)) # type: ignore[arg-type] + + sentry_sdk.logger.warning( + "Hello, world!", + attributes={ + "float_list": [3.0, 3.5, 4.2], + "bool_tuple": (False, False, True), + "inhomogeneous_list": [3.2, True, None], + }, + ) + + get_client().flush() + + assert len(envelopes) == 1 + assert len(envelopes[0].items) == 1 + item = envelopes[0].items[0] + serialized_attributes = item.payload.json["items"][0]["attributes"] + + assert serialized_attributes["string_list"] == { + "value": ["value1", "value2"], + "type": "string[]", + } + assert serialized_attributes["int_tuple"] == { + "value": [3, 2, 1, 4], + "type": "integer[]", + } + assert serialized_attributes["inhomogeneous_tuple"] == { + "value": "(3, 2.0, 1, 4)", + "type": "string", + } + + assert serialized_attributes["float_list"] == { + "value": [3.0, 3.5, 4.2], + "type": "double[]", + } + assert serialized_attributes["bool_tuple"] == { + "value": [False, False, True], + "type": "boolean[]", + } + assert serialized_attributes["inhomogeneous_list"] == { + "value": "[3.2, True, None]", + "type": "string", + } + + @minimum_python_37 def test_attributes_preserialized_in_before_send(sentry_init, capture_envelopes): - """We don't surface references to objects in attributes.""" + """We don't surface user-held references to objects in attributes.""" def before_send_log(log, _): assert isinstance(log["attributes"]["instance"], str) assert isinstance(log["attributes"]["dictionary"], str) + assert isinstance(log["attributes"]["inhomogeneous_list"], str) + assert isinstance(log["attributes"]["inhomogeneous_tuple"], str) return log @@ -623,6 +744,8 @@ class Cat: attributes={ "instance": instance, "dictionary": dictionary, + "inhomogeneous_list": [3.2, True, None], + "inhomogeneous_tuple": (3, 2.0, 1, 4), }, ) @@ -633,3 +756,31 @@ class Cat: assert isinstance(log["attributes"]["instance"], str) assert isinstance(log["attributes"]["dictionary"], str) + assert isinstance(log["attributes"]["inhomogeneous_list"], str) + assert isinstance(log["attributes"]["inhomogeneous_tuple"], str) + + +@minimum_python_37 +def test_array_attributes_deep_copied_in_before_send(sentry_init, capture_envelopes): + """We don't surface user-held references to objects in attributes.""" + + strings = ["value1", "value2"] + ints = (3, 2, 1, 4) + + def before_send_log(log, _): + assert log["attributes"]["string_list"] is not strings + assert log["attributes"]["int_tuple"] is not ints + + return log + + sentry_init(enable_logs=True, before_send_log=before_send_log) + + sentry_sdk.logger.warning( + "Hello world!", + attributes={ + "string_list": strings, + "int_tuple": ints, + }, + ) + + get_client().flush() diff --git a/tests/test_metrics.py b/tests/test_metrics.py index d64ce748e4..185c1c594d 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -1,6 +1,8 @@ import json import sys from typing import List, Any, Mapping +from unittest import mock + import pytest import sentry_sdk @@ -268,6 +270,61 @@ def _before_metric(record, hint): assert before_metric_called +def test_transport_format(sentry_init, capture_envelopes): + sentry_init(server_name="test-server", release="1.0.0") + + envelopes = capture_envelopes() + + sentry_sdk.metrics.count("test.counter", 1) + + sentry_sdk.get_client().flush() + + assert len(envelopes) == 1 + assert len(envelopes[0].items) == 1 + item = envelopes[0].items[0] + + assert item.type == "trace_metric" + assert item.headers == { + "type": "trace_metric", + "item_count": 1, + "content_type": "application/vnd.sentry.items.trace-metric+json", + } + assert item.payload.json == { + "items": [ + { + "name": "test.counter", + "type": "counter", + "value": 1, + "timestamp": mock.ANY, + "trace_id": mock.ANY, + "span_id": mock.ANY, + "attributes": { + "sentry.environment": { + "type": "string", + "value": "production", + }, + "sentry.release": { + "type": "string", + "value": "1.0.0", + }, + "sentry.sdk.name": { + "type": "string", + "value": mock.ANY, + }, + "sentry.sdk.version": { + "type": "string", + "value": VERSION, + }, + "server.address": { + "type": "string", + "value": "test-server", + }, + }, + } + ] + } + + def test_batcher_drops_metrics(sentry_init, monkeypatch): sentry_init() client = sentry_sdk.get_client() @@ -337,12 +394,70 @@ def test_metric_attributes_override_scope_attributes(sentry_init, capture_envelo assert metric["attributes"]["temp.attribute"] == "value2" +def test_log_array_attributes(sentry_init, capture_envelopes): + """Test homogeneous list and tuple attributes, and fallback for inhomogeneous collections.""" + + sentry_init() + + envelopes = capture_envelopes() + + with sentry_sdk.new_scope() as scope: + scope.set_attribute("string_list.attribute", ["value1", "value2"]) + scope.set_attribute("int_tuple.attribute", (3, 2, 1, 4)) + scope.set_attribute("inhomogeneous_tuple.attribute", (3, 2.0, 1, 4)) # type: ignore[arg-type] + + sentry_sdk.metrics.count( + "test", + 1, + attributes={ + "float_list.attribute": [3.0, 3.5, 4.2], + "bool_tuple.attribute": (False, False, True), + "inhomogeneous_list.attribute": [3.2, True, None], + }, + ) + + get_client().flush() + + assert len(envelopes) == 1 + assert len(envelopes[0].items) == 1 + item = envelopes[0].items[0] + serialized_attributes = item.payload.json["items"][0]["attributes"] + + assert serialized_attributes["string_list.attribute"] == { + "value": ["value1", "value2"], + "type": "string[]", + } + assert serialized_attributes["int_tuple.attribute"] == { + "value": [3, 2, 1, 4], + "type": "integer[]", + } + assert serialized_attributes["inhomogeneous_tuple.attribute"] == { + "value": "(3, 2.0, 1, 4)", + "type": "string", + } + + assert serialized_attributes["float_list.attribute"] == { + "value": [3.0, 3.5, 4.2], + "type": "double[]", + } + assert serialized_attributes["bool_tuple.attribute"] == { + "value": [False, False, True], + "type": "boolean[]", + } + assert serialized_attributes["inhomogeneous_list.attribute"] == { + "value": "[3.2, True, None]", + "type": "string", + } + + def test_attributes_preserialized_in_before_send(sentry_init, capture_envelopes): - """We don't surface references to objects in attributes.""" + """We don't surface user-held references to objects in attributes.""" def before_send_metric(metric, _): assert isinstance(metric["attributes"]["instance"], str) assert isinstance(metric["attributes"]["dictionary"], str) + assert isinstance(metric["attributes"]["inhomogeneous_list"], str) + assert isinstance(metric["attributes"]["inhomogeneous_tuple"], str) return metric @@ -362,6 +477,8 @@ class Cat: attributes={ "instance": instance, "dictionary": dictionary, + "inhomogeneous_list": [3.2, True, None], + "inhomogeneous_tuple": (3, 2.0, 1, 4), }, ) @@ -372,3 +489,29 @@ class Cat: assert isinstance(metric["attributes"]["instance"], str) assert isinstance(metric["attributes"]["dictionary"], str) + + +def test_array_attributes_deep_copied_in_before_send(sentry_init, capture_envelopes): + """We don't surface user-held references to objects in attributes.""" + + strings = ["value1", "value2"] + ints = (3, 2, 1, 4) + + def before_send_metric(metric, _): + assert metric["attributes"]["string_list"] is not strings + assert metric["attributes"]["int_tuple"] is not ints + + return metric + + sentry_init(before_send_metric=before_send_metric) + + sentry_sdk.metrics.count( + "test.counter", + 1, + attributes={ + "string_list": strings, + "int_tuple": ints, + }, + ) + + get_client().flush() diff --git a/tests/test_scope.py b/tests/test_scope.py index 86a0551a44..710ce33849 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -1021,3 +1021,54 @@ def test_trace_context_without_performance(sentry_init): assert trace_context["span_id"] == propagation_context.span_id assert trace_context["parent_span_id"] == propagation_context.parent_span_id assert "dynamic_sampling_context" in trace_context + + +def test_conversation_id_set_get(): + """Test that set_conversation_id and get_conversation_id work correctly.""" + scope = Scope() + assert scope.get_conversation_id() is None + + scope.set_conversation_id("test-conv-123") + assert scope.get_conversation_id() == "test-conv-123" + + +def test_conversation_id_remove(): + """Test that remove_conversation_id clears the conversation ID.""" + scope = Scope() + scope.set_conversation_id("test-conv-456") + assert scope.get_conversation_id() == "test-conv-456" + + scope.remove_conversation_id() + assert scope.get_conversation_id() is None + + +def test_conversation_id_overwrite(): + """Test that set_conversation_id overwrites existing value.""" + scope = Scope() + scope.set_conversation_id("first-conv") + scope.set_conversation_id("second-conv") + assert scope.get_conversation_id() == "second-conv" + + +def test_conversation_id_copy(): + """Test that conversation_id is preserved when scope is copied.""" + scope1 = Scope() + scope1.set_conversation_id("copy-test-conv") + + scope2 = copy.copy(scope1) + assert scope2.get_conversation_id() == "copy-test-conv" + + # Modifying copy should not affect original + scope2.set_conversation_id("modified-conv") + assert scope1.get_conversation_id() == "copy-test-conv" + assert scope2.get_conversation_id() == "modified-conv" + + +def test_conversation_id_clear(): + """Test that conversation_id is cleared when scope.clear() is called.""" + scope = Scope() + scope.set_conversation_id("clear-test-conv") + assert scope.get_conversation_id() == "clear-test-conv" + + scope.clear() + assert scope.get_conversation_id() is None diff --git a/tests/test_shadowed_module.py b/tests/test_shadowed_module.py index efca19746a..10d86f285b 100644 --- a/tests/test_shadowed_module.py +++ b/tests/test_shadowed_module.py @@ -26,15 +26,7 @@ def pytest_generate_tests(metafunc): metafunc.parametrize( "integration_submodule_name", - # Temporarily skip some integrations - submodule_names - - { - "clickhouse_driver", - "litellm", - "pure_eval", - "ray", - "typer", - }, + submodule_names, ) diff --git a/tests/test_transport.py b/tests/test_transport.py index b5b88e0e2c..8601a4f138 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -385,6 +385,33 @@ def test_parse_rate_limits(input, expected): assert dict(_parse_rate_limits(input, now=NOW)) == expected +def test_envelope_too_large_response(capturing_server, make_client): + client = make_client() + + capturing_server.respond_with(code=413) + client.capture_event({"type": "error"}) + client.capture_event({"type": "transaction"}) + client.flush() + + # Error, transaction, and client report payloads + assert len(capturing_server.captured) == 3 + report = parse_json(capturing_server.captured[2].envelope.items[0].get_bytes()) + + # Client reports for error, transaction and included span + assert len(report["discarded_events"]) == 3 + assert {"reason": "send_error", "category": "error", "quantity": 1} in report[ + "discarded_events" + ] + assert {"reason": "send_error", "category": "span", "quantity": 1} in report[ + "discarded_events" + ] + assert {"reason": "send_error", "category": "transaction", "quantity": 1} in report[ + "discarded_events" + ] + + capturing_server.clear_captured() + + def test_simple_rate_limits(capturing_server, make_client): client = make_client() capturing_server.respond_with(code=429, headers={"Retry-After": "4"}) diff --git a/tests/tracing/test_misc.py b/tests/tracing/test_misc.py index e1de847102..8895c98dbc 100644 --- a/tests/tracing/test_misc.py +++ b/tests/tracing/test_misc.py @@ -37,6 +37,26 @@ def test_span_trimming(sentry_init, capture_events): assert "dropped_spans" not in event +def test_span_trimming_produces_client_report( + sentry_init, capture_events, capture_record_lost_event_calls +): + sentry_init(traces_sample_rate=1.0, _experiments={"max_spans": 3}) + events = capture_events() + record_lost_event_calls = capture_record_lost_event_calls() + + with start_transaction(name="hi"): + for i in range(10): + with start_span(op="foo{}".format(i)): + pass + + (event,) = events + + assert len(event["spans"]) == 3 + + # 7 spans were dropped (10 total - 3 kept = 7 dropped) + assert ("buffer_overflow", "span", None, 7) in record_lost_event_calls + + def test_span_data_scrubbing_and_trimming(sentry_init, capture_events): sentry_init(traces_sample_rate=1.0, _experiments={"max_spans": 3}) events = capture_events() @@ -585,3 +605,168 @@ def test_update_current_span(sentry_init, capture_events): "thread.id": mock.ANY, "thread.name": mock.ANY, } + + +class TestConversationIdPropagation: + """Tests for conversation_id propagation to AI spans.""" + + def test_conversation_id_propagates_to_span_with_gen_ai_operation_name( + self, sentry_init, capture_events + ): + """Span with gen_ai.operation.name data should get conversation_id.""" + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id("conv-op-name-test") + + with sentry_sdk.start_transaction(name="test-tx"): + with start_span(op="http.client") as span: + span.set_data("gen_ai.operation.name", "chat") + + (event,) = events + span_data = event["spans"][0]["data"] + assert span_data.get("gen_ai.conversation.id") == "conv-op-name-test" + + def test_conversation_id_propagates_to_span_with_ai_op( + self, sentry_init, capture_events + ): + """Span with ai.* op should get conversation_id.""" + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id("conv-ai-op-test") + + with sentry_sdk.start_transaction(name="test-tx"): + with start_span(op="ai.chat.completions"): + pass + + (event,) = events + span_data = event["spans"][0]["data"] + assert span_data.get("gen_ai.conversation.id") == "conv-ai-op-test" + + def test_conversation_id_propagates_to_span_with_gen_ai_op( + self, sentry_init, capture_events + ): + """Span with gen_ai.* op should get conversation_id.""" + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id("conv-gen-ai-op-test") + + with sentry_sdk.start_transaction(name="test-tx"): + with start_span(op="gen_ai.invoke_agent"): + pass + + (event,) = events + span_data = event["spans"][0]["data"] + assert span_data.get("gen_ai.conversation.id") == "conv-gen-ai-op-test" + + def test_conversation_id_not_propagated_to_non_ai_span( + self, sentry_init, capture_events + ): + """Non-AI span should NOT get conversation_id.""" + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id("conv-should-not-appear") + + with sentry_sdk.start_transaction(name="test-tx"): + with start_span(op="http.client") as span: + span.set_data("some.other.data", "value") + + (event,) = events + span_data = event["spans"][0]["data"] + assert "gen_ai.conversation.id" not in span_data + + def test_conversation_id_not_propagated_when_not_set( + self, sentry_init, capture_events + ): + """AI span should not have conversation_id if not set on scope.""" + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + # Ensure no conversation_id is set + scope = sentry_sdk.get_current_scope() + scope.remove_conversation_id() + + with sentry_sdk.start_transaction(name="test-tx"): + with start_span(op="ai.chat.completions"): + pass + + (event,) = events + span_data = event["spans"][0]["data"] + assert "gen_ai.conversation.id" not in span_data + + def test_conversation_id_not_propagated_to_span_without_op( + self, sentry_init, capture_events + ): + """Span without op and without gen_ai.operation.name should NOT get conversation_id.""" + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id("conv-no-op-test") + + with sentry_sdk.start_transaction(name="test-tx"): + with start_span(name="unnamed-span") as span: + span.set_data("regular.data", "value") + + (event,) = events + span_data = event["spans"][0]["data"] + assert "gen_ai.conversation.id" not in span_data + + def test_conversation_id_propagates_with_gen_ai_operation_name_no_op( + self, sentry_init, capture_events + ): + """Span with gen_ai.operation.name but no op should still get conversation_id.""" + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id("conv-no-op-but-data-test") + + with sentry_sdk.start_transaction(name="test-tx"): + with start_span(name="unnamed-span") as span: + span.set_data("gen_ai.operation.name", "embedding") + + (event,) = events + span_data = event["spans"][0]["data"] + assert span_data.get("gen_ai.conversation.id") == "conv-no-op-but-data-test" + + def test_conversation_id_propagates_to_transaction_with_ai_op( + self, sentry_init, capture_events + ): + """Transaction with ai.* op should get conversation_id.""" + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id("conv-tx-ai-op-test") + + with sentry_sdk.start_transaction(op="ai.workflow", name="AI Workflow"): + pass + + (event,) = events + trace_data = event["contexts"]["trace"]["data"] + assert trace_data.get("gen_ai.conversation.id") == "conv-tx-ai-op-test" + + def test_conversation_id_not_propagated_to_non_ai_transaction( + self, sentry_init, capture_events + ): + """Non-AI transaction should NOT get conversation_id.""" + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id("conv-tx-should-not-appear") + + with sentry_sdk.start_transaction(op="http.server", name="HTTP Request"): + pass + + (event,) = events + trace_data = event["contexts"]["trace"]["data"] + assert "gen_ai.conversation.id" not in trace_data diff --git a/tox.ini b/tox.ini index 26166555c1..5530002884 100644 --- a/tox.ini +++ b/tox.ini @@ -57,80 +57,81 @@ envlist = # ~~~ MCP ~~~ {py3.10,py3.12,py3.13}-mcp-v1.15.0 - {py3.10,py3.12,py3.13}-mcp-v1.18.0 - {py3.10,py3.12,py3.13}-mcp-v1.21.2 - {py3.10,py3.12,py3.13}-mcp-v1.24.0 + {py3.10,py3.12,py3.13}-mcp-v1.19.0 + {py3.10,py3.12,py3.13}-mcp-v1.23.3 + {py3.10,py3.12,py3.13}-mcp-v1.26.0 {py3.10,py3.13,py3.14,py3.14t}-fastmcp-v0.1.0 {py3.10,py3.13,py3.14,py3.14t}-fastmcp-v0.4.1 {py3.10,py3.13,py3.14,py3.14t}-fastmcp-v1.0 - {py3.10,py3.12,py3.13}-fastmcp-v2.14.1 + {py3.10,py3.12,py3.13}-fastmcp-v2.14.4 + {py3.10,py3.12,py3.13}-fastmcp-v3.0.0b1 # ~~~ Agents ~~~ {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.2.11 {py3.10,py3.12,py3.13}-openai_agents-v0.4.2 - {py3.10,py3.13,py3.14,py3.14t}-openai_agents-v0.6.4 + {py3.10,py3.13,py3.14,py3.14t}-openai_agents-v0.7.0 {py3.10,py3.12,py3.13}-pydantic_ai-v1.0.18 - {py3.10,py3.12,py3.13}-pydantic_ai-v1.12.0 - {py3.10,py3.12,py3.13}-pydantic_ai-v1.24.0 - {py3.10,py3.12,py3.13}-pydantic_ai-v1.36.0 + {py3.10,py3.12,py3.13}-pydantic_ai-v1.17.0 + {py3.10,py3.12,py3.13}-pydantic_ai-v1.34.0 + {py3.10,py3.12,py3.13}-pydantic_ai-v1.51.0 # ~~~ AI Workflow ~~~ {py3.9,py3.11,py3.12}-langchain-base-v0.1.20 {py3.9,py3.12,py3.13}-langchain-base-v0.3.27 - {py3.10,py3.13,py3.14}-langchain-base-v1.2.0 + {py3.10,py3.13,py3.14}-langchain-base-v1.2.7 {py3.9,py3.11,py3.12}-langchain-notiktoken-v0.1.20 {py3.9,py3.12,py3.13}-langchain-notiktoken-v0.3.27 - {py3.10,py3.13,py3.14}-langchain-notiktoken-v1.2.0 + {py3.10,py3.13,py3.14}-langchain-notiktoken-v1.2.7 {py3.9,py3.13,py3.14}-langgraph-v0.6.11 - {py3.10,py3.12,py3.13}-langgraph-v1.0.5 + {py3.10,py3.12,py3.13}-langgraph-v1.0.7 # ~~~ AI ~~~ {py3.8,py3.11,py3.12}-anthropic-v0.16.0 {py3.8,py3.11,py3.12}-anthropic-v0.36.2 {py3.8,py3.11,py3.12}-anthropic-v0.56.0 - {py3.9,py3.12,py3.13}-anthropic-v0.75.0 + {py3.9,py3.13,py3.14,py3.14t}-anthropic-v0.77.0 {py3.9,py3.10,py3.11}-cohere-v5.4.0 {py3.9,py3.11,py3.12}-cohere-v5.10.0 {py3.9,py3.11,py3.12}-cohere-v5.15.0 - {py3.9,py3.11,py3.12}-cohere-v5.20.1 + {py3.9,py3.11,py3.12}-cohere-v5.20.2 {py3.9,py3.12,py3.13}-google_genai-v1.29.0 - {py3.9,py3.12,py3.13}-google_genai-v1.38.0 - {py3.9,py3.13,py3.14,py3.14t}-google_genai-v1.47.0 - {py3.10,py3.13,py3.14,py3.14t}-google_genai-v1.56.0 + {py3.9,py3.12,py3.13}-google_genai-v1.40.0 + {py3.10,py3.13,py3.14,py3.14t}-google_genai-v1.51.0 + {py3.10,py3.13,py3.14,py3.14t}-google_genai-v1.61.0 {py3.8,py3.10,py3.11}-huggingface_hub-v0.24.7 - {py3.8,py3.12,py3.13}-huggingface_hub-v0.36.0 - {py3.9,py3.13,py3.14,py3.14t}-huggingface_hub-v1.2.3 + {py3.8,py3.12,py3.13}-huggingface_hub-v0.36.1 + {py3.9,py3.13,py3.14,py3.14t}-huggingface_hub-v1.3.7 {py3.9,py3.12,py3.13}-litellm-v1.77.7 {py3.9,py3.12,py3.13}-litellm-v1.78.7 {py3.9,py3.12,py3.13}-litellm-v1.79.3 - {py3.9,py3.12,py3.13}-litellm-v1.80.10 + {py3.9,py3.12,py3.13}-litellm-v1.81.6 {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.12,py3.13}-openai-base-v1.109.1 - {py3.9,py3.13,py3.14,py3.14t}-openai-base-v2.14.0 + {py3.9,py3.13,py3.14,py3.14t}-openai-base-v2.16.0 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.12,py3.13}-openai-notiktoken-v1.109.1 - {py3.9,py3.13,py3.14,py3.14t}-openai-notiktoken-v2.14.0 + {py3.9,py3.13,py3.14,py3.14t}-openai-notiktoken-v2.16.0 # ~~~ Cloud ~~~ {py3.6,py3.7}-boto3-v1.12.49 {py3.6,py3.9,py3.10}-boto3-v1.21.46 {py3.7,py3.11,py3.12}-boto3-v1.33.13 - {py3.9,py3.13,py3.14,py3.14t}-boto3-v1.42.13 + {py3.9,py3.13,py3.14,py3.14t}-boto3-v1.42.39 {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.9,py3.12,py3.13}-chalice-v1.32.0 @@ -146,7 +147,7 @@ envlist = {py3.6}-pymongo-v3.5.1 {py3.6,py3.10,py3.11}-pymongo-v3.13.0 - {py3.9,py3.13,py3.14,py3.14t}-pymongo-v4.15.5 + {py3.9,py3.13,py3.14,py3.14t}-pymongo-v4.16.0 {py3.6}-redis-v2.10.6 {py3.6,py3.7,py3.8}-redis-v3.5.3 @@ -158,9 +159,10 @@ envlist = {py3.6}-redis_py_cluster_legacy-v1.3.6 {py3.6,py3.7,py3.8}-redis_py_cluster_legacy-v2.1.3 - {py3.6,py3.8,py3.9}-sqlalchemy-v1.3.24 + {py3.6,py3.8,py3.9}-sqlalchemy-v1.2.19 {py3.6,py3.11,py3.12}-sqlalchemy-v1.4.54 - {py3.7,py3.12,py3.13}-sqlalchemy-v2.0.45 + {py3.7,py3.12,py3.13}-sqlalchemy-v2.0.46 + {py3.10,py3.13,py3.14,py3.14t}-sqlalchemy-v2.1.0b1 # ~~~ Flags ~~~ @@ -171,25 +173,26 @@ envlist = {py3.9,py3.13,py3.14,py3.14t}-openfeature-v0.8.4 {py3.7,py3.13,py3.14}-statsig-v0.55.3 - {py3.7,py3.13,py3.14}-statsig-v0.66.2 + {py3.7,py3.13,py3.14}-statsig-v0.67.0 {py3.8,py3.12,py3.13}-unleash-v6.0.1 - {py3.8,py3.12,py3.13}-unleash-v6.4.1 + {py3.8,py3.12,py3.13}-unleash-v6.5.1 # ~~~ GraphQL ~~~ {py3.8,py3.10,py3.11}-ariadne-v0.20.1 - {py3.9,py3.12,py3.13}-ariadne-v0.26.2 + {py3.10,py3.13,py3.14,py3.14t}-ariadne-v0.27.1 + {py3.10,py3.13,py3.14,py3.14t}-ariadne-v0.28.0rc2 {py3.6,py3.9,py3.10}-gql-v3.4.1 {py3.9,py3.12,py3.13}-gql-v4.0.0 - {py3.9,py3.12,py3.13}-gql-v4.2.0b0 + {py3.9,py3.13,py3.14,py3.14t}-gql-v4.3.0b0 {py3.6,py3.9,py3.10}-graphene-v3.3 {py3.8,py3.12,py3.13}-graphene-v3.4.3 {py3.8,py3.10,py3.11}-strawberry-v0.209.8 - {py3.10,py3.13,py3.14,py3.14t}-strawberry-v0.287.3 + {py3.10,py3.13,py3.14,py3.14t}-strawberry-v0.291.0 # ~~~ Network ~~~ @@ -197,6 +200,7 @@ envlist = {py3.7,py3.9,py3.10}-grpc-v1.47.5 {py3.7,py3.11,py3.12}-grpc-v1.62.3 {py3.9,py3.13,py3.14}-grpc-v1.76.0 + {py3.9,py3.13,py3.14}-grpc-v1.78.0rc2 {py3.6,py3.8,py3.9}-httpx-v0.16.1 {py3.6,py3.9,py3.10}-httpx-v0.20.0 @@ -209,31 +213,31 @@ envlist = # ~~~ Tasks ~~~ {py3.7,py3.9,py3.10}-arq-v0.23 - {py3.8,py3.11,py3.12}-arq-v0.26.3 + {py3.9,py3.12,py3.13}-arq-v0.27.0 {py3.7}-beam-v2.14.0 - {py3.10,py3.12,py3.13}-beam-v2.70.0 + {py3.10,py3.12,py3.13}-beam-v2.71.0 {py3.6,py3.7,py3.8}-celery-v4.4.7 - {py3.9,py3.12,py3.13}-celery-v5.6.0 + {py3.9,py3.12,py3.13}-celery-v5.6.2 {py3.6,py3.7}-dramatiq-v1.9.0 - {py3.10,py3.13,py3.14,py3.14t}-dramatiq-v2.0.0 + {py3.10,py3.13,py3.14,py3.14t}-dramatiq-v2.0.1 {py3.6,py3.7}-huey-v2.1.3 - {py3.6,py3.13,py3.14,py3.14t}-huey-v2.5.5 + {py3.6,py3.13,py3.14,py3.14t}-huey-v2.6.0 {py3.9,py3.10}-ray-v2.7.2 - {py3.10,py3.12,py3.13}-ray-v2.52.1 + {py3.10,py3.12,py3.13}-ray-v2.53.0 - {py3.6}-rq-v0.8.2 + {py3.6}-rq-v0.6.0 {py3.6,py3.7}-rq-v0.13.0 {py3.7,py3.11,py3.12}-rq-v1.16.2 {py3.9,py3.12,py3.13}-rq-v2.6.1 {py3.8,py3.9}-spark-v3.0.3 - {py3.8,py3.10,py3.11}-spark-v3.5.7 - {py3.10,py3.13,py3.14}-spark-v4.1.0 + {py3.8,py3.10,py3.11}-spark-v3.5.8 + {py3.10,py3.13,py3.14}-spark-v4.1.1 # ~~~ Web 1 ~~~ @@ -241,29 +245,29 @@ envlist = {py3.6,py3.8,py3.9}-django-v2.2.28 {py3.6,py3.9,py3.10}-django-v3.2.25 {py3.8,py3.11,py3.12}-django-v4.2.27 - {py3.10,py3.13,py3.14,py3.14t}-django-v5.2.9 - {py3.12,py3.13,py3.14,py3.14t}-django-v6.0 + {py3.10,py3.13,py3.14,py3.14t}-django-v5.2.10 + {py3.12,py3.13,py3.14,py3.14t}-django-v6.0.1 {py3.6,py3.7,py3.8}-flask-v1.1.4 {py3.8,py3.13,py3.14,py3.14t}-flask-v2.3.3 {py3.9,py3.13,py3.14,py3.14t}-flask-v3.1.2 {py3.6,py3.9,py3.10}-starlette-v0.16.0 - {py3.7,py3.10,py3.11}-starlette-v0.27.0 - {py3.8,py3.12,py3.13}-starlette-v0.38.6 - {py3.10,py3.13,py3.14,py3.14t}-starlette-v0.50.0 + {py3.7,py3.10,py3.11}-starlette-v0.28.0 + {py3.8,py3.12,py3.13}-starlette-v0.40.0 + {py3.10,py3.13,py3.14,py3.14t}-starlette-v0.52.1 {py3.6,py3.9,py3.10}-fastapi-v0.79.1 - {py3.7,py3.10,py3.11}-fastapi-v0.94.1 - {py3.8,py3.11,py3.12}-fastapi-v0.109.2 - {py3.9,py3.13,py3.14,py3.14t}-fastapi-v0.125.0 + {py3.7,py3.10,py3.11}-fastapi-v0.95.2 + {py3.8,py3.11,py3.12}-fastapi-v0.111.1 + {py3.9,py3.13,py3.14,py3.14t}-fastapi-v0.128.0 # ~~~ Web 2 ~~~ {py3.7}-aiohttp-v3.4.4 {py3.7,py3.8,py3.9}-aiohttp-v3.7.4 {py3.8,py3.12,py3.13}-aiohttp-v3.10.11 - {py3.9,py3.13,py3.14,py3.14t}-aiohttp-v3.13.2 + {py3.9,py3.13,py3.14,py3.14t}-aiohttp-v3.13.3 {py3.6,py3.7}-bottle-v0.12.25 {py3.8,py3.12,py3.13}-bottle-v0.13.4 @@ -288,7 +292,7 @@ envlist = {py3.6}-sanic-v0.8.3 {py3.6,py3.8,py3.9}-sanic-v20.12.7 {py3.8,py3.10,py3.11}-sanic-v23.12.2 - {py3.9,py3.12,py3.13}-sanic-v25.3.0 + {py3.10,py3.13,py3.14}-sanic-v25.12.0 {py3.8,py3.10,py3.11}-starlite-v1.48.1 {py3.8,py3.10,py3.11}-starlite-v1.51.16 @@ -307,10 +311,10 @@ envlist = {py3.6}-trytond-v4.8.18 {py3.6,py3.7,py3.8}-trytond-v5.8.16 {py3.8,py3.10,py3.11}-trytond-v6.8.17 - {py3.9,py3.12,py3.13}-trytond-v7.8.0 + {py3.9,py3.12,py3.13}-trytond-v7.8.4 {py3.7,py3.12,py3.13}-typer-v0.15.4 - {py3.8,py3.13,py3.14,py3.14t}-typer-v0.20.0 + {py3.9,py3.13,py3.14,py3.14t}-typer-v0.21.1 @@ -380,15 +384,16 @@ deps = # ~~~ MCP ~~~ mcp-v1.15.0: mcp==1.15.0 - mcp-v1.18.0: mcp==1.18.0 - mcp-v1.21.2: mcp==1.21.2 - mcp-v1.24.0: mcp==1.24.0 + mcp-v1.19.0: mcp==1.19.0 + mcp-v1.23.3: mcp==1.23.3 + mcp-v1.26.0: mcp==1.26.0 mcp: pytest-asyncio fastmcp-v0.1.0: fastmcp==0.1.0 fastmcp-v0.4.1: fastmcp==0.4.1 fastmcp-v1.0: fastmcp==1.0 - fastmcp-v2.14.1: fastmcp==2.14.1 + fastmcp-v2.14.4: fastmcp==2.14.4 + fastmcp-v3.0.0b1: fastmcp==3.0.0b1 fastmcp: pytest-asyncio @@ -396,47 +401,47 @@ deps = openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.2.11: openai-agents==0.2.11 openai_agents-v0.4.2: openai-agents==0.4.2 - openai_agents-v0.6.4: openai-agents==0.6.4 + openai_agents-v0.7.0: openai-agents==0.7.0 openai_agents: pytest-asyncio pydantic_ai-v1.0.18: pydantic-ai==1.0.18 - pydantic_ai-v1.12.0: pydantic-ai==1.12.0 - pydantic_ai-v1.24.0: pydantic-ai==1.24.0 - pydantic_ai-v1.36.0: pydantic-ai==1.36.0 + pydantic_ai-v1.17.0: pydantic-ai==1.17.0 + pydantic_ai-v1.34.0: pydantic-ai==1.34.0 + pydantic_ai-v1.51.0: pydantic-ai==1.51.0 pydantic_ai: pytest-asyncio # ~~~ AI Workflow ~~~ langchain-base-v0.1.20: langchain==0.1.20 langchain-base-v0.3.27: langchain==0.3.27 - langchain-base-v1.2.0: langchain==1.2.0 + langchain-base-v1.2.7: langchain==1.2.7 langchain-base: pytest-asyncio langchain-base: openai langchain-base: tiktoken langchain-base: langchain-openai langchain-base-v0.3.27: langchain-community - langchain-base-v1.2.0: langchain-community - langchain-base-v1.2.0: langchain-classic + langchain-base-v1.2.7: langchain-community + langchain-base-v1.2.7: langchain-classic langchain-notiktoken-v0.1.20: langchain==0.1.20 langchain-notiktoken-v0.3.27: langchain==0.3.27 - langchain-notiktoken-v1.2.0: langchain==1.2.0 + langchain-notiktoken-v1.2.7: langchain==1.2.7 langchain-notiktoken: pytest-asyncio langchain-notiktoken: openai langchain-notiktoken: langchain-openai langchain-notiktoken-v0.3.27: langchain-community - langchain-notiktoken-v1.2.0: langchain-community - langchain-notiktoken-v1.2.0: langchain-classic + langchain-notiktoken-v1.2.7: langchain-community + langchain-notiktoken-v1.2.7: langchain-classic langgraph-v0.6.11: langgraph==0.6.11 - langgraph-v1.0.5: langgraph==1.0.5 + langgraph-v1.0.7: langgraph==1.0.7 # ~~~ AI ~~~ anthropic-v0.16.0: anthropic==0.16.0 anthropic-v0.36.2: anthropic==0.36.2 anthropic-v0.56.0: anthropic==0.56.0 - anthropic-v0.75.0: anthropic==0.75.0 + anthropic-v0.77.0: anthropic==0.77.0 anthropic: pytest-asyncio anthropic-v0.16.0: httpx<0.28.0 anthropic-v0.36.2: httpx<0.28.0 @@ -444,35 +449,35 @@ deps = cohere-v5.4.0: cohere==5.4.0 cohere-v5.10.0: cohere==5.10.0 cohere-v5.15.0: cohere==5.15.0 - cohere-v5.20.1: cohere==5.20.1 + cohere-v5.20.2: cohere==5.20.2 google_genai-v1.29.0: google-genai==1.29.0 - google_genai-v1.38.0: google-genai==1.38.0 - google_genai-v1.47.0: google-genai==1.47.0 - google_genai-v1.56.0: google-genai==1.56.0 + google_genai-v1.40.0: google-genai==1.40.0 + google_genai-v1.51.0: google-genai==1.51.0 + google_genai-v1.61.0: google-genai==1.61.0 google_genai: pytest-asyncio huggingface_hub-v0.24.7: huggingface_hub==0.24.7 - huggingface_hub-v0.36.0: huggingface_hub==0.36.0 - huggingface_hub-v1.2.3: huggingface_hub==1.2.3 + huggingface_hub-v0.36.1: huggingface_hub==0.36.1 + huggingface_hub-v1.3.7: huggingface_hub==1.3.7 huggingface_hub: responses huggingface_hub: pytest-httpx litellm-v1.77.7: litellm==1.77.7 litellm-v1.78.7: litellm==1.78.7 litellm-v1.79.3: litellm==1.79.3 - litellm-v1.80.10: litellm==1.80.10 + litellm-v1.81.6: litellm==1.81.6 openai-base-v1.0.1: openai==1.0.1 openai-base-v1.109.1: openai==1.109.1 - openai-base-v2.14.0: openai==2.14.0 + openai-base-v2.16.0: openai==2.16.0 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.109.1: openai==1.109.1 - openai-notiktoken-v2.14.0: openai==2.14.0 + openai-notiktoken-v2.16.0: openai==2.16.0 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 @@ -481,7 +486,7 @@ deps = boto3-v1.12.49: boto3==1.12.49 boto3-v1.21.46: boto3==1.21.46 boto3-v1.33.13: boto3==1.33.13 - boto3-v1.42.13: boto3==1.42.13 + boto3-v1.42.39: boto3==1.42.39 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 @@ -500,7 +505,7 @@ deps = pymongo-v3.5.1: pymongo==3.5.1 pymongo-v3.13.0: pymongo==3.13.0 - pymongo-v4.15.5: pymongo==4.15.5 + pymongo-v4.16.0: pymongo==4.16.0 pymongo: mockupdb redis-v2.10.6: redis==2.10.6 @@ -518,9 +523,10 @@ deps = redis_py_cluster_legacy-v1.3.6: redis-py-cluster==1.3.6 redis_py_cluster_legacy-v2.1.3: redis-py-cluster==2.1.3 - sqlalchemy-v1.3.24: sqlalchemy==1.3.24 + sqlalchemy-v1.2.19: sqlalchemy==1.2.19 sqlalchemy-v1.4.54: sqlalchemy==1.4.54 - sqlalchemy-v2.0.45: sqlalchemy==2.0.45 + sqlalchemy-v2.0.46: sqlalchemy==2.0.46 + sqlalchemy-v2.1.0b1: sqlalchemy==2.1.0b1 # ~~~ Flags ~~~ @@ -531,23 +537,24 @@ deps = openfeature-v0.8.4: openfeature-sdk==0.8.4 statsig-v0.55.3: statsig==0.55.3 - statsig-v0.66.2: statsig==0.66.2 + statsig-v0.67.0: statsig==0.67.0 statsig: typing_extensions unleash-v6.0.1: UnleashClient==6.0.1 - unleash-v6.4.1: UnleashClient==6.4.1 + unleash-v6.5.1: UnleashClient==6.5.1 # ~~~ GraphQL ~~~ ariadne-v0.20.1: ariadne==0.20.1 - ariadne-v0.26.2: ariadne==0.26.2 + ariadne-v0.27.1: ariadne==0.27.1 + ariadne-v0.28.0rc2: ariadne==0.28.0rc2 ariadne: fastapi ariadne: flask ariadne: httpx gql-v3.4.1: gql[all]==3.4.1 gql-v4.0.0: gql[all]==4.0.0 - gql-v4.2.0b0: gql[all]==4.2.0b0 + gql-v4.3.0b0: gql[all]==4.3.0b0 graphene-v3.3: graphene==3.3 graphene-v3.4.3: graphene==3.4.3 @@ -558,7 +565,7 @@ deps = {py3.6}-graphene: aiocontextvars strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8 - strawberry-v0.287.3: strawberry-graphql[fastapi,flask]==0.287.3 + strawberry-v0.291.0: strawberry-graphql[fastapi,flask]==0.291.0 strawberry: httpx strawberry-v0.209.8: pydantic<2.11 @@ -568,6 +575,7 @@ deps = grpc-v1.47.5: grpcio==1.47.5 grpc-v1.62.3: grpcio==1.62.3 grpc-v1.76.0: grpcio==1.76.0 + grpc-v1.78.0rc2: grpcio==1.78.0rc2 grpc: protobuf grpc: mypy-protobuf grpc: types-protobuf @@ -589,44 +597,44 @@ deps = # ~~~ Tasks ~~~ arq-v0.23: arq==0.23 - arq-v0.26.3: arq==0.26.3 + arq-v0.27.0: arq==0.27.0 arq: async-timeout arq: pytest-asyncio arq: fakeredis>=2.2.0,<2.8 arq-v0.23: pydantic<2 beam-v2.14.0: apache-beam==2.14.0 - beam-v2.70.0: apache-beam==2.70.0 + beam-v2.71.0: apache-beam==2.71.0 beam: dill celery-v4.4.7: celery==4.4.7 - celery-v5.6.0: celery==5.6.0 + celery-v5.6.2: celery==5.6.2 celery: newrelic<10.17.0 celery: redis {py3.7}-celery: importlib-metadata<5.0 dramatiq-v1.9.0: dramatiq==1.9.0 - dramatiq-v2.0.0: dramatiq==2.0.0 + dramatiq-v2.0.1: dramatiq==2.0.1 huey-v2.1.3: huey==2.1.3 - huey-v2.5.5: huey==2.5.5 + huey-v2.6.0: huey==2.6.0 ray-v2.7.2: ray==2.7.2 - ray-v2.52.1: ray==2.52.1 + ray-v2.53.0: ray==2.53.0 - rq-v0.8.2: rq==0.8.2 + rq-v0.6.0: rq==0.6.0 rq-v0.13.0: rq==0.13.0 rq-v1.16.2: rq==1.16.2 rq-v2.6.1: rq==2.6.1 rq: fakeredis<2.28.0 - rq-v0.8.2: fakeredis<1.0 - rq-v0.8.2: redis<3.2.2 + rq-v0.6.0: fakeredis<1.0 + rq-v0.6.0: redis<3.2.2 rq-v0.13.0: fakeredis>=1.0,<1.7.4 {py3.6,py3.7}-rq: fakeredis!=2.26.0 spark-v3.0.3: pyspark==3.0.3 - spark-v3.5.7: pyspark==3.5.7 - spark-v4.1.0: pyspark==4.1.0 + spark-v3.5.8: pyspark==3.5.8 + spark-v4.1.1: pyspark==4.1.1 # ~~~ Web 1 ~~~ @@ -634,8 +642,8 @@ deps = django-v2.2.28: django==2.2.28 django-v3.2.25: django==3.2.25 django-v4.2.27: django==4.2.27 - django-v5.2.9: django==5.2.9 - django-v6.0: django==6.0 + django-v5.2.10: django==5.2.10 + django-v6.0.1: django==6.0.1 django: psycopg2-binary django: djangorestframework django: pytest-django @@ -643,13 +651,13 @@ deps = django-v2.2.28: channels[daphne] django-v3.2.25: channels[daphne] django-v4.2.27: channels[daphne] - django-v5.2.9: channels[daphne] - django-v6.0: channels[daphne] + django-v5.2.10: channels[daphne] + django-v6.0.1: channels[daphne] django-v2.2.28: six django-v3.2.25: pytest-asyncio django-v4.2.27: pytest-asyncio - django-v5.2.9: pytest-asyncio - django-v6.0: pytest-asyncio + django-v5.2.10: pytest-asyncio + django-v6.0.1: pytest-asyncio django-v1.11.29: djangorestframework>=3.0,<4.0 django-v1.11.29: Werkzeug<2.1.0 django-v2.2.28: djangorestframework>=3.0,<4.0 @@ -669,9 +677,9 @@ deps = flask-v1.1.4: markupsafe<2.1.0 starlette-v0.16.0: starlette==0.16.0 - starlette-v0.27.0: starlette==0.27.0 - starlette-v0.38.6: starlette==0.38.6 - starlette-v0.50.0: starlette==0.50.0 + starlette-v0.28.0: starlette==0.28.0 + starlette-v0.40.0: starlette==0.40.0 + starlette-v0.52.1: starlette==0.52.1 starlette: pytest-asyncio starlette: python-multipart starlette: requests @@ -679,21 +687,20 @@ deps = starlette: jinja2 starlette: httpx starlette-v0.16.0: httpx<0.28.0 - starlette-v0.27.0: httpx<0.28.0 + starlette-v0.28.0: httpx<0.28.0 {py3.6}-starlette: aiocontextvars fastapi-v0.79.1: fastapi==0.79.1 - fastapi-v0.94.1: fastapi==0.94.1 - fastapi-v0.109.2: fastapi==0.109.2 - fastapi-v0.125.0: fastapi==0.125.0 + fastapi-v0.95.2: fastapi==0.95.2 + fastapi-v0.111.1: fastapi==0.111.1 + fastapi-v0.128.0: fastapi==0.128.0 fastapi: httpx fastapi: pytest-asyncio fastapi: python-multipart fastapi: requests fastapi: anyio<4 fastapi-v0.79.1: httpx<0.28.0 - fastapi-v0.94.1: httpx<0.28.0 - fastapi-v0.109.2: httpx<0.28.0 + fastapi-v0.95.2: httpx<0.28.0 {py3.6}-fastapi: aiocontextvars @@ -701,10 +708,10 @@ deps = aiohttp-v3.4.4: aiohttp==3.4.4 aiohttp-v3.7.4: aiohttp==3.7.4 aiohttp-v3.10.11: aiohttp==3.10.11 - aiohttp-v3.13.2: aiohttp==3.13.2 + aiohttp-v3.13.3: aiohttp==3.13.3 aiohttp: pytest-aiohttp aiohttp-v3.10.11: pytest-asyncio - aiohttp-v3.13.2: pytest-asyncio + aiohttp-v3.13.3: pytest-asyncio bottle-v0.12.25: bottle==0.12.25 bottle-v0.13.4: bottle==0.13.4 @@ -747,11 +754,11 @@ deps = sanic-v0.8.3: sanic==0.8.3 sanic-v20.12.7: sanic==20.12.7 sanic-v23.12.2: sanic==23.12.2 - sanic-v25.3.0: sanic==25.3.0 + sanic-v25.12.0: sanic==25.12.0 sanic: websockets<11.0 sanic: aiohttp sanic-v23.12.2: sanic-testing - sanic-v25.3.0: sanic-testing + sanic-v25.12.0: sanic-testing {py3.6}-sanic: aiocontextvars==0.2.1 {py3.8}-sanic: tracerite<1.1.2 @@ -781,13 +788,13 @@ deps = trytond-v4.8.18: trytond==4.8.18 trytond-v5.8.16: trytond==5.8.16 trytond-v6.8.17: trytond==6.8.17 - trytond-v7.8.0: trytond==7.8.0 + trytond-v7.8.4: trytond==7.8.4 trytond: werkzeug trytond-v4.6.22: werkzeug<1.0 trytond-v4.8.18: werkzeug<1.0 typer-v0.15.4: typer==0.15.4 - typer-v0.20.0: typer==0.20.0 + typer-v0.21.1: typer==0.21.1 @@ -928,3 +935,4 @@ commands = ruff check tests sentry_sdk ruff format --check tests sentry_sdk mypy sentry_sdk + python scripts/find_raise_from_none.py