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
[](https://discord.com/invite/Ww9hbqr)
[](https://x.com/intent/follow?screen_name=sentry)
[](https://pypi.python.org/pypi/sentry-sdk)
-
+
[](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