From 878dc5be36ab12ac34149c5ade344a0c340aa833 Mon Sep 17 00:00:00 2001 From: "Alexandr N. Zamaraev" Date: Thu, 14 Aug 2025 16:42:59 +0700 Subject: [PATCH 1/7] integration for aiomysql --- sentry_sdk/integrations/aiomysql.py | 174 ++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 sentry_sdk/integrations/aiomysql.py diff --git a/sentry_sdk/integrations/aiomysql.py b/sentry_sdk/integrations/aiomysql.py new file mode 100644 index 0000000000..8f2ff51b1d --- /dev/null +++ b/sentry_sdk/integrations/aiomysql.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +""" +Adapted from module sentry_sdk.integrations.asyncpg +""" +from __future__ import annotations +import contextlib +from typing import Any, TypeVar, Callable, Awaitable, Iterator + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import add_query_source, record_sql_queries +from sentry_sdk.utils import ( + ensure_integration_enabled, + parse_version, + capture_internal_exceptions, +) + +try: + import aiomysql # type: ignore[import-not-found] + from aiomysql.connection import Connection, Cursor # type: ignore +except ImportError: + raise DidNotEnable("aiomysql not installed.") + + +class AioMySQLIntegration(Integration): + identifier = "aiomysql" + origin = f"auto.db.{identifier}" + _record_params = False + + def __init__(self, *, record_params: bool = False): + AioMySQLIntegration._record_params = record_params + + @staticmethod + def setup_once() -> None: + aiomysql_version = parse_version(aiomysql.__version__) + _check_minimum_version(AioMySQLIntegration, aiomysql_version) + + aiomysql.Connection.query = _wrap_execute( + aiomysql.Connection.query, + ) + + aiomysql.Connection.cursor = _wrap_cursor_creation( + aiomysql.Connection.cursor + ) + + aiomysql.connect = _wrap_connect(aiomysql.connect) + + +T = TypeVar("T") + + +def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + conn = args[0] + query = args[1] # В aiomysql запрос передается первым аргументом + with record_sql_queries( + cursor=None, + query=query, + params_list=None, + paramstyle=None, + executemany=False, + span_origin=AioMySQLIntegration.origin, + ) as span: + res = await f(*args, **kwargs) + span.set_data("db.affected_rows", res) + + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +SubCursor = TypeVar("SubCursor", bound=Cursor) + + +@contextlib.contextmanager +def _record( + cursor: SubCursor | None, + query: str, + params_list: tuple[Any, ...] | None, + *, + executemany: bool = False, +) -> Iterator[Span]: + integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) + if integration is not None and not integration._record_params: + params_list = None + + param_style = "pyformat" if params_list else None + + with record_sql_queries( + cursor=cursor, + query=query, + params_list=params_list, + paramstyle=param_style, + executemany=executemany, + record_cursor_repr=cursor is not None, + span_origin=AioMySQLIntegration.origin, + ) as span: + yield span + + +def _wrap_cursor_creation(f: Callable[..., T]) -> Callable[..., T]: + @ensure_integration_enabled(AioMySQLIntegration, f) + def _inner(*args: Any, **kwargs: Any) -> T: + query = args[0] + params_list = args[1] if len(args) > 1 else None + + with _record(None, query, params_list, executemany=False) as span: + _set_db_data(span, args[0]) + res = f(*args, **kwargs) + span.set_data("db.cursor", res._coro.result()) + + return res + + return _inner + + +def _wrap_connect(f: Callable[..., T]) -> Callable[..., T]: + def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return f(*args, **kwargs) + + host = kwargs["host"] + port = kwargs.get("port") or 3306 + user = kwargs["user"] + db = kwargs["db"] + + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=AioMySQLIntegration.origin, + ) as span: + span.set_data(SPANDATA.DB_SYSTEM, "mysql") + span.set_data(SPANDATA.SERVER_ADDRESS, host) + span.set_data(SPANDATA.SERVER_PORT, port) + span.set_data(SPANDATA.DB_NAME, db) + span.set_data(SPANDATA.DB_USER, user) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=span._data + ) + res = f(*args, **kwargs) + + return res + + return _inner + + +def _set_db_data(span: Span, conn: Any) -> None: + span.set_data(SPANDATA.DB_SYSTEM, "mysql") + + host = conn.host + if host: + span.set_data(SPANDATA.SERVER_ADDRESS, host) + + port = conn.port + if port: + span.set_data(SPANDATA.SERVER_PORT, port) + + database = conn.db + if database: + span.set_data(SPANDATA.DB_NAME, database) + + user = conn.user + if user: + span.set_data(SPANDATA.DB_USER, user) From 29b4e85a5dcdf25644642e8bcdb2106181e450c4 Mon Sep 17 00:00:00 2001 From: "Alexandr N. Zamaraev" Date: Wed, 20 Aug 2025 16:24:17 +0700 Subject: [PATCH 2/7] Drop _wrap_cursor_creation in integration aiomysql.py --- sentry_sdk/integrations/aiomysql.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/sentry_sdk/integrations/aiomysql.py b/sentry_sdk/integrations/aiomysql.py index 8f2ff51b1d..b48b31ab0d 100644 --- a/sentry_sdk/integrations/aiomysql.py +++ b/sentry_sdk/integrations/aiomysql.py @@ -41,10 +41,6 @@ def setup_once() -> None: aiomysql.Connection.query, ) - aiomysql.Connection.cursor = _wrap_cursor_creation( - aiomysql.Connection.cursor - ) - aiomysql.connect = _wrap_connect(aiomysql.connect) @@ -106,22 +102,6 @@ def _record( yield span -def _wrap_cursor_creation(f: Callable[..., T]) -> Callable[..., T]: - @ensure_integration_enabled(AioMySQLIntegration, f) - def _inner(*args: Any, **kwargs: Any) -> T: - query = args[0] - params_list = args[1] if len(args) > 1 else None - - with _record(None, query, params_list, executemany=False) as span: - _set_db_data(span, args[0]) - res = f(*args, **kwargs) - span.set_data("db.cursor", res._coro.result()) - - return res - - return _inner - - def _wrap_connect(f: Callable[..., T]) -> Callable[..., T]: def _inner(*args: Any, **kwargs: Any) -> T: if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: From 28e79b6ffe95c54e55eedb43f30ffed05d083661 Mon Sep 17 00:00:00 2001 From: "Alexandr N. Zamaraev" Date: Wed, 20 Aug 2025 16:30:40 +0700 Subject: [PATCH 3/7] Potential KeyError in _wrap_connect --- sentry_sdk/integrations/aiomysql.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/aiomysql.py b/sentry_sdk/integrations/aiomysql.py index b48b31ab0d..3c2920d650 100644 --- a/sentry_sdk/integrations/aiomysql.py +++ b/sentry_sdk/integrations/aiomysql.py @@ -107,10 +107,10 @@ def _inner(*args: Any, **kwargs: Any) -> T: if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: return f(*args, **kwargs) - host = kwargs["host"] + host = kwargs.get("host", "localhost") port = kwargs.get("port") or 3306 - user = kwargs["user"] - db = kwargs["db"] + user = kwargs.get("user") + db = kwargs.get("db") with sentry_sdk.start_span( op=OP.DB, From 89b86699a052e625a97cc15961dafbf437a58c64 Mon Sep 17 00:00:00 2001 From: "Alexandr N Zamaraev (aka tonal)" Date: Sun, 12 Apr 2026 18:57:32 +0700 Subject: [PATCH 4/7] feat(aiomysql): Add aiomysql integration Rewrite the aiomysql integration to fix issues raised in PR #4703: - Patch Cursor.execute and Cursor.executemany instead of Connection.query - Make _wrap_connect async (await inside span context) - Handle bytes/bytearray queries from executemany's internal batching - Normalize query text using " ".join(query.split()) for performance - Protect _sentry_skip_next_execute flag with try/finally to prevent leakage - Remove dead _wrap_cursor and unused _record context manager Add 17 end-to-end tests covering connect, execute, executemany, record_params, cursor iteration, connection pools, query source, span origin, and normalization. Update CI configuration: tox.ini envlist, GitHub Actions workflow with MySQL service container, and test suite config. Co-Authored-By: Qwen Code Co-authored-by: Qwen-Coder --- .github/workflows/test-integrations-dbs.yml | 22 + scripts/populate_tox/config.py | 7 + .../populate_tox/package_dependencies.jsonl | 2 + scripts/populate_tox/releases.jsonl | 7 + .../split_tox_gh_actions.py | 6 + .../templates/test_group.jinja | 21 + sentry_sdk/integrations/aiomysql.py | 293 ++++--- tests/integrations/aiomysql/__init__.py | 4 + tests/integrations/aiomysql/test_aiomysql.py | 742 ++++++++++++++++++ tox.ini | 12 + 10 files changed, 995 insertions(+), 121 deletions(-) create mode 100644 tests/integrations/aiomysql/__init__.py create mode 100644 tests/integrations/aiomysql/test_aiomysql.py diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index 380ded2f80..2826d253b2 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -56,6 +56,24 @@ jobs: SENTRY_PYTHON_TEST_POSTGRES_HOST: ${{ matrix.python-version == '3.6' && 'postgres' || 'localhost' }} SENTRY_PYTHON_TEST_POSTGRES_USER: postgres SENTRY_PYTHON_TEST_POSTGRES_PASSWORD: sentry + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + ports: + - 3306:3306 + env: + SENTRY_PYTHON_TEST_MYSQL_HOST: ${{ matrix.python-version == '3.6' && 'mysql' || 'localhost' }} + SENTRY_PYTHON_TEST_MYSQL_USER: root + SENTRY_PYTHON_TEST_MYSQL_PASSWORD: root + SENTRY_PYTHON_TEST_MYSQL_DB: test # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: @@ -73,6 +91,10 @@ jobs: - name: Erase coverage run: | coverage erase + - name: Test aiomysql + run: | + set -x # print commands that are executed + ./scripts/runtox.sh "py${{ matrix.python-version }}-aiomysql" - name: Test asyncpg run: | set -x # print commands that are executed diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index ba2f4dc6d5..aed00456a7 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -47,6 +47,13 @@ }, "python": ">=3.7", }, + "aiomysql": { + "package": "aiomysql", + "deps": { + "*": ["pytest-asyncio"], + }, + "python": ">=3.7", + }, "beam": { "package": "apache-beam", "python": ">=3.7", diff --git a/scripts/populate_tox/package_dependencies.jsonl b/scripts/populate_tox/package_dependencies.jsonl index a3a5cd4a99..563db0d817 100644 --- a/scripts/populate_tox/package_dependencies.jsonl +++ b/scripts/populate_tox/package_dependencies.jsonl @@ -40,3 +40,5 @@ {"name": "statsig", "version": "0.71.6", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/b7/b1/aba1475b024e99883b5b43405e5713520129ce42004bc39a3fb40312724b/statsig-0.71.6-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/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.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/38/14/52b6613fdda4078c62eb5b4fe3efc724ddc55a4ad524c93de51830107aa3/ijson-3.5.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/d6/b7/48a7f1ab9eee62f1113114207df7e7e6bc29227389d554f42cc11bc98108/ip3country-0.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a4/e7/14dc9366696dcb53a413449881743426ed289d687bcf3d5aee4726c32ebb/protobuf-7.34.0-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/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-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/3e/6f/73a4d37deefb159556d39d654b5bad67b6874d1ad0b20b96fb5a04de3949/ua_parser_builtins-202603-py3-none-any.whl"}}]} {"name": "strawberry-graphql", "version": "0.312.2", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/69/6d/f22f1cc6c2faf4307c96f8f2a58a031ebf42a8ee0f9fa046bf6304f3dc0e/strawberry_graphql-0.312.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/86/41/cb887d9afc5dabd78feefe6ccbaf83ff423c206a7a1b7aeeac05120b2125/graphql_core-3.2.8-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.24.1", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-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/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-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"}}]} +{"name": "openai-agents", "version": "0.13.6", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/1c/83/a991b2ad389abadabf13f6c4228bd88ac8dc363e4b50fcae8c5ea966bd41/openai_agents-0.13.6-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.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/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.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/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-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/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-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/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.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/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-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/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-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/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-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/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.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": "pydantic-ai", "version": "1.80.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/2c/9a/6d65ea560f8e84f57cf5cf7f5b611d48bd5d1869120bc7fcc3b40aed0556/pydantic_ai-1.80.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/23/bf/ef273265ef3530cf432fd6d0014ceed57d1cc5b1550fd975bc91152633c8/pydantic_ai_slim-1.80.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ee/28/8480d2e53728a891e1157f6c741a7f2ec27da06b56936d5c6e1f64af0e51/pydantic_evals-1.80.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/12/483b7402d302021ff8537a746eebf018f4e0bb5892c7bef769ab968e03c1/pydantic_graph-1.80.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/8c/c7a33f3efaa8d6a5bc40e012e5ecc2d72c2e6124550ca9085fe0ceed9993/huggingface_hub-1.10.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_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/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.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/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/e4/a0/a73398d30bb0f9ad70cd70426151a4a19527a7296e48a3a16a50e1d5db05/ag_ui_protocol-0.1.15-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/0c/ac/7185750e8688f6ff79b0e3d6a61372c88b81ba81fcda8798c70598e18aca/anthropic-0.94.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.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/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-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/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/2b/8bfddb39a19f5fbc16a869f1a394771e6223f07160dbc0ff6b38e05ea0ae/boto3-1.42.88-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/46/ad14e41245adb8b0c83663ba13e822b68a0df08999dd250e75b0750fdf6c/botocore-1.42.88-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/9e/b4/00c2f9f8387a2e77faf8410210466c46d55dd30a0388de41c54441b148fb/cohere-6.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a7/1c/6dfd082a205be4510543221b734b1191299e6a1810c452b6bc76dfa6968e/fastavro-1.12.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/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f5/48/84b6dcba793178a44b9d99b4def6cd62f870dcfc5bb7b9153ac390135812/fastmcp-3.2.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-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/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b4/bd/05055d8360cef0757d79367157f3b15c0a0715e81e08f86a04018ec045f0/cyclopts-4.10.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a3/f6/8ef7e4c286deb2709d11ca96a5237caae3ef4876ab3c48095856cfd2df30/genai_prices-0.0.56-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-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/9f/3d/9f70246114cdf56a2615a40428ced08bc844f5a26247fe812b2f0dd4eaca/google_genai-1.72.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/34/b0/83e3892a4597a4b8ebf8a662aeaf314765c4c2340516eb1d049b459b24fc/groq-1.1.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/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-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/04/d5/4e96c44f6c1ea3d812cf5391d81a4f5abaa540abf8d04ecd7f66e0ed11df/jsonschema_path-0.4.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-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/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/02/33/81b13e1f2044b5fe0112068a2494526db9cfdf784030a2ea57688279360a/logfire-4.32.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/58/56/fe8d8aa60e796e059992fb7359ec5dda4ef72db4fccfbd362a2ee0595ec1/logfire_api-4.32.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/e0/9f246d18f8fe6e815f7cae88c437c986b342d3ff568607c8e0d53f4710d0/mistralai-2.3.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/28/50/1a313fb700526b134c71eb8a225d8b83be0385dbb0204337b4379c698cef/jsonpath_python-1.1.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/43/59/b98e84eebf745ffc75397eaad4763795bff8a30cbf2373a50ed4e70646c5/opentelemetry_instrumentation_httpx-0.60b1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-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/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/99/5f/86b1630be61bdebf253c2f953a6c3f073ec21bb0725565ea3896802e1ca3/pydantic_handlebars-0.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-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/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-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/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/de/9c/3782bab0bf11a40b550147c19a5d1a476c17405391751982408902d9f138/temporalio-1.25.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/11/52/6327a5f4fda01207205038a106a99848a41c83e933cd23ea2cab3d2ebc6c/nexus_rpc-1.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2e/e8/1fd38926f9cf031188fbc5a96694203ea6f24b0e34bd64a225ec6f6291ba/types_protobuf-6.32.1.20260221-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-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/11/e1/7ec67882ad8fc9f86384bef6421fa252c9cbe5744f8df6ce77afc9eca1f5/uncalled_for-0.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/76/86d9a3589c725ce825d2ed3e7cb3ecf7f956d3fd015353d52197bb341bcd/xai_sdk-1.11.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.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/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.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/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.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/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.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"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-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/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl"}}]} diff --git a/scripts/populate_tox/releases.jsonl b/scripts/populate_tox/releases.jsonl index 47921bef28..0be1fca990 100644 --- a/scripts/populate_tox/releases.jsonl +++ b/scripts/populate_tox/releases.jsonl @@ -238,3 +238,10 @@ {"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"}]} {"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 :: 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.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-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.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "sdist", "filename": "uuid_utils-0.14.1.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 :: 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 :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "zope.interface", "requires_python": ">=3.10", "version": "8.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-win_amd64.whl"}, {"packagetype": "sdist", "filename": "zope_interface-8.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.27.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "mcp-1.27.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "mcp-1.27.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": "3.2.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastmcp-3.2.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastmcp-3.2.3.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", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.10", "version": "0.13.6", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai_agents-0.13.6-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai_agents-0.13.6.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.26.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.26.0-py3-none-any.whl"}]} +{"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.53.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.53.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pydantic_ai-1.53.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", "Programming Language :: Python :: 3.14", "Topic :: Internet", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "pydantic-ai", "requires_python": ">=3.10", "version": "1.80.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.80.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pydantic_ai-1.80.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.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "langchain", "requires_python": "<4.0.0,>=3.10.0", "version": "1.2.15", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "langchain-1.2.15-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "langchain-1.2.15.tar.gz"}]} diff --git a/scripts/split_tox_gh_actions/split_tox_gh_actions.py b/scripts/split_tox_gh_actions/split_tox_gh_actions.py index 122a02065d..bc5700e5bf 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -46,6 +46,10 @@ "asyncpg", } +FRAMEWORKS_NEEDING_MYSQL = { + "aiomysql", +} + FRAMEWORKS_NEEDING_REDIS = { "celery", } @@ -99,6 +103,7 @@ "gcp", ], "DBs": [ + "aiomysql", "asyncpg", "clickhouse_driver", "pymongo", @@ -324,6 +329,7 @@ def render_template(group, frameworks, py_versions): "frameworks": frameworks, "needs_clickhouse": bool(set(frameworks) & FRAMEWORKS_NEEDING_CLICKHOUSE), "needs_docker": bool(set(frameworks) & FRAMEWORKS_NEEDING_DOCKER), + "needs_mysql": bool(set(frameworks) & FRAMEWORKS_NEEDING_MYSQL), "needs_postgres": bool(set(frameworks) & FRAMEWORKS_NEEDING_POSTGRES), "needs_redis": bool(set(frameworks) & FRAMEWORKS_NEEDING_REDIS), "needs_java": bool(set(frameworks) & FRAMEWORKS_NEEDING_JAVA), diff --git a/scripts/split_tox_gh_actions/templates/test_group.jinja b/scripts/split_tox_gh_actions/templates/test_group.jinja index fa6d31c55f..331123dcb7 100644 --- a/scripts/split_tox_gh_actions/templates/test_group.jinja +++ b/scripts/split_tox_gh_actions/templates/test_group.jinja @@ -38,6 +38,27 @@ SENTRY_PYTHON_TEST_POSTGRES_USER: postgres SENTRY_PYTHON_TEST_POSTGRES_PASSWORD: sentry + {% endif %} + {% if needs_mysql %} + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + ports: + - 3306:3306 + env: + SENTRY_PYTHON_TEST_MYSQL_HOST: {% raw %}${{ matrix.python-version == '3.6' && 'mysql' || 'localhost' }}{% endraw %} + SENTRY_PYTHON_TEST_MYSQL_USER: root + SENTRY_PYTHON_TEST_MYSQL_PASSWORD: root + SENTRY_PYTHON_TEST_MYSQL_DB: test + {% endif %} # Use Docker container only for Python 3.6 {% raw %}container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }}{% endraw %} diff --git a/sentry_sdk/integrations/aiomysql.py b/sentry_sdk/integrations/aiomysql.py index 3c2920d650..4e1bcdb491 100644 --- a/sentry_sdk/integrations/aiomysql.py +++ b/sentry_sdk/integrations/aiomysql.py @@ -1,154 +1,205 @@ # -*- coding: utf-8 -*- -""" -Adapted from module sentry_sdk.integrations.asyncpg -""" from __future__ import annotations -import contextlib -from typing import Any, TypeVar, Callable, Awaitable, Iterator + +from typing import Any, TypeVar, Callable, Awaitable import sentry_sdk from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable -from sentry_sdk.tracing import Span from sentry_sdk.tracing_utils import add_query_source, record_sql_queries from sentry_sdk.utils import ( - ensure_integration_enabled, - parse_version, - capture_internal_exceptions, + capture_internal_exceptions, + parse_version, ) try: - import aiomysql # type: ignore[import-not-found] - from aiomysql.connection import Connection, Cursor # type: ignore + import aiomysql # type: ignore[import-untyped] + from aiomysql.cursors import Cursor # type: ignore[import-untyped] except ImportError: - raise DidNotEnable("aiomysql not installed.") + raise DidNotEnable("aiomysql not installed.") class AioMySQLIntegration(Integration): - identifier = "aiomysql" - origin = f"auto.db.{identifier}" - _record_params = False - - def __init__(self, *, record_params: bool = False): - AioMySQLIntegration._record_params = record_params + identifier = "aiomysql" + origin = f"auto.db.{identifier}" + _record_params = False - @staticmethod - def setup_once() -> None: - aiomysql_version = parse_version(aiomysql.__version__) - _check_minimum_version(AioMySQLIntegration, aiomysql_version) + def __init__(self, *, record_params: bool = False): + AioMySQLIntegration._record_params = record_params - aiomysql.Connection.query = _wrap_execute( - aiomysql.Connection.query, - ) + @staticmethod + def setup_once() -> None: + aiomysql_version = parse_version(aiomysql.__version__) + _check_minimum_version(AioMySQLIntegration, aiomysql_version) - aiomysql.connect = _wrap_connect(aiomysql.connect) + Cursor.execute = _wrap_execute(Cursor.execute) + Cursor.executemany = _wrap_executemany(Cursor.executemany) + aiomysql.connect = _wrap_connect(aiomysql.connect) T = TypeVar("T") +def _normalize_query(query: str | bytes | bytearray) -> str: + if isinstance(query, (bytes, bytearray)): + query = query.decode("utf-8", errors="replace") + return " ".join(query.split()) + + def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - async def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return await f(*args, **kwargs) - - conn = args[0] - query = args[1] # В aiomysql запрос передается первым аргументом - with record_sql_queries( - cursor=None, - query=query, - params_list=None, - paramstyle=None, - executemany=False, - span_origin=AioMySQLIntegration.origin, - ) as span: - res = await f(*args, **kwargs) - span.set_data("db.affected_rows", res) - - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -SubCursor = TypeVar("SubCursor", bound=Cursor) - - -@contextlib.contextmanager -def _record( - cursor: SubCursor | None, - query: str, - params_list: tuple[Any, ...] | None, - *, - executemany: bool = False, -) -> Iterator[Span]: - integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) - if integration is not None and not integration._record_params: - params_list = None - - param_style = "pyformat" if params_list else None - - with record_sql_queries( - cursor=cursor, - query=query, - params_list=params_list, - paramstyle=param_style, - executemany=executemany, - record_cursor_repr=cursor is not None, - span_origin=AioMySQLIntegration.origin, - ) as span: - yield span - - -def _wrap_connect(f: Callable[..., T]) -> Callable[..., T]: - def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return f(*args, **kwargs) - - host = kwargs.get("host", "localhost") - port = kwargs.get("port") or 3306 - user = kwargs.get("user") - db = kwargs.get("db") - - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=AioMySQLIntegration.origin, - ) as span: - span.set_data(SPANDATA.DB_SYSTEM, "mysql") - span.set_data(SPANDATA.SERVER_ADDRESS, host) - span.set_data(SPANDATA.SERVER_PORT, port) - span.set_data(SPANDATA.DB_NAME, db) - span.set_data(SPANDATA.DB_USER, user) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=span._data + """Wrap Cursor.execute to capture SQL queries.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + + # Skip if flagged by executemany (avoids double-recording) + if getattr(cursor, "_sentry_skip_next_execute", False): + cursor._sentry_skip_next_execute = False + return await f(*args, **kwargs) + + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + params = args[2] if len(args) > 2 else kwargs.get("args") + + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration( + AioMySQLIntegration ) - res = f(*args, **kwargs) + params_list = params if integration and integration._record_params else None + param_style = "pyformat" if params_list else None + + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=False, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) - return res + with capture_internal_exceptions(): + add_query_source(span) - return _inner + return res + return _inner -def _set_db_data(span: Span, conn: Any) -> None: - span.set_data(SPANDATA.DB_SYSTEM, "mysql") - host = conn.host - if host: - span.set_data(SPANDATA.SERVER_ADDRESS, host) +def _wrap_executemany( + f: Callable[..., Awaitable[T]] +) -> Callable[..., Awaitable[T]]: + """Wrap Cursor.executemany to capture SQL queries.""" - port = conn.port - if port: - span.set_data(SPANDATA.SERVER_PORT, port) + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) - database = conn.db - if database: - span.set_data(SPANDATA.DB_NAME, database) + cursor = args[0] + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + seq_of_params = args[2] if len(args) > 2 else kwargs.get("args") - user = conn.user - if user: - span.set_data(SPANDATA.DB_USER, user) + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration( + AioMySQLIntegration + ) + params_list = ( + seq_of_params if integration and integration._record_params else None + ) + param_style = "pyformat" if params_list else None + + # Prevent double-recording: _do_execute_many calls self.execute internally + cursor._sentry_skip_next_execute = True + try: + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=True, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) + + with capture_internal_exceptions(): + add_query_source(span) + + return res + finally: + cursor._sentry_skip_next_execute = False + + return _inner + + +def _get_connection(cursor: Any) -> Any: + """Get the underlying connection from a cursor.""" + return getattr(cursor, "_connection", None) + + +def _wrap_connect( + f: Callable[..., Awaitable[T]] +) -> Callable[..., Awaitable[T]]: + """Wrap aiomysql.connect to capture connection spans.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + host = kwargs.get("host", "localhost") + port = kwargs.get("port") or 3306 + user = kwargs.get("user") + db = kwargs.get("db") or kwargs.get("database") + + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=AioMySQLIntegration.origin, + ) as span: + span.set_data(SPANDATA.DB_SYSTEM, "mysql") + span.set_data(SPANDATA.SERVER_ADDRESS, host) + span.set_data(SPANDATA.SERVER_PORT, port) + span.set_data(SPANDATA.DB_NAME, db) + span.set_data(SPANDATA.DB_USER, user) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", + category="query", + data=span._data, + ) + res = await f(*args, **kwargs) + + return res + + return _inner + + +def _set_db_data(span: Any, conn: Any) -> None: + """Set database-related span data from connection object.""" + span.set_data(SPANDATA.DB_SYSTEM, "mysql") + + host = getattr(conn, "host", None) + if host: + span.set_data(SPANDATA.SERVER_ADDRESS, host) + + port = getattr(conn, "port", None) + if port: + span.set_data(SPANDATA.SERVER_PORT, port) + + database = getattr(conn, "db", None) + if database: + span.set_data(SPANDATA.DB_NAME, database) + + user = getattr(conn, "user", None) + if user: + span.set_data(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aiomysql/__init__.py b/tests/integrations/aiomysql/__init__.py new file mode 100644 index 0000000000..d927c2ddea --- /dev/null +++ b/tests/integrations/aiomysql/__init__.py @@ -0,0 +1,4 @@ +import pytest + +pytest.importorskip("aiomysql") +pytest.importorskip("pytest_asyncio") diff --git a/tests/integrations/aiomysql/test_aiomysql.py b/tests/integrations/aiomysql/test_aiomysql.py new file mode 100644 index 0000000000..c221a725a4 --- /dev/null +++ b/tests/integrations/aiomysql/test_aiomysql.py @@ -0,0 +1,742 @@ +""" +Tests need pytest-asyncio installed. + +Tests need a local MySQL instance running. This can be done using: +```sh +docker run --rm --name some-mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=test -d -p 3306:3306 mysql:8.0 +``` + +The tests use the following credentials to establish a database connection. +""" + +import os +import datetime +from contextlib import contextmanager +from unittest import mock + +import aiomysql +import pytest +import pytest_asyncio + +from sentry_sdk import capture_message, start_transaction +from sentry_sdk.integrations.aiomysql import AioMySQLIntegration +from sentry_sdk.consts import SPANDATA +from sentry_sdk.tracing_utils import record_sql_queries +from tests.conftest import ApproxDict + +MYSQL_HOST = os.getenv("SENTRY_PYTHON_TEST_MYSQL_HOST", "localhost") +MYSQL_PORT = int(os.getenv("SENTRY_PYTHON_TEST_MYSQL_PORT", "3306")) +MYSQL_USER = os.getenv("SENTRY_PYTHON_TEST_MYSQL_USER", "root") +MYSQL_PASSWORD = os.getenv("SENTRY_PYTHON_TEST_MYSQL_PASSWORD", "root") +MYSQL_DB_BASE = os.getenv("SENTRY_PYTHON_TEST_MYSQL_DB", "test") + + +def _get_db_name(): + pid = os.getpid() + return f"{MYSQL_DB_BASE}_{pid}" + + +MYSQL_DB = _get_db_name() + +CRUMBS_CONNECT = { + "category": "query", + "data": ApproxDict( + { + "db.name": MYSQL_DB, + "db.system": "mysql", + "db.user": MYSQL_USER, + "server.address": MYSQL_HOST, + "server.port": MYSQL_PORT, + } + ), + "message": "connect", + "type": "default", +} + + +@pytest_asyncio.fixture(autouse=True) +async def _clean_mysql(): + conn = await aiomysql.connect( + host=MYSQL_HOST, + port=MYSQL_PORT, + user=MYSQL_USER, + password=MYSQL_PASSWORD, + autocommit=True, + ) + try: + async with conn.cursor() as cur: + await cur.execute(f"CREATE DATABASE IF NOT EXISTS `{MYSQL_DB}`") + await cur.execute(f"USE `{MYSQL_DB}`") + await cur.execute("DROP TABLE IF EXISTS users") + await cur.execute( + """ + CREATE TABLE users( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255), + password VARCHAR(255), + dob DATE + ) + """ + ) + finally: + conn.close() + + +def _connect_args(): + return { + "host": MYSQL_HOST, + "port": MYSQL_PORT, + "user": MYSQL_USER, + "password": MYSQL_PASSWORD, + "db": MYSQL_DB, + "autocommit": True, + } + + +@pytest.mark.asyncio +async def test_connect(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + assert event["breadcrumbs"]["values"] == [CRUMBS_CONNECT] + + +@pytest.mark.asyncio +async def test_execute(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'pw', '1990-12-25')", + ) + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + ("Bob", "secret_pw", datetime.date(1984, 3, 1)), + ) + await cur.execute("SELECT * FROM users WHERE name = %s", ("Bob",)) + row = await cur.fetchone() + assert row[1] == "Bob" + + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + assert event["breadcrumbs"]["values"] == [ + CRUMBS_CONNECT, + { + "category": "query", + "data": {}, + "message": "INSERT INTO users(name, password, dob) VALUES ('Alice', 'pw', '1990-12-25')", + "type": "default", + }, + { + "category": "query", + "data": {}, + "message": "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + "type": "default", + }, + { + "category": "query", + "data": {}, + "message": "SELECT * FROM users WHERE name = %s", + "type": "default", + }, + ] + + +@pytest.mark.asyncio +async def test_execute_many(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.executemany( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + [ + ("Bob", "secret_pw", datetime.date(1984, 3, 1)), + ("Alice", "pw", datetime.date(1990, 12, 25)), + ], + ) + + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + assert event["breadcrumbs"]["values"] == [ + CRUMBS_CONNECT, + { + "category": "query", + "data": {"db.executemany": True}, + "message": "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + "type": "default", + }, + ] + + +@pytest.mark.asyncio +async def test_record_params(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration(record_params=True)], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + ("Bob", "secret_pw", datetime.date(1984, 3, 1)), + ) + + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + assert event["breadcrumbs"]["values"] == [ + CRUMBS_CONNECT, + { + "category": "query", + "data": { + "db.params": ["Bob", "secret_pw", "datetime.date(1984, 3, 1)"], + "db.paramstyle": "format", + }, + "message": "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + "type": "default", + }, + ] + + +@pytest.mark.asyncio +async def test_cursor_context_manager(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.executemany( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + [ + ("Bob", "secret_pw", datetime.date(1984, 3, 1)), + ("Alice", "pw", datetime.date(1990, 12, 25)), + ], + ) + await cur.execute( + "SELECT * FROM users WHERE dob > %s", + (datetime.date(1970, 1, 1),), + ) + rows = await cur.fetchall() + assert len(rows) == 2 + + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + crumbs = event["breadcrumbs"]["values"] + assert crumbs[0] == CRUMBS_CONNECT + assert crumbs[1]["category"] == "query" + assert "INSERT" in crumbs[1]["message"] + assert crumbs[2]["category"] == "query" + assert "SELECT" in crumbs[2]["message"] + + +@pytest.mark.asyncio +async def test_cursor_async_iteration(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + ("Charlie", "pw3", datetime.date(1995, 5, 15)), + ) + await cur.execute("SELECT * FROM users WHERE name = %s", ("Charlie",)) + async for row in cur: + assert row[1] == "Charlie" + + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + crumbs = event["breadcrumbs"]["values"] + assert crumbs[0] == CRUMBS_CONNECT + assert len([c for c in crumbs if c["category"] == "query"]) >= 2 + + +@pytest.mark.asyncio +async def test_connection_pool(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + pool_size = 2 + + pool = await aiomysql.create_pool( + host=MYSQL_HOST, + port=MYSQL_PORT, + user=MYSQL_USER, + password=MYSQL_PASSWORD, + db=MYSQL_DB, + autocommit=True, + minsize=pool_size, + maxsize=pool_size, + ) + + async with pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + ("Dave", "pw4", datetime.date(1988, 7, 20)), + ) + await cur.execute("SELECT * FROM users WHERE name = %s", ("Dave",)) + row = await cur.fetchone() + assert row is not None + assert row[1] == "Dave" + + pool.close() + await pool.wait_closed() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + crumbs = event["breadcrumbs"]["values"] + # Verify queries were captured + query_crumbs = [c for c in crumbs if c["category"] == "query"] + assert len(query_crumbs) >= 2 # INSERT + SELECT + + +@pytest.mark.asyncio +async def test_query_source_disabled(sentry_init, capture_events): + sentry_options = { + "integrations": [AioMySQLIntegration()], + "enable_tracing": True, + "enable_db_query_source": False, + "db_query_source_threshold_ms": 0, + } + + sentry_init(**sentry_options) + + events = capture_events() + + with start_transaction(name="test_transaction", sampled=True): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')", + ) + + conn.close() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("INSERT INTO") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data + + +@pytest.mark.asyncio +@pytest.mark.parametrize("enable_db_query_source", [None, True]) +async def test_query_source_enabled( + sentry_init, capture_events, enable_db_query_source +): + sentry_options = { + "integrations": [AioMySQLIntegration()], + "enable_tracing": True, + "db_query_source_threshold_ms": 0, + } + if enable_db_query_source is not None: + sentry_options["enable_db_query_source"] = enable_db_query_source + + sentry_init(**sentry_options) + + events = capture_events() + + with start_transaction(name="test_transaction", sampled=True): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')", + ) + + conn.close() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("INSERT INTO") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + +@pytest.mark.asyncio +async def test_query_source(sentry_init, capture_events): + sentry_init( + integrations=[AioMySQLIntegration()], + enable_tracing=True, + enable_db_query_source=True, + db_query_source_threshold_ms=0, + ) + + events = capture_events() + + with start_transaction(name="test_transaction", sampled=True): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')", + ) + + conn.close() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("INSERT INTO") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert ( + data.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.aiomysql.test_aiomysql" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/aiomysql/test_aiomysql.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "test_query_source" + + +@pytest.mark.asyncio +async def test_no_query_source_if_duration_too_short(sentry_init, capture_events): + sentry_init( + integrations=[AioMySQLIntegration()], + enable_tracing=True, + enable_db_query_source=True, + db_query_source_threshold_ms=100, + ) + + events = capture_events() + + with start_transaction(name="test_transaction", sampled=True): + conn = await aiomysql.connect(**_connect_args()) + + @contextmanager + def fake_record_sql_queries(*args, **kwargs): + with record_sql_queries(*args, **kwargs) as span: + pass + span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span.timestamp = datetime.datetime(2024, 1, 1, microsecond=99999) + yield span + + async with conn.cursor() as cur: + with mock.patch( + "sentry_sdk.integrations.aiomysql.record_sql_queries", + fake_record_sql_queries, + ): + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')", + ) + + conn.close() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("INSERT INTO") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data + + +@pytest.mark.asyncio +async def test_query_source_if_duration_over_threshold(sentry_init, capture_events): + sentry_init( + integrations=[AioMySQLIntegration()], + enable_tracing=True, + enable_db_query_source=True, + db_query_source_threshold_ms=100, + ) + + events = capture_events() + + with start_transaction(name="test_transaction", sampled=True): + conn = await aiomysql.connect(**_connect_args()) + + @contextmanager + def fake_record_sql_queries(*args, **kwargs): + with record_sql_queries(*args, **kwargs) as span: + pass + span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span.timestamp = datetime.datetime(2024, 1, 1, microsecond=100001) + yield span + + async with conn.cursor() as cur: + with mock.patch( + "sentry_sdk.integrations.aiomysql.record_sql_queries", + fake_record_sql_queries, + ): + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')", + ) + + conn.close() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("INSERT INTO") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert ( + data.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.aiomysql.test_aiomysql" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/aiomysql/test_aiomysql.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert ( + data.get(SPANDATA.CODE_FUNCTION) + == "test_query_source_if_duration_over_threshold" + ) + + +@pytest.mark.asyncio +async def test_span_origin(sentry_init, capture_events): + sentry_init( + integrations=[AioMySQLIntegration()], + traces_sample_rate=1.0, + ) + + events = capture_events() + + with start_transaction(name="test_transaction"): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute("SELECT 1") + await cur.execute("SELECT 2") + + conn.close() + + (event,) = events + + assert event["contexts"]["trace"]["origin"] == "manual" + + for span in event["spans"]: + assert span["origin"] == "auto.db.aiomysql" + + +@pytest.mark.asyncio +async def test_multiline_query_description_normalized(sentry_init, capture_events): + sentry_init( + integrations=[AioMySQLIntegration()], + traces_sample_rate=1.0, + ) + events = capture_events() + + with start_transaction(name="test_transaction"): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + """ + SELECT + id, + name + FROM + users + WHERE + name = 'Alice' + """ + ) + + conn.close() + + (event,) = events + + spans = [ + s + for s in event["spans"] + if s["op"] == "db" and "SELECT" in s.get("description", "") + ] + assert len(spans) == 1 + assert spans[0]["description"] == "SELECT id, name FROM users WHERE name = 'Alice'" + + +@pytest.mark.asyncio +async def test_before_send_transaction_sees_normalized_description( + sentry_init, capture_events +): + def before_send_transaction(event, hint): + for span in event.get("spans", []): + desc = span.get("description", "") + if "SELECT id, name FROM users" in desc: + span["description"] = "filtered" + return event + + sentry_init( + integrations=[AioMySQLIntegration()], + traces_sample_rate=1.0, + before_send_transaction=before_send_transaction, + ) + events = capture_events() + + with start_transaction(name="test_transaction"): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + """ + SELECT + id, + name + FROM + users + """ + ) + + conn.close() + + (event,) = events + spans = [ + s + for s in event["spans"] + if s["op"] == "db" and "filtered" in s.get("description", "") + ] + + assert len(spans) == 1 + assert spans[0]["description"] == "filtered" + + +@pytest.mark.asyncio +async def test_db_data_on_spans(sentry_init, capture_events): + """Test that database connection data is properly set on spans.""" + sentry_init( + integrations=[AioMySQLIntegration()], + traces_sample_rate=1.0, + ) + events = capture_events() + + with start_transaction(name="test_transaction"): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute("SELECT 1") + + conn.close() + + (event,) = events + + db_spans = [s for s in event["spans"] if s["op"] == "db"] + assert len(db_spans) > 0 + + query_span = [s for s in db_spans if "SELECT" in s.get("description", "")][0] + assert query_span["data"].get(SPANDATA.DB_SYSTEM) == "mysql" + assert query_span["data"].get(SPANDATA.SERVER_ADDRESS) == MYSQL_HOST + assert query_span["data"].get(SPANDATA.SERVER_PORT) == MYSQL_PORT + assert query_span["data"].get(SPANDATA.DB_NAME) == MYSQL_DB + assert query_span["data"].get(SPANDATA.DB_USER) == MYSQL_USER diff --git a/tox.ini b/tox.ini index 956ff86b8b..fce9dee56c 100644 --- a/tox.ini +++ b/tox.ini @@ -160,6 +160,11 @@ envlist = {py3.9,py3.13,py3.14,py3.14t}-asyncpg-v0.31.0 {py3.9,py3.13,py3.14,py3.14t}-asyncpg-latest + {py3.7,py3.8,py3.9}-aiomysql-v0.1.1 + {py3.7,py3.9,py3.10}-aiomysql-v0.2.0 + {py3.9,py3.13,py3.14,py3.14t}-aiomysql-v0.3.2 + {py3.9,py3.13,py3.14,py3.14t}-aiomysql-latest + {py3.9,py3.13,py3.14}-clickhouse_driver-v0.2.10 {py3.9,py3.13,py3.14}-clickhouse_driver-latest @@ -589,6 +594,12 @@ deps = asyncpg-latest: asyncpg==0.31.0 asyncpg: pytest-asyncio + aiomysql-v0.1.1: aiomysql==0.1.1 + aiomysql-v0.2.0: aiomysql==0.2.0 + aiomysql-v0.3.2: aiomysql==0.3.2 + aiomysql-latest: aiomysql==0.3.2 + aiomysql: pytest-asyncio + clickhouse_driver-v0.2.10: clickhouse-driver==0.2.10 clickhouse_driver-latest: clickhouse-driver==0.2.10 @@ -973,6 +984,7 @@ setenv = ariadne: _TESTPATH=tests/integrations/ariadne arq: _TESTPATH=tests/integrations/arq asyncpg: _TESTPATH=tests/integrations/asyncpg + aiomysql: _TESTPATH=tests/integrations/aiomysql beam: _TESTPATH=tests/integrations/beam boto3: _TESTPATH=tests/integrations/boto3 bottle: _TESTPATH=tests/integrations/bottle From ad11c32ee4672b25b9c98b84f829e111d9c53c3b Mon Sep 17 00:00:00 2001 From: "Alexandr N Zamaraev (aka tonal)" Date: Mon, 13 Apr 2026 11:25:22 +0700 Subject: [PATCH 5/7] fix(aiomysql): Address PR review comments - Add AioMySQLIntegration to _AUTO_ENABLING_INTEGRATIONS and _MIN_VERSIONS (fixes sentry bot: auto-enabling and minimum version check) - Protect _sentry_skip_next_execute flag with try/finally in _wrap_executemany and do NOT reset it in _wrap_execute (fixes cursor bot: flag resets prematurely on non-INSERT row-by-row fallback) - Normalize query text using " ".join(query.split()) instead of re.sub for performance - Fix duplicate YAML keys bug in test_group.jinja template that caused postgres service to be silently dropped when mysql service is added - Add test for non-INSERT executemany (row-by-row fallback) Co-Authored-By: Qwen Code Co-authored-by: Qwen-Coder --- .github/workflows/test-integrations-dbs.yml | 22 +- .github/workflows/test-integrations-web-1.yml | 2 - scripts/populate_tox/config.py | 7 + .../populate_tox/package_dependencies.jsonl | 2 + scripts/populate_tox/releases.jsonl | 7 + .../split_tox_gh_actions.py | 6 + .../templates/test_group.jinja | 40 +- sentry_sdk/integrations/__init__.py | 2 + sentry_sdk/integrations/aiomysql.py | 296 ++++--- tests/integrations/aiomysql/__init__.py | 4 + tests/integrations/aiomysql/test_aiomysql.py | 790 ++++++++++++++++++ tox.ini | 12 + 12 files changed, 1056 insertions(+), 134 deletions(-) create mode 100644 tests/integrations/aiomysql/__init__.py create mode 100644 tests/integrations/aiomysql/test_aiomysql.py diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index 380ded2f80..b56427d568 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -43,19 +43,33 @@ jobs: image: postgres env: POSTGRES_PASSWORD: sentry - # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - # Maps tcp port 5432 on service container to the host ports: - 5432:5432 + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + ports: + - 3306:3306 env: SENTRY_PYTHON_TEST_POSTGRES_HOST: ${{ matrix.python-version == '3.6' && 'postgres' || 'localhost' }} SENTRY_PYTHON_TEST_POSTGRES_USER: postgres SENTRY_PYTHON_TEST_POSTGRES_PASSWORD: sentry + SENTRY_PYTHON_TEST_MYSQL_HOST: ${{ matrix.python-version == '3.6' && 'mysql' || 'localhost' }} + SENTRY_PYTHON_TEST_MYSQL_USER: root + SENTRY_PYTHON_TEST_MYSQL_PASSWORD: root + SENTRY_PYTHON_TEST_MYSQL_DB: test # Use Docker container only for Python 3.6 container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: @@ -73,6 +87,10 @@ jobs: - name: Erase coverage run: | coverage erase + - name: Test aiomysql + run: | + set -x # print commands that are executed + ./scripts/runtox.sh "py${{ matrix.python-version }}-aiomysql" - name: Test asyncpg run: | set -x # print commands that are executed diff --git a/.github/workflows/test-integrations-web-1.yml b/.github/workflows/test-integrations-web-1.yml index 48797f5dcf..76d77255fa 100644 --- a/.github/workflows/test-integrations-web-1.yml +++ b/.github/workflows/test-integrations-web-1.yml @@ -43,13 +43,11 @@ jobs: image: postgres env: POSTGRES_PASSWORD: sentry - # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - # Maps tcp port 5432 on service container to the host ports: - 5432:5432 env: diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index ba2f4dc6d5..aed00456a7 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -47,6 +47,13 @@ }, "python": ">=3.7", }, + "aiomysql": { + "package": "aiomysql", + "deps": { + "*": ["pytest-asyncio"], + }, + "python": ">=3.7", + }, "beam": { "package": "apache-beam", "python": ">=3.7", diff --git a/scripts/populate_tox/package_dependencies.jsonl b/scripts/populate_tox/package_dependencies.jsonl index a3a5cd4a99..563db0d817 100644 --- a/scripts/populate_tox/package_dependencies.jsonl +++ b/scripts/populate_tox/package_dependencies.jsonl @@ -40,3 +40,5 @@ {"name": "statsig", "version": "0.71.6", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/b7/b1/aba1475b024e99883b5b43405e5713520129ce42004bc39a3fb40312724b/statsig-0.71.6-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/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.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/38/14/52b6613fdda4078c62eb5b4fe3efc724ddc55a4ad524c93de51830107aa3/ijson-3.5.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/d6/b7/48a7f1ab9eee62f1113114207df7e7e6bc29227389d554f42cc11bc98108/ip3country-0.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a4/e7/14dc9366696dcb53a413449881743426ed289d687bcf3d5aee4726c32ebb/protobuf-7.34.0-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/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-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/3e/6f/73a4d37deefb159556d39d654b5bad67b6874d1ad0b20b96fb5a04de3949/ua_parser_builtins-202603-py3-none-any.whl"}}]} {"name": "strawberry-graphql", "version": "0.312.2", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/69/6d/f22f1cc6c2faf4307c96f8f2a58a031ebf42a8ee0f9fa046bf6304f3dc0e/strawberry_graphql-0.312.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/86/41/cb887d9afc5dabd78feefe6ccbaf83ff423c206a7a1b7aeeac05120b2125/graphql_core-3.2.8-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.24.1", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-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/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-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"}}]} +{"name": "openai-agents", "version": "0.13.6", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/1c/83/a991b2ad389abadabf13f6c4228bd88ac8dc363e4b50fcae8c5ea966bd41/openai_agents-0.13.6-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.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/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.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/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-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/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-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/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.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/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-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/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-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/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-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/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.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": "pydantic-ai", "version": "1.80.0", "dependencies": [{"download_info": {"url": "https://files.pythonhosted.org/packages/2c/9a/6d65ea560f8e84f57cf5cf7f5b611d48bd5d1869120bc7fcc3b40aed0556/pydantic_ai-1.80.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/23/bf/ef273265ef3530cf432fd6d0014ceed57d1cc5b1550fd975bc91152633c8/pydantic_ai_slim-1.80.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ee/28/8480d2e53728a891e1157f6c741a7f2ec27da06b56936d5c6e1f64af0e51/pydantic_evals-1.80.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/38/12/483b7402d302021ff8537a746eebf018f4e0bb5892c7bef769ab968e03c1/pydantic_graph-1.80.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/8c/c7a33f3efaa8d6a5bc40e012e5ecc2d72c2e6124550ca9085fe0ceed9993/huggingface_hub-1.10.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_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/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.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/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/e4/a0/a73398d30bb0f9ad70cd70426151a4a19527a7296e48a3a16a50e1d5db05/ag_ui_protocol-0.1.15-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/0c/ac/7185750e8688f6ff79b0e3d6a61372c88b81ba81fcda8798c70598e18aca/anthropic-0.94.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.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/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-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/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0a/2b/8bfddb39a19f5fbc16a869f1a394771e6223f07160dbc0ff6b38e05ea0ae/boto3-1.42.88-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2a/46/ad14e41245adb8b0c83663ba13e822b68a0df08999dd250e75b0750fdf6c/botocore-1.42.88-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/9e/b4/00c2f9f8387a2e77faf8410210466c46d55dd30a0388de41c54441b148fb/cohere-6.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a7/1c/6dfd082a205be4510543221b734b1191299e6a1810c452b6bc76dfa6968e/fastavro-1.12.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/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f5/48/84b6dcba793178a44b9d99b4def6cd62f870dcfc5bb7b9153ac390135812/fastmcp-3.2.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-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/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b4/bd/05055d8360cef0757d79367157f3b15c0a0715e81e08f86a04018ec045f0/cyclopts-4.10.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a3/f6/8ef7e4c286deb2709d11ca96a5237caae3ef4876ab3c48095856cfd2df30/genai_prices-0.0.56-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-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/9f/3d/9f70246114cdf56a2615a40428ced08bc844f5a26247fe812b2f0dd4eaca/google_genai-1.72.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/34/b0/83e3892a4597a4b8ebf8a662aeaf314765c4c2340516eb1d049b459b24fc/groq-1.1.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/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-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/04/d5/4e96c44f6c1ea3d812cf5391d81a4f5abaa540abf8d04ecd7f66e0ed11df/jsonschema_path-0.4.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-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/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/02/33/81b13e1f2044b5fe0112068a2494526db9cfdf784030a2ea57688279360a/logfire-4.32.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/58/56/fe8d8aa60e796e059992fb7359ec5dda4ef72db4fccfbd362a2ee0595ec1/logfire_api-4.32.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/83/e0/9f246d18f8fe6e815f7cae88c437c986b342d3ff568607c8e0d53f4710d0/mistralai-2.3.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/28/50/1a313fb700526b134c71eb8a225d8b83be0385dbb0204337b4379c698cef/jsonpath_python-1.1.5-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/43/59/b98e84eebf745ffc75397eaad4763795bff8a30cbf2373a50ed4e70646c5/opentelemetry_instrumentation_httpx-0.60b1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-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/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/99/5f/86b1630be61bdebf253c2f953a6c3f073ec21bb0725565ea3896802e1ca3/pydantic_handlebars-0.1.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-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/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-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/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/de/9c/3782bab0bf11a40b550147c19a5d1a476c17405391751982408902d9f138/temporalio-1.25.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/11/52/6327a5f4fda01207205038a106a99848a41c83e933cd23ea2cab3d2ebc6c/nexus_rpc-1.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/2e/e8/1fd38926f9cf031188fbc5a96694203ea6f24b0e34bd64a225ec6f6291ba/types_protobuf-6.32.1.20260221-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-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/11/e1/7ec67882ad8fc9f86384bef6421fa252c9cbe5744f8df6ce77afc9eca1f5/uncalled_for-0.3.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/04/76/86d9a3589c725ce825d2ed3e7cb3ecf7f956d3fd015353d52197bb341bcd/xai_sdk-1.11.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.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/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.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/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.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/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.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"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-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/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl"}}, {"download_info": {"url": "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl"}}]} diff --git a/scripts/populate_tox/releases.jsonl b/scripts/populate_tox/releases.jsonl index 47921bef28..0be1fca990 100644 --- a/scripts/populate_tox/releases.jsonl +++ b/scripts/populate_tox/releases.jsonl @@ -238,3 +238,10 @@ {"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"}]} {"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 :: 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.1", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-win32.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-cp39-abi3-win_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-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.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl"}, {"packagetype": "sdist", "filename": "uuid_utils-0.14.1.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 :: 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 :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "zope.interface", "requires_python": ">=3.10", "version": "8.2", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp310-cp310-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp311-cp311-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp312-cp312-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp313-cp313-win_amd64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-macosx_10_9_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-macosx_11_0_arm64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl"}, {"packagetype": "bdist_wheel", "filename": "zope_interface-8.2-cp314-cp314-win_amd64.whl"}, {"packagetype": "sdist", "filename": "zope_interface-8.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.27.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "mcp-1.27.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "mcp-1.27.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": "3.2.3", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "fastmcp-3.2.3-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "fastmcp-3.2.3.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", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.10", "version": "0.13.6", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "openai_agents-0.13.6-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "openai_agents-0.13.6.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.26.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.26.0-py3-none-any.whl"}]} +{"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.53.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.53.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pydantic_ai-1.53.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", "Programming Language :: Python :: 3.14", "Topic :: Internet", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "pydantic-ai", "requires_python": ">=3.10", "version": "1.80.0", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "pydantic_ai-1.80.0-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "pydantic_ai-1.80.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.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "langchain", "requires_python": "<4.0.0,>=3.10.0", "version": "1.2.15", "yanked": false}, "urls": [{"packagetype": "bdist_wheel", "filename": "langchain-1.2.15-py3-none-any.whl"}, {"packagetype": "sdist", "filename": "langchain-1.2.15.tar.gz"}]} diff --git a/scripts/split_tox_gh_actions/split_tox_gh_actions.py b/scripts/split_tox_gh_actions/split_tox_gh_actions.py index 122a02065d..bc5700e5bf 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -46,6 +46,10 @@ "asyncpg", } +FRAMEWORKS_NEEDING_MYSQL = { + "aiomysql", +} + FRAMEWORKS_NEEDING_REDIS = { "celery", } @@ -99,6 +103,7 @@ "gcp", ], "DBs": [ + "aiomysql", "asyncpg", "clickhouse_driver", "pymongo", @@ -324,6 +329,7 @@ def render_template(group, frameworks, py_versions): "frameworks": frameworks, "needs_clickhouse": bool(set(frameworks) & FRAMEWORKS_NEEDING_CLICKHOUSE), "needs_docker": bool(set(frameworks) & FRAMEWORKS_NEEDING_DOCKER), + "needs_mysql": bool(set(frameworks) & FRAMEWORKS_NEEDING_MYSQL), "needs_postgres": bool(set(frameworks) & FRAMEWORKS_NEEDING_POSTGRES), "needs_redis": bool(set(frameworks) & FRAMEWORKS_NEEDING_REDIS), "needs_java": bool(set(frameworks) & FRAMEWORKS_NEEDING_JAVA), diff --git a/scripts/split_tox_gh_actions/templates/test_group.jinja b/scripts/split_tox_gh_actions/templates/test_group.jinja index fa6d31c55f..5a7ceb80f6 100644 --- a/scripts/split_tox_gh_actions/templates/test_group.jinja +++ b/scripts/split_tox_gh_actions/templates/test_group.jinja @@ -12,32 +12,54 @@ # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-22.04] - {% if needs_docker %} + {% if needs_postgres or needs_mysql or needs_docker %} services: - docker: - image: docker:dind # Required for Docker network management - options: --privileged # Required for Docker-in-Docker operations - {% endif %} {% if needs_postgres %} - services: postgres: image: postgres env: POSTGRES_PASSWORD: sentry - # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - # Maps tcp port 5432 on service container to the host ports: - 5432:5432 + {% endif %} + {% if needs_mysql %} + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + ports: + - 3306:3306 + {% endif %} + {% if needs_docker %} + docker: + image: docker:dind # Required for Docker network management + options: --privileged # Required for Docker-in-Docker operations + {% endif %} + {% endif %} + {% if needs_postgres or needs_mysql %} env: + {% if needs_postgres %} SENTRY_PYTHON_TEST_POSTGRES_HOST: {% raw %}${{ matrix.python-version == '3.6' && 'postgres' || 'localhost' }}{% endraw %} SENTRY_PYTHON_TEST_POSTGRES_USER: postgres SENTRY_PYTHON_TEST_POSTGRES_PASSWORD: sentry - + {% endif %} + {% if needs_mysql %} + SENTRY_PYTHON_TEST_MYSQL_HOST: {% raw %}${{ matrix.python-version == '3.6' && 'mysql' || 'localhost' }}{% endraw %} + SENTRY_PYTHON_TEST_MYSQL_USER: root + SENTRY_PYTHON_TEST_MYSQL_PASSWORD: root + SENTRY_PYTHON_TEST_MYSQL_DB: test + {% endif %} {% endif %} # Use Docker container only for Python 3.6 {% raw %}container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }}{% endraw %} diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 599f0507d0..3bca9f4ad7 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -66,6 +66,7 @@ def iter_default_integrations( _AUTO_ENABLING_INTEGRATIONS = [ "sentry_sdk.integrations.aiohttp.AioHttpIntegration", + "sentry_sdk.integrations.aiomysql.AioMySQLIntegration", "sentry_sdk.integrations.anthropic.AnthropicIntegration", "sentry_sdk.integrations.ariadne.AriadneIntegration", "sentry_sdk.integrations.arq.ArqIntegration", @@ -117,6 +118,7 @@ def iter_default_integrations( _MIN_VERSIONS = { "aiohttp": (3, 4), + "aiomysql": (0, 1, 1), "anthropic": (0, 16), "ariadne": (0, 20), "arq": (0, 23), diff --git a/sentry_sdk/integrations/aiomysql.py b/sentry_sdk/integrations/aiomysql.py index 3c2920d650..7b1aa9c489 100644 --- a/sentry_sdk/integrations/aiomysql.py +++ b/sentry_sdk/integrations/aiomysql.py @@ -1,154 +1,208 @@ # -*- coding: utf-8 -*- -""" -Adapted from module sentry_sdk.integrations.asyncpg -""" from __future__ import annotations -import contextlib -from typing import Any, TypeVar, Callable, Awaitable, Iterator + +from typing import Any, TypeVar, Callable, Awaitable import sentry_sdk from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable -from sentry_sdk.tracing import Span from sentry_sdk.tracing_utils import add_query_source, record_sql_queries from sentry_sdk.utils import ( - ensure_integration_enabled, - parse_version, - capture_internal_exceptions, + capture_internal_exceptions, + parse_version, ) try: - import aiomysql # type: ignore[import-not-found] - from aiomysql.connection import Connection, Cursor # type: ignore + import aiomysql # type: ignore[import-untyped] + from aiomysql.cursors import Cursor # type: ignore[import-untyped] except ImportError: - raise DidNotEnable("aiomysql not installed.") + raise DidNotEnable("aiomysql not installed.") class AioMySQLIntegration(Integration): - identifier = "aiomysql" - origin = f"auto.db.{identifier}" - _record_params = False - - def __init__(self, *, record_params: bool = False): - AioMySQLIntegration._record_params = record_params + identifier = "aiomysql" + origin = f"auto.db.{identifier}" + _record_params = False - @staticmethod - def setup_once() -> None: - aiomysql_version = parse_version(aiomysql.__version__) - _check_minimum_version(AioMySQLIntegration, aiomysql_version) + def __init__(self, *, record_params: bool = False): + AioMySQLIntegration._record_params = record_params - aiomysql.Connection.query = _wrap_execute( - aiomysql.Connection.query, - ) + @staticmethod + def setup_once() -> None: + aiomysql_version = parse_version(aiomysql.__version__) + _check_minimum_version(AioMySQLIntegration, aiomysql_version) - aiomysql.connect = _wrap_connect(aiomysql.connect) + Cursor.execute = _wrap_execute(Cursor.execute) + Cursor.executemany = _wrap_executemany(Cursor.executemany) + aiomysql.connect = _wrap_connect(aiomysql.connect) T = TypeVar("T") +def _normalize_query(query: str | bytes | bytearray) -> str: + if isinstance(query, (bytes, bytearray)): + query = query.decode("utf-8", errors="replace") + return " ".join(query.split()) + + def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - async def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return await f(*args, **kwargs) - - conn = args[0] - query = args[1] # В aiomysql запрос передается первым аргументом - with record_sql_queries( - cursor=None, - query=query, - params_list=None, - paramstyle=None, - executemany=False, - span_origin=AioMySQLIntegration.origin, - ) as span: - res = await f(*args, **kwargs) - span.set_data("db.affected_rows", res) - - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -SubCursor = TypeVar("SubCursor", bound=Cursor) - - -@contextlib.contextmanager -def _record( - cursor: SubCursor | None, - query: str, - params_list: tuple[Any, ...] | None, - *, - executemany: bool = False, -) -> Iterator[Span]: - integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) - if integration is not None and not integration._record_params: - params_list = None - - param_style = "pyformat" if params_list else None - - with record_sql_queries( - cursor=cursor, - query=query, - params_list=params_list, - paramstyle=param_style, - executemany=executemany, - record_cursor_repr=cursor is not None, - span_origin=AioMySQLIntegration.origin, - ) as span: - yield span - - -def _wrap_connect(f: Callable[..., T]) -> Callable[..., T]: - def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return f(*args, **kwargs) - - host = kwargs.get("host", "localhost") - port = kwargs.get("port") or 3306 - user = kwargs.get("user") - db = kwargs.get("db") - - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=AioMySQLIntegration.origin, - ) as span: - span.set_data(SPANDATA.DB_SYSTEM, "mysql") - span.set_data(SPANDATA.SERVER_ADDRESS, host) - span.set_data(SPANDATA.SERVER_PORT, port) - span.set_data(SPANDATA.DB_NAME, db) - span.set_data(SPANDATA.DB_USER, user) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=span._data + """Wrap Cursor.execute to capture SQL queries.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + + # Skip if flagged by executemany (avoids double-recording). + # Do NOT reset the flag here — it must stay True for the entire + # duration of executemany, which may call execute multiple times + # in a loop (non-INSERT fallback). Only _wrap_executemany's + # finally block should clear it. + if getattr(cursor, "_sentry_skip_next_execute", False): + return await f(*args, **kwargs) + + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + params = args[2] if len(args) > 2 else kwargs.get("args") + + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration( + AioMySQLIntegration ) - res = f(*args, **kwargs) + params_list = params if integration and integration._record_params else None + param_style = "pyformat" if params_list else None + + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=False, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) - return res + with capture_internal_exceptions(): + add_query_source(span) - return _inner + return res + return _inner -def _set_db_data(span: Span, conn: Any) -> None: - span.set_data(SPANDATA.DB_SYSTEM, "mysql") - host = conn.host - if host: - span.set_data(SPANDATA.SERVER_ADDRESS, host) +def _wrap_executemany( + f: Callable[..., Awaitable[T]] +) -> Callable[..., Awaitable[T]]: + """Wrap Cursor.executemany to capture SQL queries.""" - port = conn.port - if port: - span.set_data(SPANDATA.SERVER_PORT, port) + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) - database = conn.db - if database: - span.set_data(SPANDATA.DB_NAME, database) + cursor = args[0] + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + seq_of_params = args[2] if len(args) > 2 else kwargs.get("args") - user = conn.user - if user: - span.set_data(SPANDATA.DB_USER, user) + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration( + AioMySQLIntegration + ) + params_list = ( + seq_of_params if integration and integration._record_params else None + ) + param_style = "pyformat" if params_list else None + + # Prevent double-recording: _do_execute_many calls self.execute internally + cursor._sentry_skip_next_execute = True + try: + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=True, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) + + with capture_internal_exceptions(): + add_query_source(span) + + return res + finally: + cursor._sentry_skip_next_execute = False + + return _inner + + +def _get_connection(cursor: Any) -> Any: + """Get the underlying connection from a cursor.""" + return getattr(cursor, "_connection", None) + + +def _wrap_connect( + f: Callable[..., Awaitable[T]] +) -> Callable[..., Awaitable[T]]: + """Wrap aiomysql.connect to capture connection spans.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + host = kwargs.get("host", "localhost") + port = kwargs.get("port") or 3306 + user = kwargs.get("user") + db = kwargs.get("db") or kwargs.get("database") + + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=AioMySQLIntegration.origin, + ) as span: + span.set_data(SPANDATA.DB_SYSTEM, "mysql") + span.set_data(SPANDATA.SERVER_ADDRESS, host) + span.set_data(SPANDATA.SERVER_PORT, port) + span.set_data(SPANDATA.DB_NAME, db) + span.set_data(SPANDATA.DB_USER, user) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", + category="query", + data=span._data, + ) + res = await f(*args, **kwargs) + + return res + + return _inner + + +def _set_db_data(span: Any, conn: Any) -> None: + """Set database-related span data from connection object.""" + span.set_data(SPANDATA.DB_SYSTEM, "mysql") + + host = getattr(conn, "host", None) + if host: + span.set_data(SPANDATA.SERVER_ADDRESS, host) + + port = getattr(conn, "port", None) + if port: + span.set_data(SPANDATA.SERVER_PORT, port) + + database = getattr(conn, "db", None) + if database: + span.set_data(SPANDATA.DB_NAME, database) + + user = getattr(conn, "user", None) + if user: + span.set_data(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aiomysql/__init__.py b/tests/integrations/aiomysql/__init__.py new file mode 100644 index 0000000000..d927c2ddea --- /dev/null +++ b/tests/integrations/aiomysql/__init__.py @@ -0,0 +1,4 @@ +import pytest + +pytest.importorskip("aiomysql") +pytest.importorskip("pytest_asyncio") diff --git a/tests/integrations/aiomysql/test_aiomysql.py b/tests/integrations/aiomysql/test_aiomysql.py new file mode 100644 index 0000000000..6ced782f80 --- /dev/null +++ b/tests/integrations/aiomysql/test_aiomysql.py @@ -0,0 +1,790 @@ +""" +Tests need pytest-asyncio installed. + +Tests need a local MySQL instance running. This can be done using: +```sh +docker run --rm --name some-mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=test -d -p 3306:3306 mysql:8.0 +``` + +The tests use the following credentials to establish a database connection. +""" + +import os +import datetime +from contextlib import contextmanager +from unittest import mock + +import aiomysql +import pytest +import pytest_asyncio + +from sentry_sdk import capture_message, start_transaction +from sentry_sdk.integrations.aiomysql import AioMySQLIntegration +from sentry_sdk.consts import SPANDATA +from sentry_sdk.tracing_utils import record_sql_queries +from tests.conftest import ApproxDict + +MYSQL_HOST = os.getenv("SENTRY_PYTHON_TEST_MYSQL_HOST", "localhost") +MYSQL_PORT = int(os.getenv("SENTRY_PYTHON_TEST_MYSQL_PORT", "3306")) +MYSQL_USER = os.getenv("SENTRY_PYTHON_TEST_MYSQL_USER", "root") +MYSQL_PASSWORD = os.getenv("SENTRY_PYTHON_TEST_MYSQL_PASSWORD", "root") +MYSQL_DB_BASE = os.getenv("SENTRY_PYTHON_TEST_MYSQL_DB", "test") + + +def _get_db_name(): + pid = os.getpid() + return f"{MYSQL_DB_BASE}_{pid}" + + +MYSQL_DB = _get_db_name() + +CRUMBS_CONNECT = { + "category": "query", + "data": ApproxDict( + { + "db.name": MYSQL_DB, + "db.system": "mysql", + "db.user": MYSQL_USER, + "server.address": MYSQL_HOST, + "server.port": MYSQL_PORT, + } + ), + "message": "connect", + "type": "default", +} + + +@pytest_asyncio.fixture(autouse=True) +async def _clean_mysql(): + conn = await aiomysql.connect( + host=MYSQL_HOST, + port=MYSQL_PORT, + user=MYSQL_USER, + password=MYSQL_PASSWORD, + autocommit=True, + ) + try: + async with conn.cursor() as cur: + await cur.execute(f"CREATE DATABASE IF NOT EXISTS `{MYSQL_DB}`") + await cur.execute(f"USE `{MYSQL_DB}`") + await cur.execute("DROP TABLE IF EXISTS users") + await cur.execute( + """ + CREATE TABLE users( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255), + password VARCHAR(255), + dob DATE + ) + """ + ) + finally: + conn.close() + + +def _connect_args(): + return { + "host": MYSQL_HOST, + "port": MYSQL_PORT, + "user": MYSQL_USER, + "password": MYSQL_PASSWORD, + "db": MYSQL_DB, + "autocommit": True, + } + + +@pytest.mark.asyncio +async def test_connect(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + assert event["breadcrumbs"]["values"] == [CRUMBS_CONNECT] + + +@pytest.mark.asyncio +async def test_execute(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'pw', '1990-12-25')", + ) + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + ("Bob", "secret_pw", datetime.date(1984, 3, 1)), + ) + await cur.execute("SELECT * FROM users WHERE name = %s", ("Bob",)) + row = await cur.fetchone() + assert row[1] == "Bob" + + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + assert event["breadcrumbs"]["values"] == [ + CRUMBS_CONNECT, + { + "category": "query", + "data": {}, + "message": "INSERT INTO users(name, password, dob) VALUES ('Alice', 'pw', '1990-12-25')", + "type": "default", + }, + { + "category": "query", + "data": {}, + "message": "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + "type": "default", + }, + { + "category": "query", + "data": {}, + "message": "SELECT * FROM users WHERE name = %s", + "type": "default", + }, + ] + + +@pytest.mark.asyncio +async def test_execute_many(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.executemany( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + [ + ("Bob", "secret_pw", datetime.date(1984, 3, 1)), + ("Alice", "pw", datetime.date(1990, 12, 25)), + ], + ) + + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + assert event["breadcrumbs"]["values"] == [ + CRUMBS_CONNECT, + { + "category": "query", + "data": {"db.executemany": True}, + "message": "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + "type": "default", + }, + ] + + +@pytest.mark.asyncio +async def test_execute_many_non_insert(sentry_init, capture_events) -> None: + """Test executemany with non-INSERT queries (falls back to row-by-row).""" + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + # Pre-populate users table + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + ("Alice", "pw1", datetime.date(1990, 1, 1)), + ) + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + ("Bob", "pw2", datetime.date(1991, 2, 2)), + ) + # Non-INSERT executemany — uses row-by-row fallback internally + await cur.executemany( + "UPDATE users SET password = %s WHERE name = %s", + [ + ("new_pw_1", "Alice"), + ("new_pw_2", "Bob"), + ], + ) + + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + # Should have: connect + INSERT*2 + single executemany span (no double-recording) + crumbs = event["breadcrumbs"]["values"] + query_crumbs = [c for c in crumbs if c["category"] == "query"] + executemany_crumbs = [c for c in query_crumbs if c.get("data", {}).get("db.executemany")] + # Only ONE executemany breadcrumb — no duplicates from internal execute calls + assert len(executemany_crumbs) == 1 + assert "UPDATE users SET password = %s" in executemany_crumbs[0]["message"] + + +@pytest.mark.asyncio +async def test_record_params(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration(record_params=True)], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + ("Bob", "secret_pw", datetime.date(1984, 3, 1)), + ) + + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + assert event["breadcrumbs"]["values"] == [ + CRUMBS_CONNECT, + { + "category": "query", + "data": { + "db.params": ["Bob", "secret_pw", "datetime.date(1984, 3, 1)"], + "db.paramstyle": "format", + }, + "message": "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + "type": "default", + }, + ] + + +@pytest.mark.asyncio +async def test_cursor_context_manager(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.executemany( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + [ + ("Bob", "secret_pw", datetime.date(1984, 3, 1)), + ("Alice", "pw", datetime.date(1990, 12, 25)), + ], + ) + await cur.execute( + "SELECT * FROM users WHERE dob > %s", + (datetime.date(1970, 1, 1),), + ) + rows = await cur.fetchall() + assert len(rows) == 2 + + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + crumbs = event["breadcrumbs"]["values"] + assert crumbs[0] == CRUMBS_CONNECT + assert crumbs[1]["category"] == "query" + assert "INSERT" in crumbs[1]["message"] + assert crumbs[2]["category"] == "query" + assert "SELECT" in crumbs[2]["message"] + + +@pytest.mark.asyncio +async def test_cursor_async_iteration(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + ("Charlie", "pw3", datetime.date(1995, 5, 15)), + ) + await cur.execute("SELECT * FROM users WHERE name = %s", ("Charlie",)) + async for row in cur: + assert row[1] == "Charlie" + + conn.close() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + crumbs = event["breadcrumbs"]["values"] + assert crumbs[0] == CRUMBS_CONNECT + assert len([c for c in crumbs if c["category"] == "query"]) >= 2 + + +@pytest.mark.asyncio +async def test_connection_pool(sentry_init, capture_events) -> None: + sentry_init( + integrations=[AioMySQLIntegration()], + _experiments={"record_sql_params": True}, + ) + events = capture_events() + + pool_size = 2 + + pool = await aiomysql.create_pool( + host=MYSQL_HOST, + port=MYSQL_PORT, + user=MYSQL_USER, + password=MYSQL_PASSWORD, + db=MYSQL_DB, + autocommit=True, + minsize=pool_size, + maxsize=pool_size, + ) + + async with pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)", + ("Dave", "pw4", datetime.date(1988, 7, 20)), + ) + await cur.execute("SELECT * FROM users WHERE name = %s", ("Dave",)) + row = await cur.fetchone() + assert row is not None + assert row[1] == "Dave" + + pool.close() + await pool.wait_closed() + + capture_message("hi") + + (event,) = events + + for crumb in event["breadcrumbs"]["values"]: + del crumb["timestamp"] + + crumbs = event["breadcrumbs"]["values"] + # Verify queries were captured + query_crumbs = [c for c in crumbs if c["category"] == "query"] + assert len(query_crumbs) >= 2 # INSERT + SELECT + + +@pytest.mark.asyncio +async def test_query_source_disabled(sentry_init, capture_events): + sentry_options = { + "integrations": [AioMySQLIntegration()], + "enable_tracing": True, + "enable_db_query_source": False, + "db_query_source_threshold_ms": 0, + } + + sentry_init(**sentry_options) + + events = capture_events() + + with start_transaction(name="test_transaction", sampled=True): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')", + ) + + conn.close() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("INSERT INTO") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data + + +@pytest.mark.asyncio +@pytest.mark.parametrize("enable_db_query_source", [None, True]) +async def test_query_source_enabled( + sentry_init, capture_events, enable_db_query_source +): + sentry_options = { + "integrations": [AioMySQLIntegration()], + "enable_tracing": True, + "db_query_source_threshold_ms": 0, + } + if enable_db_query_source is not None: + sentry_options["enable_db_query_source"] = enable_db_query_source + + sentry_init(**sentry_options) + + events = capture_events() + + with start_transaction(name="test_transaction", sampled=True): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')", + ) + + conn.close() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("INSERT INTO") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + +@pytest.mark.asyncio +async def test_query_source(sentry_init, capture_events): + sentry_init( + integrations=[AioMySQLIntegration()], + enable_tracing=True, + enable_db_query_source=True, + db_query_source_threshold_ms=0, + ) + + events = capture_events() + + with start_transaction(name="test_transaction", sampled=True): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')", + ) + + conn.close() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("INSERT INTO") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert ( + data.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.aiomysql.test_aiomysql" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/aiomysql/test_aiomysql.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "test_query_source" + + +@pytest.mark.asyncio +async def test_no_query_source_if_duration_too_short(sentry_init, capture_events): + sentry_init( + integrations=[AioMySQLIntegration()], + enable_tracing=True, + enable_db_query_source=True, + db_query_source_threshold_ms=100, + ) + + events = capture_events() + + with start_transaction(name="test_transaction", sampled=True): + conn = await aiomysql.connect(**_connect_args()) + + @contextmanager + def fake_record_sql_queries(*args, **kwargs): + with record_sql_queries(*args, **kwargs) as span: + pass + span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span.timestamp = datetime.datetime(2024, 1, 1, microsecond=99999) + yield span + + async with conn.cursor() as cur: + with mock.patch( + "sentry_sdk.integrations.aiomysql.record_sql_queries", + fake_record_sql_queries, + ): + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')", + ) + + conn.close() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("INSERT INTO") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data + + +@pytest.mark.asyncio +async def test_query_source_if_duration_over_threshold(sentry_init, capture_events): + sentry_init( + integrations=[AioMySQLIntegration()], + enable_tracing=True, + enable_db_query_source=True, + db_query_source_threshold_ms=100, + ) + + events = capture_events() + + with start_transaction(name="test_transaction", sampled=True): + conn = await aiomysql.connect(**_connect_args()) + + @contextmanager + def fake_record_sql_queries(*args, **kwargs): + with record_sql_queries(*args, **kwargs) as span: + pass + span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span.timestamp = datetime.datetime(2024, 1, 1, microsecond=100001) + yield span + + async with conn.cursor() as cur: + with mock.patch( + "sentry_sdk.integrations.aiomysql.record_sql_queries", + fake_record_sql_queries, + ): + await cur.execute( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')", + ) + + conn.close() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("INSERT INTO") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert ( + data.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.aiomysql.test_aiomysql" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/aiomysql/test_aiomysql.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert ( + data.get(SPANDATA.CODE_FUNCTION) + == "test_query_source_if_duration_over_threshold" + ) + + +@pytest.mark.asyncio +async def test_span_origin(sentry_init, capture_events): + sentry_init( + integrations=[AioMySQLIntegration()], + traces_sample_rate=1.0, + ) + + events = capture_events() + + with start_transaction(name="test_transaction"): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute("SELECT 1") + await cur.execute("SELECT 2") + + conn.close() + + (event,) = events + + assert event["contexts"]["trace"]["origin"] == "manual" + + for span in event["spans"]: + assert span["origin"] == "auto.db.aiomysql" + + +@pytest.mark.asyncio +async def test_multiline_query_description_normalized(sentry_init, capture_events): + sentry_init( + integrations=[AioMySQLIntegration()], + traces_sample_rate=1.0, + ) + events = capture_events() + + with start_transaction(name="test_transaction"): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + """ + SELECT + id, + name + FROM + users + WHERE + name = 'Alice' + """ + ) + + conn.close() + + (event,) = events + + spans = [ + s + for s in event["spans"] + if s["op"] == "db" and "SELECT" in s.get("description", "") + ] + assert len(spans) == 1 + assert spans[0]["description"] == "SELECT id, name FROM users WHERE name = 'Alice'" + + +@pytest.mark.asyncio +async def test_before_send_transaction_sees_normalized_description( + sentry_init, capture_events +): + def before_send_transaction(event, hint): + for span in event.get("spans", []): + desc = span.get("description", "") + if "SELECT id, name FROM users" in desc: + span["description"] = "filtered" + return event + + sentry_init( + integrations=[AioMySQLIntegration()], + traces_sample_rate=1.0, + before_send_transaction=before_send_transaction, + ) + events = capture_events() + + with start_transaction(name="test_transaction"): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute( + """ + SELECT + id, + name + FROM + users + """ + ) + + conn.close() + + (event,) = events + spans = [ + s + for s in event["spans"] + if s["op"] == "db" and "filtered" in s.get("description", "") + ] + + assert len(spans) == 1 + assert spans[0]["description"] == "filtered" + + +@pytest.mark.asyncio +async def test_db_data_on_spans(sentry_init, capture_events): + """Test that database connection data is properly set on spans.""" + sentry_init( + integrations=[AioMySQLIntegration()], + traces_sample_rate=1.0, + ) + events = capture_events() + + with start_transaction(name="test_transaction"): + conn = await aiomysql.connect(**_connect_args()) + + async with conn.cursor() as cur: + await cur.execute("SELECT 1") + + conn.close() + + (event,) = events + + db_spans = [s for s in event["spans"] if s["op"] == "db"] + assert len(db_spans) > 0 + + query_span = [s for s in db_spans if "SELECT" in s.get("description", "")][0] + assert query_span["data"].get(SPANDATA.DB_SYSTEM) == "mysql" + assert query_span["data"].get(SPANDATA.SERVER_ADDRESS) == MYSQL_HOST + assert query_span["data"].get(SPANDATA.SERVER_PORT) == MYSQL_PORT + assert query_span["data"].get(SPANDATA.DB_NAME) == MYSQL_DB + assert query_span["data"].get(SPANDATA.DB_USER) == MYSQL_USER diff --git a/tox.ini b/tox.ini index 956ff86b8b..fce9dee56c 100644 --- a/tox.ini +++ b/tox.ini @@ -160,6 +160,11 @@ envlist = {py3.9,py3.13,py3.14,py3.14t}-asyncpg-v0.31.0 {py3.9,py3.13,py3.14,py3.14t}-asyncpg-latest + {py3.7,py3.8,py3.9}-aiomysql-v0.1.1 + {py3.7,py3.9,py3.10}-aiomysql-v0.2.0 + {py3.9,py3.13,py3.14,py3.14t}-aiomysql-v0.3.2 + {py3.9,py3.13,py3.14,py3.14t}-aiomysql-latest + {py3.9,py3.13,py3.14}-clickhouse_driver-v0.2.10 {py3.9,py3.13,py3.14}-clickhouse_driver-latest @@ -589,6 +594,12 @@ deps = asyncpg-latest: asyncpg==0.31.0 asyncpg: pytest-asyncio + aiomysql-v0.1.1: aiomysql==0.1.1 + aiomysql-v0.2.0: aiomysql==0.2.0 + aiomysql-v0.3.2: aiomysql==0.3.2 + aiomysql-latest: aiomysql==0.3.2 + aiomysql: pytest-asyncio + clickhouse_driver-v0.2.10: clickhouse-driver==0.2.10 clickhouse_driver-latest: clickhouse-driver==0.2.10 @@ -973,6 +984,7 @@ setenv = ariadne: _TESTPATH=tests/integrations/ariadne arq: _TESTPATH=tests/integrations/arq asyncpg: _TESTPATH=tests/integrations/asyncpg + aiomysql: _TESTPATH=tests/integrations/aiomysql beam: _TESTPATH=tests/integrations/beam boto3: _TESTPATH=tests/integrations/boto3 bottle: _TESTPATH=tests/integrations/bottle From 5f26753358f859795781bd68de5ee98178d0ecc2 Mon Sep 17 00:00:00 2001 From: "Alexandr N Zamaraev (aka tonal)" Date: Mon, 13 Apr 2026 11:44:40 +0700 Subject: [PATCH 6/7] fix(aiomysql): Remove duplicate MySQL service block from Jinja template The leftover {% if needs_mysql %} block created duplicate 'services:' and 'env:' keys, causing YAML parsers to silently overwrite earlier definitions. Co-Authored-By: Qwen Code Co-authored-by: Qwen-Coder --- .../templates/test_group.jinja | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/scripts/split_tox_gh_actions/templates/test_group.jinja b/scripts/split_tox_gh_actions/templates/test_group.jinja index e5fa8ef4f9..5a7ceb80f6 100644 --- a/scripts/split_tox_gh_actions/templates/test_group.jinja +++ b/scripts/split_tox_gh_actions/templates/test_group.jinja @@ -60,27 +60,6 @@ SENTRY_PYTHON_TEST_MYSQL_PASSWORD: root SENTRY_PYTHON_TEST_MYSQL_DB: test {% endif %} - {% endif %} - {% if needs_mysql %} - services: - mysql: - image: mysql:8.0 - env: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: test - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=5 - ports: - - 3306:3306 - env: - SENTRY_PYTHON_TEST_MYSQL_HOST: {% raw %}${{ matrix.python-version == '3.6' && 'mysql' || 'localhost' }}{% endraw %} - SENTRY_PYTHON_TEST_MYSQL_USER: root - SENTRY_PYTHON_TEST_MYSQL_PASSWORD: root - SENTRY_PYTHON_TEST_MYSQL_DB: test - {% endif %} # Use Docker container only for Python 3.6 {% raw %}container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }}{% endraw %} From a7319c9faa908e8845f56b9601fa1d5ea2ebabce Mon Sep 17 00:00:00 2001 From: "Alexandr N Zamaraev (aka tonal)" Date: Mon, 13 Apr 2026 12:08:12 +0700 Subject: [PATCH 7/7] fix(aiomysql): Use public cursor.connection instead of private _connection Fixes sentry bot review comment on PR #4703. Co-Authored-By: Qwen Code Co-authored-by: Qwen-Coder --- sentry_sdk/integrations/aiomysql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/aiomysql.py b/sentry_sdk/integrations/aiomysql.py index 7b1aa9c489..d6a533da8f 100644 --- a/sentry_sdk/integrations/aiomysql.py +++ b/sentry_sdk/integrations/aiomysql.py @@ -146,7 +146,7 @@ async def _inner(*args: Any, **kwargs: Any) -> T: def _get_connection(cursor: Any) -> Any: """Get the underlying connection from a cursor.""" - return getattr(cursor, "_connection", None) + return getattr(cursor, "connection", None) def _wrap_connect(