From 57a340568fddd0c566ba59d01f80d747e6c19a7f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 26 Aug 2025 11:10:07 +0200 Subject: [PATCH 01/88] Update tox.ini (#4721) Regular update --- .../openai_agents/test_openai_agents.py | 2 +- tox.ini | 72 ++++++++++--------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 09fca2fbf3..a3075e6415 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -87,7 +87,7 @@ def test_agent_custom_model(): name="test_agent_custom_model", instructions="You are a helpful test assistant.", # the model could be agents.OpenAIChatCompletionsModel() - model=MagicMock(model="my-custom-model"), + model="my-custom-model", model_settings=ModelSettings( max_tokens=100, temperature=0.7, diff --git a/tox.ini b/tox.ini index a1b1327af5..bbc1d57c12 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-08-12T07:16:34.585160+00:00 +# Last generated: 2025-08-26T08:59:42.512502+00:00 [tox] requires = @@ -136,18 +136,18 @@ envlist = # ~~~ AI ~~~ {py3.8,py3.11,py3.12}-anthropic-v0.16.0 - {py3.8,py3.11,py3.12}-anthropic-v0.31.2 - {py3.8,py3.11,py3.12}-anthropic-v0.46.0 - {py3.8,py3.12,py3.13}-anthropic-v0.62.0 + {py3.8,py3.11,py3.12}-anthropic-v0.32.0 + {py3.8,py3.11,py3.12}-anthropic-v0.48.0 + {py3.8,py3.12,py3.13}-anthropic-v0.64.0 {py3.9,py3.10,py3.11}-cohere-v5.4.0 {py3.9,py3.11,py3.12}-cohere-v5.9.4 {py3.9,py3.11,py3.12}-cohere-v5.13.12 - {py3.9,py3.11,py3.12}-cohere-v5.16.3 + {py3.9,py3.11,py3.12}-cohere-v5.17.0 {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 - {py3.10,py3.12,py3.13}-openai_agents-v0.2.6 + {py3.10,py3.12,py3.13}-openai_agents-v0.2.9 {py3.8,py3.10,py3.11}-huggingface_hub-v0.22.2 {py3.8,py3.11,py3.12}-huggingface_hub-v0.26.5 @@ -162,7 +162,7 @@ envlist = {py3.6}-pymongo-v3.5.1 {py3.6,py3.10,py3.11}-pymongo-v3.13.0 {py3.6,py3.9,py3.10}-pymongo-v4.0.2 - {py3.9,py3.12,py3.13}-pymongo-v4.14.0 + {py3.9,py3.12,py3.13}-pymongo-v4.14.1 {py3.6}-redis_py_cluster_legacy-v1.3.6 {py3.6,py3.7}-redis_py_cluster_legacy-v2.0.0 @@ -183,9 +183,9 @@ envlist = {py3.9,py3.12,py3.13}-openfeature-v0.8.2 {py3.7,py3.12,py3.13}-statsig-v0.55.3 - {py3.7,py3.12,py3.13}-statsig-v0.57.3 - {py3.7,py3.12,py3.13}-statsig-v0.59.1 + {py3.7,py3.12,py3.13}-statsig-v0.58.4 {py3.7,py3.12,py3.13}-statsig-v0.61.0 + {py3.7,py3.12,py3.13}-statsig-v0.63.0 {py3.8,py3.12,py3.13}-unleash-v6.0.1 {py3.8,py3.12,py3.13}-unleash-v6.1.0 @@ -201,15 +201,16 @@ envlist = {py3.6,py3.9,py3.10}-gql-v3.4.1 {py3.7,py3.11,py3.12}-gql-v3.5.3 - {py3.9,py3.12,py3.13}-gql-v4.0.0b0 + {py3.9,py3.12,py3.13}-gql-v4.0.0 + {py3.9,py3.12,py3.13}-gql-v4.1.0b0 {py3.6,py3.9,py3.10}-graphene-v3.3 {py3.8,py3.12,py3.13}-graphene-v3.4.3 {py3.8,py3.10,py3.11}-strawberry-v0.209.8 - {py3.8,py3.11,py3.12}-strawberry-v0.232.2 - {py3.8,py3.12,py3.13}-strawberry-v0.255.0 - {py3.9,py3.12,py3.13}-strawberry-v0.278.1 + {py3.8,py3.11,py3.12}-strawberry-v0.233.3 + {py3.9,py3.12,py3.13}-strawberry-v0.257.0 + {py3.9,py3.12,py3.13}-strawberry-v0.280.0 # ~~~ Network ~~~ @@ -250,12 +251,12 @@ envlist = {py3.6,py3.7,py3.8}-flask-v1.1.4 {py3.8,py3.12,py3.13}-flask-v2.3.3 {py3.8,py3.12,py3.13}-flask-v3.0.3 - {py3.9,py3.12,py3.13}-flask-v3.1.1 + {py3.9,py3.12,py3.13}-flask-v3.1.2 {py3.6,py3.9,py3.10}-starlette-v0.16.0 {py3.7,py3.10,py3.11}-starlette-v0.26.1 {py3.8,py3.11,py3.12}-starlette-v0.36.3 - {py3.9,py3.12,py3.13}-starlette-v0.47.2 + {py3.9,py3.12,py3.13}-starlette-v0.47.3 {py3.6,py3.9,py3.10}-fastapi-v0.79.1 {py3.7,py3.10,py3.11}-fastapi-v0.91.0 @@ -309,7 +310,7 @@ envlist = {py3.9,py3.12,py3.13}-trytond-v7.6.5 {py3.7,py3.12,py3.13}-typer-v0.15.4 - {py3.7,py3.12,py3.13}-typer-v0.16.0 + {py3.7,py3.12,py3.13}-typer-v0.16.1 @@ -511,22 +512,22 @@ deps = # ~~~ AI ~~~ anthropic-v0.16.0: anthropic==0.16.0 - anthropic-v0.31.2: anthropic==0.31.2 - anthropic-v0.46.0: anthropic==0.46.0 - anthropic-v0.62.0: anthropic==0.62.0 + anthropic-v0.32.0: anthropic==0.32.0 + anthropic-v0.48.0: anthropic==0.48.0 + anthropic-v0.64.0: anthropic==0.64.0 anthropic: pytest-asyncio anthropic-v0.16.0: httpx<0.28.0 - anthropic-v0.31.2: httpx<0.28.0 - anthropic-v0.46.0: httpx<0.28.0 + anthropic-v0.32.0: httpx<0.28.0 + anthropic-v0.48.0: httpx<0.28.0 cohere-v5.4.0: cohere==5.4.0 cohere-v5.9.4: cohere==5.9.4 cohere-v5.13.12: cohere==5.13.12 - cohere-v5.16.3: cohere==5.16.3 + cohere-v5.17.0: cohere==5.17.0 openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.1.0: openai-agents==0.1.0 - openai_agents-v0.2.6: openai-agents==0.2.6 + openai_agents-v0.2.9: openai-agents==0.2.9 openai_agents: pytest-asyncio huggingface_hub-v0.22.2: huggingface_hub==0.22.2 @@ -542,7 +543,7 @@ deps = pymongo-v3.5.1: pymongo==3.5.1 pymongo-v3.13.0: pymongo==3.13.0 pymongo-v4.0.2: pymongo==4.0.2 - pymongo-v4.14.0: pymongo==4.14.0 + pymongo-v4.14.1: pymongo==4.14.1 pymongo: mockupdb redis_py_cluster_legacy-v1.3.6: redis-py-cluster==1.3.6 @@ -564,9 +565,9 @@ deps = openfeature-v0.8.2: openfeature-sdk==0.8.2 statsig-v0.55.3: statsig==0.55.3 - statsig-v0.57.3: statsig==0.57.3 - statsig-v0.59.1: statsig==0.59.1 + statsig-v0.58.4: statsig==0.58.4 statsig-v0.61.0: statsig==0.61.0 + statsig-v0.63.0: statsig==0.63.0 statsig: typing_extensions unleash-v6.0.1: UnleashClient==6.0.1 @@ -586,7 +587,8 @@ deps = gql-v3.4.1: gql[all]==3.4.1 gql-v3.5.3: gql[all]==3.5.3 - gql-v4.0.0b0: gql[all]==4.0.0b0 + gql-v4.0.0: gql[all]==4.0.0 + gql-v4.1.0b0: gql[all]==4.1.0b0 graphene-v3.3: graphene==3.3 graphene-v3.4.3: graphene==3.4.3 @@ -597,13 +599,13 @@ deps = py3.6-graphene: aiocontextvars strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8 - strawberry-v0.232.2: strawberry-graphql[fastapi,flask]==0.232.2 - strawberry-v0.255.0: strawberry-graphql[fastapi,flask]==0.255.0 - strawberry-v0.278.1: strawberry-graphql[fastapi,flask]==0.278.1 + strawberry-v0.233.3: strawberry-graphql[fastapi,flask]==0.233.3 + strawberry-v0.257.0: strawberry-graphql[fastapi,flask]==0.257.0 + strawberry-v0.280.0: strawberry-graphql[fastapi,flask]==0.280.0 strawberry: httpx strawberry-v0.209.8: pydantic<2.11 - strawberry-v0.232.2: pydantic<2.11 - strawberry-v0.255.0: pydantic<2.11 + strawberry-v0.233.3: pydantic<2.11 + strawberry-v0.257.0: pydantic<2.11 # ~~~ Network ~~~ @@ -673,7 +675,7 @@ deps = flask-v1.1.4: flask==1.1.4 flask-v2.3.3: flask==2.3.3 flask-v3.0.3: flask==3.0.3 - flask-v3.1.1: flask==3.1.1 + flask-v3.1.2: flask==3.1.2 flask: flask-login flask: werkzeug flask-v1.1.4: werkzeug<2.1.0 @@ -682,7 +684,7 @@ deps = starlette-v0.16.0: starlette==0.16.0 starlette-v0.26.1: starlette==0.26.1 starlette-v0.36.3: starlette==0.36.3 - starlette-v0.47.2: starlette==0.47.2 + starlette-v0.47.3: starlette==0.47.3 starlette: pytest-asyncio starlette: python-multipart starlette: requests @@ -779,7 +781,7 @@ deps = trytond-v4.8.18: werkzeug<1.0 typer-v0.15.4: typer==0.15.4 - typer-v0.16.0: typer==0.16.0 + typer-v0.16.1: typer==0.16.1 From bf4d921a5d779076e693545652359b4c0668f384 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 29 Aug 2025 14:20:30 +0200 Subject: [PATCH 02/88] fix(logs): Do not attach template if there are no parameters (#4728) Closes https://github.com/getsentry/sentry-python/issues/4725 --- sentry_sdk/integrations/logging.py | 15 ++++++++++++-- sentry_sdk/logger.py | 7 ++++--- tests/integrations/logging/test_logging.py | 19 +++++++++++++++++ tests/integrations/loguru/test_loguru.py | 18 ++++++++++++++++ tests/test_logs.py | 24 ++++++++++++++++++++++ 5 files changed, 78 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index 15ff2ed233..bfb30fc67b 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -356,12 +356,14 @@ def _capture_log_from_record(self, client, record): record.levelno, SEVERITY_TO_OTEL_SEVERITY ) project_root = client.options["project_root"] + attrs = self._extra_from_record(record) # type: Any attrs["sentry.origin"] = "auto.logger.log" - if isinstance(record.msg, str): - attrs["sentry.message.template"] = record.msg + + parameters_set = False if record.args is not None: if isinstance(record.args, tuple): + parameters_set = bool(record.args) for i, arg in enumerate(record.args): attrs[f"sentry.message.parameter.{i}"] = ( arg @@ -369,19 +371,28 @@ def _capture_log_from_record(self, client, record): else safe_repr(arg) ) elif isinstance(record.args, dict): + parameters_set = bool(record.args) for key, value in record.args.items(): attrs[f"sentry.message.parameter.{key}"] = ( value if isinstance(value, (str, float, int, bool)) else safe_repr(value) ) + + if parameters_set and isinstance(record.msg, str): + # only include template if there is at least one + # sentry.message.parameter.X set + attrs["sentry.message.template"] = record.msg + if record.lineno: attrs["code.line.number"] = record.lineno + if record.pathname: if project_root is not None and record.pathname.startswith(project_root): attrs["code.file.path"] = record.pathname[len(project_root) + 1 :] else: attrs["code.file.path"] = record.pathname + if record.funcName: attrs["code.function.name"] = record.funcName diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index c18cf91ff2..bc98f35155 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -22,13 +22,14 @@ def _capture_log(severity_text, severity_number, template, **kwargs): # type: (str, int, str, **Any) -> None client = get_client() - attrs = { - "sentry.message.template": template, - } # type: dict[str, str | bool | float | int] + attrs = {} # type: dict[str, str | bool | float | int] if "attributes" in kwargs: attrs.update(kwargs.pop("attributes")) for k, v in kwargs.items(): attrs[f"sentry.message.parameter.{k}"] = v + if kwargs: + # only attach template if there are parameters + attrs["sentry.message.template"] = template attrs = { k: ( diff --git a/tests/integrations/logging/test_logging.py b/tests/integrations/logging/test_logging.py index 7ecdf42500..7a00ceadd2 100644 --- a/tests/integrations/logging/test_logging.py +++ b/tests/integrations/logging/test_logging.py @@ -571,3 +571,22 @@ def test_sentry_logs_named_parameters_complex_values(sentry_init, capture_envelo assert isinstance(complex_param, str) assert "nested" in complex_param assert "data" in complex_param + + +def test_sentry_logs_no_parameters_no_template(sentry_init, capture_envelopes): + """ + There shouldn't be a template if there are no parameters. + """ + sentry_init(enable_logs=True) + envelopes = capture_envelopes() + + python_logger = logging.Logger("test-logger") + python_logger.warning("Warning about something without any parameters.") + + get_client().flush() + logs = envelopes_to_logs(envelopes) + + assert len(logs) == 1 + + attrs = logs[0]["attributes"] + assert "sentry.message.template" not in attrs diff --git a/tests/integrations/loguru/test_loguru.py b/tests/integrations/loguru/test_loguru.py index 38093d24cb..3d04d7d1ea 100644 --- a/tests/integrations/loguru/test_loguru.py +++ b/tests/integrations/loguru/test_loguru.py @@ -467,3 +467,21 @@ def test_logger_with_all_attributes( "sentry.severity_number": 13, "sentry.severity_text": "warn", } + + +def test_no_parameters_no_template( + sentry_init, capture_envelopes, uninstall_integration, request +): + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + + sentry_init(enable_logs=True) + envelopes = capture_envelopes() + + logger.warning("Logging a hardcoded warning") + sentry_sdk.get_client().flush() + + logs = envelopes_to_logs(envelopes) + + attributes = logs[0]["attributes"] + assert "sentry.message.template" not in attributes diff --git a/tests/test_logs.py b/tests/test_logs.py index b2578d83d5..596a31922e 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -254,30 +254,54 @@ def test_logs_message_params(sentry_init, capture_envelopes): sentry_sdk.logger.error( "The recorded error was '{error}'", error=Exception("some error") ) + sentry_sdk.logger.warning("The recorded value was hardcoded.") get_client().flush() logs = envelopes_to_logs(envelopes) assert logs[0]["body"] == "The recorded value was '1'" assert logs[0]["attributes"]["sentry.message.parameter.int_var"] == 1 + assert ( + logs[0]["attributes"]["sentry.message.template"] + == "The recorded value was '{int_var}'" + ) assert logs[1]["body"] == "The recorded value was '2.0'" assert logs[1]["attributes"]["sentry.message.parameter.float_var"] == 2.0 + assert ( + logs[1]["attributes"]["sentry.message.template"] + == "The recorded value was '{float_var}'" + ) assert logs[2]["body"] == "The recorded value was 'False'" assert logs[2]["attributes"]["sentry.message.parameter.bool_var"] is False + assert ( + logs[2]["attributes"]["sentry.message.template"] + == "The recorded value was '{bool_var}'" + ) assert logs[3]["body"] == "The recorded value was 'some string value'" assert ( logs[3]["attributes"]["sentry.message.parameter.string_var"] == "some string value" ) + assert ( + logs[3]["attributes"]["sentry.message.template"] + == "The recorded value was '{string_var}'" + ) assert logs[4]["body"] == "The recorded error was 'some error'" assert ( logs[4]["attributes"]["sentry.message.parameter.error"] == "Exception('some error')" ) + assert ( + logs[4]["attributes"]["sentry.message.template"] + == "The recorded error was '{error}'" + ) + + assert logs[5]["body"] == "The recorded value was hardcoded." + assert "sentry.message.template" not in logs[5]["attributes"] @minimum_python_37 From d2cb532459b3e954edbee3605af5e631749c547d Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 1 Sep 2025 09:56:39 +0000 Subject: [PATCH 03/88] release: 2.35.2 --- CHANGELOG.md | 7 +++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3af3f63a0..92cd4573b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2.35.2 + +### Various fixes & improvements + +- fix(logs): Do not attach template if there are no parameters (#4728) by @sentrivana +- Update tox.ini (#4721) by @sentrivana + ## 2.35.1 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index 7ad137b9ed..0863980aac 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.35.1" +release = "2.35.2" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 2d3ab230b6..d7a0603a10 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1329,4 +1329,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.35.1" +VERSION = "2.35.2" diff --git a/setup.py b/setup.py index f16f4e3fd0..ecb24290c8 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.35.1", + version="2.35.2", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From 814cd5959b58350cb81fa8b21502fcdfe3adf960 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 1 Sep 2025 12:01:32 +0200 Subject: [PATCH 04/88] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92cd4573b0..19f734976f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,6 @@ ### Various fixes & improvements - fix(logs): Do not attach template if there are no parameters (#4728) by @sentrivana -- Update tox.ini (#4721) by @sentrivana ## 2.35.1 From 173df643f8847e8e0bb0a59da7cda0a019af283e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 1 Sep 2025 15:19:24 +0200 Subject: [PATCH 05/88] Update tox.ini (#4731) Regular update --- tox.ini | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index bbc1d57c12..0dbcef2c64 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-08-26T08:59:42.512502+00:00 +# Last generated: 2025-09-01T12:08:33.833560+00:00 [tox] requires = @@ -147,7 +147,7 @@ envlist = {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 - {py3.10,py3.12,py3.13}-openai_agents-v0.2.9 + {py3.10,py3.12,py3.13}-openai_agents-v0.2.10 {py3.8,py3.10,py3.11}-huggingface_hub-v0.22.2 {py3.8,py3.11,py3.12}-huggingface_hub-v0.26.5 @@ -210,7 +210,7 @@ envlist = {py3.8,py3.10,py3.11}-strawberry-v0.209.8 {py3.8,py3.11,py3.12}-strawberry-v0.233.3 {py3.9,py3.12,py3.13}-strawberry-v0.257.0 - {py3.9,py3.12,py3.13}-strawberry-v0.280.0 + {py3.9,py3.12,py3.13}-strawberry-v0.281.0 # ~~~ Network ~~~ @@ -218,6 +218,7 @@ envlist = {py3.7,py3.9,py3.10}-grpc-v1.46.5 {py3.7,py3.11,py3.12}-grpc-v1.60.2 {py3.9,py3.12,py3.13}-grpc-v1.74.0 + {py3.9,py3.12,py3.13}-grpc-v1.75.0rc1 # ~~~ Tasks ~~~ @@ -311,6 +312,7 @@ envlist = {py3.7,py3.12,py3.13}-typer-v0.15.4 {py3.7,py3.12,py3.13}-typer-v0.16.1 + {py3.7,py3.12,py3.13}-typer-v0.17.3 @@ -527,7 +529,7 @@ deps = openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.1.0: openai-agents==0.1.0 - openai_agents-v0.2.9: openai-agents==0.2.9 + openai_agents-v0.2.10: openai-agents==0.2.10 openai_agents: pytest-asyncio huggingface_hub-v0.22.2: huggingface_hub==0.22.2 @@ -601,7 +603,7 @@ deps = strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8 strawberry-v0.233.3: strawberry-graphql[fastapi,flask]==0.233.3 strawberry-v0.257.0: strawberry-graphql[fastapi,flask]==0.257.0 - strawberry-v0.280.0: strawberry-graphql[fastapi,flask]==0.280.0 + strawberry-v0.281.0: strawberry-graphql[fastapi,flask]==0.281.0 strawberry: httpx strawberry-v0.209.8: pydantic<2.11 strawberry-v0.233.3: pydantic<2.11 @@ -613,6 +615,7 @@ deps = grpc-v1.46.5: grpcio==1.46.5 grpc-v1.60.2: grpcio==1.60.2 grpc-v1.74.0: grpcio==1.74.0 + grpc-v1.75.0rc1: grpcio==1.75.0rc1 grpc: protobuf grpc: mypy-protobuf grpc: types-protobuf @@ -782,6 +785,7 @@ deps = typer-v0.15.4: typer==0.15.4 typer-v0.16.1: typer==0.16.1 + typer-v0.17.3: typer==0.17.3 From 1d473b62490508cc3f8070bdaccc2e5bd20b182a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 2 Sep 2025 10:44:22 +0200 Subject: [PATCH 06/88] toxgen: Add variants & move OpenAI under toxgen (#4730) Adds supports for variants, i.e., the same test suite running with a slightly different setup (for instance, a different set of dependencies, like `openai` and `openai_notiktoken`). To add a variant, simply add a new test suite to the config. The tricky part is naming. I had to rename `openai` to `openai_base` since otherwise the `openai_notiktoken` and `openai_agents` test suite would be run with `tox -e py-openai` / `./scripts/runtox.sh py-openai` due to how tox works. They should be treated as three different suites. Closes https://github.com/getsentry/sentry-python/issues/4507 --- .github/workflows/test-integrations-ai.yml | 16 ++++-- scripts/populate_tox/README.md | 8 +++ scripts/populate_tox/config.py | 18 +++++++ scripts/populate_tox/populate_tox.py | 8 +-- scripts/populate_tox/tox.jinja | 24 +-------- .../split_tox_gh_actions.py | 3 +- tox.ini | 53 +++++++++++-------- 7 files changed, 77 insertions(+), 53 deletions(-) diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index a784f9fc47..a6995fa268 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -62,10 +62,14 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh "py${{ matrix.python-version }}-langchain-latest" - - name: Test openai latest + - name: Test openai_base latest run: | set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-openai-latest" + ./scripts/runtox.sh "py${{ matrix.python-version }}-openai_base-latest" + - name: Test openai_notiktoken latest + run: | + set -x # print commands that are executed + ./scripts/runtox.sh "py${{ matrix.python-version }}-openai_notiktoken-latest" - name: Test openai_agents latest run: | set -x # print commands that are executed @@ -141,10 +145,14 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-langchain" - - name: Test openai pinned + - name: Test openai_base pinned + run: | + set -x # print commands that are executed + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai_base" + - name: Test openai_notiktoken pinned run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai" + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai_notiktoken" - name: Test openai_agents pinned run: | set -x # print commands that are executed diff --git a/scripts/populate_tox/README.md b/scripts/populate_tox/README.md index c9a3b67ba0..c48d57734d 100644 --- a/scripts/populate_tox/README.md +++ b/scripts/populate_tox/README.md @@ -153,6 +153,14 @@ be expressed like so: } ``` +### `integration_name` + +Sometimes, the name of the test suite doesn't match the name of the integration. +For example, we have the `openai_base` and `openai_notiktoken` test suites, both +of which are actually testing the `openai` integration. If this is the case, you can use the `integration_name` key to define the name of the integration. If not provided, it will default to the name of the test suite. + +Linking an integration to a test suite allows the script to access integration configuration like for example the minimum version defined in `sentry_sdk/integrations/__init__.py`. + ## How-Tos diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index f395289b4a..65e463a947 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -139,6 +139,24 @@ "loguru": { "package": "loguru", }, + "openai_base": { + "package": "openai", + "integration_name": "openai", + "deps": { + "*": ["pytest-asyncio", "tiktoken"], + "<1.55": ["httpx<0.28"], + }, + "python": ">=3.8", + }, + "openai_notiktoken": { + "package": "openai", + "integration_name": "openai", + "deps": { + "*": ["pytest-asyncio"], + "<1.55": ["httpx<0.28"], + }, + "python": ">=3.8", + }, "openai_agents": { "package": "openai-agents", "deps": { diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 3ca5ab18c8..53d5609d50 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -76,8 +76,6 @@ "httpx", "langchain", "langchain_notiktoken", - "openai", - "openai_notiktoken", "pure_eval", "quart", "ray", @@ -141,7 +139,11 @@ def _prefilter_releases( - the list of prefiltered releases - an optional prerelease if there is one that should be tested """ - min_supported = _MIN_VERSIONS.get(integration) + integration_name = ( + TEST_SUITE_CONFIG[integration].get("integration_name") or integration + ) + + min_supported = _MIN_VERSIONS.get(integration_name) if min_supported is not None: min_supported = Version(".".join(map(str, min_supported))) else: diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 4c3b86af81..632ce7c71b 100644 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -83,13 +83,6 @@ envlist = {py3.9,py3.11,py3.12}-langchain-latest {py3.9,py3.11,py3.12}-langchain-notiktoken - # OpenAI - {py3.9,py3.11,py3.12}-openai-v1.0 - {py3.9,py3.11,py3.12}-openai-v1.22 - {py3.9,py3.11,py3.12}-openai-v1.55 - {py3.9,py3.11,py3.12}-openai-latest - {py3.9,py3.11,py3.12}-openai-notiktoken - # OpenTelemetry (OTel) {py3.7,py3.9,py3.12,py3.13}-opentelemetry @@ -252,20 +245,6 @@ deps = langchain-{latest,notiktoken}: openai>=1.6.1 langchain-latest: tiktoken~=0.6.0 - # OpenAI - openai: pytest-asyncio - openai-v1.0: openai~=1.0.0 - openai-v1.0: tiktoken - openai-v1.0: httpx<0.28.0 - openai-v1.22: openai~=1.22.0 - openai-v1.22: tiktoken - openai-v1.22: httpx<0.28.0 - openai-v1.55: openai~=1.55.0 - openai-v1.55: tiktoken - openai-latest: openai - openai-latest: tiktoken~=0.6.0 - openai-notiktoken: openai - # OpenTelemetry (OTel) opentelemetry: opentelemetry-distro @@ -401,7 +380,8 @@ setenv = launchdarkly: TESTPATH=tests/integrations/launchdarkly litestar: TESTPATH=tests/integrations/litestar loguru: TESTPATH=tests/integrations/loguru - openai: TESTPATH=tests/integrations/openai + openai_base: TESTPATH=tests/integrations/openai + openai_notiktoken: TESTPATH=tests/integrations/openai openai_agents: TESTPATH=tests/integrations/openai_agents openfeature: TESTPATH=tests/integrations/openfeature opentelemetry: TESTPATH=tests/integrations/opentelemetry 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 af1ff84cd6..305ceeae76 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -62,7 +62,8 @@ "anthropic", "cohere", "langchain", - "openai", + "openai_base", + "openai_notiktoken", "openai_agents", "huggingface_hub", ], diff --git a/tox.ini b/tox.ini index 0dbcef2c64..eea115876b 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-01T12:08:33.833560+00:00 +# Last generated: 2025-09-01T14:09:50.564158+00:00 [tox] requires = @@ -83,13 +83,6 @@ envlist = {py3.9,py3.11,py3.12}-langchain-latest {py3.9,py3.11,py3.12}-langchain-notiktoken - # OpenAI - {py3.9,py3.11,py3.12}-openai-v1.0 - {py3.9,py3.11,py3.12}-openai-v1.22 - {py3.9,py3.11,py3.12}-openai-v1.55 - {py3.9,py3.11,py3.12}-openai-latest - {py3.9,py3.11,py3.12}-openai-notiktoken - # OpenTelemetry (OTel) {py3.7,py3.9,py3.12,py3.13}-opentelemetry @@ -145,6 +138,16 @@ envlist = {py3.9,py3.11,py3.12}-cohere-v5.13.12 {py3.9,py3.11,py3.12}-cohere-v5.17.0 + {py3.8,py3.11,py3.12}-openai_base-v1.0.1 + {py3.8,py3.11,py3.12}-openai_base-v1.35.15 + {py3.8,py3.11,py3.12}-openai_base-v1.69.0 + {py3.8,py3.12,py3.13}-openai_base-v1.102.0 + + {py3.8,py3.11,py3.12}-openai_notiktoken-v1.0.1 + {py3.8,py3.11,py3.12}-openai_notiktoken-v1.35.15 + {py3.8,py3.11,py3.12}-openai_notiktoken-v1.69.0 + {py3.8,py3.12,py3.13}-openai_notiktoken-v1.102.0 + {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 {py3.10,py3.12,py3.13}-openai_agents-v0.2.10 @@ -423,20 +426,6 @@ deps = langchain-{latest,notiktoken}: openai>=1.6.1 langchain-latest: tiktoken~=0.6.0 - # OpenAI - openai: pytest-asyncio - openai-v1.0: openai~=1.0.0 - openai-v1.0: tiktoken - openai-v1.0: httpx<0.28.0 - openai-v1.22: openai~=1.22.0 - openai-v1.22: tiktoken - openai-v1.22: httpx<0.28.0 - openai-v1.55: openai~=1.55.0 - openai-v1.55: tiktoken - openai-latest: openai - openai-latest: tiktoken~=0.6.0 - openai-notiktoken: openai - # OpenTelemetry (OTel) opentelemetry: opentelemetry-distro @@ -527,6 +516,23 @@ deps = cohere-v5.13.12: cohere==5.13.12 cohere-v5.17.0: cohere==5.17.0 + openai_base-v1.0.1: openai==1.0.1 + openai_base-v1.35.15: openai==1.35.15 + openai_base-v1.69.0: openai==1.69.0 + openai_base-v1.102.0: openai==1.102.0 + openai_base: pytest-asyncio + openai_base: tiktoken + openai_base-v1.0.1: httpx<0.28 + openai_base-v1.35.15: httpx<0.28 + + openai_notiktoken-v1.0.1: openai==1.0.1 + openai_notiktoken-v1.35.15: openai==1.35.15 + openai_notiktoken-v1.69.0: openai==1.69.0 + openai_notiktoken-v1.102.0: openai==1.102.0 + openai_notiktoken: pytest-asyncio + openai_notiktoken-v1.0.1: httpx<0.28 + openai_notiktoken-v1.35.15: httpx<0.28 + openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.1.0: openai-agents==0.1.0 openai_agents-v0.2.10: openai-agents==0.2.10 @@ -831,7 +837,8 @@ setenv = launchdarkly: TESTPATH=tests/integrations/launchdarkly litestar: TESTPATH=tests/integrations/litestar loguru: TESTPATH=tests/integrations/loguru - openai: TESTPATH=tests/integrations/openai + openai_base: TESTPATH=tests/integrations/openai + openai_notiktoken: TESTPATH=tests/integrations/openai openai_agents: TESTPATH=tests/integrations/openai_agents openfeature: TESTPATH=tests/integrations/openfeature opentelemetry: TESTPATH=tests/integrations/opentelemetry From 65755f95351581bd89101ce8eba9ff4768c9474e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 2 Sep 2025 13:42:55 +0200 Subject: [PATCH 07/88] tests: Move langchain under toxgen (#4734) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - move the langchain test suites to be governed by toxgen - this indirectly results in removing the `-latest` tests in the AI group 🎉 - `-latest` tests predate toxgen and langchain was the last non-toxgen test suite (the rest was just skipped) -- all AI tests are now pinned - updated the naming scheme to use dashes instead of underscores for variants so that it's clearer if something is part of the name of the integration or if it denotes a variant - for instance, `openai-base` means this is the `base` variant of the `openai` test suite, but `openai_agents` means this is the `openai_agents` test suite (no variant) I'm explicitly ignoring the two alpha versions of 1.0 since adapting the integration to work with those is out of scope: [dedicated issue](https://github.com/getsentry/sentry-python/issues/4735) Part of https://github.com/getsentry/sentry-python/issues/4506 --- .github/workflows/test-integrations-ai.yml | 99 ++--------------- scripts/populate_tox/config.py | 24 ++++- scripts/populate_tox/tox.jinja | 27 +---- .../split_tox_gh_actions.py | 7 +- sentry_sdk/integrations/__init__.py | 2 +- tox.ini | 100 +++++++++--------- 6 files changed, 93 insertions(+), 166 deletions(-) diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index a6995fa268..72a4253744 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -22,89 +22,6 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-ai-latest: - name: AI (latest) - timeout-minutes: 30 - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - python-version: ["3.9","3.11","3.12"] - # python3.6 reached EOL and is no longer being supported on - # new versions of hosted runners on Github Actions - # ubuntu-20.04 is the last version that supported python3.6 - # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 - os: [ubuntu-22.04] - # Use Docker container only for Python 3.6 - container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} - steps: - - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 - if: ${{ matrix.python-version != '3.6' }} - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - name: Setup Test Env - run: | - pip install "coverage[toml]" tox - - name: Erase coverage - run: | - coverage erase - - name: Test anthropic latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-anthropic-latest" - - name: Test cohere latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-cohere-latest" - - name: Test langchain latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-langchain-latest" - - name: Test openai_base latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-openai_base-latest" - - name: Test openai_notiktoken latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-openai_notiktoken-latest" - - name: Test openai_agents latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-openai_agents-latest" - - name: Test huggingface_hub latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-huggingface_hub-latest" - - name: Generate coverage XML (Python 3.6) - if: ${{ !cancelled() && matrix.python-version == '3.6' }} - run: | - export COVERAGE_RCFILE=.coveragerc36 - coverage combine .coverage-sentry-* - coverage xml --ignore-errors - - name: Generate coverage XML - if: ${{ !cancelled() && matrix.python-version != '3.6' }} - run: | - coverage combine .coverage-sentry-* - coverage xml - - name: Upload coverage to Codecov - if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml - verbose: true test-ai-pinned: name: AI (pinned) timeout-minutes: 30 @@ -141,18 +58,22 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-cohere" - - name: Test langchain pinned + - name: Test langchain-base pinned + run: | + set -x # print commands that are executed + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-langchain-base" + - name: Test langchain-notiktoken pinned run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-langchain" - - name: Test openai_base pinned + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-langchain-notiktoken" + - name: Test openai-base pinned run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai_base" - - name: Test openai_notiktoken pinned + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai-base" + - name: Test openai-notiktoken pinned run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai_notiktoken" + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai-notiktoken" - name: Test openai_agents pinned run: | set -x # print commands that are executed diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 65e463a947..0d4d0fe6ee 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -126,6 +126,26 @@ "huggingface_hub": { "package": "huggingface_hub", }, + "langchain-base": { + "package": "langchain", + "integration_name": "langchain", + "deps": { + "*": ["openai", "tiktoken", "langchain-openai"], + "<=0.1": ["httpx<0.28.0"], + ">=0.3": ["langchain-community"], + }, + "include": "<1.0", + }, + "langchain-notiktoken": { + "package": "langchain", + "integration_name": "langchain", + "deps": { + "*": ["openai", "langchain-openai"], + "<=0.1": ["httpx<0.28.0"], + ">=0.3": ["langchain-community"], + }, + "include": "<1.0", + }, "launchdarkly": { "package": "launchdarkly-server-sdk", }, @@ -139,7 +159,7 @@ "loguru": { "package": "loguru", }, - "openai_base": { + "openai-base": { "package": "openai", "integration_name": "openai", "deps": { @@ -148,7 +168,7 @@ }, "python": ">=3.8", }, - "openai_notiktoken": { + "openai-notiktoken": { "package": "openai", "integration_name": "openai", "deps": { diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 632ce7c71b..2b968b7aa1 100644 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -77,12 +77,6 @@ envlist = {py3.9,py3.11,py3.12}-httpx-v{0.25,0.27} {py3.9,py3.12,py3.13}-httpx-latest - # Langchain - {py3.9,py3.11,py3.12}-langchain-v0.1 - {py3.9,py3.11,py3.12}-langchain-v0.3 - {py3.9,py3.11,py3.12}-langchain-latest - {py3.9,py3.11,py3.12}-langchain-notiktoken - # OpenTelemetry (OTel) {py3.7,py3.9,py3.12,py3.13}-opentelemetry @@ -231,20 +225,6 @@ deps = httpx-v0.27: httpx~=0.27.0 httpx-latest: httpx - # Langchain - langchain-v0.1: openai~=1.0.0 - langchain-v0.1: langchain~=0.1.11 - langchain-v0.1: tiktoken~=0.6.0 - langchain-v0.1: httpx<0.28.0 - langchain-v0.3: langchain~=0.3.0 - langchain-v0.3: langchain-community - langchain-v0.3: tiktoken - langchain-v0.3: openai - langchain-{latest,notiktoken}: langchain - langchain-{latest,notiktoken}: langchain-openai - langchain-{latest,notiktoken}: openai>=1.6.1 - langchain-latest: tiktoken~=0.6.0 - # OpenTelemetry (OTel) opentelemetry: opentelemetry-distro @@ -376,12 +356,13 @@ setenv = httpx: TESTPATH=tests/integrations/httpx huey: TESTPATH=tests/integrations/huey huggingface_hub: TESTPATH=tests/integrations/huggingface_hub - langchain: TESTPATH=tests/integrations/langchain + langchain-base: TESTPATH=tests/integrations/langchain + langchain-notiktoken: TESTPATH=tests/integrations/langchain launchdarkly: TESTPATH=tests/integrations/launchdarkly litestar: TESTPATH=tests/integrations/litestar loguru: TESTPATH=tests/integrations/loguru - openai_base: TESTPATH=tests/integrations/openai - openai_notiktoken: TESTPATH=tests/integrations/openai + openai-base: TESTPATH=tests/integrations/openai + openai-notiktoken: TESTPATH=tests/integrations/openai openai_agents: TESTPATH=tests/integrations/openai_agents openfeature: TESTPATH=tests/integrations/openfeature opentelemetry: TESTPATH=tests/integrations/opentelemetry 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 305ceeae76..1c3435f43b 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -61,9 +61,10 @@ "AI": [ "anthropic", "cohere", - "langchain", - "openai_base", - "openai_notiktoken", + "langchain-base", + "langchain-notiktoken", + "openai-base", + "openai-notiktoken", "openai_agents", "huggingface_hub", ], diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index e2eadd523d..6f0109aced 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -141,7 +141,7 @@ def iter_default_integrations(with_auto_enabling_integrations): "graphene": (3, 3), "grpc": (1, 32, 0), # grpcio "huggingface_hub": (0, 22), - "langchain": (0, 0, 210), + "langchain": (0, 1, 0), "launchdarkly": (9, 8, 0), "loguru": (0, 7, 0), "openai": (1, 0, 0), diff --git a/tox.ini b/tox.ini index eea115876b..0898bc888f 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-01T14:09:50.564158+00:00 +# Last generated: 2025-09-02T10:59:55.249513+00:00 [tox] requires = @@ -77,12 +77,6 @@ envlist = {py3.9,py3.11,py3.12}-httpx-v{0.25,0.27} {py3.9,py3.12,py3.13}-httpx-latest - # Langchain - {py3.9,py3.11,py3.12}-langchain-v0.1 - {py3.9,py3.11,py3.12}-langchain-v0.3 - {py3.9,py3.11,py3.12}-langchain-latest - {py3.9,py3.11,py3.12}-langchain-notiktoken - # OpenTelemetry (OTel) {py3.7,py3.9,py3.12,py3.13}-opentelemetry @@ -138,15 +132,23 @@ envlist = {py3.9,py3.11,py3.12}-cohere-v5.13.12 {py3.9,py3.11,py3.12}-cohere-v5.17.0 - {py3.8,py3.11,py3.12}-openai_base-v1.0.1 - {py3.8,py3.11,py3.12}-openai_base-v1.35.15 - {py3.8,py3.11,py3.12}-openai_base-v1.69.0 - {py3.8,py3.12,py3.13}-openai_base-v1.102.0 + {py3.9,py3.11,py3.12}-langchain-base-v0.1.20 + {py3.9,py3.11,py3.12}-langchain-base-v0.2.17 + {py3.9,py3.12,py3.13}-langchain-base-v0.3.27 + + {py3.9,py3.11,py3.12}-langchain-notiktoken-v0.1.20 + {py3.9,py3.11,py3.12}-langchain-notiktoken-v0.2.17 + {py3.9,py3.12,py3.13}-langchain-notiktoken-v0.3.27 - {py3.8,py3.11,py3.12}-openai_notiktoken-v1.0.1 - {py3.8,py3.11,py3.12}-openai_notiktoken-v1.35.15 - {py3.8,py3.11,py3.12}-openai_notiktoken-v1.69.0 - {py3.8,py3.12,py3.13}-openai_notiktoken-v1.102.0 + {py3.8,py3.11,py3.12}-openai-base-v1.0.1 + {py3.8,py3.11,py3.12}-openai-base-v1.35.15 + {py3.8,py3.11,py3.12}-openai-base-v1.69.0 + {py3.8,py3.12,py3.13}-openai-base-v1.102.0 + + {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 + {py3.8,py3.11,py3.12}-openai-notiktoken-v1.35.15 + {py3.8,py3.11,py3.12}-openai-notiktoken-v1.69.0 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.102.0 {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 @@ -412,20 +414,6 @@ deps = httpx-v0.27: httpx~=0.27.0 httpx-latest: httpx - # Langchain - langchain-v0.1: openai~=1.0.0 - langchain-v0.1: langchain~=0.1.11 - langchain-v0.1: tiktoken~=0.6.0 - langchain-v0.1: httpx<0.28.0 - langchain-v0.3: langchain~=0.3.0 - langchain-v0.3: langchain-community - langchain-v0.3: tiktoken - langchain-v0.3: openai - langchain-{latest,notiktoken}: langchain - langchain-{latest,notiktoken}: langchain-openai - langchain-{latest,notiktoken}: openai>=1.6.1 - langchain-latest: tiktoken~=0.6.0 - # OpenTelemetry (OTel) opentelemetry: opentelemetry-distro @@ -516,22 +504,37 @@ deps = cohere-v5.13.12: cohere==5.13.12 cohere-v5.17.0: cohere==5.17.0 - openai_base-v1.0.1: openai==1.0.1 - openai_base-v1.35.15: openai==1.35.15 - openai_base-v1.69.0: openai==1.69.0 - openai_base-v1.102.0: openai==1.102.0 - openai_base: pytest-asyncio - openai_base: tiktoken - openai_base-v1.0.1: httpx<0.28 - openai_base-v1.35.15: httpx<0.28 - - openai_notiktoken-v1.0.1: openai==1.0.1 - openai_notiktoken-v1.35.15: openai==1.35.15 - openai_notiktoken-v1.69.0: openai==1.69.0 - openai_notiktoken-v1.102.0: openai==1.102.0 - openai_notiktoken: pytest-asyncio - openai_notiktoken-v1.0.1: httpx<0.28 - openai_notiktoken-v1.35.15: httpx<0.28 + langchain-base-v0.1.20: langchain==0.1.20 + langchain-base-v0.2.17: langchain==0.2.17 + langchain-base-v0.3.27: langchain==0.3.27 + langchain-base: openai + langchain-base: tiktoken + langchain-base: langchain-openai + langchain-base-v0.3.27: langchain-community + + langchain-notiktoken-v0.1.20: langchain==0.1.20 + langchain-notiktoken-v0.2.17: langchain==0.2.17 + langchain-notiktoken-v0.3.27: langchain==0.3.27 + langchain-notiktoken: openai + langchain-notiktoken: langchain-openai + langchain-notiktoken-v0.3.27: langchain-community + + openai-base-v1.0.1: openai==1.0.1 + openai-base-v1.35.15: openai==1.35.15 + openai-base-v1.69.0: openai==1.69.0 + openai-base-v1.102.0: openai==1.102.0 + openai-base: pytest-asyncio + openai-base: tiktoken + openai-base-v1.0.1: httpx<0.28 + openai-base-v1.35.15: httpx<0.28 + + openai-notiktoken-v1.0.1: openai==1.0.1 + openai-notiktoken-v1.35.15: openai==1.35.15 + openai-notiktoken-v1.69.0: openai==1.69.0 + openai-notiktoken-v1.102.0: openai==1.102.0 + openai-notiktoken: pytest-asyncio + openai-notiktoken-v1.0.1: httpx<0.28 + openai-notiktoken-v1.35.15: httpx<0.28 openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.1.0: openai-agents==0.1.0 @@ -833,12 +836,13 @@ setenv = httpx: TESTPATH=tests/integrations/httpx huey: TESTPATH=tests/integrations/huey huggingface_hub: TESTPATH=tests/integrations/huggingface_hub - langchain: TESTPATH=tests/integrations/langchain + langchain-base: TESTPATH=tests/integrations/langchain + langchain-notiktoken: TESTPATH=tests/integrations/langchain launchdarkly: TESTPATH=tests/integrations/launchdarkly litestar: TESTPATH=tests/integrations/litestar loguru: TESTPATH=tests/integrations/loguru - openai_base: TESTPATH=tests/integrations/openai - openai_notiktoken: TESTPATH=tests/integrations/openai + openai-base: TESTPATH=tests/integrations/openai + openai-notiktoken: TESTPATH=tests/integrations/openai openai_agents: TESTPATH=tests/integrations/openai_agents openfeature: TESTPATH=tests/integrations/openfeature opentelemetry: TESTPATH=tests/integrations/opentelemetry From b1a8b6333ceafd116fbaf1f50e1e14967f5d9f94 Mon Sep 17 00:00:00 2001 From: Alex Alderman Webb Date: Tue, 2 Sep 2025 14:14:36 +0200 Subject: [PATCH 08/88] fix(openai): Avoid double exit causing an unraisable exception (#4736) Add parameter to the method capturing exceptions in the OpenAI integration, to determine if the span context is closed with __exit__() or not. The option is used to prevent double exit scenarios when a span context is managed automatically. Related to: https://github.com/getsentry/sentry-python/issues/4723 --- sentry_sdk/integrations/openai.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 187f795807..6ea545322c 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -78,12 +78,12 @@ def count_tokens(self, s): return 0 -def _capture_exception(exc): - # type: (Any) -> None +def _capture_exception(exc, manual_span_cleanup=True): + # type: (Any, bool) -> None # Close an eventually open span # We need to do this by hand because we are not using the start_span context manager current_span = sentry_sdk.get_current_span() - if current_span is not None: + if manual_span_cleanup and current_span is not None: current_span.__exit__(None, None, None) event, hint = event_from_exception( @@ -516,7 +516,7 @@ def _execute_sync(f, *args, **kwargs): try: result = f(*args, **kwargs) except Exception as e: - _capture_exception(e) + _capture_exception(e, manual_span_cleanup=False) raise e from None return gen.send(result) @@ -550,7 +550,7 @@ async def _execute_async(f, *args, **kwargs): try: result = await f(*args, **kwargs) except Exception as e: - _capture_exception(e) + _capture_exception(e, manual_span_cleanup=False) raise e from None return gen.send(result) From 9c4eb5e272910aafadd65e8ae92696034647e47f Mon Sep 17 00:00:00 2001 From: Alex Alderman Webb Date: Tue, 2 Sep 2025 15:27:41 +0200 Subject: [PATCH 09/88] tests: Trigger Pytest failure when an unraisable exception occurs (#4738) Set Pytest command-line argument to return non-zero exit code when an unraisable exception is encountered. Closes https://github.com/getsentry/sentry-python/issues/4723. --- scripts/populate_tox/tox.jinja | 2 +- tox.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) mode change 100644 => 100755 scripts/populate_tox/tox.jinja diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja old mode 100644 new mode 100755 index 2b968b7aa1..42c570b111 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -429,7 +429,7 @@ commands = ; Running `pytest` as an executable suffers from an import error ; when loading tests in scenarios. In particular, django fails to ; load the settings from the test module. - python -m pytest {env:TESTPATH} -o junit_suite_name={envname} {posargs} + python -m pytest -W error::pytest.PytestUnraisableExceptionWarning {env:TESTPATH} -o junit_suite_name={envname} {posargs} [testenv:linters] commands = diff --git a/tox.ini b/tox.ini index 0898bc888f..a8e66cb80f 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-02T10:59:55.249513+00:00 +# Last generated: 2025-09-02T12:34:09.591543+00:00 [tox] requires = @@ -909,7 +909,7 @@ commands = ; Running `pytest` as an executable suffers from an import error ; when loading tests in scenarios. In particular, django fails to ; load the settings from the test module. - python -m pytest {env:TESTPATH} -o junit_suite_name={envname} {posargs} + python -m pytest -W error::pytest.PytestUnraisableExceptionWarning {env:TESTPATH} -o junit_suite_name={envname} {posargs} [testenv:linters] commands = From c213abf4a4ea9a09f8387fd192e8ee1992851657 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 2 Sep 2025 16:01:12 +0200 Subject: [PATCH 10/88] Remove old langchain test suites from ignore list (#4737) Forgot to remove these two from the toxgen ignore list. Shouldn't have any actual effect on tests since the test suites are now called differently. --- scripts/populate_tox/populate_tox.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 53d5609d50..179a466944 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -74,8 +74,6 @@ "chalice", "gcp", "httpx", - "langchain", - "langchain_notiktoken", "pure_eval", "quart", "ray", From 4456351b9156fd44b5797583d37889ed2af70517 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 3 Sep 2025 08:46:15 +0200 Subject: [PATCH 11/88] Fix `openai_agents` in CI (#4742) A new version of `openai`, which is a dependency of `openai_agents`, [came out an hour ago](https://pypi.org/project/openai/#history), which [broke](https://github.com/getsentry/sentry-python/actions/runs/17405958869/job/49410259073) our CI. Pinning for now. --- scripts/populate_tox/config.py | 1 + tox.ini | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 0d4d0fe6ee..69f7b02e21 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -181,6 +181,7 @@ "package": "openai-agents", "deps": { "*": ["pytest-asyncio"], + "<=0.2.10": ["openai<1.103.0"], }, "python": ">=3.10", }, diff --git a/tox.ini b/tox.ini index a8e66cb80f..c45c72bf85 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-02T12:34:09.591543+00:00 +# Last generated: 2025-09-02T14:49:13.002983+00:00 [tox] requires = @@ -143,12 +143,12 @@ envlist = {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.11,py3.12}-openai-base-v1.35.15 {py3.8,py3.11,py3.12}-openai-base-v1.69.0 - {py3.8,py3.12,py3.13}-openai-base-v1.102.0 + {py3.8,py3.12,py3.13}-openai-base-v1.103.0 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.35.15 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.69.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.102.0 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.103.0 {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 @@ -522,7 +522,7 @@ deps = openai-base-v1.0.1: openai==1.0.1 openai-base-v1.35.15: openai==1.35.15 openai-base-v1.69.0: openai==1.69.0 - openai-base-v1.102.0: openai==1.102.0 + openai-base-v1.103.0: openai==1.103.0 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 @@ -531,7 +531,7 @@ deps = openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.35.15: openai==1.35.15 openai-notiktoken-v1.69.0: openai==1.69.0 - openai-notiktoken-v1.102.0: openai==1.102.0 + openai-notiktoken-v1.103.0: openai==1.103.0 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.35.15: httpx<0.28 @@ -540,6 +540,9 @@ deps = openai_agents-v0.1.0: openai-agents==0.1.0 openai_agents-v0.2.10: openai-agents==0.2.10 openai_agents: pytest-asyncio + openai_agents-v0.0.19: openai<1.103.0 + openai_agents-v0.1.0: openai<1.103.0 + openai_agents-v0.2.10: openai<1.103.0 huggingface_hub-v0.22.2: huggingface_hub==0.22.2 huggingface_hub-v0.26.5: huggingface_hub==0.26.5 From f702ec94badc17bd09d7d3ccf7414fde01a173c8 Mon Sep 17 00:00:00 2001 From: Alex Alderman Webb Date: Wed, 3 Sep 2025 09:44:55 +0200 Subject: [PATCH 12/88] fix: Constrain types of ai_track decorator (#4745) I followed how other functions in the SDK are typed. For example, other wrappers have the signature `(F) -> F` for a type variable `F`, although here the function can be async as well. Closes https://github.com/getsentry/sentry-python/issues/4663 --- sentry_sdk/ai/monitoring.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/ai/monitoring.py b/sentry_sdk/ai/monitoring.py index e3f372c3ba..9dd1aa132c 100644 --- a/sentry_sdk/ai/monitoring.py +++ b/sentry_sdk/ai/monitoring.py @@ -10,7 +10,9 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Optional, Callable, Any + from typing import Optional, Callable, Awaitable, Any, Union, TypeVar + + F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) _ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) @@ -26,9 +28,9 @@ def get_ai_pipeline_name(): def ai_track(description, **span_kwargs): - # type: (str, Any) -> Callable[..., Any] + # type: (str, Any) -> Callable[[F], F] def decorator(f): - # type: (Callable[..., Any]) -> Callable[..., Any] + # type: (F) -> F def sync_wrapped(*args, **kwargs): # type: (Any, Any) -> Any curr_pipeline = _ai_pipeline_name.get() @@ -88,9 +90,9 @@ async def async_wrapped(*args, **kwargs): return res if inspect.iscoroutinefunction(f): - return wraps(f)(async_wrapped) + return wraps(f)(async_wrapped) # type: ignore else: - return wraps(f)(sync_wrapped) + return wraps(f)(sync_wrapped) # type: ignore return decorator From 5f2adcffecff85b1f736f93701cf154d58f85653 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 3 Sep 2025 16:11:11 +0200 Subject: [PATCH 13/88] Wrap span restoration in `__exit__` in `capture_internal_exceptions` (#4719) Ref https://github.com/getsentry/sentry-python/issues/4718 Does not solve the underlying issue and might leave things in an inconsistent state, but it's still preferable to letting an error bubble up to the user. --- sentry_sdk/tracing.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index c9b357305a..0d1fcc45da 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -8,6 +8,7 @@ from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA, SPANTEMPLATE from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.utils import ( + capture_internal_exceptions, get_current_thread_meta, is_valid_sample_rate, logger, @@ -418,10 +419,11 @@ def __exit__(self, ty, value, tb): if value is not None and should_be_treated_as_error(ty, value): self.set_status(SPANSTATUS.INTERNAL_ERROR) - scope, old_span = self._context_manager_state - del self._context_manager_state - self.finish(scope) - scope.span = old_span + with capture_internal_exceptions(): + scope, old_span = self._context_manager_state + del self._context_manager_state + self.finish(scope) + scope.span = old_span @property def containing_transaction(self): From 6d6e8a2e70fee7553675288b82e28ba311ffca3c Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 3 Sep 2025 16:13:51 +0200 Subject: [PATCH 14/88] Don't fail if there is no `_context_manager_state` (#4698) This is not a fix -- it just makes the SDK not propagate an internal SDK exception upwards. --- sentry_sdk/profiler/transaction_profiler.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/profiler/transaction_profiler.py b/sentry_sdk/profiler/transaction_profiler.py index 3743b7c905..d228f77de9 100644 --- a/sentry_sdk/profiler/transaction_profiler.py +++ b/sentry_sdk/profiler/transaction_profiler.py @@ -45,6 +45,7 @@ ) from sentry_sdk.utils import ( capture_internal_exception, + capture_internal_exceptions, get_current_thread_meta, is_gevent, is_valid_sample_rate, @@ -369,12 +370,13 @@ def __enter__(self): def __exit__(self, ty, value, tb): # type: (Optional[Any], Optional[Any], Optional[Any]) -> None - self.stop() + with capture_internal_exceptions(): + self.stop() - scope, old_profile = self._context_manager_state - del self._context_manager_state + scope, old_profile = self._context_manager_state + del self._context_manager_state - scope.profile = old_profile + scope.profile = old_profile def write(self, ts, sample): # type: (int, ExtractedSample) -> None From a6e3b50004c13f32de8c30e7632c164a39d7babe Mon Sep 17 00:00:00 2001 From: Alex Alderman Webb Date: Wed, 3 Sep 2025 16:49:29 +0200 Subject: [PATCH 15/88] feat(integrations): Add unraisable exception integration (#4733) Adds an uncaught exception integration, enabled by default. The integration forwards the exception to Sentry only if the exception value and stacktrace are set. Closes https://github.com/getsentry/sentry-python/issues/374 --- sentry_sdk/integrations/unraisablehook.py | 53 ++++++++++++++++++ .../unraisablehook/test_unraisablehook.py | 56 +++++++++++++++++++ tests/test_basics.py | 1 + 3 files changed, 110 insertions(+) create mode 100644 sentry_sdk/integrations/unraisablehook.py create mode 100644 tests/integrations/unraisablehook/test_unraisablehook.py diff --git a/sentry_sdk/integrations/unraisablehook.py b/sentry_sdk/integrations/unraisablehook.py new file mode 100644 index 0000000000..cfb8212c71 --- /dev/null +++ b/sentry_sdk/integrations/unraisablehook.py @@ -0,0 +1,53 @@ +import sys + +import sentry_sdk +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) +from sentry_sdk.integrations import Integration + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Callable + from typing import Any + + +class UnraisablehookIntegration(Integration): + identifier = "unraisablehook" + + @staticmethod + def setup_once(): + # type: () -> None + sys.unraisablehook = _make_unraisable(sys.unraisablehook) + + +def _make_unraisable(old_unraisablehook): + # type: (Callable[[sys.UnraisableHookArgs], Any]) -> Callable[[sys.UnraisableHookArgs], Any] + def sentry_sdk_unraisablehook(unraisable): + # type: (sys.UnraisableHookArgs) -> None + integration = sentry_sdk.get_client().get_integration(UnraisablehookIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_unraisablehook(unraisable) + + if unraisable.exc_value and unraisable.exc_traceback: + with capture_internal_exceptions(): + event, hint = event_from_exception( + ( + unraisable.exc_type, + unraisable.exc_value, + unraisable.exc_traceback, + ), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "unraisablehook", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_unraisablehook(unraisable) + + return sentry_sdk_unraisablehook diff --git a/tests/integrations/unraisablehook/test_unraisablehook.py b/tests/integrations/unraisablehook/test_unraisablehook.py new file mode 100644 index 0000000000..2f97886ce8 --- /dev/null +++ b/tests/integrations/unraisablehook/test_unraisablehook.py @@ -0,0 +1,56 @@ +import pytest +import sys +import subprocess + +from textwrap import dedent + + +TEST_PARAMETERS = [ + ("", "HttpTransport"), + ('_experiments={"transport_http2": True}', "Http2Transport"), +] + +minimum_python_38 = pytest.mark.skipif( + sys.version_info < (3, 8), + reason="The unraisable exception hook is only available in Python 3.8 and above.", +) + + +@minimum_python_38 +@pytest.mark.parametrize("options, transport", TEST_PARAMETERS) +def test_unraisablehook(tmpdir, options, transport): + app = tmpdir.join("app.py") + app.write( + dedent( + """ + from sentry_sdk import init, transport + from sentry_sdk.integrations.unraisablehook import UnraisablehookIntegration + + class Undeletable: + def __del__(self): + 1 / 0 + + def capture_envelope(self, envelope): + print("capture_envelope was called") + event = envelope.get_event() + if event is not None: + print(event) + + transport.{transport}.capture_envelope = capture_envelope + + init("http://foobar@localhost/123", integrations=[UnraisablehookIntegration()], {options}) + + undeletable = Undeletable() + del undeletable + """.format( + transport=transport, options=options + ) + ) + ) + + output = subprocess.check_output( + [sys.executable, str(app)], stderr=subprocess.STDOUT + ) + + assert b"ZeroDivisionError" in output + assert b"capture_envelope was called" in output diff --git a/tests/test_basics.py b/tests/test_basics.py index 2eeba78216..45303c9a59 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -870,6 +870,7 @@ def foo(event, hint): (["celery"], "sentry.python"), (["dedupe"], "sentry.python"), (["excepthook"], "sentry.python"), + (["unraisablehook"], "sentry.python"), (["executing"], "sentry.python"), (["modules"], "sentry.python"), (["pure_eval"], "sentry.python"), From 4e845d5767f8fd43f7eee310afc269bc070c9b3f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 3 Sep 2025 16:51:28 +0200 Subject: [PATCH 16/88] tests: Support dashes in test suite names (#4740) - actually support dashes in integration names in `tox.ini`/toxgen - make `split_tox_gh_actions.py` actually fail if it fails to parse `tox.ini` Context: `split_tox_gh_actions.py` was actually failing because it assumed there can't be dashes in test suite names, but since it was just printing the error instead of actually exiting with an error code, we didn't notice this in CI. --- .../split_tox_gh_actions.py | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) 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 1c3435f43b..cf83e0a3fe 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -17,6 +17,7 @@ import configparser import hashlib +import re import sys from collections import defaultdict from functools import reduce @@ -25,6 +26,18 @@ from jinja2 import Environment, FileSystemLoader +TOXENV_REGEX = re.compile( + r""" + {?(?P(py\d+\.\d+,?)+)}? + -(?P[a-z](?:[a-z_]|-(?!v{?\d|latest))*[a-z0-9]) + (?:-( + (v{?(?P[0-9.]+[0-9a-z,.]*}?)) + | + (?Platest) + ))? +""", + re.VERBOSE, +) OUT_DIR = Path(__file__).resolve().parent.parent.parent / ".github" / "workflows" TOX_FILE = Path(__file__).resolve().parent.parent.parent / "tox.ini" @@ -202,29 +215,37 @@ def parse_tox(): py_versions_pinned = defaultdict(set) py_versions_latest = defaultdict(set) + parsed_correctly = True + for line in lines: # normalize lines line = line.strip().lower() try: # parse tox environment definition - try: - (raw_python_versions, framework, framework_versions) = line.split("-") - except ValueError: - (raw_python_versions, framework) = line.split("-") - framework_versions = [] + parsed = TOXENV_REGEX.match(line) + if not parsed: + print(f"ERROR reading line {line}") + raise ValueError("Failed to parse tox environment definition") + + groups = parsed.groupdict() + raw_python_versions = groups["py_versions"] + framework = groups["framework"] + framework_versions_latest = groups.get("framework_versions_latest") or False # collect python versions to test the framework in - raw_python_versions = set( - raw_python_versions.replace("{", "").replace("}", "").split(",") - ) - if "latest" in framework_versions: + raw_python_versions = set(raw_python_versions.split(",")) + if framework_versions_latest: py_versions_latest[framework] |= raw_python_versions else: py_versions_pinned[framework] |= raw_python_versions - except ValueError: + except Exception: print(f"ERROR reading line {line}") + parsed_correctly = False + + if not parsed_correctly: + raise RuntimeError("Failed to parse tox.ini") py_versions_pinned = _normalize_py_versions(py_versions_pinned) py_versions_latest = _normalize_py_versions(py_versions_latest) From 7bc91eda417db023b76bd4193c80288943a27a65 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 3 Sep 2025 17:10:33 +0200 Subject: [PATCH 17/88] tests: Move arq under toxgen (#4739) Remove hardcoded arq config, generate with toxgen instead. --- scripts/populate_tox/config.py | 7 +++ scripts/populate_tox/populate_tox.py | 1 - scripts/populate_tox/tox.jinja | 12 ---- tox.ini | 87 ++++++++++++++-------------- 4 files changed, 51 insertions(+), 56 deletions(-) diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 69f7b02e21..f6093b0250 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -29,6 +29,13 @@ }, "python": ">=3.8", }, + "arq": { + "package": "arq", + "deps": { + "*": ["async-timeout", "pytest-asyncio", "fakeredis>=2.2.0,<2.8"], + "<=0.23": ["pydantic<2"], + }, + }, "bottle": { "package": "bottle", "deps": { diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 179a466944..a8c58938ae 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -67,7 +67,6 @@ "potel", # Integrations that can be migrated -- we should eventually remove all # of these from the IGNORE list - "arq", "asyncpg", "beam", "boto3", diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 42c570b111..115b99fd5c 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -36,10 +36,6 @@ envlist = # At a minimum, we should test against at least the lowest # and the latest supported version of a framework. - # Arq - {py3.7,py3.11}-arq-v{0.23} - {py3.7,py3.12,py3.13}-arq-latest - # Asgi {py3.7,py3.12,py3.13}-asgi @@ -164,14 +160,6 @@ deps = # === Integrations === - # Arq - arq-v0.23: arq~=0.23.0 - arq-v0.23: pydantic<2 - arq-latest: arq - arq: fakeredis>=2.2.0,<2.8 - arq: pytest-asyncio - arq: async-timeout - # Asgi asgi: pytest-asyncio asgi: async-asgi-testclient diff --git a/tox.ini b/tox.ini index c45c72bf85..f2ad720a25 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-02T14:49:13.002983+00:00 +# Last generated: 2025-09-03T15:01:21.035943+00:00 [tox] requires = @@ -36,10 +36,6 @@ envlist = # At a minimum, we should test against at least the lowest # and the latest supported version of a framework. - # Arq - {py3.7,py3.11}-arq-v{0.23} - {py3.7,py3.12,py3.13}-arq-latest - # Asgi {py3.7,py3.12,py3.13}-asgi @@ -123,9 +119,9 @@ envlist = # ~~~ AI ~~~ {py3.8,py3.11,py3.12}-anthropic-v0.16.0 - {py3.8,py3.11,py3.12}-anthropic-v0.32.0 - {py3.8,py3.11,py3.12}-anthropic-v0.48.0 - {py3.8,py3.12,py3.13}-anthropic-v0.64.0 + {py3.8,py3.11,py3.12}-anthropic-v0.33.1 + {py3.8,py3.11,py3.12}-anthropic-v0.50.0 + {py3.8,py3.12,py3.13}-anthropic-v0.66.0 {py3.9,py3.10,py3.11}-cohere-v5.4.0 {py3.9,py3.11,py3.12}-cohere-v5.9.4 @@ -141,14 +137,14 @@ envlist = {py3.9,py3.12,py3.13}-langchain-notiktoken-v0.3.27 {py3.8,py3.11,py3.12}-openai-base-v1.0.1 - {py3.8,py3.11,py3.12}-openai-base-v1.35.15 - {py3.8,py3.11,py3.12}-openai-base-v1.69.0 - {py3.8,py3.12,py3.13}-openai-base-v1.103.0 + {py3.8,py3.11,py3.12}-openai-base-v1.36.1 + {py3.8,py3.11,py3.12}-openai-base-v1.71.0 + {py3.8,py3.12,py3.13}-openai-base-v1.105.0 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 - {py3.8,py3.11,py3.12}-openai-notiktoken-v1.35.15 - {py3.8,py3.11,py3.12}-openai-notiktoken-v1.69.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.103.0 + {py3.8,py3.11,py3.12}-openai-notiktoken-v1.36.1 + {py3.8,py3.11,py3.12}-openai-notiktoken-v1.71.0 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.105.0 {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 @@ -227,6 +223,11 @@ envlist = # ~~~ Tasks ~~~ + {py3.7,py3.9,py3.10}-arq-v0.23 + {py3.7,py3.10,py3.11}-arq-v0.24.0 + {py3.7,py3.10,py3.11}-arq-v0.25.0 + {py3.8,py3.11,py3.12}-arq-v0.26.3 + {py3.6,py3.7,py3.8}-celery-v4.4.7 {py3.6,py3.7,py3.8}-celery-v5.0.5 {py3.8,py3.12,py3.13}-celery-v5.5.3 @@ -250,9 +251,9 @@ envlist = {py3.6,py3.7}-django-v1.11.29 {py3.6,py3.8,py3.9}-django-v2.2.28 {py3.6,py3.9,py3.10}-django-v3.2.25 - {py3.8,py3.11,py3.12}-django-v4.2.23 + {py3.8,py3.11,py3.12}-django-v4.2.24 {py3.10,py3.11,py3.12}-django-v5.0.14 - {py3.10,py3.12,py3.13}-django-v5.2.5 + {py3.10,py3.12,py3.13}-django-v5.2.6 {py3.6,py3.7,py3.8}-flask-v1.1.4 {py3.8,py3.12,py3.13}-flask-v2.3.3 @@ -353,14 +354,6 @@ deps = # === Integrations === - # Arq - arq-v0.23: arq~=0.23.0 - arq-v0.23: pydantic<2 - arq-latest: arq - arq: fakeredis>=2.2.0,<2.8 - arq: pytest-asyncio - arq: async-timeout - # Asgi asgi: pytest-asyncio asgi: async-asgi-testclient @@ -491,13 +484,12 @@ deps = # ~~~ AI ~~~ anthropic-v0.16.0: anthropic==0.16.0 - anthropic-v0.32.0: anthropic==0.32.0 - anthropic-v0.48.0: anthropic==0.48.0 - anthropic-v0.64.0: anthropic==0.64.0 + anthropic-v0.33.1: anthropic==0.33.1 + anthropic-v0.50.0: anthropic==0.50.0 + anthropic-v0.66.0: anthropic==0.66.0 anthropic: pytest-asyncio anthropic-v0.16.0: httpx<0.28.0 - anthropic-v0.32.0: httpx<0.28.0 - anthropic-v0.48.0: httpx<0.28.0 + anthropic-v0.33.1: httpx<0.28.0 cohere-v5.4.0: cohere==5.4.0 cohere-v5.9.4: cohere==5.9.4 @@ -520,21 +512,21 @@ deps = langchain-notiktoken-v0.3.27: langchain-community openai-base-v1.0.1: openai==1.0.1 - openai-base-v1.35.15: openai==1.35.15 - openai-base-v1.69.0: openai==1.69.0 - openai-base-v1.103.0: openai==1.103.0 + openai-base-v1.36.1: openai==1.36.1 + openai-base-v1.71.0: openai==1.71.0 + openai-base-v1.105.0: openai==1.105.0 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 - openai-base-v1.35.15: httpx<0.28 + openai-base-v1.36.1: httpx<0.28 openai-notiktoken-v1.0.1: openai==1.0.1 - openai-notiktoken-v1.35.15: openai==1.35.15 - openai-notiktoken-v1.69.0: openai==1.69.0 - openai-notiktoken-v1.103.0: openai==1.103.0 + openai-notiktoken-v1.36.1: openai==1.36.1 + openai-notiktoken-v1.71.0: openai==1.71.0 + openai-notiktoken-v1.105.0: openai==1.105.0 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 - openai-notiktoken-v1.35.15: httpx<0.28 + openai-notiktoken-v1.36.1: httpx<0.28 openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.1.0: openai-agents==0.1.0 @@ -635,6 +627,15 @@ deps = # ~~~ Tasks ~~~ + arq-v0.23: arq==0.23 + arq-v0.24.0: arq==0.24.0 + arq-v0.25.0: arq==0.25.0 + arq-v0.26.3: arq==0.26.3 + arq: async-timeout + arq: pytest-asyncio + arq: fakeredis>=2.2.0,<2.8 + arq-v0.23: pydantic<2 + celery-v4.4.7: celery==4.4.7 celery-v5.0.5: celery==5.0.5 celery-v5.5.3: celery==5.5.3 @@ -661,23 +662,23 @@ deps = django-v1.11.29: django==1.11.29 django-v2.2.28: django==2.2.28 django-v3.2.25: django==3.2.25 - django-v4.2.23: django==4.2.23 + django-v4.2.24: django==4.2.24 django-v5.0.14: django==5.0.14 - django-v5.2.5: django==5.2.5 + django-v5.2.6: django==5.2.6 django: psycopg2-binary django: djangorestframework django: pytest-django django: Werkzeug django-v2.2.28: channels[daphne] django-v3.2.25: channels[daphne] - django-v4.2.23: channels[daphne] + django-v4.2.24: channels[daphne] django-v5.0.14: channels[daphne] - django-v5.2.5: channels[daphne] + django-v5.2.6: channels[daphne] django-v2.2.28: six django-v3.2.25: pytest-asyncio - django-v4.2.23: pytest-asyncio + django-v4.2.24: pytest-asyncio django-v5.0.14: pytest-asyncio - django-v5.2.5: pytest-asyncio + django-v5.2.6: pytest-asyncio django-v1.11.29: djangorestframework>=3.0,<4.0 django-v1.11.29: Werkzeug<2.1.0 django-v2.2.28: djangorestframework>=3.0,<4.0 From 6f396f490a0a2c9bee4f4f8e66db49cd1f738d81 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 4 Sep 2025 09:24:28 +0200 Subject: [PATCH 18/88] meta: Update instructions on release process (#4755) --- CONTRIBUTING.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 024a374f85..313910fe56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,18 +138,18 @@ _(only relevant for Python SDK core team)_ - On GitHub in the `sentry-python` repository, go to "Actions" and select the "Release" workflow. - Click on "Run workflow" on the right side, and make sure the `master` branch is selected. -- Set the "Version to release" input field. Here you decide if it is a major, minor or patch release. (See "Versioning Policy" below) +- Set the "Version to release" input field. Here you decide if it is a major, minor or patch release (see "Versioning Policy" below). - Click "Run Workflow". -This will trigger [Craft](https://github.com/getsentry/craft) to prepare everything needed for a release. (For more information, see [craft prepare](https://github.com/getsentry/craft#craft-prepare-preparing-a-new-release).) At the end of this process a release issue is created in the [Publish](https://github.com/getsentry/publish) repository. (Example release issue: https://github.com/getsentry/publish/issues/815) +This will trigger [Craft](https://github.com/getsentry/craft) to prepare everything needed for a release. (For more information, see [craft prepare](https://github.com/getsentry/craft#craft-prepare-preparing-a-new-release).) At the end of this process a release issue is created in the [Publish](https://github.com/getsentry/publish) repository (example issue: https://github.com/getsentry/publish/issues/815). -Now one of the persons with release privileges (most probably your engineering manager) will review this issue and then add the `accepted` label to the issue. +At the same time, the action will create a release branch in the `sentry-python` repository called `release/`. You may want to check out this branch and polish the auto-generated `CHANGELOG.md` before proceeding by including code snippets, descriptions, reordering and reformatting entries, in order to make the changelog as useful and actionable to users as possible. -There are always two persons involved in a release. +CI must be passing on the release branch; if there's any failure, Craft will not create a release. -If you are in a hurry and the release should be out immediately, there is a Slack channel called `#proj-release-approval` where you can see your release issue and where you can ping people to please have a look immediately. +Once the release branch is ready and green, notify your team (or your manager). They will need to add the `accepted` label to the issue in the `publish` repo. There are always two people involved in a release. Do not accept your own releases. -When the release issue is labeled `accepted`, [Craft](https://github.com/getsentry/craft) is triggered again to publish the release to all the right platforms. (See [craft publish](https://github.com/getsentry/craft#craft-publish-publishing-the-release) for more information.) At the end of this process the release issue on GitHub will be closed and the release is completed! Congratulations! +When the release issue is labeled `accepted`, [Craft](https://github.com/getsentry/craft) is triggered again to publish the release to all the right platforms. See [craft publish](https://github.com/getsentry/craft#craft-publish-publishing-the-release) for more information. At the end of this process, the release issue on GitHub will be closed and the release is completed! Congratulations! There is a sequence diagram visualizing all this in the [README.md](https://github.com/getsentry/publish) of the `Publish` repository. From c7f3b396920f652c9776baf4be7ebfedfbda7df7 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 4 Sep 2025 07:31:06 +0000 Subject: [PATCH 19/88] release: 2.36.0 --- CHANGELOG.md | 19 +++++++++++++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f734976f..a0b3c1647e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 2.36.0 + +### Various fixes & improvements + +- meta: Update instructions on release process (#4755) by @sentrivana +- tests: Move arq under toxgen (#4739) by @sentrivana +- tests: Support dashes in test suite names (#4740) by @sentrivana +- feat(integrations): Add unraisable exception integration (#4733) by @alexander-alderman-webb +- Don't fail if there is no `_context_manager_state` (#4698) by @sentrivana +- Wrap span restoration in `__exit__` in `capture_internal_exceptions` (#4719) by @sentrivana +- fix: Constrain types of ai_track decorator (#4745) by @alexander-alderman-webb +- Fix `openai_agents` in CI (#4742) by @sentrivana +- Remove old langchain test suites from ignore list (#4737) by @sentrivana +- tests: Trigger Pytest failure when an unraisable exception occurs (#4738) by @alexander-alderman-webb +- fix(openai): Avoid double exit causing an unraisable exception (#4736) by @alexander-alderman-webb +- tests: Move langchain under toxgen (#4734) by @sentrivana +- toxgen: Add variants & move OpenAI under toxgen (#4730) by @sentrivana +- Update tox.ini (#4731) by @sentrivana + ## 2.35.2 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index 0863980aac..835c20b112 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.35.2" +release = "2.36.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index d7a0603a10..3ed8efd506 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1329,4 +1329,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.35.2" +VERSION = "2.36.0" diff --git a/setup.py b/setup.py index ecb24290c8..828dd43461 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.35.2", + version="2.36.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From ea097ede7d8c90b65b6412250070033b66aa8283 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 4 Sep 2025 09:44:07 +0200 Subject: [PATCH 20/88] docs: Add snippet to configure sending unraisable exceptions to Sentry --- CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0b3c1647e..2c10ef8b7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,24 @@ ### Various fixes & improvements +- **New integration:** Unraisable exceptions (#4733) by @alexander-alderman-webb + + Add the unraisable exception integration to your sentry_sdk.init call: +```python +import sentry_sdk +from sentry_sdk.integrations.unraisablehook import UnraisablehookIntegration + +sentry_sdk.init( + dsn="...", + integrations=[ + UnraisablehookIntegration(), + ] +) +``` + - meta: Update instructions on release process (#4755) by @sentrivana - tests: Move arq under toxgen (#4739) by @sentrivana - tests: Support dashes in test suite names (#4740) by @sentrivana -- feat(integrations): Add unraisable exception integration (#4733) by @alexander-alderman-webb - Don't fail if there is no `_context_manager_state` (#4698) by @sentrivana - Wrap span restoration in `__exit__` in `capture_internal_exceptions` (#4719) by @sentrivana - fix: Constrain types of ai_track decorator (#4745) by @alexander-alderman-webb From ff9b1c37f2ccbdad39e1950dab09546b29e2d247 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 4 Sep 2025 10:23:49 +0200 Subject: [PATCH 21/88] tests: Remove openai pin and update tox (#4748) [v1.104.2](https://github.com/openai/openai-python/releases/tag/v1.104.2) of openai added back the missing export that was causing problems for `openai_agents`, so we can remove the version pin again. --- scripts/populate_tox/config.py | 1 - tox.ini | 17 +++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index f6093b0250..a2c4c8770c 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -188,7 +188,6 @@ "package": "openai-agents", "deps": { "*": ["pytest-asyncio"], - "<=0.2.10": ["openai<1.103.0"], }, "python": ">=3.10", }, diff --git a/tox.ini b/tox.ini index f2ad720a25..67ba6eadc6 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-03T15:01:21.035943+00:00 +# Last generated: 2025-09-04T07:00:53.509946+00:00 [tox] requires = @@ -148,7 +148,7 @@ envlist = {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 - {py3.10,py3.12,py3.13}-openai_agents-v0.2.10 + {py3.10,py3.12,py3.13}-openai_agents-v0.2.11 {py3.8,py3.10,py3.11}-huggingface_hub-v0.22.2 {py3.8,py3.11,py3.12}-huggingface_hub-v0.26.5 @@ -313,8 +313,8 @@ envlist = {py3.6}-trytond-v4.8.18 {py3.6,py3.7,py3.8}-trytond-v5.8.16 {py3.8,py3.10,py3.11}-trytond-v6.8.17 - {py3.8,py3.11,py3.12}-trytond-v7.0.34 - {py3.9,py3.12,py3.13}-trytond-v7.6.5 + {py3.8,py3.11,py3.12}-trytond-v7.0.35 + {py3.9,py3.12,py3.13}-trytond-v7.6.6 {py3.7,py3.12,py3.13}-typer-v0.15.4 {py3.7,py3.12,py3.13}-typer-v0.16.1 @@ -530,11 +530,8 @@ deps = openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.1.0: openai-agents==0.1.0 - openai_agents-v0.2.10: openai-agents==0.2.10 + openai_agents-v0.2.11: openai-agents==0.2.11 openai_agents: pytest-asyncio - openai_agents-v0.0.19: openai<1.103.0 - openai_agents-v0.1.0: openai<1.103.0 - openai_agents-v0.2.10: openai<1.103.0 huggingface_hub-v0.22.2: huggingface_hub==0.22.2 huggingface_hub-v0.26.5: huggingface_hub==0.26.5 @@ -790,8 +787,8 @@ deps = trytond-v4.8.18: trytond==4.8.18 trytond-v5.8.16: trytond==5.8.16 trytond-v6.8.17: trytond==6.8.17 - trytond-v7.0.34: trytond==7.0.34 - trytond-v7.6.5: trytond==7.6.5 + trytond-v7.0.35: trytond==7.0.35 + trytond-v7.6.6: trytond==7.6.6 trytond: werkzeug trytond-v4.6.22: werkzeug<1.0 trytond-v4.8.18: werkzeug<1.0 From c378c2d8d9032a50f4371df20a1929756342b245 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 4 Sep 2025 13:45:07 +0200 Subject: [PATCH 22/88] tests: Move beam under toxgen (#4759) - move beam under toxgen - lower the waiting time between pypi requests Ref https://github.com/getsentry/sentry-python/issues/4506 --- .github/workflows/test-integrations-tasks.yml | 2 +- scripts/populate_tox/config.py | 4 ++++ scripts/populate_tox/populate_tox.py | 3 +-- scripts/populate_tox/tox.jinja | 8 -------- tox.ini | 20 ++++++++++--------- 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/.github/workflows/test-integrations-tasks.yml b/.github/workflows/test-integrations-tasks.yml index a489f64410..f842683285 100644 --- a/.github/workflows/test-integrations-tasks.yml +++ b/.github/workflows/test-integrations-tasks.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7","3.8","3.10","3.11","3.12","3.13"] + python-version: ["3.7","3.10","3.11","3.12","3.13"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index a2c4c8770c..689253e889 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -36,6 +36,10 @@ "<=0.23": ["pydantic<2"], }, }, + "beam": { + "package": "apache-beam", + "python": ">=3.7", + }, "bottle": { "package": "bottle", "deps": { diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index a8c58938ae..3d9ef23b66 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -40,7 +40,7 @@ lstrip_blocks=True, ) -PYPI_COOLDOWN = 0.15 # seconds to wait between requests to PyPI +PYPI_COOLDOWN = 0.1 # seconds to wait between requests to PyPI PYPI_PROJECT_URL = "https://pypi.python.org/pypi/{project}/json" PYPI_VERSION_URL = "https://pypi.python.org/pypi/{project}/{version}/json" @@ -68,7 +68,6 @@ # Integrations that can be migrated -- we should eventually remove all # of these from the IGNORE list "asyncpg", - "beam", "boto3", "chalice", "gcp", diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 115b99fd5c..65a5ba3f36 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -46,10 +46,6 @@ envlist = # AWS Lambda {py3.8,py3.9,py3.11,py3.13}-aws_lambda - # Beam - {py3.7}-beam-v{2.12} - {py3.8,py3.11}-beam-latest - # Boto3 {py3.6,py3.7}-boto3-v{1.12} {py3.7,py3.11,py3.12}-boto3-v{1.23} @@ -177,10 +173,6 @@ deps = aws_lambda: requests aws_lambda: uvicorn - # Beam - beam-v2.12: apache-beam~=2.12.0 - beam-latest: apache-beam - # Boto3 boto3-v1.12: boto3~=1.12.0 boto3-v1.23: boto3~=1.23.0 diff --git a/tox.ini b/tox.ini index 67ba6eadc6..fd633654be 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-04T07:00:53.509946+00:00 +# Last generated: 2025-09-04T10:35:13.756355+00:00 [tox] requires = @@ -46,10 +46,6 @@ envlist = # AWS Lambda {py3.8,py3.9,py3.11,py3.13}-aws_lambda - # Beam - {py3.7}-beam-v{2.12} - {py3.8,py3.11}-beam-latest - # Boto3 {py3.6,py3.7}-boto3-v{1.12} {py3.7,py3.11,py3.12}-boto3-v{1.23} @@ -228,6 +224,11 @@ envlist = {py3.7,py3.10,py3.11}-arq-v0.25.0 {py3.8,py3.11,py3.12}-arq-v0.26.3 + {py3.7}-beam-v2.14.0 + {py3.7,py3.8}-beam-v2.32.0 + {py3.8,py3.10,py3.11}-beam-v2.50.0 + {py3.9,py3.12,py3.13}-beam-v2.67.0 + {py3.6,py3.7,py3.8}-celery-v4.4.7 {py3.6,py3.7,py3.8}-celery-v5.0.5 {py3.8,py3.12,py3.13}-celery-v5.5.3 @@ -371,10 +372,6 @@ deps = aws_lambda: requests aws_lambda: uvicorn - # Beam - beam-v2.12: apache-beam~=2.12.0 - beam-latest: apache-beam - # Boto3 boto3-v1.12: boto3~=1.12.0 boto3-v1.23: boto3~=1.23.0 @@ -633,6 +630,11 @@ deps = arq: fakeredis>=2.2.0,<2.8 arq-v0.23: pydantic<2 + beam-v2.14.0: apache-beam==2.14.0 + beam-v2.32.0: apache-beam==2.32.0 + beam-v2.50.0: apache-beam==2.50.0 + beam-v2.67.0: apache-beam==2.67.0 + celery-v4.4.7: celery==4.4.7 celery-v5.0.5: celery==5.0.5 celery-v5.5.3: celery==5.5.3 From 58a9827e1a5bb207a34651409a303bc21890fb66 Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Thu, 4 Sep 2025 15:38:33 +0200 Subject: [PATCH 23/88] feat: Add LangGraph integration (#4727) - Add LangGraph integration - Compilation of StateGraphs results in an agent creation span (according to [OTEL semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/)) - Runtime executions are done on Pregel instances - we are wrapping their invoke & ainvoke which produces the invoke_agent spans - There's some internals that automatically switch between invoke & stream on CompiledStateGraph (which is a subclass of Pregel), which results in duplicate spans if both are instrumented. For now, only invoke is wrapped to prevent this duplication. - Agent handoffs in LangGraph are done via tools - so there is no real possibility to create handoff spans within the SDK. Looks like this will be handled in product logic instead. Closes TET-991 Closes PY-1799 --------- Co-authored-by: Anton Pirker --- .github/workflows/test-integrations-ai.yml | 4 + pyproject.toml | 4 + scripts/populate_tox/config.py | 3 + scripts/populate_tox/tox.jinja | 1 + .../split_tox_gh_actions.py | 1 + sentry_sdk/consts.py | 1 + sentry_sdk/integrations/__init__.py | 2 + sentry_sdk/integrations/langgraph.py | 321 +++++++++ setup.py | 1 + tests/integrations/langgraph/__init__.py | 3 + .../integrations/langgraph/test_langgraph.py | 632 ++++++++++++++++++ tox.ini | 9 +- 12 files changed, 981 insertions(+), 1 deletion(-) create mode 100644 sentry_sdk/integrations/langgraph.py create mode 100644 tests/integrations/langgraph/__init__.py create mode 100644 tests/integrations/langgraph/test_langgraph.py diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index 72a4253744..26a8bdb8bb 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -74,6 +74,10 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai-notiktoken" + - name: Test langgraph pinned + run: | + set -x # print commands that are executed + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-langgraph" - name: Test openai_agents pinned run: | set -x # print commands that are executed diff --git a/pyproject.toml b/pyproject.toml index deba247e39..44eded7641 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,6 +130,10 @@ ignore_missing_imports = true module = "langchain.*" ignore_missing_imports = true +[[tool.mypy.overrides]] +module = "langgraph.*" +ignore_missing_imports = true + [[tool.mypy.overrides]] module = "executing.*" ignore_missing_imports = true diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 689253e889..6795e36303 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -157,6 +157,9 @@ }, "include": "<1.0", }, + "langgraph": { + "package": "langgraph", + }, "launchdarkly": { "package": "launchdarkly-server-sdk", }, diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 65a5ba3f36..241e0ca288 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -338,6 +338,7 @@ setenv = huggingface_hub: TESTPATH=tests/integrations/huggingface_hub langchain-base: TESTPATH=tests/integrations/langchain langchain-notiktoken: TESTPATH=tests/integrations/langchain + langgraph: TESTPATH=tests/integrations/langgraph launchdarkly: TESTPATH=tests/integrations/launchdarkly litestar: TESTPATH=tests/integrations/litestar loguru: TESTPATH=tests/integrations/loguru 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 cf83e0a3fe..51ee614d04 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -78,6 +78,7 @@ "langchain-notiktoken", "openai-base", "openai-notiktoken", + "langgraph", "openai_agents", "huggingface_hub", ], diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 3ed8efd506..5480ef5dce 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -792,6 +792,7 @@ class OP: FUNCTION_AWS = "function.aws" FUNCTION_GCP = "function.gcp" GEN_AI_CHAT = "gen_ai.chat" + GEN_AI_CREATE_AGENT = "gen_ai.create_agent" GEN_AI_EMBEDDINGS = "gen_ai.embeddings" GEN_AI_EXECUTE_TOOL = "gen_ai.execute_tool" GEN_AI_HANDOFF = "gen_ai.handoff" diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 6f0109aced..7f202221a7 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -95,6 +95,7 @@ def iter_default_integrations(with_auto_enabling_integrations): "sentry_sdk.integrations.huey.HueyIntegration", "sentry_sdk.integrations.huggingface_hub.HuggingfaceHubIntegration", "sentry_sdk.integrations.langchain.LangchainIntegration", + "sentry_sdk.integrations.langgraph.LanggraphIntegration", "sentry_sdk.integrations.litestar.LitestarIntegration", "sentry_sdk.integrations.loguru.LoguruIntegration", "sentry_sdk.integrations.openai.OpenAIIntegration", @@ -142,6 +143,7 @@ def iter_default_integrations(with_auto_enabling_integrations): "grpc": (1, 32, 0), # grpcio "huggingface_hub": (0, 22), "langchain": (0, 1, 0), + "langgraph": (0, 6, 6), "launchdarkly": (9, 8, 0), "loguru": (0, 7, 0), "openai": (1, 0, 0), diff --git a/sentry_sdk/integrations/langgraph.py b/sentry_sdk/integrations/langgraph.py new file mode 100644 index 0000000000..4b241fe895 --- /dev/null +++ b/sentry_sdk/integrations/langgraph.py @@ -0,0 +1,321 @@ +from functools import wraps +from typing import Any, Callable, List, Optional + +import sentry_sdk +from sentry_sdk.ai.utils import set_data_normalized +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import safe_serialize + + +try: + from langgraph.graph import StateGraph + from langgraph.pregel import Pregel +except ImportError: + raise DidNotEnable("langgraph not installed") + + +class LanggraphIntegration(Integration): + identifier = "langgraph" + origin = f"auto.ai.{identifier}" + + def __init__(self, include_prompts=True): + # type: (LanggraphIntegration, bool) -> None + self.include_prompts = include_prompts + + @staticmethod + def setup_once(): + # type: () -> None + # LangGraph lets users create agents using a StateGraph or the Functional API. + # StateGraphs are then compiled to a CompiledStateGraph. Both CompiledStateGraph and + # the functional API execute on a Pregel instance. Pregel is the runtime for the graph + # and the invocation happens on Pregel, so patching the invoke methods takes care of both. + # The streaming methods are not patched, because due to some internal reasons, LangGraph + # will automatically patch the streaming methods to run through invoke, and by doing this + # we prevent duplicate spans for invocations. + StateGraph.compile = _wrap_state_graph_compile(StateGraph.compile) + if hasattr(Pregel, "invoke"): + Pregel.invoke = _wrap_pregel_invoke(Pregel.invoke) + if hasattr(Pregel, "ainvoke"): + Pregel.ainvoke = _wrap_pregel_ainvoke(Pregel.ainvoke) + + +def _get_graph_name(graph_obj): + # type: (Any) -> Optional[str] + for attr in ["name", "graph_name", "__name__", "_name"]: + if hasattr(graph_obj, attr): + name = getattr(graph_obj, attr) + if name and isinstance(name, str): + return name + return None + + +def _normalize_langgraph_message(message): + # type: (Any) -> Any + if not hasattr(message, "content"): + return None + + parsed = {"role": getattr(message, "type", None), "content": message.content} + + for attr in ["name", "tool_calls", "function_call", "tool_call_id"]: + if hasattr(message, attr): + value = getattr(message, attr) + if value is not None: + parsed[attr] = value + + return parsed + + +def _parse_langgraph_messages(state): + # type: (Any) -> Optional[List[Any]] + if not state: + return None + + messages = None + + if isinstance(state, dict): + messages = state.get("messages") + elif hasattr(state, "messages"): + messages = state.messages + elif hasattr(state, "get") and callable(state.get): + try: + messages = state.get("messages") + except Exception: + pass + + if not messages or not isinstance(messages, (list, tuple)): + return None + + normalized_messages = [] + for message in messages: + try: + normalized = _normalize_langgraph_message(message) + if normalized: + normalized_messages.append(normalized) + except Exception: + continue + + return normalized_messages if normalized_messages else None + + +def _wrap_state_graph_compile(f): + # type: (Callable[..., Any]) -> Callable[..., Any] + @wraps(f) + def new_compile(self, *args, **kwargs): + # type: (Any, Any, Any) -> Any + integration = sentry_sdk.get_client().get_integration(LanggraphIntegration) + if integration is None: + return f(self, *args, **kwargs) + with sentry_sdk.start_span( + op=OP.GEN_AI_CREATE_AGENT, + origin=LanggraphIntegration.origin, + ) as span: + compiled_graph = f(self, *args, **kwargs) + + compiled_graph_name = getattr(compiled_graph, "name", None) + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "create_agent") + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, compiled_graph_name) + + if compiled_graph_name: + span.description = f"create_agent {compiled_graph_name}" + else: + span.description = "create_agent" + + if kwargs.get("model", None) is not None: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, kwargs.get("model")) + + tools = None + get_graph = getattr(compiled_graph, "get_graph", None) + if get_graph and callable(get_graph): + graph_obj = compiled_graph.get_graph() + nodes = getattr(graph_obj, "nodes", None) + if nodes and isinstance(nodes, dict): + tools_node = nodes.get("tools") + if tools_node: + data = getattr(tools_node, "data", None) + if data and hasattr(data, "tools_by_name"): + tools = list(data.tools_by_name.keys()) + + if tools is not None: + span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools) + + return compiled_graph + + return new_compile + + +def _wrap_pregel_invoke(f): + # type: (Callable[..., Any]) -> Callable[..., Any] + + @wraps(f) + def new_invoke(self, *args, **kwargs): + # type: (Any, Any, Any) -> Any + integration = sentry_sdk.get_client().get_integration(LanggraphIntegration) + if integration is None: + return f(self, *args, **kwargs) + + graph_name = _get_graph_name(self) + span_name = ( + f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" + ) + + with sentry_sdk.start_span( + op=OP.GEN_AI_INVOKE_AGENT, + name=span_name, + origin=LanggraphIntegration.origin, + ) as span: + if graph_name: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + # Store input messages to later compare with output + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + safe_serialize(input_messages), + ) + + result = f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + return new_invoke + + +def _wrap_pregel_ainvoke(f): + # type: (Callable[..., Any]) -> Callable[..., Any] + + @wraps(f) + async def new_ainvoke(self, *args, **kwargs): + # type: (Any, Any, Any) -> Any + integration = sentry_sdk.get_client().get_integration(LanggraphIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + graph_name = _get_graph_name(self) + span_name = ( + f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" + ) + + with sentry_sdk.start_span( + op=OP.GEN_AI_INVOKE_AGENT, + name=span_name, + origin=LanggraphIntegration.origin, + ) as span: + if graph_name: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + safe_serialize(input_messages), + ) + + result = await f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + return new_ainvoke + + +def _get_new_messages(input_messages, output_messages): + # type: (Optional[List[Any]], Optional[List[Any]]) -> Optional[List[Any]] + """Extract only the new messages added during this invocation.""" + if not output_messages: + return None + + if not input_messages: + return output_messages + + # only return the new messages, aka the output messages that are not in the input messages + input_count = len(input_messages) + new_messages = ( + output_messages[input_count:] if len(output_messages) > input_count else [] + ) + + return new_messages if new_messages else None + + +def _extract_llm_response_text(messages): + # type: (Optional[List[Any]]) -> Optional[str] + if not messages: + return None + + for message in reversed(messages): + if isinstance(message, dict): + role = message.get("role") + if role in ["assistant", "ai"]: + content = message.get("content") + if content and isinstance(content, str): + return content + + return None + + +def _extract_tool_calls(messages): + # type: (Optional[List[Any]]) -> Optional[List[Any]] + if not messages: + return None + + tool_calls = [] + for message in messages: + if isinstance(message, dict): + msg_tool_calls = message.get("tool_calls") + if msg_tool_calls and isinstance(msg_tool_calls, list): + tool_calls.extend(msg_tool_calls) + + return tool_calls if tool_calls else None + + +def _set_response_attributes(span, input_messages, result, integration): + # type: (Any, Optional[List[Any]], Any, LanggraphIntegration) -> None + if not (should_send_default_pii() and integration.include_prompts): + return + + parsed_response_messages = _parse_langgraph_messages(result) + new_messages = _get_new_messages(input_messages, parsed_response_messages) + + llm_response_text = _extract_llm_response_text(new_messages) + if llm_response_text: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, llm_response_text) + elif new_messages: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(new_messages) + ) + else: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(result)) + + tool_calls = _extract_tool_calls(new_messages) + if tool_calls: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(tool_calls), + unpack=False, + ) diff --git a/setup.py b/setup.py index 828dd43461..ca6e7ec534 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ def get_file_text(file_name): "huey": ["huey>=2"], "huggingface_hub": ["huggingface_hub>=0.22"], "langchain": ["langchain>=0.0.210"], + "langgraph": ["langgraph>=0.6.6"], "launchdarkly": ["launchdarkly-server-sdk>=9.8.0"], "litestar": ["litestar>=2.0.0"], "loguru": ["loguru>=0.5"], diff --git a/tests/integrations/langgraph/__init__.py b/tests/integrations/langgraph/__init__.py new file mode 100644 index 0000000000..b7dd1cb562 --- /dev/null +++ b/tests/integrations/langgraph/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("langgraph") diff --git a/tests/integrations/langgraph/test_langgraph.py b/tests/integrations/langgraph/test_langgraph.py new file mode 100644 index 0000000000..5e35f772f5 --- /dev/null +++ b/tests/integrations/langgraph/test_langgraph.py @@ -0,0 +1,632 @@ +import asyncio +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from sentry_sdk import start_transaction +from sentry_sdk.consts import SPANDATA, OP + + +def mock_langgraph_imports(): + """Mock langgraph modules to prevent import errors.""" + mock_state_graph = MagicMock() + mock_pregel = MagicMock() + + langgraph_graph_mock = MagicMock() + langgraph_graph_mock.StateGraph = mock_state_graph + + langgraph_pregel_mock = MagicMock() + langgraph_pregel_mock.Pregel = mock_pregel + + sys.modules["langgraph"] = MagicMock() + sys.modules["langgraph.graph"] = langgraph_graph_mock + sys.modules["langgraph.pregel"] = langgraph_pregel_mock + + return mock_state_graph, mock_pregel + + +mock_state_graph, mock_pregel = mock_langgraph_imports() + +from sentry_sdk.integrations.langgraph import ( # noqa: E402 + LanggraphIntegration, + _parse_langgraph_messages, + _wrap_state_graph_compile, + _wrap_pregel_invoke, + _wrap_pregel_ainvoke, +) + + +class MockStateGraph: + def __init__(self, schema=None): + self.name = "test_graph" + self.schema = schema + self._compiled_graph = None + + def compile(self, *args, **kwargs): + compiled = MockCompiledGraph(self.name) + compiled.graph = self + return compiled + + +class MockCompiledGraph: + def __init__(self, name="test_graph"): + self.name = name + self._graph = None + + def get_graph(self): + return MockGraphRepresentation() + + def invoke(self, state, config=None): + return {"messages": [MockMessage("Response from graph")]} + + async def ainvoke(self, state, config=None): + return {"messages": [MockMessage("Async response from graph")]} + + +class MockGraphRepresentation: + def __init__(self): + self.nodes = {"tools": MockToolsNode()} + + +class MockToolsNode: + def __init__(self): + self.data = MockToolsData() + + +class MockToolsData: + def __init__(self): + self.tools_by_name = { + "search_tool": MockTool("search_tool"), + "calculator": MockTool("calculator"), + } + + +class MockTool: + def __init__(self, name): + self.name = name + + +class MockMessage: + def __init__( + self, + content, + name=None, + tool_calls=None, + function_call=None, + role=None, + type=None, + ): + self.content = content + self.name = name + self.tool_calls = tool_calls + self.function_call = function_call + self.role = role + # The integration uses getattr(message, "type", None) for the role in _normalize_langgraph_message + # Set default type based on name if type not explicitly provided + if type is None and name in ["assistant", "ai", "user", "system", "function"]: + self.type = name + else: + self.type = type + + +class MockPregelInstance: + def __init__(self, name="test_pregel"): + self.name = name + self.graph_name = name + + def invoke(self, state, config=None): + return {"messages": [MockMessage("Pregel response")]} + + async def ainvoke(self, state, config=None): + return {"messages": [MockMessage("Async Pregel response")]} + + +def test_langgraph_integration_init(): + """Test LanggraphIntegration initialization with different parameters.""" + integration = LanggraphIntegration() + assert integration.include_prompts is True + assert integration.identifier == "langgraph" + assert integration.origin == "auto.ai.langgraph" + + integration = LanggraphIntegration(include_prompts=False) + assert integration.include_prompts is False + assert integration.identifier == "langgraph" + assert integration.origin == "auto.ai.langgraph" + + +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +def test_state_graph_compile( + sentry_init, capture_events, send_default_pii, include_prompts +): + """Test StateGraph.compile() wrapper creates proper create_agent span.""" + sentry_init( + integrations=[LanggraphIntegration(include_prompts=include_prompts)], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + ) + events = capture_events() + graph = MockStateGraph() + + def original_compile(self, *args, **kwargs): + return MockCompiledGraph(self.name) + + with patch("sentry_sdk.integrations.langgraph.StateGraph"): + with start_transaction(): + wrapped_compile = _wrap_state_graph_compile(original_compile) + compiled_graph = wrapped_compile( + graph, model="test-model", checkpointer=None + ) + + assert compiled_graph is not None + assert compiled_graph.name == "test_graph" + + tx = events[0] + assert tx["type"] == "transaction" + + agent_spans = [span for span in tx["spans"] if span["op"] == OP.GEN_AI_CREATE_AGENT] + assert len(agent_spans) == 1 + + agent_span = agent_spans[0] + assert agent_span["description"] == "create_agent test_graph" + assert agent_span["origin"] == "auto.ai.langgraph" + assert agent_span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "create_agent" + assert agent_span["data"][SPANDATA.GEN_AI_AGENT_NAME] == "test_graph" + assert agent_span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "test-model" + assert SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS in agent_span["data"] + + tools_data = agent_span["data"][SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS] + assert tools_data == ["search_tool", "calculator"] + assert len(tools_data) == 2 + assert "search_tool" in tools_data + assert "calculator" in tools_data + + +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +def test_pregel_invoke(sentry_init, capture_events, send_default_pii, include_prompts): + """Test Pregel.invoke() wrapper creates proper invoke_agent span.""" + sentry_init( + integrations=[LanggraphIntegration(include_prompts=include_prompts)], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + ) + events = capture_events() + + test_state = { + "messages": [ + MockMessage("Hello, can you help me?", name="user"), + MockMessage("Of course! How can I assist you?", name="assistant"), + ] + } + + pregel = MockPregelInstance("test_graph") + + expected_assistant_response = "I'll help you with that task!" + expected_tool_calls = [ + { + "id": "call_test_123", + "type": "function", + "function": {"name": "search_tool", "arguments": '{"query": "help"}'}, + } + ] + + def original_invoke(self, *args, **kwargs): + input_messages = args[0].get("messages", []) + new_messages = input_messages + [ + MockMessage( + content=expected_assistant_response, + name="assistant", + tool_calls=expected_tool_calls, + ) + ] + return {"messages": new_messages} + + with start_transaction(): + wrapped_invoke = _wrap_pregel_invoke(original_invoke) + result = wrapped_invoke(pregel, test_state) + + assert result is not None + + tx = events[0] + assert tx["type"] == "transaction" + + invoke_spans = [ + span for span in tx["spans"] if span["op"] == OP.GEN_AI_INVOKE_AGENT + ] + assert len(invoke_spans) == 1 + + invoke_span = invoke_spans[0] + assert invoke_span["description"] == "invoke_agent test_graph" + assert invoke_span["origin"] == "auto.ai.langgraph" + assert invoke_span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "invoke_agent" + assert invoke_span["data"][SPANDATA.GEN_AI_PIPELINE_NAME] == "test_graph" + assert invoke_span["data"][SPANDATA.GEN_AI_AGENT_NAME] == "test_graph" + + if send_default_pii and include_prompts: + assert SPANDATA.GEN_AI_REQUEST_MESSAGES in invoke_span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT in invoke_span["data"] + + request_messages = invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + + if isinstance(request_messages, str): + import json + + request_messages = json.loads(request_messages) + assert len(request_messages) == 2 + assert request_messages[0]["content"] == "Hello, can you help me?" + assert request_messages[1]["content"] == "Of course! How can I assist you?" + + response_text = invoke_span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + assert response_text == expected_assistant_response + + assert SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS in invoke_span["data"] + tool_calls_data = invoke_span["data"][SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS] + if isinstance(tool_calls_data, str): + import json + + tool_calls_data = json.loads(tool_calls_data) + + assert len(tool_calls_data) == 1 + assert tool_calls_data[0]["id"] == "call_test_123" + assert tool_calls_data[0]["function"]["name"] == "search_tool" + else: + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in invoke_span.get("data", {}) + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in invoke_span.get("data", {}) + assert SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS not in invoke_span.get("data", {}) + + +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +def test_pregel_ainvoke(sentry_init, capture_events, send_default_pii, include_prompts): + """Test Pregel.ainvoke() async wrapper creates proper invoke_agent span.""" + sentry_init( + integrations=[LanggraphIntegration(include_prompts=include_prompts)], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + ) + events = capture_events() + test_state = {"messages": [MockMessage("What's the weather like?", name="user")]} + pregel = MockPregelInstance("async_graph") + + expected_assistant_response = "It's sunny and 72°F today!" + expected_tool_calls = [ + { + "id": "call_weather_456", + "type": "function", + "function": {"name": "get_weather", "arguments": '{"location": "current"}'}, + } + ] + + async def original_ainvoke(self, *args, **kwargs): + input_messages = args[0].get("messages", []) + new_messages = input_messages + [ + MockMessage( + content=expected_assistant_response, + name="assistant", + tool_calls=expected_tool_calls, + ) + ] + return {"messages": new_messages} + + async def run_test(): + with start_transaction(): + + wrapped_ainvoke = _wrap_pregel_ainvoke(original_ainvoke) + result = await wrapped_ainvoke(pregel, test_state) + return result + + result = asyncio.run(run_test()) + assert result is not None + + tx = events[0] + assert tx["type"] == "transaction" + + invoke_spans = [ + span for span in tx["spans"] if span["op"] == OP.GEN_AI_INVOKE_AGENT + ] + assert len(invoke_spans) == 1 + + invoke_span = invoke_spans[0] + assert invoke_span["description"] == "invoke_agent async_graph" + assert invoke_span["origin"] == "auto.ai.langgraph" + assert invoke_span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "invoke_agent" + assert invoke_span["data"][SPANDATA.GEN_AI_PIPELINE_NAME] == "async_graph" + assert invoke_span["data"][SPANDATA.GEN_AI_AGENT_NAME] == "async_graph" + + if send_default_pii and include_prompts: + assert SPANDATA.GEN_AI_REQUEST_MESSAGES in invoke_span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT in invoke_span["data"] + + response_text = invoke_span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + assert response_text == expected_assistant_response + + assert SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS in invoke_span["data"] + tool_calls_data = invoke_span["data"][SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS] + if isinstance(tool_calls_data, str): + import json + + tool_calls_data = json.loads(tool_calls_data) + + assert len(tool_calls_data) == 1 + assert tool_calls_data[0]["id"] == "call_weather_456" + assert tool_calls_data[0]["function"]["name"] == "get_weather" + else: + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in invoke_span.get("data", {}) + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in invoke_span.get("data", {}) + assert SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS not in invoke_span.get("data", {}) + + +def test_pregel_invoke_error(sentry_init, capture_events): + """Test error handling during graph execution.""" + sentry_init( + integrations=[LanggraphIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + test_state = {"messages": [MockMessage("This will fail")]} + pregel = MockPregelInstance("error_graph") + + def original_invoke(self, *args, **kwargs): + raise Exception("Graph execution failed") + + with start_transaction(), pytest.raises(Exception, match="Graph execution failed"): + + wrapped_invoke = _wrap_pregel_invoke(original_invoke) + wrapped_invoke(pregel, test_state) + + tx = events[0] + invoke_spans = [ + span for span in tx["spans"] if span["op"] == OP.GEN_AI_INVOKE_AGENT + ] + assert len(invoke_spans) == 1 + + invoke_span = invoke_spans[0] + assert invoke_span.get("tags", {}).get("status") == "internal_error" + + +def test_pregel_ainvoke_error(sentry_init, capture_events): + """Test error handling during async graph execution.""" + sentry_init( + integrations=[LanggraphIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + test_state = {"messages": [MockMessage("This will fail async")]} + pregel = MockPregelInstance("async_error_graph") + + async def original_ainvoke(self, *args, **kwargs): + raise Exception("Async graph execution failed") + + async def run_error_test(): + with start_transaction(), pytest.raises( + Exception, match="Async graph execution failed" + ): + + wrapped_ainvoke = _wrap_pregel_ainvoke(original_ainvoke) + await wrapped_ainvoke(pregel, test_state) + + asyncio.run(run_error_test()) + + tx = events[0] + invoke_spans = [ + span for span in tx["spans"] if span["op"] == OP.GEN_AI_INVOKE_AGENT + ] + assert len(invoke_spans) == 1 + + invoke_span = invoke_spans[0] + assert invoke_span.get("tags", {}).get("status") == "internal_error" + + +def test_span_origin(sentry_init, capture_events): + """Test that span origins are correctly set.""" + sentry_init( + integrations=[LanggraphIntegration()], + traces_sample_rate=1.0, + ) + events = capture_events() + + graph = MockStateGraph() + + def original_compile(self, *args, **kwargs): + return MockCompiledGraph(self.name) + + with start_transaction(): + from sentry_sdk.integrations.langgraph import _wrap_state_graph_compile + + wrapped_compile = _wrap_state_graph_compile(original_compile) + wrapped_compile(graph) + + tx = events[0] + assert tx["contexts"]["trace"]["origin"] == "manual" + + for span in tx["spans"]: + assert span["origin"] == "auto.ai.langgraph" + + +@pytest.mark.parametrize("graph_name", ["my_graph", None, ""]) +def test_pregel_invoke_with_different_graph_names( + sentry_init, capture_events, graph_name +): + """Test Pregel.invoke() with different graph name scenarios.""" + sentry_init( + integrations=[LanggraphIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + pregel = MockPregelInstance(graph_name) if graph_name else MockPregelInstance() + if not graph_name: + + delattr(pregel, "name") + delattr(pregel, "graph_name") + + def original_invoke(self, *args, **kwargs): + return {"result": "test"} + + with start_transaction(): + + wrapped_invoke = _wrap_pregel_invoke(original_invoke) + wrapped_invoke(pregel, {"messages": []}) + + tx = events[0] + invoke_spans = [ + span for span in tx["spans"] if span["op"] == OP.GEN_AI_INVOKE_AGENT + ] + assert len(invoke_spans) == 1 + + invoke_span = invoke_spans[0] + + if graph_name and graph_name.strip(): + assert invoke_span["description"] == "invoke_agent my_graph" + assert invoke_span["data"][SPANDATA.GEN_AI_PIPELINE_NAME] == graph_name + assert invoke_span["data"][SPANDATA.GEN_AI_AGENT_NAME] == graph_name + else: + assert invoke_span["description"] == "invoke_agent" + assert SPANDATA.GEN_AI_PIPELINE_NAME not in invoke_span.get("data", {}) + assert SPANDATA.GEN_AI_AGENT_NAME not in invoke_span.get("data", {}) + + +def test_complex_message_parsing(): + """Test message parsing with complex message structures.""" + messages = [ + MockMessage(content="User query", name="user"), + MockMessage( + content="Assistant response with tools", + name="assistant", + tool_calls=[ + { + "id": "call_1", + "type": "function", + "function": {"name": "search", "arguments": "{}"}, + }, + { + "id": "call_2", + "type": "function", + "function": {"name": "calculate", "arguments": '{"x": 5}'}, + }, + ], + ), + MockMessage( + content="Function call response", + name="function", + function_call={"name": "search", "arguments": '{"query": "test"}'}, + ), + ] + + state = {"messages": messages} + result = _parse_langgraph_messages(state) + + assert result is not None + assert len(result) == 3 + + assert result[0]["content"] == "User query" + assert result[0]["name"] == "user" + assert "tool_calls" not in result[0] + assert "function_call" not in result[0] + + assert result[1]["content"] == "Assistant response with tools" + assert result[1]["name"] == "assistant" + assert len(result[1]["tool_calls"]) == 2 + + assert result[2]["content"] == "Function call response" + assert result[2]["name"] == "function" + assert result[2]["function_call"]["name"] == "search" + + +def test_extraction_functions_complex_scenario(sentry_init, capture_events): + """Test extraction functions with complex scenarios including multiple messages and edge cases.""" + sentry_init( + integrations=[LanggraphIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + pregel = MockPregelInstance("complex_graph") + test_state = {"messages": [MockMessage("Complex request", name="user")]} + + def original_invoke(self, *args, **kwargs): + input_messages = args[0].get("messages", []) + new_messages = input_messages + [ + MockMessage( + content="I'll help with multiple tasks", + name="assistant", + tool_calls=[ + { + "id": "call_multi_1", + "type": "function", + "function": { + "name": "search", + "arguments": '{"query": "complex"}', + }, + }, + { + "id": "call_multi_2", + "type": "function", + "function": { + "name": "calculate", + "arguments": '{"expr": "2+2"}', + }, + }, + ], + ), + MockMessage("", name="assistant"), + MockMessage("Final response", name="ai", type="ai"), + ] + return {"messages": new_messages} + + with start_transaction(): + wrapped_invoke = _wrap_pregel_invoke(original_invoke) + result = wrapped_invoke(pregel, test_state) + + assert result is not None + + tx = events[0] + invoke_spans = [ + span for span in tx["spans"] if span["op"] == OP.GEN_AI_INVOKE_AGENT + ] + assert len(invoke_spans) == 1 + + invoke_span = invoke_spans[0] + assert SPANDATA.GEN_AI_RESPONSE_TEXT in invoke_span["data"] + response_text = invoke_span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + assert response_text == "Final response" + + assert SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS in invoke_span["data"] + import json + + tool_calls_data = invoke_span["data"][SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS] + if isinstance(tool_calls_data, str): + tool_calls_data = json.loads(tool_calls_data) + + assert len(tool_calls_data) == 2 + assert tool_calls_data[0]["id"] == "call_multi_1" + assert tool_calls_data[0]["function"]["name"] == "search" + assert tool_calls_data[1]["id"] == "call_multi_2" + assert tool_calls_data[1]["function"]["name"] == "calculate" diff --git a/tox.ini b/tox.ini index fd633654be..40afc2a6a7 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-04T10:35:13.756355+00:00 +# Last generated: 2025-09-04T12:59:44.328902+00:00 [tox] requires = @@ -142,6 +142,9 @@ envlist = {py3.8,py3.11,py3.12}-openai-notiktoken-v1.71.0 {py3.8,py3.12,py3.13}-openai-notiktoken-v1.105.0 + {py3.9,py3.12,py3.13}-langgraph-v0.6.6 + {py3.10,py3.12,py3.13}-langgraph-v1.0.0a2 + {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 {py3.10,py3.12,py3.13}-openai_agents-v0.2.11 @@ -525,6 +528,9 @@ deps = openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.36.1: httpx<0.28 + langgraph-v0.6.6: langgraph==0.6.6 + langgraph-v1.0.0a2: langgraph==1.0.0a2 + openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.1.0: openai-agents==0.1.0 openai_agents-v0.2.11: openai-agents==0.2.11 @@ -841,6 +847,7 @@ setenv = huggingface_hub: TESTPATH=tests/integrations/huggingface_hub langchain-base: TESTPATH=tests/integrations/langchain langchain-notiktoken: TESTPATH=tests/integrations/langchain + langgraph: TESTPATH=tests/integrations/langgraph launchdarkly: TESTPATH=tests/integrations/launchdarkly litestar: TESTPATH=tests/integrations/litestar loguru: TESTPATH=tests/integrations/loguru From 9711b3be884263bb34f1ae5a0f719cb9acb4b0ca Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 4 Sep 2025 16:08:26 +0200 Subject: [PATCH 24/88] tests: Move asyncpg under toxgen (#4757) - remove hardcoded asyncpg config, let toxgen take care of generating it - isolate DB so that multiple asyncpg test suites for different envs can run at the same time without touching the same DB - update instructions on running the test suite locally Ref https://github.com/getsentry/sentry-python/issues/4506 --- .github/workflows/test-integrations-dbs.yml | 2 +- scripts/populate_tox/config.py | 7 ++++ scripts/populate_tox/populate_tox.py | 1 - scripts/populate_tox/tox.jinja | 9 ----- tests/integrations/asyncpg/test_asyncpg.py | 38 ++++++++++++++++----- tox.ini | 30 ++++++++-------- 6 files changed, 53 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index 5fc0be029b..2d6af43bc3 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7","3.8","3.11","3.12","3.13"] + python-version: ["3.7","3.12","3.13"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 6795e36303..1dbc78ccf0 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -36,6 +36,13 @@ "<=0.23": ["pydantic<2"], }, }, + "asyncpg": { + "package": "asyncpg", + "deps": { + "*": ["pytest-asyncio"], + }, + "python": ">=3.7", + }, "beam": { "package": "apache-beam", "python": ">=3.7", diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 3d9ef23b66..076a8358f7 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -67,7 +67,6 @@ "potel", # Integrations that can be migrated -- we should eventually remove all # of these from the IGNORE list - "asyncpg", "boto3", "chalice", "gcp", diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 241e0ca288..0ad9af8321 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -39,10 +39,6 @@ envlist = # Asgi {py3.7,py3.12,py3.13}-asgi - # asyncpg - {py3.7,py3.10}-asyncpg-v{0.23} - {py3.8,py3.11,py3.12}-asyncpg-latest - # AWS Lambda {py3.8,py3.9,py3.11,py3.13}-aws_lambda @@ -160,11 +156,6 @@ deps = asgi: pytest-asyncio asgi: async-asgi-testclient - # Asyncpg - asyncpg-v0.23: asyncpg~=0.23.0 - asyncpg-latest: asyncpg - asyncpg: pytest-asyncio - # AWS Lambda aws_lambda: aws-cdk-lib aws_lambda: aws-sam-cli diff --git a/tests/integrations/asyncpg/test_asyncpg.py b/tests/integrations/asyncpg/test_asyncpg.py index e36d15c5d2..e23612c055 100644 --- a/tests/integrations/asyncpg/test_asyncpg.py +++ b/tests/integrations/asyncpg/test_asyncpg.py @@ -3,21 +3,13 @@ Tests need a local postgresql instance running, this can best be done using ```sh -docker run --rm --name some-postgres -e POSTGRES_USER=foo -e POSTGRES_PASSWORD=bar -d -p 5432:5432 postgres +docker run --rm --name some-postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=sentry -d -p 5432:5432 postgres ``` The tests use the following credentials to establish a database connection. """ import os - - -PG_HOST = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost") -PG_PORT = int(os.getenv("SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432")) -PG_USER = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_USER", "postgres") -PG_PASSWORD = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_PASSWORD", "sentry") -PG_NAME = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_NAME", "postgres") - import datetime from contextlib import contextmanager from unittest import mock @@ -33,6 +25,19 @@ from sentry_sdk.tracing_utils import record_sql_queries from tests.conftest import ApproxDict +PG_HOST = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost") +PG_PORT = int(os.getenv("SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432")) +PG_USER = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_USER", "postgres") +PG_PASSWORD = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_PASSWORD", "sentry") +PG_NAME_BASE = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_NAME", "postgres") + + +def _get_db_name(): + pid = os.getpid() + return f"{PG_NAME_BASE}_{pid}" + + +PG_NAME = _get_db_name() PG_CONNECTION_URI = "postgresql://{}:{}@{}/{}".format( PG_USER, PG_PASSWORD, PG_HOST, PG_NAME @@ -55,6 +60,21 @@ @pytest_asyncio.fixture(autouse=True) async def _clean_pg(): + # Create the test database if it doesn't exist + default_conn = await connect( + "postgresql://{}:{}@{}".format(PG_USER, PG_PASSWORD, PG_HOST) + ) + try: + # Check if database exists, create if not + result = await default_conn.fetchval( + "SELECT 1 FROM pg_database WHERE datname = $1", PG_NAME + ) + if not result: + await default_conn.execute(f'CREATE DATABASE "{PG_NAME}"') + finally: + await default_conn.close() + + # Now connect to our test database and set up the table conn = await connect(PG_CONNECTION_URI) await conn.execute("DROP TABLE IF EXISTS users") await conn.execute( diff --git a/tox.ini b/tox.ini index 40afc2a6a7..1627cf2458 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-04T12:59:44.328902+00:00 +# Last generated: 2025-09-04T13:56:54.117272+00:00 [tox] requires = @@ -39,10 +39,6 @@ envlist = # Asgi {py3.7,py3.12,py3.13}-asgi - # asyncpg - {py3.7,py3.10}-asyncpg-v{0.23} - {py3.8,py3.11,py3.12}-asyncpg-latest - # AWS Lambda {py3.8,py3.9,py3.11,py3.13}-aws_lambda @@ -135,12 +131,12 @@ envlist = {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.11,py3.12}-openai-base-v1.36.1 {py3.8,py3.11,py3.12}-openai-base-v1.71.0 - {py3.8,py3.12,py3.13}-openai-base-v1.105.0 + {py3.8,py3.12,py3.13}-openai-base-v1.106.0 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.36.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.71.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.105.0 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.106.0 {py3.9,py3.12,py3.13}-langgraph-v0.6.6 {py3.10,py3.12,py3.13}-langgraph-v1.0.0a2 @@ -157,6 +153,11 @@ envlist = # ~~~ DBs ~~~ + {py3.7,py3.8,py3.9}-asyncpg-v0.23.0 + {py3.7,py3.9,py3.10}-asyncpg-v0.25.0 + {py3.7,py3.9,py3.10}-asyncpg-v0.27.0 + {py3.8,py3.11,py3.12}-asyncpg-v0.30.0 + {py3.7,py3.11,py3.12}-clickhouse_driver-v0.2.9 {py3.6}-pymongo-v3.5.1 @@ -362,11 +363,6 @@ deps = asgi: pytest-asyncio asgi: async-asgi-testclient - # Asyncpg - asyncpg-v0.23: asyncpg~=0.23.0 - asyncpg-latest: asyncpg - asyncpg: pytest-asyncio - # AWS Lambda aws_lambda: aws-cdk-lib aws_lambda: aws-sam-cli @@ -514,7 +510,7 @@ deps = openai-base-v1.0.1: openai==1.0.1 openai-base-v1.36.1: openai==1.36.1 openai-base-v1.71.0: openai==1.71.0 - openai-base-v1.105.0: openai==1.105.0 + openai-base-v1.106.0: openai==1.106.0 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 @@ -523,7 +519,7 @@ deps = openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.36.1: openai==1.36.1 openai-notiktoken-v1.71.0: openai==1.71.0 - openai-notiktoken-v1.105.0: openai==1.105.0 + openai-notiktoken-v1.106.0: openai==1.106.0 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.36.1: httpx<0.28 @@ -544,6 +540,12 @@ deps = # ~~~ DBs ~~~ + asyncpg-v0.23.0: asyncpg==0.23.0 + asyncpg-v0.25.0: asyncpg==0.25.0 + asyncpg-v0.27.0: asyncpg==0.27.0 + asyncpg-v0.30.0: asyncpg==0.30.0 + asyncpg: pytest-asyncio + clickhouse_driver-v0.2.9: clickhouse-driver==0.2.9 pymongo-v3.5.1: pymongo==3.5.1 From b50f7e4a68c69caccdf29bc6b645f51c215e7ada Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 5 Sep 2025 08:46:08 +0200 Subject: [PATCH 25/88] Format span attributes in AI integrations (#4762) The AI Agents integrations render stringified json-like data in a nice way (make the sub nodes of the data structure collapsible) In Javascript it comes down to having double quotes in a string: - Good: `'{"role": "system", "content": "some context"}'` - Bad: `"{'role': 'system', 'content': 'some context'}"` Also pydantics `model_dump()` sometimes returns `function` or `class` objects that can not be json serialized so I updated `_normalize_data()` to make sure everything is converted to a primitive data type, always. --- sentry_sdk/ai/utils.py | 15 +++++++++------ tests/integrations/cohere/test_cohere.py | 8 ++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/ai/utils.py b/sentry_sdk/ai/utils.py index cf52cba6e8..2dc0de4ef3 100644 --- a/sentry_sdk/ai/utils.py +++ b/sentry_sdk/ai/utils.py @@ -1,30 +1,33 @@ +import json + from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any + from sentry_sdk.tracing import Span -from sentry_sdk.tracing import Span from sentry_sdk.utils import logger def _normalize_data(data, unpack=True): # type: (Any, bool) -> Any - # convert pydantic data (e.g. OpenAI v1+) to json compatible format if hasattr(data, "model_dump"): try: - return data.model_dump() + return _normalize_data(data.model_dump(), unpack=unpack) except Exception as e: logger.warning("Could not convert pydantic data to JSON: %s", e) - return data + return data if isinstance(data, (int, float, bool, str)) else str(data) + if isinstance(data, list): if unpack and len(data) == 1: return _normalize_data(data[0], unpack=unpack) # remove empty dimensions return list(_normalize_data(x, unpack=unpack) for x in data) + if isinstance(data, dict): return {k: _normalize_data(v, unpack=unpack) for (k, v) in data.items()} - return data + return data if isinstance(data, (int, float, bool, str)) else str(data) def set_data_normalized(span, key, value, unpack=True): @@ -33,4 +36,4 @@ def set_data_normalized(span, key, value, unpack=True): if isinstance(normalized, (int, float, bool, str)): span.set_data(key, normalized) else: - span.set_data(key, str(normalized)) + span.set_data(key, json.dumps(normalized)) diff --git a/tests/integrations/cohere/test_cohere.py b/tests/integrations/cohere/test_cohere.py index b8b6067625..ee876172d1 100644 --- a/tests/integrations/cohere/test_cohere.py +++ b/tests/integrations/cohere/test_cohere.py @@ -58,11 +58,11 @@ def test_nonstreaming_chat( if send_default_pii and include_prompts: assert ( - "{'role': 'system', 'content': 'some context'}" + '{"role": "system", "content": "some context"}' in span["data"][SPANDATA.AI_INPUT_MESSAGES] ) assert ( - "{'role': 'user', 'content': 'hello'}" + '{"role": "user", "content": "hello"}' in span["data"][SPANDATA.AI_INPUT_MESSAGES] ) assert "the model response" in span["data"][SPANDATA.AI_RESPONSES] @@ -135,11 +135,11 @@ def test_streaming_chat(sentry_init, capture_events, send_default_pii, include_p if send_default_pii and include_prompts: assert ( - "{'role': 'system', 'content': 'some context'}" + '{"role": "system", "content": "some context"}' in span["data"][SPANDATA.AI_INPUT_MESSAGES] ) assert ( - "{'role': 'user', 'content': 'hello'}" + '{"role": "user", "content": "hello"}' in span["data"][SPANDATA.AI_INPUT_MESSAGES] ) assert "the model response" in span["data"][SPANDATA.AI_RESPONSES] From 0c0a8d8497647e40ae8b285f5a53069394b084ad Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 5 Sep 2025 09:06:19 +0200 Subject: [PATCH 26/88] ci: Fix celery (#4765) We have some newrelic interference/compatibility tests that were failing with the newest newrelic release. Looking at that release, newrelic completely [rehauled](https://github.com/newrelic/newrelic-python-agent/commit/3cfce55a51ec0cf81919ebd475765707d39c90e0) their celery instrumentation, so I'm pinning our tests to only test against older newrelic versions where we had the problem in the first place. Rerunning toxgen on the updated config also pulled in a new openai release. --- scripts/populate_tox/config.py | 2 +- tox.ini | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 1dbc78ccf0..921098e7e6 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -56,7 +56,7 @@ "celery": { "package": "celery", "deps": { - "*": ["newrelic", "redis"], + "*": ["newrelic<10.17.0", "redis"], "py3.7": ["importlib-metadata<5.0"], }, }, diff --git a/tox.ini b/tox.ini index 1627cf2458..994ad22314 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-04T13:56:54.117272+00:00 +# Last generated: 2025-09-05T06:53:57.545461+00:00 [tox] requires = @@ -131,12 +131,12 @@ envlist = {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.11,py3.12}-openai-base-v1.36.1 {py3.8,py3.11,py3.12}-openai-base-v1.71.0 - {py3.8,py3.12,py3.13}-openai-base-v1.106.0 + {py3.8,py3.12,py3.13}-openai-base-v1.106.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.36.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.71.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.106.0 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.106.1 {py3.9,py3.12,py3.13}-langgraph-v0.6.6 {py3.10,py3.12,py3.13}-langgraph-v1.0.0a2 @@ -510,7 +510,7 @@ deps = openai-base-v1.0.1: openai==1.0.1 openai-base-v1.36.1: openai==1.36.1 openai-base-v1.71.0: openai==1.71.0 - openai-base-v1.106.0: openai==1.106.0 + openai-base-v1.106.1: openai==1.106.1 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 @@ -519,7 +519,7 @@ deps = openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.36.1: openai==1.36.1 openai-notiktoken-v1.71.0: openai==1.71.0 - openai-notiktoken-v1.106.0: openai==1.106.0 + openai-notiktoken-v1.106.1: openai==1.106.1 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.36.1: httpx<0.28 @@ -646,7 +646,7 @@ deps = celery-v4.4.7: celery==4.4.7 celery-v5.0.5: celery==5.0.5 celery-v5.5.3: celery==5.5.3 - celery: newrelic + celery: newrelic<10.17.0 celery: redis py3.7-celery: importlib-metadata<5.0 From ad3c435398d78949eda68dff66ef8eb8b4928679 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 5 Sep 2025 09:44:32 +0200 Subject: [PATCH 27/88] tests: Move boto3 tests under toxgen (#4761) Move boto3 under toxgen. Also, update how Python version constraints are generated. Ref https://github.com/getsentry/sentry-python/issues/4506 --- .github/workflows/test-integrations-cloud.yml | 4 +- scripts/populate_tox/config.py | 6 +++ scripts/populate_tox/populate_tox.py | 3 +- scripts/populate_tox/tox.jinja | 12 ------ tox.ini | 39 ++++++++++--------- 5 files changed, 30 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test-integrations-cloud.yml b/.github/workflows/test-integrations-cloud.yml index a04d57497a..8688a1d48e 100644 --- a/.github/workflows/test-integrations-cloud.yml +++ b/.github/workflows/test-integrations-cloud.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8","3.11","3.12","3.13"] + python-version: ["3.8","3.12","3.13"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 @@ -108,7 +108,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6","3.7","3.8","3.9","3.11","3.12","3.13"] + python-version: ["3.6","3.7","3.8","3.9","3.10","3.11","3.12","3.13"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 921098e7e6..5aba82b11b 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -47,6 +47,12 @@ "package": "apache-beam", "python": ">=3.7", }, + "boto3": { + "package": "boto3", + "deps": { + "py3.7,py3.8": ["urllib3<2.0.0"], + }, + }, "bottle": { "package": "bottle", "deps": { diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 076a8358f7..b8cc988fda 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -67,7 +67,6 @@ "potel", # Integrations that can be migrated -- we should eventually remove all # of these from the IGNORE list - "boto3", "chalice", "gcp", "httpx", @@ -439,7 +438,7 @@ def _render_dependencies(integration: str, releases: list[Version]) -> list[str] rendered.append(f"{integration}: {dep}") elif constraint.startswith("py3"): for dep in deps: - rendered.append(f"{constraint}-{integration}: {dep}") + rendered.append(f"{{{constraint}}}-{integration}: {dep}") else: restriction = SpecifierSet(constraint) for release in releases: diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 0ad9af8321..7f23d1fbc7 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -42,12 +42,6 @@ envlist = # AWS Lambda {py3.8,py3.9,py3.11,py3.13}-aws_lambda - # Boto3 - {py3.6,py3.7}-boto3-v{1.12} - {py3.7,py3.11,py3.12}-boto3-v{1.23} - {py3.11,py3.12}-boto3-v{1.34} - {py3.11,py3.12,py3.13}-boto3-latest - # Chalice {py3.6,py3.9}-chalice-v{1.16} {py3.8,py3.12,py3.13}-chalice-latest @@ -164,12 +158,6 @@ deps = aws_lambda: requests aws_lambda: uvicorn - # Boto3 - boto3-v1.12: boto3~=1.12.0 - boto3-v1.23: boto3~=1.23.0 - boto3-v1.34: boto3~=1.34.0 - boto3-latest: boto3 - # Chalice chalice: pytest-chalice==0.0.5 chalice-v1.16: chalice~=1.16.0 diff --git a/tox.ini b/tox.ini index 994ad22314..948887f1dd 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-05T06:53:57.545461+00:00 +# Last generated: 2025-09-05T07:14:50.663886+00:00 [tox] requires = @@ -42,12 +42,6 @@ envlist = # AWS Lambda {py3.8,py3.9,py3.11,py3.13}-aws_lambda - # Boto3 - {py3.6,py3.7}-boto3-v{1.12} - {py3.7,py3.11,py3.12}-boto3-v{1.23} - {py3.11,py3.12}-boto3-v{1.34} - {py3.11,py3.12,py3.13}-boto3-latest - # Chalice {py3.6,py3.9}-chalice-v{1.16} {py3.8,py3.12,py3.13}-chalice-latest @@ -152,6 +146,13 @@ envlist = {py3.8,py3.12,py3.13}-huggingface_hub-v0.35.0rc0 + # ~~~ Cloud ~~~ + {py3.6,py3.7}-boto3-v1.12.49 + {py3.6,py3.9,py3.10}-boto3-v1.20.54 + {py3.7,py3.11,py3.12}-boto3-v1.28.85 + {py3.9,py3.12,py3.13}-boto3-v1.40.24 + + # ~~~ DBs ~~~ {py3.7,py3.8,py3.9}-asyncpg-v0.23.0 {py3.7,py3.9,py3.10}-asyncpg-v0.25.0 @@ -371,12 +372,6 @@ deps = aws_lambda: requests aws_lambda: uvicorn - # Boto3 - boto3-v1.12: boto3~=1.12.0 - boto3-v1.23: boto3~=1.23.0 - boto3-v1.34: boto3~=1.34.0 - boto3-latest: boto3 - # Chalice chalice: pytest-chalice==0.0.5 chalice-v1.16: chalice~=1.16.0 @@ -539,6 +534,14 @@ deps = huggingface_hub-v0.35.0rc0: huggingface_hub==0.35.0rc0 + # ~~~ Cloud ~~~ + boto3-v1.12.49: boto3==1.12.49 + boto3-v1.20.54: boto3==1.20.54 + boto3-v1.28.85: boto3==1.28.85 + boto3-v1.40.24: boto3==1.40.24 + {py3.7,py3.8}-boto3: urllib3<2.0.0 + + # ~~~ DBs ~~~ asyncpg-v0.23.0: asyncpg==0.23.0 asyncpg-v0.25.0: asyncpg==0.25.0 @@ -604,7 +607,7 @@ deps = graphene: fastapi graphene: flask graphene: httpx - py3.6-graphene: aiocontextvars + {py3.6}-graphene: aiocontextvars strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8 strawberry-v0.233.3: strawberry-graphql[fastapi,flask]==0.233.3 @@ -648,7 +651,7 @@ deps = celery-v5.5.3: celery==5.5.3 celery: newrelic<10.17.0 celery: redis - py3.7-celery: importlib-metadata<5.0 + {py3.7}-celery: importlib-metadata<5.0 dramatiq-v1.9.0: dramatiq==1.9.0 dramatiq-v1.12.3: dramatiq==1.12.3 @@ -717,7 +720,7 @@ deps = starlette-v0.16.0: httpx<0.28.0 starlette-v0.26.1: httpx<0.28.0 starlette-v0.36.3: httpx<0.28.0 - py3.6-starlette: aiocontextvars + {py3.6}-starlette: aiocontextvars fastapi-v0.79.1: fastapi==0.79.1 fastapi-v0.91.0: fastapi==0.91.0 @@ -731,7 +734,7 @@ deps = fastapi-v0.79.1: httpx<0.28.0 fastapi-v0.91.0: httpx<0.28.0 fastapi-v0.103.2: httpx<0.28.0 - py3.6-fastapi: aiocontextvars + {py3.6}-fastapi: aiocontextvars # ~~~ Web 2 ~~~ @@ -787,7 +790,7 @@ deps = tornado: pytest tornado-v6.0.4: pytest<8.2 tornado-v6.2: pytest<8.2 - py3.6-tornado: aiocontextvars + {py3.6}-tornado: aiocontextvars # ~~~ Misc ~~~ From dee6de1579ba37acb46af622e2892d862e9c70ef Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Fri, 5 Sep 2025 13:13:16 +0200 Subject: [PATCH 28/88] feat(agents): improve instrumentation of input messages (#4750) - Improve the instrumentation of input messages in the AI agents instrumentations. Before: Screenshot 2025-09-03 at 16 30 05 After: Screenshot 2025-09-03 at 16 30 08 Closes TET-1058 --------- Co-authored-by: Anton Pirker --- sentry_sdk/integrations/langchain.py | 138 +++++++++++++----- sentry_sdk/integrations/langgraph.py | 12 +- sentry_sdk/integrations/openai.py | 43 ++++-- .../integrations/openai_agents/utils.py | 9 +- .../integrations/langchain/test_langchain.py | 73 +++++++++ tests/integrations/openai/test_openai.py | 16 +- .../openai_agents/test_openai_agents.py | 5 +- 7 files changed, 234 insertions(+), 62 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 7e04a740ed..a53115a2a9 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -51,7 +51,6 @@ "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, "tool_calls": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, } @@ -203,8 +202,12 @@ def on_llm_start( if key in all_params and all_params[key] is not None: set_data_normalized(span, attribute, all_params[key], unpack=False) + _set_tools_on_span(span, all_params.get("tools")) + if should_send_default_pii() and self.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompts) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompts, unpack=False + ) def on_chat_model_start(self, serialized, messages, *, run_id, **kwargs): # type: (SentryLangchainCallback, Dict[str, Any], List[List[BaseMessage]], UUID, Any) -> Any @@ -246,14 +249,20 @@ def on_chat_model_start(self, serialized, messages, *, run_id, **kwargs): if key in all_params and all_params[key] is not None: set_data_normalized(span, attribute, all_params[key], unpack=False) + _set_tools_on_span(span, all_params.get("tools")) + if should_send_default_pii() and self.include_prompts: + normalized_messages = [] + for list_ in messages: + for message in list_: + normalized_messages.append( + self._normalize_langchain_message(message) + ) set_data_normalized( span, SPANDATA.GEN_AI_REQUEST_MESSAGES, - [ - [self._normalize_langchain_message(x) for x in list_] - for list_ in messages - ], + normalized_messages, + unpack=False, ) def on_chat_model_end(self, response, *, run_id, **kwargs): @@ -351,9 +360,7 @@ def on_agent_finish(self, finish, *, run_id, **kwargs): if should_send_default_pii() and self.include_prompts: set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - finish.return_values.items(), + span, SPANDATA.GEN_AI_RESPONSE_TEXT, finish.return_values.items() ) self._exit_span(span_data, run_id) @@ -473,13 +480,11 @@ def _get_token_usage(obj): if usage is not None: return usage - # check for usage in the object itself for name in possible_names: usage = _get_value(obj, name) if usage is not None: return usage - # no usage found anywhere return None @@ -531,6 +536,87 @@ def _get_request_data(obj, args, kwargs): return (agent_name, tools) +def _simplify_langchain_tools(tools): + # type: (Any) -> Optional[List[Any]] + """Parse and simplify tools into a cleaner format.""" + if not tools: + return None + + if not isinstance(tools, (list, tuple)): + return None + + simplified_tools = [] + for tool in tools: + try: + if isinstance(tool, dict): + + if "function" in tool and isinstance(tool["function"], dict): + func = tool["function"] + simplified_tool = { + "name": func.get("name"), + "description": func.get("description"), + } + if simplified_tool["name"]: + simplified_tools.append(simplified_tool) + elif "name" in tool: + simplified_tool = { + "name": tool.get("name"), + "description": tool.get("description"), + } + simplified_tools.append(simplified_tool) + else: + name = ( + tool.get("name") + or tool.get("tool_name") + or tool.get("function_name") + ) + if name: + simplified_tools.append( + { + "name": name, + "description": tool.get("description") + or tool.get("desc"), + } + ) + elif hasattr(tool, "name"): + simplified_tool = { + "name": getattr(tool, "name", None), + "description": getattr(tool, "description", None) + or getattr(tool, "desc", None), + } + if simplified_tool["name"]: + simplified_tools.append(simplified_tool) + elif hasattr(tool, "__name__"): + simplified_tools.append( + { + "name": tool.__name__, + "description": getattr(tool, "__doc__", None), + } + ) + else: + tool_str = str(tool) + if tool_str and tool_str != "": + simplified_tools.append({"name": tool_str, "description": None}) + except Exception: + continue + + return simplified_tools if simplified_tools else None + + +def _set_tools_on_span(span, tools): + # type: (Span, Any) -> None + """Set available tools data on a span if tools are provided.""" + if tools is not None: + simplified_tools = _simplify_langchain_tools(tools) + if simplified_tools: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + simplified_tools, + unpack=False, + ) + + def _wrap_configure(f): # type: (Callable[..., Any]) -> Callable[..., Any] @@ -601,7 +687,7 @@ def new_configure( ] elif isinstance(local_callbacks, BaseCallbackHandler): local_callbacks = [local_callbacks, sentry_handler] - else: # local_callbacks is a list + else: local_callbacks = [*local_callbacks, sentry_handler] return f( @@ -638,10 +724,7 @@ def new_invoke(self, *args, **kwargs): span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) - if tools: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools, unpack=False - ) + _set_tools_on_span(span, tools) # Run the agent result = f(self, *args, **kwargs) @@ -653,11 +736,7 @@ def new_invoke(self, *args, **kwargs): and integration.include_prompts ): set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - [ - input, - ], + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, [input], unpack=False ) output = result.get("output") @@ -666,7 +745,7 @@ def new_invoke(self, *args, **kwargs): and should_send_default_pii() and integration.include_prompts ): - span.set_data(SPANDATA.GEN_AI_RESPONSE_TEXT, output) + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) return result @@ -698,10 +777,7 @@ def new_stream(self, *args, **kwargs): span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - if tools: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools, unpack=False - ) + _set_tools_on_span(span, tools) input = args[0].get("input") if len(args) >= 1 else None if ( @@ -710,11 +786,7 @@ def new_stream(self, *args, **kwargs): and integration.include_prompts ): set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - [ - input, - ], + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, [input], unpack=False ) # Run the agent @@ -737,7 +809,7 @@ def new_iterator(): and should_send_default_pii() and integration.include_prompts ): - span.set_data(SPANDATA.GEN_AI_RESPONSE_TEXT, output) + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) span.__exit__(None, None, None) @@ -756,7 +828,7 @@ async def new_iterator_async(): and should_send_default_pii() and integration.include_prompts ): - span.set_data(SPANDATA.GEN_AI_RESPONSE_TEXT, output) + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) span.__exit__(None, None, None) diff --git a/sentry_sdk/integrations/langgraph.py b/sentry_sdk/integrations/langgraph.py index 4b241fe895..df3941bb13 100644 --- a/sentry_sdk/integrations/langgraph.py +++ b/sentry_sdk/integrations/langgraph.py @@ -183,7 +183,8 @@ def new_invoke(self, *args, **kwargs): set_data_normalized( span, SPANDATA.GEN_AI_REQUEST_MESSAGES, - safe_serialize(input_messages), + input_messages, + unpack=False, ) result = f(self, *args, **kwargs) @@ -232,7 +233,8 @@ async def new_ainvoke(self, *args, **kwargs): set_data_normalized( span, SPANDATA.GEN_AI_REQUEST_MESSAGES, - safe_serialize(input_messages), + input_messages, + unpack=False, ) result = await f(self, *args, **kwargs) @@ -305,11 +307,9 @@ def _set_response_attributes(span, input_messages, result, integration): if llm_response_text: set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, llm_response_text) elif new_messages: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(new_messages) - ) + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, new_messages) else: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(result)) + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, result) tool_calls = _extract_tool_calls(new_messages) if tool_calls: diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 6ea545322c..467116c8f4 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -179,7 +179,9 @@ def _set_input_data(span, kwargs, operation, integration): and should_send_default_pii() and integration.include_prompts ): - set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages, unpack=False + ) # Input attributes: Common set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, "openai") @@ -227,25 +229,46 @@ def _set_output_data(span, response, kwargs, integration, finish_span=True): if should_send_default_pii() and integration.include_prompts: response_text = [choice.message.dict() for choice in response.choices] if len(response_text) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - safe_serialize(response_text), - ) + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_text) + _calculate_token_usage(messages, response, span, None, integration.count_tokens) + if finish_span: span.__exit__(None, None, None) elif hasattr(response, "output"): if should_send_default_pii() and integration.include_prompts: - response_text = [item.to_dict() for item in response.output] - if len(response_text) > 0: + output_messages = { + "response": [], + "tool": [], + } # type: (dict[str, list[Any]]) + + for output in response.output: + if output.type == "function_call": + output_messages["tool"].append(output.dict()) + elif output.type == "message": + for output_message in output.content: + try: + output_messages["response"].append(output_message.text) + except AttributeError: + # Unknown output message type, just return the json + output_messages["response"].append(output_message.dict()) + + if len(output_messages["tool"]) > 0: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - safe_serialize(response_text), + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + output_messages["tool"], + unpack=False, + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] ) + _calculate_token_usage(messages, response, span, None, integration.count_tokens) + if finish_span: span.__exit__(None, None, None) diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 1525346726..44b260d4bc 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -1,4 +1,5 @@ import sentry_sdk +from sentry_sdk.ai.utils import set_data_normalized from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable from sentry_sdk.scope import should_send_default_pii @@ -127,7 +128,9 @@ def _set_input_data(span, get_response_kwargs): if len(messages) > 0: request_messages.append({"role": role, "content": messages}) - span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, safe_serialize(request_messages)) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, request_messages, unpack=False + ) def _set_output_data(span, result): @@ -157,6 +160,6 @@ def _set_output_data(span, result): ) if len(output_messages["response"]) > 0: - span.set_data( - SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(output_messages["response"]) + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] ) diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 9a06ac05d4..99dc5f4e37 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -589,3 +589,76 @@ def test_langchain_callback_list_existing_callback(sentry_init): [handler] = passed_callbacks assert handler is sentry_callback + + +def test_tools_integration_in_spans(sentry_init, capture_events): + """Test that tools are properly set on spans in actual LangChain integration.""" + global llm_type + llm_type = "openai-chat" + + sentry_init( + integrations=[LangchainIntegration(include_prompts=False)], + traces_sample_rate=1.0, + ) + events = capture_events() + + prompt = ChatPromptTemplate.from_messages( + [ + ("system", "You are a helpful assistant"), + ("user", "{input}"), + MessagesPlaceholder(variable_name="agent_scratchpad"), + ] + ) + + global stream_result_mock + stream_result_mock = Mock( + side_effect=[ + [ + ChatGenerationChunk( + type="ChatGenerationChunk", + message=AIMessageChunk(content="Simple response"), + ), + ] + ] + ) + + llm = MockOpenAI( + model_name="gpt-3.5-turbo", + temperature=0, + openai_api_key="badkey", + ) + agent = create_openai_tools_agent(llm, [get_word_length], prompt) + agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True) + + with start_transaction(): + list(agent_executor.stream({"input": "Hello"})) + + # Check that events were captured and contain tools data + if events: + tx = events[0] + spans = tx.get("spans", []) + + # Look for spans that should have tools data + tools_found = False + for span in spans: + span_data = span.get("data", {}) + if SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS in span_data: + tools_found = True + tools_data = span_data[SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS] + # Verify tools are in the expected format + assert isinstance(tools_data, (str, list)) # Could be serialized + if isinstance(tools_data, str): + # If serialized as string, should contain tool name + assert "get_word_length" in tools_data + else: + # If still a list, verify structure + assert len(tools_data) >= 1 + names = [ + tool.get("name") + for tool in tools_data + if isinstance(tool, dict) + ] + assert "get_word_length" in names + + # Ensure we found at least one span with tools data + assert tools_found, "No spans found with tools data" diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index a3c7bdd9d9..18968fb36a 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -1036,7 +1036,7 @@ def test_ai_client_span_responses_api(sentry_init, capture_events): assert spans[0]["origin"] == "auto.ai.openai" assert spans[0]["data"] == { "gen_ai.operation.name": "responses", - "gen_ai.request.messages": "How do I check if a Python object is an instance of a class?", + "gen_ai.request.messages": '["How do I check if a Python object is an instance of a class?"]', "gen_ai.request.model": "gpt-4o", "gen_ai.system": "openai", "gen_ai.response.model": "response-model-id", @@ -1045,7 +1045,7 @@ def test_ai_client_span_responses_api(sentry_init, capture_events): "gen_ai.usage.output_tokens": 10, "gen_ai.usage.output_tokens.reasoning": 8, "gen_ai.usage.total_tokens": 30, - "gen_ai.response.text": '[{"id": "message-id", "content": [{"annotations": [], "text": "the model response", "type": "output_text"}], "role": "assistant", "status": "completed", "type": "message"}]', + "gen_ai.response.text": "the model response", "thread.id": mock.ANY, "thread.name": mock.ANY, } @@ -1116,7 +1116,7 @@ async def test_ai_client_span_responses_async_api(sentry_init, capture_events): assert spans[0]["origin"] == "auto.ai.openai" assert spans[0]["data"] == { "gen_ai.operation.name": "responses", - "gen_ai.request.messages": "How do I check if a Python object is an instance of a class?", + "gen_ai.request.messages": '["How do I check if a Python object is an instance of a class?"]', "gen_ai.request.model": "gpt-4o", "gen_ai.response.model": "response-model-id", "gen_ai.system": "openai", @@ -1125,7 +1125,7 @@ async def test_ai_client_span_responses_async_api(sentry_init, capture_events): "gen_ai.usage.output_tokens": 10, "gen_ai.usage.output_tokens.reasoning": 8, "gen_ai.usage.total_tokens": 30, - "gen_ai.response.text": '[{"id": "message-id", "content": [{"annotations": [], "text": "the model response", "type": "output_text"}], "role": "assistant", "status": "completed", "type": "message"}]', + "gen_ai.response.text": "the model response", "thread.id": mock.ANY, "thread.name": mock.ANY, } @@ -1162,7 +1162,7 @@ async def test_ai_client_span_streaming_responses_async_api( assert spans[0]["origin"] == "auto.ai.openai" assert spans[0]["data"] == { "gen_ai.operation.name": "responses", - "gen_ai.request.messages": "How do I check if a Python object is an instance of a class?", + "gen_ai.request.messages": '["How do I check if a Python object is an instance of a class?"]', "gen_ai.request.model": "gpt-4o", "gen_ai.response.model": "response-model-id", "gen_ai.response.streaming": True, @@ -1172,7 +1172,7 @@ async def test_ai_client_span_streaming_responses_async_api( "gen_ai.usage.output_tokens": 10, "gen_ai.usage.output_tokens.reasoning": 8, "gen_ai.usage.total_tokens": 30, - "gen_ai.response.text": '[{"id": "message-id", "content": [{"annotations": [], "text": "the model response", "type": "output_text"}], "role": "assistant", "status": "completed", "type": "message"}]', + "gen_ai.response.text": "the model response", "thread.id": mock.ANY, "thread.name": mock.ANY, } @@ -1332,7 +1332,7 @@ def test_streaming_responses_api( assert span["op"] == "gen_ai.responses" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == "hello" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '["hello"]' assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "hello world" else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -1387,7 +1387,7 @@ async def test_streaming_responses_api_async( assert span["op"] == "gen_ai.responses" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == "hello" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '["hello"]' assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "hello world" else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index a3075e6415..fab8d9e13f 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -582,8 +582,9 @@ def simple_test_tool(message: str) -> str: assert ai_client_span2["data"]["gen_ai.request.model"] == "gpt-4" assert ai_client_span2["data"]["gen_ai.request.temperature"] == 0.7 assert ai_client_span2["data"]["gen_ai.request.top_p"] == 1.0 - assert ai_client_span2["data"]["gen_ai.response.text"] == safe_serialize( - ["Task completed using the tool"] + assert ( + ai_client_span2["data"]["gen_ai.response.text"] + == "Task completed using the tool" ) assert ai_client_span2["data"]["gen_ai.system"] == "openai" assert ai_client_span2["data"]["gen_ai.usage.input_tokens.cached"] == 0 From f78552480e894b7a5a152530c589d9677f81bc14 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Fri, 5 Sep 2025 11:25:19 +0000 Subject: [PATCH 29/88] release: 2.37.0 --- CHANGELOG.md | 13 +++++++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c10ef8b7f..29dfdfff07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 2.37.0 + +### Various fixes & improvements + +- feat(agents): improve instrumentation of input messages (#4750) by @shellmayr +- tests: Move boto3 tests under toxgen (#4761) by @sentrivana +- ci: Fix celery (#4765) by @sentrivana +- Format span attributes in AI integrations (#4762) by @antonpirker +- tests: Move asyncpg under toxgen (#4757) by @sentrivana +- feat: Add LangGraph integration (#4727) by @shellmayr +- tests: Move beam under toxgen (#4759) by @sentrivana +- tests: Remove openai pin and update tox (#4748) by @sentrivana + ## 2.36.0 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index 835c20b112..935f45f6af 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.36.0" +release = "2.37.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 5480ef5dce..68a44fe88f 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1330,4 +1330,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.36.0" +VERSION = "2.37.0" diff --git a/setup.py b/setup.py index ca6e7ec534..8c4ea96ab9 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.36.0", + version="2.37.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From 7d7c8ea0a50252151b05eeaeaa9c8f87cbe1b0c6 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 5 Sep 2025 13:28:41 +0200 Subject: [PATCH 30/88] tests: Move chalice under toxgen (#4766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chalice was the last test suite in the Cloud group that was still hardcoded, so moving it under toxgen also gets rid of the whole `latest` group 🎉 Ref https://github.com/getsentry/sentry-python/issues/4506 --- .github/workflows/test-integrations-cloud.yml | 79 ------------------- scripts/populate_tox/config.py | 6 ++ scripts/populate_tox/populate_tox.py | 1 - scripts/populate_tox/tox.jinja | 9 --- tests/integrations/chalice/test_chalice.py | 4 +- tox.ini | 22 +++--- 6 files changed, 20 insertions(+), 101 deletions(-) diff --git a/.github/workflows/test-integrations-cloud.yml b/.github/workflows/test-integrations-cloud.yml index 8688a1d48e..62e70d759d 100644 --- a/.github/workflows/test-integrations-cloud.yml +++ b/.github/workflows/test-integrations-cloud.yml @@ -22,85 +22,6 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-cloud-latest: - name: Cloud (latest) - timeout-minutes: 30 - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - python-version: ["3.8","3.12","3.13"] - # python3.6 reached EOL and is no longer being supported on - # new versions of hosted runners on Github Actions - # ubuntu-20.04 is the last version that supported python3.6 - # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 - os: [ubuntu-22.04] - services: - docker: - image: docker:dind # Required for Docker network management - options: --privileged # Required for Docker-in-Docker operations - # Use Docker container only for Python 3.6 - container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} - steps: - - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 - if: ${{ matrix.python-version != '3.6' }} - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - name: Setup Test Env - run: | - pip install "coverage[toml]" tox - - name: Erase coverage - run: | - coverage erase - - name: Test aws_lambda latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-aws_lambda-latest" - - name: Test boto3 latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-boto3-latest" - - name: Test chalice latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-chalice-latest" - - name: Test cloud_resource_context latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-cloud_resource_context-latest" - - name: Test gcp latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-gcp-latest" - - name: Generate coverage XML (Python 3.6) - if: ${{ !cancelled() && matrix.python-version == '3.6' }} - run: | - export COVERAGE_RCFILE=.coveragerc36 - coverage combine .coverage-sentry-* - coverage xml --ignore-errors - - name: Generate coverage XML - if: ${{ !cancelled() && matrix.python-version != '3.6' }} - run: | - coverage combine .coverage-sentry-* - coverage xml - - name: Upload coverage to Codecov - if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml - verbose: true test-cloud-pinned: name: Cloud (pinned) timeout-minutes: 30 diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 5aba82b11b..b05c4297f1 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -66,6 +66,12 @@ "py3.7": ["importlib-metadata<5.0"], }, }, + "chalice": { + "package": "chalice", + "deps": { + "*": ["pytest-chalice"], + }, + }, "clickhouse_driver": { "package": "clickhouse-driver", }, diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index b8cc988fda..9aed9fa718 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -67,7 +67,6 @@ "potel", # Integrations that can be migrated -- we should eventually remove all # of these from the IGNORE list - "chalice", "gcp", "httpx", "pure_eval", diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 7f23d1fbc7..c243b5752e 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -42,10 +42,6 @@ envlist = # AWS Lambda {py3.8,py3.9,py3.11,py3.13}-aws_lambda - # Chalice - {py3.6,py3.9}-chalice-v{1.16} - {py3.8,py3.12,py3.13}-chalice-latest - # Cloud Resource Context {py3.6,py3.12,py3.13}-cloud_resource_context @@ -158,11 +154,6 @@ deps = aws_lambda: requests aws_lambda: uvicorn - # Chalice - chalice: pytest-chalice==0.0.5 - chalice-v1.16: chalice~=1.16.0 - chalice-latest: chalice - # HTTPX httpx-v0.16: pytest-httpx==0.10.0 httpx-v0.18: pytest-httpx==0.12.0 diff --git a/tests/integrations/chalice/test_chalice.py b/tests/integrations/chalice/test_chalice.py index fbd4be4e59..ec8106eb5f 100644 --- a/tests/integrations/chalice/test_chalice.py +++ b/tests/integrations/chalice/test_chalice.py @@ -110,7 +110,7 @@ def every_hour(event): @pytest.mark.skipif( - parse_version(CHALICE_VERSION) >= (1, 28), + parse_version(CHALICE_VERSION) >= (1, 26, 0), reason="different behavior based on chalice version", ) def test_bad_request_old(client: RequestHandler) -> None: @@ -124,7 +124,7 @@ def test_bad_request_old(client: RequestHandler) -> None: @pytest.mark.skipif( - parse_version(CHALICE_VERSION) < (1, 28), + parse_version(CHALICE_VERSION) < (1, 26, 0), reason="different behavior based on chalice version", ) def test_bad_request(client: RequestHandler) -> None: diff --git a/tox.ini b/tox.ini index 948887f1dd..335007664a 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-05T07:14:50.663886+00:00 +# Last generated: 2025-09-05T07:52:27.350774+00:00 [tox] requires = @@ -42,10 +42,6 @@ envlist = # AWS Lambda {py3.8,py3.9,py3.11,py3.13}-aws_lambda - # Chalice - {py3.6,py3.9}-chalice-v{1.16} - {py3.8,py3.12,py3.13}-chalice-latest - # Cloud Resource Context {py3.6,py3.12,py3.13}-cloud_resource_context @@ -152,6 +148,11 @@ envlist = {py3.7,py3.11,py3.12}-boto3-v1.28.85 {py3.9,py3.12,py3.13}-boto3-v1.40.24 + {py3.6,py3.7,py3.8}-chalice-v1.16.0 + {py3.6,py3.7,py3.8}-chalice-v1.21.9 + {py3.6,py3.8,py3.9}-chalice-v1.26.6 + {py3.9,py3.12,py3.13}-chalice-v1.32.0 + # ~~~ DBs ~~~ {py3.7,py3.8,py3.9}-asyncpg-v0.23.0 @@ -372,11 +373,6 @@ deps = aws_lambda: requests aws_lambda: uvicorn - # Chalice - chalice: pytest-chalice==0.0.5 - chalice-v1.16: chalice~=1.16.0 - chalice-latest: chalice - # HTTPX httpx-v0.16: pytest-httpx==0.10.0 httpx-v0.18: pytest-httpx==0.12.0 @@ -541,6 +537,12 @@ deps = boto3-v1.40.24: boto3==1.40.24 {py3.7,py3.8}-boto3: urllib3<2.0.0 + chalice-v1.16.0: chalice==1.16.0 + chalice-v1.21.9: chalice==1.21.9 + chalice-v1.26.6: chalice==1.26.6 + chalice-v1.32.0: chalice==1.32.0 + chalice: pytest-chalice + # ~~~ DBs ~~~ asyncpg-v0.23.0: asyncpg==0.23.0 From 75ef769d494c45e6f8b133da22fe75dcf9da713e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 5 Sep 2025 13:31:56 +0200 Subject: [PATCH 31/88] Updated changelog --- CHANGELOG.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29dfdfff07..52478dd4dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,19 @@ ## 2.37.0 -### Various fixes & improvements +- **New Integration (BETA):** Add support for `langgraph` (#4727) by @shellmayr + + We can now instrument AI agents that are created with [LangGraph](https://www.langchain.com/langgraph) out of the box. + + For more information see the [LangGraph integrations documentation](https://docs.sentry.io/platforms/python/integrations/langgraph/). -- feat(agents): improve instrumentation of input messages (#4750) by @shellmayr -- tests: Move boto3 tests under toxgen (#4761) by @sentrivana -- ci: Fix celery (#4765) by @sentrivana -- Format span attributes in AI integrations (#4762) by @antonpirker -- tests: Move asyncpg under toxgen (#4757) by @sentrivana -- feat: Add LangGraph integration (#4727) by @shellmayr -- tests: Move beam under toxgen (#4759) by @sentrivana -- tests: Remove openai pin and update tox (#4748) by @sentrivana +- AI Agents: Improve rendering of input and output messages in AI agents integrations. (#4750) by @shellmayr +- AI Agents: Format span attributes in AI integrations (#4762) by @antonpirker +- CI: Fix celery (#4765) by @sentrivana +- Tests: Move asyncpg under toxgen (#4757) by @sentrivana +- Tests: Move beam under toxgen (#4759) by @sentrivana +- Tests: Move boto3 tests under toxgen (#4761) by @sentrivana +- Tests: Remove openai pin and update tox (#4748) by @sentrivana ## 2.36.0 From bdf3e6d51dd5007e37ba133a24b1e986502a6bd1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 8 Sep 2025 11:22:35 +0200 Subject: [PATCH 32/88] tests: Update tox.ini (#4777) --- tox.ini | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tox.ini b/tox.ini index 335007664a..2c1c2382f5 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-05T07:52:27.350774+00:00 +# Last generated: 2025-09-08T07:44:56.804943+00:00 [tox] requires = @@ -128,8 +128,8 @@ envlist = {py3.8,py3.11,py3.12}-openai-notiktoken-v1.71.0 {py3.8,py3.12,py3.13}-openai-notiktoken-v1.106.1 - {py3.9,py3.12,py3.13}-langgraph-v0.6.6 - {py3.10,py3.12,py3.13}-langgraph-v1.0.0a2 + {py3.9,py3.12,py3.13}-langgraph-v0.6.7 + {py3.10,py3.12,py3.13}-langgraph-v1.0.0a3 {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 @@ -146,7 +146,7 @@ envlist = {py3.6,py3.7}-boto3-v1.12.49 {py3.6,py3.9,py3.10}-boto3-v1.20.54 {py3.7,py3.11,py3.12}-boto3-v1.28.85 - {py3.9,py3.12,py3.13}-boto3-v1.40.24 + {py3.9,py3.12,py3.13}-boto3-v1.40.25 {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.6,py3.7,py3.8}-chalice-v1.21.9 @@ -205,7 +205,7 @@ envlist = {py3.6,py3.9,py3.10}-gql-v3.4.1 {py3.7,py3.11,py3.12}-gql-v3.5.3 {py3.9,py3.12,py3.13}-gql-v4.0.0 - {py3.9,py3.12,py3.13}-gql-v4.1.0b0 + {py3.9,py3.12,py3.13}-gql-v4.2.0b0 {py3.6,py3.9,py3.10}-graphene-v3.3 {py3.8,py3.12,py3.13}-graphene-v3.4.3 @@ -213,7 +213,7 @@ envlist = {py3.8,py3.10,py3.11}-strawberry-v0.209.8 {py3.8,py3.11,py3.12}-strawberry-v0.233.3 {py3.9,py3.12,py3.13}-strawberry-v0.257.0 - {py3.9,py3.12,py3.13}-strawberry-v0.281.0 + {py3.9,py3.12,py3.13}-strawberry-v0.282.0 # ~~~ Network ~~~ @@ -251,7 +251,7 @@ envlist = {py3.8,py3.9}-spark-v3.0.3 {py3.8,py3.10,py3.11}-spark-v3.5.6 - {py3.9,py3.12,py3.13}-spark-v4.0.0 + {py3.9,py3.12,py3.13}-spark-v4.0.1 # ~~~ Web 1 ~~~ @@ -325,7 +325,7 @@ envlist = {py3.7,py3.12,py3.13}-typer-v0.15.4 {py3.7,py3.12,py3.13}-typer-v0.16.1 - {py3.7,py3.12,py3.13}-typer-v0.17.3 + {py3.7,py3.12,py3.13}-typer-v0.17.4 @@ -515,8 +515,8 @@ deps = openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.36.1: httpx<0.28 - langgraph-v0.6.6: langgraph==0.6.6 - langgraph-v1.0.0a2: langgraph==1.0.0a2 + langgraph-v0.6.7: langgraph==0.6.7 + langgraph-v1.0.0a3: langgraph==1.0.0a3 openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.1.0: openai-agents==0.1.0 @@ -534,7 +534,7 @@ deps = boto3-v1.12.49: boto3==1.12.49 boto3-v1.20.54: boto3==1.20.54 boto3-v1.28.85: boto3==1.28.85 - boto3-v1.40.24: boto3==1.40.24 + boto3-v1.40.25: boto3==1.40.25 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 @@ -601,7 +601,7 @@ deps = gql-v3.4.1: gql[all]==3.4.1 gql-v3.5.3: gql[all]==3.5.3 gql-v4.0.0: gql[all]==4.0.0 - gql-v4.1.0b0: gql[all]==4.1.0b0 + gql-v4.2.0b0: gql[all]==4.2.0b0 graphene-v3.3: graphene==3.3 graphene-v3.4.3: graphene==3.4.3 @@ -614,7 +614,7 @@ deps = strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8 strawberry-v0.233.3: strawberry-graphql[fastapi,flask]==0.233.3 strawberry-v0.257.0: strawberry-graphql[fastapi,flask]==0.257.0 - strawberry-v0.281.0: strawberry-graphql[fastapi,flask]==0.281.0 + strawberry-v0.282.0: strawberry-graphql[fastapi,flask]==0.282.0 strawberry: httpx strawberry-v0.209.8: pydantic<2.11 strawberry-v0.233.3: pydantic<2.11 @@ -667,7 +667,7 @@ deps = spark-v3.0.3: pyspark==3.0.3 spark-v3.5.6: pyspark==3.5.6 - spark-v4.0.0: pyspark==4.0.0 + spark-v4.0.1: pyspark==4.0.1 # ~~~ Web 1 ~~~ @@ -810,7 +810,7 @@ deps = typer-v0.15.4: typer==0.15.4 typer-v0.16.1: typer==0.16.1 - typer-v0.17.3: typer==0.17.3 + typer-v0.17.4: typer==0.17.4 From 0eede69d8eded6785c62461d81a3b4585421d9eb Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 8 Sep 2025 13:46:08 +0200 Subject: [PATCH 33/88] tests: Move quart under toxgen (#4775) --- .github/workflows/test-integrations-web-2.yml | 2 +- scripts/populate_tox/config.py | 14 ++++++ scripts/populate_tox/populate_tox.py | 4 +- scripts/populate_tox/tox.jinja | 19 -------- tox.ini | 48 +++++++++++-------- 5 files changed, 45 insertions(+), 42 deletions(-) diff --git a/.github/workflows/test-integrations-web-2.yml b/.github/workflows/test-integrations-web-2.yml index e79a54ef67..22200f8ae1 100644 --- a/.github/workflows/test-integrations-web-2.yml +++ b/.github/workflows/test-integrations-web-2.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8","3.9","3.12","3.13"] + python-version: ["3.9","3.12","3.13"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index b05c4297f1..679ffddf2c 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -232,6 +232,20 @@ "*": ["werkzeug<2.1.0"], }, }, + "quart": { + "package": "quart", + "deps": { + "*": ["quart-auth", "pytest-asyncio", "Werkzeug"], + ">=0.19": ["quart-flask-patch"], + "<0.19": [ + "blinker<1.6", + "jinja2<3.1.0", + "Werkzeug<2.3.0", + "hypercorn<0.15.0", + ], + "py3.8": ["taskgroup==0.0.0a4"], + }, + }, "redis_py_cluster_legacy": { "package": "redis-py-cluster", }, diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 9aed9fa718..e08c2d4b95 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -57,7 +57,8 @@ # pypi package to install in different versions). # # Test suites that will have to remain hardcoded since they don't fit the - # toxgen usecase + # toxgen usecase (there is no one package that should be tested in different + # versions) "asgi", "aws_lambda", "cloud_resource_context", @@ -70,7 +71,6 @@ "gcp", "httpx", "pure_eval", - "quart", "ray", "redis", "requests", diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index c243b5752e..ef2e89c88c 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -64,11 +64,6 @@ envlist = # pure_eval {py3.6,py3.12,py3.13}-pure_eval - # Quart - {py3.7,py3.11}-quart-v{0.16} - {py3.8,py3.11,py3.12}-quart-v{0.19} - {py3.8,py3.12,py3.13}-quart-latest - # Ray {py3.10,py3.11}-ray-v{2.34} {py3.10,py3.11}-ray-latest @@ -184,20 +179,6 @@ deps = # pure_eval pure_eval: pure_eval - # Quart - quart: quart-auth - quart: pytest-asyncio - quart-{v0.19,latest}: quart-flask-patch - quart-v0.16: blinker<1.6 - quart-v0.16: jinja2<3.1.0 - quart-v0.16: Werkzeug<2.1.0 - quart-v0.16: hypercorn<0.15.0 - quart-v0.16: quart~=0.16.0 - quart-v0.19: Werkzeug>=3.0.0 - quart-v0.19: quart~=0.19.0 - {py3.8}-quart: taskgroup==0.0.0a4 - quart-latest: quart - # Ray ray-v2.34: ray~=2.34.0 ray-latest: ray diff --git a/tox.ini b/tox.ini index 2c1c2382f5..ff2403f515 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-08T07:44:56.804943+00:00 +# Last generated: 2025-09-08T11:35:09.849536+00:00 [tox] requires = @@ -64,11 +64,6 @@ envlist = # pure_eval {py3.6,py3.12,py3.13}-pure_eval - # Quart - {py3.7,py3.11}-quart-v{0.16} - {py3.8,py3.11,py3.12}-quart-v{0.19} - {py3.8,py3.12,py3.13}-quart-latest - # Ray {py3.10,py3.11}-ray-v{2.34} {py3.10,py3.11}-ray-latest @@ -302,6 +297,11 @@ envlist = {py3.6,py3.8,py3.9}-pyramid-v1.10.8 {py3.6,py3.10,py3.11}-pyramid-v2.0.2 + {py3.7,py3.9,py3.10}-quart-v0.16.3 + {py3.7,py3.9,py3.10}-quart-v0.17.0 + {py3.7,py3.10,py3.11}-quart-v0.18.4 + {py3.9,py3.12,py3.13}-quart-v0.20.0 + {py3.8,py3.10,py3.11}-starlite-v1.48.1 {py3.8,py3.10,py3.11}-starlite-v1.49.0 {py3.8,py3.10,py3.11}-starlite-v1.50.2 @@ -403,20 +403,6 @@ deps = # pure_eval pure_eval: pure_eval - # Quart - quart: quart-auth - quart: pytest-asyncio - quart-{v0.19,latest}: quart-flask-patch - quart-v0.16: blinker<1.6 - quart-v0.16: jinja2<3.1.0 - quart-v0.16: Werkzeug<2.1.0 - quart-v0.16: hypercorn<0.15.0 - quart-v0.16: quart~=0.16.0 - quart-v0.19: Werkzeug>=3.0.0 - quart-v0.19: quart~=0.19.0 - {py3.8}-quart: taskgroup==0.0.0a4 - quart-latest: quart - # Ray ray-v2.34: ray~=2.34.0 ray-latest: ray @@ -774,6 +760,28 @@ deps = pyramid-v2.0.2: pyramid==2.0.2 pyramid: werkzeug<2.1.0 + quart-v0.16.3: quart==0.16.3 + quart-v0.17.0: quart==0.17.0 + quart-v0.18.4: quart==0.18.4 + quart-v0.20.0: quart==0.20.0 + quart: quart-auth + quart: pytest-asyncio + quart: Werkzeug + quart-v0.20.0: quart-flask-patch + quart-v0.16.3: blinker<1.6 + quart-v0.16.3: jinja2<3.1.0 + quart-v0.16.3: Werkzeug<2.3.0 + quart-v0.16.3: hypercorn<0.15.0 + quart-v0.17.0: blinker<1.6 + quart-v0.17.0: jinja2<3.1.0 + quart-v0.17.0: Werkzeug<2.3.0 + quart-v0.17.0: hypercorn<0.15.0 + quart-v0.18.4: blinker<1.6 + quart-v0.18.4: jinja2<3.1.0 + quart-v0.18.4: Werkzeug<2.3.0 + quart-v0.18.4: hypercorn<0.15.0 + {py3.8}-quart: taskgroup==0.0.0a4 + starlite-v1.48.1: starlite==1.48.1 starlite-v1.49.0: starlite==1.49.0 starlite-v1.50.2: starlite==1.50.2 From 20f0f848fb7b2520c0b6082597adcce7fbdd8cee Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Tue, 9 Sep 2025 10:14:44 +0200 Subject: [PATCH 34/88] fix(langchain): make new langchain integration work with just langchain-core (#4783) - Catch ImportError for `langchain.agents` if langchain is not present and set AgentExecutor to None so the rest of the logic still keeps working - Add test for recording new OTEL-compliant data for `langchain-core` - Verified AI Agents functionality (span generation, token accounting, etc) with `langchain-core` and `langchain-openai` Closes TET-1126 Closes PY-1833 Closes https://github.com/getsentry/sentry-python/issues/4776 --- sentry_sdk/integrations/langchain.py | 6 +- .../integrations/langchain/test_langchain.py | 78 ++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index a53115a2a9..e14dd619fe 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -29,7 +29,6 @@ try: - from langchain.agents import AgentExecutor from langchain_core.agents import AgentFinish from langchain_core.callbacks import ( BaseCallbackHandler, @@ -44,6 +43,11 @@ raise DidNotEnable("langchain not installed") +try: + from langchain.agents import AgentExecutor +except ImportError: + AgentExecutor = None + DATA_FIELDS = { "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, "function_call": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 99dc5f4e37..b6b432c523 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -1,6 +1,6 @@ from typing import List, Optional, Any, Iterator from unittest import mock -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest @@ -662,3 +662,79 @@ def test_tools_integration_in_spans(sentry_init, capture_events): # Ensure we found at least one span with tools data assert tools_found, "No spans found with tools data" + + +def test_langchain_integration_with_langchain_core_only(sentry_init, capture_events): + """Test that the langchain integration works when langchain.agents.AgentExecutor + is not available or langchain is not installed, but langchain-core is. + """ + + from langchain_core.outputs import LLMResult, Generation + + with patch("sentry_sdk.integrations.langchain.AgentExecutor", None): + from sentry_sdk.integrations.langchain import ( + LangchainIntegration, + SentryLangchainCallback, + ) + + sentry_init( + integrations=[LangchainIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + try: + LangchainIntegration.setup_once() + except Exception as e: + pytest.fail(f"setup_once() failed when AgentExecutor is None: {e}") + + callback = SentryLangchainCallback(max_span_map_size=100, include_prompts=True) + + run_id = "12345678-1234-1234-1234-123456789012" + serialized = {"_type": "openai-chat", "model_name": "gpt-3.5-turbo"} + prompts = ["What is the capital of France?"] + + with start_transaction(): + callback.on_llm_start( + serialized=serialized, + prompts=prompts, + run_id=run_id, + invocation_params={ + "temperature": 0.7, + "max_tokens": 100, + "model": "gpt-3.5-turbo", + }, + ) + + response = LLMResult( + generations=[[Generation(text="The capital of France is Paris.")]], + llm_output={ + "token_usage": { + "total_tokens": 25, + "prompt_tokens": 10, + "completion_tokens": 15, + } + }, + ) + callback.on_llm_end(response=response, run_id=run_id) + + assert len(events) > 0 + tx = events[0] + assert tx["type"] == "transaction" + + llm_spans = [ + span for span in tx.get("spans", []) if span.get("op") == "gen_ai.pipeline" + ] + assert len(llm_spans) > 0 + + llm_span = llm_spans[0] + assert llm_span["description"] == "Langchain LLM call" + assert llm_span["data"]["gen_ai.request.model"] == "gpt-3.5-turbo" + assert ( + llm_span["data"]["gen_ai.response.text"] + == "The capital of France is Paris." + ) + assert llm_span["data"]["gen_ai.usage.total_tokens"] == 25 + assert llm_span["data"]["gen_ai.usage.input_tokens"] == 10 + assert llm_span["data"]["gen_ai.usage.output_tokens"] == 15 From 2d9c428d43e03ffb1789cfb9948684de8df7a551 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 9 Sep 2025 13:24:33 +0000 Subject: [PATCH 35/88] release: 2.37.1 --- CHANGELOG.md | 9 +++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52478dd4dd..7491ea63d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2.37.1 + +### Various fixes & improvements + +- fix(langchain): make new langchain integration work with just langchain-core (#4783) by @shellmayr +- tests: Move quart under toxgen (#4775) by @sentrivana +- tests: Update tox.ini (#4777) by @sentrivana +- tests: Move chalice under toxgen (#4766) by @sentrivana + ## 2.37.0 - **New Integration (BETA):** Add support for `langgraph` (#4727) by @shellmayr diff --git a/docs/conf.py b/docs/conf.py index 935f45f6af..28a49b7fa7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.37.0" +release = "2.37.1" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 68a44fe88f..4f015643d4 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1330,4 +1330,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.37.0" +VERSION = "2.37.1" diff --git a/setup.py b/setup.py index 8c4ea96ab9..1b4d0063e4 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.37.0", + version="2.37.1", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From cd23041494a7cf98350c983d69e528a772e5cd6d Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 9 Sep 2025 15:25:57 +0200 Subject: [PATCH 36/88] updated changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7491ea63d2..28c4882414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,10 @@ ### Various fixes & improvements -- fix(langchain): make new langchain integration work with just langchain-core (#4783) by @shellmayr -- tests: Move quart under toxgen (#4775) by @sentrivana -- tests: Update tox.ini (#4777) by @sentrivana -- tests: Move chalice under toxgen (#4766) by @sentrivana +- Fix(langchain): Make Langchain integration work with just langchain-core (#4783) by @shellmayr +- Tests: Move quart under toxgen (#4775) by @sentrivana +- Tests: Update tox.ini (#4777) by @sentrivana +- Tests: Move chalice under toxgen (#4766) by @sentrivana ## 2.37.0 From 6463f73e48abd3fc30d26ff07ae60fb65dc38a2a Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Tue, 9 Sep 2025 11:35:46 -0400 Subject: [PATCH 37/88] fix(profiling): Re-init continuous profiler (#4772) Re-initializing the continuous profiler should use new settings. --- sentry_sdk/profiler/continuous_profiler.py | 16 +++++++++++++--- tests/profiler/test_continuous_profiler.py | 13 ++++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/profiler/continuous_profiler.py b/sentry_sdk/profiler/continuous_profiler.py index 00dd29e36c..165bd13837 100644 --- a/sentry_sdk/profiler/continuous_profiler.py +++ b/sentry_sdk/profiler/continuous_profiler.py @@ -75,9 +75,11 @@ def setup_continuous_profiler(options, sdk_info, capture_func): # type: (Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> bool global _scheduler - if _scheduler is not None: + already_initialized = _scheduler is not None + + if already_initialized: logger.debug("[Profiling] Continuous Profiler is already setup") - return False + teardown_continuous_profiler() if is_gevent(): # If gevent has patched the threading modules then we cannot rely on @@ -117,11 +119,19 @@ def setup_continuous_profiler(options, sdk_info, capture_func): ) ) - atexit.register(teardown_continuous_profiler) + if not already_initialized: + atexit.register(teardown_continuous_profiler) return True +def is_profile_session_sampled(): + # type: () -> bool + if _scheduler is None: + return False + return _scheduler.sampled + + def try_autostart_continuous_profiler(): # type: () -> None diff --git a/tests/profiler/test_continuous_profiler.py b/tests/profiler/test_continuous_profiler.py index 7283ec7164..e4f5cb5e25 100644 --- a/tests/profiler/test_continuous_profiler.py +++ b/tests/profiler/test_continuous_profiler.py @@ -8,6 +8,7 @@ import sentry_sdk from sentry_sdk.consts import VERSION from sentry_sdk.profiler.continuous_profiler import ( + is_profile_session_sampled, get_profiler_id, setup_continuous_profiler, start_profiler, @@ -113,19 +114,25 @@ def test_continuous_profiler_valid_mode(mode, make_options, teardown_profiling): ], ) def test_continuous_profiler_setup_twice(mode, make_options, teardown_profiling): - options = make_options(mode=mode) + assert not is_profile_session_sampled() + # setting up the first time should return True to indicate success + options = make_options(mode=mode, profile_session_sample_rate=1.0) assert setup_continuous_profiler( options, mock_sdk_info, lambda envelope: None, ) - # setting up the second time should return False to indicate no-op - assert not setup_continuous_profiler( + assert is_profile_session_sampled() + + # setting up the second time should return True to indicate re-init + options = make_options(mode=mode, profile_session_sample_rate=0.0) + assert setup_continuous_profiler( options, mock_sdk_info, lambda envelope: None, ) + assert not is_profile_session_sampled() def assert_single_transaction_with_profile_chunks( From 90011260a435f5dba48785c76cb0869c9c99d111 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 10 Sep 2025 08:48:00 +0200 Subject: [PATCH 38/88] Update HuggingFace Hub integration (#4746) Make our existing `huggingface_hub` integration compatible with the new AI Agents insights module. All spans created by the integrations should should create all spans applicable form the [AI Insights ](https://develop.sentry.dev/sdk/telemetry/traces/modules/ai-agents/)documentation. All spans must have the correct span.op, span.name/span.description and span.data/span.attributes set. This makes our SDK and data compatible with v1.36.0 of the Semantic conventions for generative AI systems of OpenTelementry. There are some cases where our AI Insights documentation diverges from Otels semantic conventions. Details for those attributes can be found in the [Sentry conventions](https://getsentry.github.io/sentry-conventions/generated/attributes/gen_ai.html). --- scripts/populate_tox/config.py | 3 + sentry_sdk/consts.py | 1 + sentry_sdk/integrations/__init__.py | 2 +- sentry_sdk/integrations/huggingface_hub.py | 358 +++++-- .../huggingface_hub/test_huggingface_hub.py | 883 +++++++++++++++--- tox.ini | 23 +- 6 files changed, 1050 insertions(+), 220 deletions(-) diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 679ffddf2c..bc20d531b3 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -155,6 +155,9 @@ }, "huggingface_hub": { "package": "huggingface_hub", + "deps": { + "*": ["responses"], + }, }, "langchain-base": { "package": "langchain", diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 4f015643d4..cc3c9b1612 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -795,6 +795,7 @@ class OP: GEN_AI_CREATE_AGENT = "gen_ai.create_agent" GEN_AI_EMBEDDINGS = "gen_ai.embeddings" GEN_AI_EXECUTE_TOOL = "gen_ai.execute_tool" + GEN_AI_GENERATE_TEXT = "gen_ai.generate_text" GEN_AI_HANDOFF = "gen_ai.handoff" GEN_AI_PIPELINE = "gen_ai.pipeline" GEN_AI_INVOKE_AGENT = "gen_ai.invoke_agent" diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 7f202221a7..2f5a1f397e 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -141,7 +141,7 @@ def iter_default_integrations(with_auto_enabling_integrations): "gql": (3, 4, 1), "graphene": (3, 3), "grpc": (1, 32, 0), # grpcio - "huggingface_hub": (0, 22), + "huggingface_hub": (0, 24, 7), "langchain": (0, 1, 0), "langgraph": (0, 6, 6), "launchdarkly": (9, 8, 0), diff --git a/sentry_sdk/integrations/huggingface_hub.py b/sentry_sdk/integrations/huggingface_hub.py index 2dfcb5925a..cb76ccf507 100644 --- a/sentry_sdk/integrations/huggingface_hub.py +++ b/sentry_sdk/integrations/huggingface_hub.py @@ -1,24 +1,24 @@ +import inspect from functools import wraps -from sentry_sdk import consts +import sentry_sdk from sentry_sdk.ai.monitoring import record_token_usage from sentry_sdk.ai.utils import set_data_normalized -from sentry_sdk.consts import SPANDATA - -from typing import Any, Iterable, Callable - -import sentry_sdk -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, ) +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Iterable + try: import huggingface_hub.inference._client - - from huggingface_hub import ChatCompletionStreamOutput, TextGenerationOutput except ImportError: raise DidNotEnable("Huggingface not installed") @@ -34,9 +34,18 @@ def __init__(self, include_prompts=True): @staticmethod def setup_once(): # type: () -> None + + # Other tasks that can be called: https://huggingface.co/docs/huggingface_hub/guides/inference#supported-providers-and-tasks huggingface_hub.inference._client.InferenceClient.text_generation = ( - _wrap_text_generation( - huggingface_hub.inference._client.InferenceClient.text_generation + _wrap_huggingface_task( + huggingface_hub.inference._client.InferenceClient.text_generation, + OP.GEN_AI_GENERATE_TEXT, + ) + ) + huggingface_hub.inference._client.InferenceClient.chat_completion = ( + _wrap_huggingface_task( + huggingface_hub.inference._client.InferenceClient.chat_completion, + OP.GEN_AI_CHAT, ) ) @@ -51,131 +60,318 @@ def _capture_exception(exc): sentry_sdk.capture_event(event, hint=hint) -def _wrap_text_generation(f): - # type: (Callable[..., Any]) -> Callable[..., Any] +def _wrap_huggingface_task(f, op): + # type: (Callable[..., Any], str) -> Callable[..., Any] @wraps(f) - def new_text_generation(*args, **kwargs): + def new_huggingface_task(*args, **kwargs): # type: (*Any, **Any) -> Any integration = sentry_sdk.get_client().get_integration(HuggingfaceHubIntegration) if integration is None: return f(*args, **kwargs) + prompt = None if "prompt" in kwargs: prompt = kwargs["prompt"] + elif "messages" in kwargs: + prompt = kwargs["messages"] elif len(args) >= 2: - kwargs["prompt"] = args[1] - prompt = kwargs["prompt"] - args = (args[0],) + args[2:] - else: - # invalid call, let it return error + if isinstance(args[1], str) or isinstance(args[1], list): + prompt = args[1] + + if prompt is None: + # invalid call, dont instrument, let it return error return f(*args, **kwargs) - model = kwargs.get("model") - streaming = kwargs.get("stream") + client = args[0] + model = client.model or kwargs.get("model") or "" + operation_name = op.split(".")[-1] span = sentry_sdk.start_span( - op=consts.OP.HUGGINGFACE_HUB_CHAT_COMPLETIONS_CREATE, - name="Text Generation", + op=op, + name=f"{operation_name} {model}", origin=HuggingfaceHubIntegration.origin, ) span.__enter__() + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, operation_name) + + if model: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + # Input attributes + if should_send_default_pii() and integration.include_prompts: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False + ) + + attribute_mapping = { + "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + } + + for attribute, span_attribute in attribute_mapping.items(): + value = kwargs.get(attribute, None) + if value is not None: + if isinstance(value, (int, float, bool, str)): + span.set_data(span_attribute, value) + else: + set_data_normalized(span, span_attribute, value, unpack=False) + + # LLM Execution try: res = f(*args, **kwargs) except Exception as e: + # Error Handling + span.set_status("error") _capture_exception(e) span.__exit__(None, None, None) raise e from None + # Output attributes + finish_reason = None + response_model = None + response_text_buffer: list[str] = [] + tokens_used = 0 + tool_calls = None + usage = None + with capture_internal_exceptions(): - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.AI_INPUT_MESSAGES, prompt) + if isinstance(res, str) and res is not None: + response_text_buffer.append(res) - set_data_normalized(span, SPANDATA.AI_MODEL_ID, model) - set_data_normalized(span, SPANDATA.AI_STREAMING, streaming) + if hasattr(res, "generated_text") and res.generated_text is not None: + response_text_buffer.append(res.generated_text) - if isinstance(res, str): - if should_send_default_pii() and integration.include_prompts: - set_data_normalized( - span, - SPANDATA.AI_RESPONSES, - [res], - ) - span.__exit__(None, None, None) - return res + if hasattr(res, "model") and res.model is not None: + response_model = res.model + + if hasattr(res, "details") and hasattr(res.details, "finish_reason"): + finish_reason = res.details.finish_reason + + if ( + hasattr(res, "details") + and hasattr(res.details, "generated_tokens") + and res.details.generated_tokens is not None + ): + tokens_used = res.details.generated_tokens + + if hasattr(res, "usage") and res.usage is not None: + usage = res.usage + + if hasattr(res, "choices") and res.choices is not None: + for choice in res.choices: + if hasattr(choice, "finish_reason"): + finish_reason = choice.finish_reason + if hasattr(choice, "message") and hasattr( + choice.message, "tool_calls" + ): + tool_calls = choice.message.tool_calls + if ( + hasattr(choice, "message") + and hasattr(choice.message, "content") + and choice.message.content is not None + ): + response_text_buffer.append(choice.message.content) - if isinstance(res, TextGenerationOutput): - if should_send_default_pii() and integration.include_prompts: + if response_model is not None: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: set_data_normalized( span, - SPANDATA.AI_RESPONSES, - [res.generated_text], - ) - if res.details is not None and res.details.generated_tokens > 0: - record_token_usage( - span, - total_tokens=res.details.generated_tokens, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, ) - span.__exit__(None, None, None) - return res - if not isinstance(res, Iterable): - # we only know how to deal with strings and iterables, ignore - set_data_normalized(span, "unknown_response", True) + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + elif tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) + + # If the response is not a generator (meaning a streaming response) + # we are done and can return the response + if not inspect.isgenerator(res): span.__exit__(None, None, None) return res if kwargs.get("details", False): - # res is Iterable[TextGenerationStreamOutput] + # text-generation stream output def new_details_iterator(): - # type: () -> Iterable[ChatCompletionStreamOutput] + # type: () -> Iterable[Any] + finish_reason = None + response_text_buffer: list[str] = [] + tokens_used = 0 + with capture_internal_exceptions(): - tokens_used = 0 - data_buf: list[str] = [] - for x in res: - if hasattr(x, "token") and hasattr(x.token, "text"): - data_buf.append(x.token.text) - if hasattr(x, "details") and hasattr( - x.details, "generated_tokens" + for chunk in res: + if ( + hasattr(chunk, "token") + and hasattr(chunk.token, "text") + and chunk.token.text is not None + ): + response_text_buffer.append(chunk.token.text) + + if hasattr(chunk, "details") and hasattr( + chunk.details, "finish_reason" + ): + finish_reason = chunk.details.finish_reason + + if ( + hasattr(chunk, "details") + and hasattr(chunk.details, "generated_tokens") + and chunk.details.generated_tokens is not None ): - tokens_used = x.details.generated_tokens - yield x - if ( - len(data_buf) > 0 - and should_send_default_pii() - and integration.include_prompts - ): + tokens_used = chunk.details.generated_tokens + + yield chunk + + if finish_reason is not None: set_data_normalized( - span, SPANDATA.AI_RESPONSES, "".join(data_buf) + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, ) + + if should_send_default_pii() and integration.include_prompts: + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + if tokens_used > 0: record_token_usage( span, total_tokens=tokens_used, ) + span.__exit__(None, None, None) return new_details_iterator() - else: - # res is Iterable[str] + else: + # chat-completion stream output def new_iterator(): # type: () -> Iterable[str] - data_buf: list[str] = [] + finish_reason = None + response_model = None + response_text_buffer: list[str] = [] + tool_calls = None + usage = None + with capture_internal_exceptions(): - for s in res: - if isinstance(s, str): - data_buf.append(s) - yield s - if ( - len(data_buf) > 0 - and should_send_default_pii() - and integration.include_prompts - ): + for chunk in res: + if hasattr(chunk, "model") and chunk.model is not None: + response_model = chunk.model + + if hasattr(chunk, "usage") and chunk.usage is not None: + usage = chunk.usage + + if isinstance(chunk, str): + if chunk is not None: + response_text_buffer.append(chunk) + + if hasattr(chunk, "choices") and chunk.choices is not None: + for choice in chunk.choices: + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "content") + and choice.delta.content is not None + ): + response_text_buffer.append( + choice.delta.content + ) + + if ( + hasattr(choice, "finish_reason") + and choice.finish_reason is not None + ): + finish_reason = choice.finish_reason + + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "tool_calls") + and choice.delta.tool_calls is not None + ): + tool_calls = choice.delta.tool_calls + + yield chunk + + if response_model is not None: + span.set_data( + SPANDATA.GEN_AI_RESPONSE_MODEL, response_model + ) + + if finish_reason is not None: set_data_normalized( - span, SPANDATA.AI_RESPONSES, "".join(data_buf) + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, ) + + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + span.__exit__(None, None, None) return new_iterator() - return new_text_generation + return new_huggingface_task diff --git a/tests/integrations/huggingface_hub/test_huggingface_hub.py b/tests/integrations/huggingface_hub/test_huggingface_hub.py index df0c6c6d76..86f9c10109 100644 --- a/tests/integrations/huggingface_hub/test_huggingface_hub.py +++ b/tests/integrations/huggingface_hub/test_huggingface_hub.py @@ -1,186 +1,815 @@ -import itertools from unittest import mock - import pytest -from huggingface_hub import ( - InferenceClient, -) -from huggingface_hub.errors import OverloadedError +import responses + +from huggingface_hub import InferenceClient -from sentry_sdk import start_transaction -from sentry_sdk.consts import SPANDATA +import sentry_sdk +from sentry_sdk.utils import package_version from sentry_sdk.integrations.huggingface_hub import HuggingfaceHubIntegration +from typing import TYPE_CHECKING -def mock_client_post(client, post_mock): - # huggingface-hub==0.28.0 deprecates the `post` method - # so patch `_inner_post` instead - if hasattr(client, "post"): - client.post = post_mock - if hasattr(client, "_inner_post"): - client._inner_post = post_mock +try: + from huggingface_hub.utils._errors import HfHubHTTPError +except ImportError: + from huggingface_hub.errors import HfHubHTTPError -@pytest.mark.parametrize( - "send_default_pii, include_prompts, details_arg", - itertools.product([True, False], repeat=3), -) -def test_nonstreaming_chat_completion( - sentry_init, capture_events, send_default_pii, include_prompts, details_arg -): - sentry_init( - integrations=[HuggingfaceHubIntegration(include_prompts=include_prompts)], - traces_sample_rate=1.0, - send_default_pii=send_default_pii, +if TYPE_CHECKING: + from typing import Any + + +HF_VERSION = package_version("huggingface-hub") + +if HF_VERSION and HF_VERSION < (0, 30, 0): + MODEL_ENDPOINT = "https://api-inference.huggingface.co/models/{model_name}" + INFERENCE_ENDPOINT = "https://api-inference.huggingface.co/models/{model_name}" +else: + MODEL_ENDPOINT = "https://huggingface.co/api/models/{model_name}" + INFERENCE_ENDPOINT = ( + "https://router.huggingface.co/hf-inference/models/{model_name}" ) - events = capture_events() - client = InferenceClient(model="https://") - if details_arg: - post_mock = mock.Mock( - return_value=b"""[{ - "generated_text": "the model response", +@pytest.fixture +def mock_hf_text_generation_api(): + # type: () -> Any + """Mock HuggingFace text generation API""" + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + model_name = "test-model" + + # Mock model info endpoint + rsps.add( + responses.GET, + MODEL_ENDPOINT.format(model_name=model_name), + json={ + "id": model_name, + "pipeline_tag": "text-generation", + "inferenceProviderMapping": { + "hf-inference": { + "status": "live", + "providerId": model_name, + "task": "text-generation", + } + }, + }, + status=200, + ) + + # Mock text generation endpoint + rsps.add( + responses.POST, + INFERENCE_ENDPOINT.format(model_name=model_name), + json={ + "generated_text": "[mocked] Hello! How can i help you?", "details": { "finish_reason": "length", "generated_tokens": 10, "prefill": [], - "tokens": [] - } - }]""" + "tokens": [], + }, + }, + status=200, + ) + + yield rsps + + +@pytest.fixture +def mock_hf_api_with_errors(): + # type: () -> Any + """Mock HuggingFace API that always raises errors for any request""" + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + model_name = "test-model" + + # Mock model info endpoint with error + rsps.add( + responses.GET, + MODEL_ENDPOINT.format(model_name=model_name), + json={"error": "Model not found"}, + status=404, + ) + + # Mock text generation endpoint with error + rsps.add( + responses.POST, + INFERENCE_ENDPOINT.format(model_name=model_name), + json={"error": "Internal server error", "message": "Something went wrong"}, + status=500, + ) + + # Mock chat completion endpoint with error + rsps.add( + responses.POST, + INFERENCE_ENDPOINT.format(model_name=model_name) + "/v1/chat/completions", + json={"error": "Internal server error", "message": "Something went wrong"}, + status=500, + ) + + # Catch-all pattern for any other model requests + rsps.add( + responses.GET, + "https://huggingface.co/api/models/test-model-error", + json={"error": "Generic model error"}, + status=500, + ) + + yield rsps + + +@pytest.fixture +def mock_hf_text_generation_api_streaming(): + # type: () -> Any + """Mock streaming HuggingFace text generation API""" + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + model_name = "test-model" + + # Mock model info endpoint + rsps.add( + responses.GET, + MODEL_ENDPOINT.format(model_name=model_name), + json={ + "id": model_name, + "pipeline_tag": "text-generation", + "inferenceProviderMapping": { + "hf-inference": { + "status": "live", + "providerId": model_name, + "task": "text-generation", + } + }, + }, + status=200, + ) + + # Mock text generation endpoint for streaming + streaming_response = b'data:{"token":{"id":1, "special": false, "text": "the mocked "}}\n\ndata:{"token":{"id":2, "special": false, "text": "model response"}, "details":{"finish_reason": "length", "generated_tokens": 10, "seed": 0}}\n\n' + + rsps.add( + responses.POST, + INFERENCE_ENDPOINT.format(model_name=model_name), + body=streaming_response, + status=200, + headers={ + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }, + ) + + yield rsps + + +@pytest.fixture +def mock_hf_chat_completion_api(): + # type: () -> Any + """Mock HuggingFace chat completion API""" + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + model_name = "test-model" + + # Mock model info endpoint + rsps.add( + responses.GET, + MODEL_ENDPOINT.format(model_name=model_name), + json={ + "id": model_name, + "pipeline_tag": "conversational", + "inferenceProviderMapping": { + "hf-inference": { + "status": "live", + "providerId": model_name, + "task": "conversational", + } + }, + }, + status=200, + ) + + # Mock chat completion endpoint + rsps.add( + responses.POST, + INFERENCE_ENDPOINT.format(model_name=model_name) + "/v1/chat/completions", + json={ + "id": "xyz-123", + "created": 1234567890, + "model": f"{model_name}-123", + "system_fingerprint": "fp_123", + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "[mocked] Hello! How can I help you today?", + }, + } + ], + "usage": { + "completion_tokens": 8, + "prompt_tokens": 10, + "total_tokens": 18, + }, + }, + status=200, + ) + + yield rsps + + +@pytest.fixture +def mock_hf_chat_completion_api_tools(): + # type: () -> Any + """Mock HuggingFace chat completion API with tool calls.""" + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + model_name = "test-model" + + # Mock model info endpoint + rsps.add( + responses.GET, + MODEL_ENDPOINT.format(model_name=model_name), + json={ + "id": model_name, + "pipeline_tag": "conversational", + "inferenceProviderMapping": { + "hf-inference": { + "status": "live", + "providerId": model_name, + "task": "conversational", + } + }, + }, + status=200, + ) + + # Mock chat completion endpoint + rsps.add( + responses.POST, + INFERENCE_ENDPOINT.format(model_name=model_name) + "/v1/chat/completions", + json={ + "id": "xyz-123", + "created": 1234567890, + "model": f"{model_name}-123", + "system_fingerprint": "fp_123", + "choices": [ + { + "index": 0, + "finish_reason": "tool_calls", + "message": { + "role": "assistant", + "tool_calls": [ + { + "id": "call_123", + "type": "function", + "function": { + "name": "get_weather", + "arguments": {"location": "Paris"}, + }, + } + ], + }, + } + ], + "usage": { + "completion_tokens": 8, + "prompt_tokens": 10, + "total_tokens": 18, + }, + }, + status=200, + ) + + yield rsps + + +@pytest.fixture +def mock_hf_chat_completion_api_streaming(): + # type: () -> Any + """Mock streaming HuggingFace chat completion API""" + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + model_name = "test-model" + + # Mock model info endpoint + rsps.add( + responses.GET, + MODEL_ENDPOINT.format(model_name=model_name), + json={ + "id": model_name, + "pipeline_tag": "conversational", + "inferenceProviderMapping": { + "hf-inference": { + "status": "live", + "providerId": model_name, + "task": "conversational", + } + }, + }, + status=200, + ) + + # Mock chat completion streaming endpoint + streaming_chat_response = ( + b'data:{"id":"xyz-123","created":1234567890,"model":"test-model-123","system_fingerprint":"fp_123","choices":[{"delta":{"role":"assistant","content":"the mocked "},"index":0,"finish_reason":null}],"usage":null}\n\n' + b'data:{"id":"xyz-124","created":1234567890,"model":"test-model-123","system_fingerprint":"fp_123","choices":[{"delta":{"role":"assistant","content":"model response"},"index":0,"finish_reason":"stop"}],"usage":{"prompt_tokens":183,"completion_tokens":14,"total_tokens":197}}\n\n' + ) + + rsps.add( + responses.POST, + INFERENCE_ENDPOINT.format(model_name=model_name) + "/v1/chat/completions", + body=streaming_chat_response, + status=200, + headers={ + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }, + ) + + yield rsps + + +@pytest.fixture +def mock_hf_chat_completion_api_streaming_tools(): + # type: () -> Any + """Mock streaming HuggingFace chat completion API with tool calls.""" + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + model_name = "test-model" + + # Mock model info endpoint + rsps.add( + responses.GET, + MODEL_ENDPOINT.format(model_name=model_name), + json={ + "id": model_name, + "pipeline_tag": "conversational", + "inferenceProviderMapping": { + "hf-inference": { + "status": "live", + "providerId": model_name, + "task": "conversational", + } + }, + }, + status=200, + ) + + # Mock chat completion streaming endpoint + streaming_chat_response = ( + b'data:{"id":"xyz-123","created":1234567890,"model":"test-model-123","system_fingerprint":"fp_123","choices":[{"delta":{"role":"assistant","content":"response with tool calls follows"},"index":0,"finish_reason":null}],"usage":null}\n\n' + b'data:{"id":"xyz-124","created":1234567890,"model":"test-model-123","system_fingerprint":"fp_123","choices":[{"delta":{"role":"assistant","tool_calls": [{"id": "call_123","type": "function","function": {"name": "get_weather", "arguments": {"location": "Paris"}}}]},"index":0,"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":183,"completion_tokens":14,"total_tokens":197}}\n\n' ) - else: - post_mock = mock.Mock( - return_value=b'[{"generated_text": "the model response"}]' + + rsps.add( + responses.POST, + INFERENCE_ENDPOINT.format(model_name=model_name) + "/v1/chat/completions", + body=streaming_chat_response, + status=200, + headers={ + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }, ) - mock_client_post(client, post_mock) - with start_transaction(name="huggingface_hub tx"): - response = client.text_generation( - prompt="hello", - details=details_arg, + yield rsps + + +@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize("include_prompts", [True, False]) +def test_text_generation( + sentry_init, + capture_events, + send_default_pii, + include_prompts, + mock_hf_text_generation_api, +): + # type: (Any, Any, Any, Any, Any) -> None + sentry_init( + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + integrations=[HuggingfaceHubIntegration(include_prompts=include_prompts)], + ) + events = capture_events() + + client = InferenceClient(model="test-model") + + with sentry_sdk.start_transaction(name="test"): + client.text_generation( + "Hello", stream=False, + details=True, ) - if details_arg: - assert response.generated_text == "the model response" - else: - assert response == "the model response" - tx = events[0] - assert tx["type"] == "transaction" - span = tx["spans"][0] - assert span["op"] == "ai.chat_completions.create.huggingface_hub" + + (transaction,) = events + (span,) = transaction["spans"] + + assert span["op"] == "gen_ai.generate_text" + assert span["description"] == "generate_text test-model" + assert span["origin"] == "auto.ai.huggingface_hub" + + expected_data = { + "gen_ai.operation.name": "generate_text", + "gen_ai.request.model": "test-model", + "gen_ai.response.finish_reasons": "length", + "gen_ai.response.streaming": False, + "gen_ai.usage.total_tokens": 10, + "thread.id": mock.ANY, + "thread.name": mock.ANY, + } if send_default_pii and include_prompts: - assert "hello" in span["data"][SPANDATA.AI_INPUT_MESSAGES] - assert "the model response" in span["data"][SPANDATA.AI_RESPONSES] - else: - assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] - assert SPANDATA.AI_RESPONSES not in span["data"] - - if details_arg: - assert span["data"]["gen_ai.usage.total_tokens"] == 10 - - -@pytest.mark.parametrize( - "send_default_pii, include_prompts, details_arg", - itertools.product([True, False], repeat=3), -) -def test_streaming_chat_completion( - sentry_init, capture_events, send_default_pii, include_prompts, details_arg + expected_data["gen_ai.request.messages"] = "Hello" + expected_data["gen_ai.response.text"] = "[mocked] Hello! How can i help you?" + + if not send_default_pii or not include_prompts: + assert "gen_ai.request.messages" not in expected_data + assert "gen_ai.response.text" not in expected_data + + assert span["data"] == expected_data + + # text generation does not set the response model + assert "gen_ai.response.model" not in span["data"] + + +@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize("include_prompts", [True, False]) +def test_text_generation_streaming( + sentry_init, + capture_events, + send_default_pii, + include_prompts, + mock_hf_text_generation_api_streaming, ): + # type: (Any, Any, Any, Any, Any) -> None sentry_init( + traces_sample_rate=1.0, + send_default_pii=send_default_pii, integrations=[HuggingfaceHubIntegration(include_prompts=include_prompts)], + ) + events = capture_events() + + client = InferenceClient(model="test-model") + + with sentry_sdk.start_transaction(name="test"): + for _ in client.text_generation( + prompt="Hello", + stream=True, + details=True, + ): + pass + + (transaction,) = events + (span,) = transaction["spans"] + + assert span["op"] == "gen_ai.generate_text" + assert span["description"] == "generate_text test-model" + assert span["origin"] == "auto.ai.huggingface_hub" + + expected_data = { + "gen_ai.operation.name": "generate_text", + "gen_ai.request.model": "test-model", + "gen_ai.response.finish_reasons": "length", + "gen_ai.response.streaming": True, + "gen_ai.usage.total_tokens": 10, + "thread.id": mock.ANY, + "thread.name": mock.ANY, + } + + if send_default_pii and include_prompts: + expected_data["gen_ai.request.messages"] = "Hello" + expected_data["gen_ai.response.text"] = "the mocked model response" + + if not send_default_pii or not include_prompts: + assert "gen_ai.request.messages" not in expected_data + assert "gen_ai.response.text" not in expected_data + + assert span["data"] == expected_data + + # text generation does not set the response model + assert "gen_ai.response.model" not in span["data"] + + +@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize("include_prompts", [True, False]) +def test_chat_completion( + sentry_init, + capture_events, + send_default_pii, + include_prompts, + mock_hf_chat_completion_api, +): + # type: (Any, Any, Any, Any, Any) -> None + sentry_init( traces_sample_rate=1.0, send_default_pii=send_default_pii, + integrations=[HuggingfaceHubIntegration(include_prompts=include_prompts)], ) events = capture_events() - client = InferenceClient(model="https://") - - post_mock = mock.Mock( - return_value=[ - b"""data:{ - "token":{"id":1, "special": false, "text": "the model "} - }""", - b"""data:{ - "token":{"id":2, "special": false, "text": "response"}, - "details":{"finish_reason": "length", "generated_tokens": 10, "seed": 0} - }""", - ] + client = InferenceClient(model="test-model") + + with sentry_sdk.start_transaction(name="test"): + client.chat_completion( + messages=[{"role": "user", "content": "Hello!"}], + stream=False, + ) + + (transaction,) = events + (span,) = transaction["spans"] + + assert span["op"] == "gen_ai.chat" + assert span["description"] == "chat test-model" + assert span["origin"] == "auto.ai.huggingface_hub" + + expected_data = { + "gen_ai.operation.name": "chat", + "gen_ai.request.model": "test-model", + "gen_ai.response.finish_reasons": "stop", + "gen_ai.response.model": "test-model-123", + "gen_ai.response.streaming": False, + "gen_ai.usage.input_tokens": 10, + "gen_ai.usage.output_tokens": 8, + "gen_ai.usage.total_tokens": 18, + "thread.id": mock.ANY, + "thread.name": mock.ANY, + } + + if send_default_pii and include_prompts: + expected_data["gen_ai.request.messages"] = ( + '[{"role": "user", "content": "Hello!"}]' + ) + expected_data["gen_ai.response.text"] = ( + "[mocked] Hello! How can I help you today?" + ) + + if not send_default_pii or not include_prompts: + assert "gen_ai.request.messages" not in expected_data + assert "gen_ai.response.text" not in expected_data + + assert span["data"] == expected_data + + +@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize("include_prompts", [True, False]) +def test_chat_completion_streaming( + sentry_init, + capture_events, + send_default_pii, + include_prompts, + mock_hf_chat_completion_api_streaming, +): + # type: (Any, Any, Any, Any, Any) -> None + sentry_init( + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + integrations=[HuggingfaceHubIntegration(include_prompts=include_prompts)], ) - mock_client_post(client, post_mock) + events = capture_events() + + client = InferenceClient(model="test-model") - with start_transaction(name="huggingface_hub tx"): - response = list( - client.text_generation( - prompt="hello", - details=details_arg, + with sentry_sdk.start_transaction(name="test"): + _ = list( + client.chat_completion( + [{"role": "user", "content": "Hello!"}], stream=True, ) ) - assert len(response) == 2 - if details_arg: - assert response[0].token.text + response[1].token.text == "the model response" - else: - assert response[0] + response[1] == "the model response" - tx = events[0] - assert tx["type"] == "transaction" - span = tx["spans"][0] - assert span["op"] == "ai.chat_completions.create.huggingface_hub" + (transaction,) = events + (span,) = transaction["spans"] + + assert span["op"] == "gen_ai.chat" + assert span["description"] == "chat test-model" + assert span["origin"] == "auto.ai.huggingface_hub" + + expected_data = { + "gen_ai.operation.name": "chat", + "gen_ai.request.model": "test-model", + "gen_ai.response.finish_reasons": "stop", + "gen_ai.response.model": "test-model-123", + "gen_ai.response.streaming": True, + "thread.id": mock.ANY, + "thread.name": mock.ANY, + } + # usage is not available in older versions of the library + if HF_VERSION and HF_VERSION >= (0, 26, 0): + expected_data["gen_ai.usage.input_tokens"] = 183 + expected_data["gen_ai.usage.output_tokens"] = 14 + expected_data["gen_ai.usage.total_tokens"] = 197 if send_default_pii and include_prompts: - assert "hello" in span["data"][SPANDATA.AI_INPUT_MESSAGES] - assert "the model response" in span["data"][SPANDATA.AI_RESPONSES] - else: - assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] - assert SPANDATA.AI_RESPONSES not in span["data"] + expected_data["gen_ai.request.messages"] = ( + '[{"role": "user", "content": "Hello!"}]' + ) + expected_data["gen_ai.response.text"] = "the mocked model response" - if details_arg: - assert span["data"]["gen_ai.usage.total_tokens"] == 10 + if not send_default_pii or not include_prompts: + assert "gen_ai.request.messages" not in expected_data + assert "gen_ai.response.text" not in expected_data + assert span["data"] == expected_data -def test_bad_chat_completion(sentry_init, capture_events): - sentry_init(integrations=[HuggingfaceHubIntegration()], traces_sample_rate=1.0) + +def test_chat_completion_api_error( + sentry_init, capture_events, mock_hf_api_with_errors +): + # type: (Any, Any, Any) -> None + sentry_init(traces_sample_rate=1.0) events = capture_events() - client = InferenceClient(model="https://") - post_mock = mock.Mock(side_effect=OverloadedError("The server is overloaded")) - mock_client_post(client, post_mock) + client = InferenceClient(model="test-model") + + with sentry_sdk.start_transaction(name="test"): + with pytest.raises(HfHubHTTPError): + client.chat_completion( + messages=[{"role": "user", "content": "Hello!"}], + ) + + ( + error, + transaction, + ) = events - with pytest.raises(OverloadedError): - client.text_generation(prompt="hello") + assert error["exception"]["values"][0]["mechanism"]["type"] == "huggingface_hub" + assert not error["exception"]["values"][0]["mechanism"]["handled"] - (event,) = events - assert event["level"] == "error" + (span,) = transaction["spans"] + assert span["op"] == "gen_ai.chat" + assert span["description"] == "chat test-model" + assert span["origin"] == "auto.ai.huggingface_hub" + assert span.get("tags", {}).get("status") == "error" -def test_span_origin(sentry_init, capture_events): + assert ( + error["contexts"]["trace"]["trace_id"] + == transaction["contexts"]["trace"]["trace_id"] + ) + expected_data = { + "gen_ai.operation.name": "chat", + "gen_ai.request.model": "test-model", + "thread.id": mock.ANY, + "thread.name": mock.ANY, + } + assert span["data"] == expected_data + + +@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize("include_prompts", [True, False]) +def test_chat_completion_with_tools( + sentry_init, + capture_events, + send_default_pii, + include_prompts, + mock_hf_chat_completion_api_tools, +): + # type: (Any, Any, Any, Any, Any) -> None sentry_init( - integrations=[HuggingfaceHubIntegration()], traces_sample_rate=1.0, + send_default_pii=send_default_pii, + integrations=[HuggingfaceHubIntegration(include_prompts=include_prompts)], ) events = capture_events() - client = InferenceClient(model="https://") - post_mock = mock.Mock( - return_value=[ - b"""data:{ - "token":{"id":1, "special": false, "text": "the model "} - }""", - ] + client = InferenceClient(model="test-model") + + tools = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get current weather", + "parameters": { + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"], + }, + }, + } + ] + + with sentry_sdk.start_transaction(name="test"): + client.chat_completion( + messages=[{"role": "user", "content": "What is the weather in Paris?"}], + tools=tools, + tool_choice="auto", + ) + + (transaction,) = events + (span,) = transaction["spans"] + + assert span["op"] == "gen_ai.chat" + assert span["description"] == "chat test-model" + assert span["origin"] == "auto.ai.huggingface_hub" + + expected_data = { + "gen_ai.operation.name": "chat", + "gen_ai.request.available_tools": '[{"type": "function", "function": {"name": "get_weather", "description": "Get current weather", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}}}]', + "gen_ai.request.model": "test-model", + "gen_ai.response.finish_reasons": "tool_calls", + "gen_ai.response.model": "test-model-123", + "gen_ai.usage.input_tokens": 10, + "gen_ai.usage.output_tokens": 8, + "gen_ai.usage.total_tokens": 18, + "thread.id": mock.ANY, + "thread.name": mock.ANY, + } + + if send_default_pii and include_prompts: + expected_data["gen_ai.request.messages"] = ( + '[{"role": "user", "content": "What is the weather in Paris?"}]' + ) + expected_data["gen_ai.response.tool_calls"] = ( + '[{"function": {"arguments": {"location": "Paris"}, "name": "get_weather", "description": "None"}, "id": "call_123", "type": "function"}]' + ) + + if not send_default_pii or not include_prompts: + assert "gen_ai.request.messages" not in expected_data + assert "gen_ai.response.text" not in expected_data + assert "gen_ai.response.tool_calls" not in expected_data + + assert span["data"] == expected_data + + +@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize("include_prompts", [True, False]) +def test_chat_completion_streaming_with_tools( + sentry_init, + capture_events, + send_default_pii, + include_prompts, + mock_hf_chat_completion_api_streaming_tools, +): + # type: (Any, Any, Any, Any, Any) -> None + sentry_init( + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + integrations=[HuggingfaceHubIntegration(include_prompts=include_prompts)], ) - mock_client_post(client, post_mock) + events = capture_events() - with start_transaction(name="huggingface_hub tx"): - list( - client.text_generation( - prompt="hello", + client = InferenceClient(model="test-model") + + tools = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get current weather", + "parameters": { + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"], + }, + }, + } + ] + + with sentry_sdk.start_transaction(name="test"): + _ = list( + client.chat_completion( + messages=[{"role": "user", "content": "What is the weather in Paris?"}], stream=True, + tools=tools, + tool_choice="auto", ) ) - (event,) = events + (transaction,) = events + (span,) = transaction["spans"] + + assert span["op"] == "gen_ai.chat" + assert span["description"] == "chat test-model" + assert span["origin"] == "auto.ai.huggingface_hub" + + expected_data = { + "gen_ai.operation.name": "chat", + "gen_ai.request.available_tools": '[{"type": "function", "function": {"name": "get_weather", "description": "Get current weather", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}}}]', + "gen_ai.request.model": "test-model", + "gen_ai.response.finish_reasons": "tool_calls", + "gen_ai.response.model": "test-model-123", + "gen_ai.response.streaming": True, + "thread.id": mock.ANY, + "thread.name": mock.ANY, + } + + if HF_VERSION and HF_VERSION >= (0, 26, 0): + expected_data["gen_ai.usage.input_tokens"] = 183 + expected_data["gen_ai.usage.output_tokens"] = 14 + expected_data["gen_ai.usage.total_tokens"] = 197 + + if send_default_pii and include_prompts: + expected_data["gen_ai.request.messages"] = ( + '[{"role": "user", "content": "What is the weather in Paris?"}]' + ) + expected_data["gen_ai.response.text"] = "response with tool calls follows" + expected_data["gen_ai.response.tool_calls"] = ( + '[{"function": {"arguments": {"location": "Paris"}, "name": "get_weather"}, "id": "call_123", "type": "function", "index": "None"}]' + ) + + if not send_default_pii or not include_prompts: + assert "gen_ai.request.messages" not in expected_data + assert "gen_ai.response.text" not in expected_data + assert "gen_ai.response.tool_calls" not in expected_data - assert event["contexts"]["trace"]["origin"] == "manual" - assert event["spans"][0]["origin"] == "auto.ai.huggingface_hub" + assert span["data"] == expected_data diff --git a/tox.ini b/tox.ini index ff2403f515..1bc9757b9a 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-08T11:35:09.849536+00:00 +# Last generated: 2025-09-09T08:24:12.875177+00:00 [tox] requires = @@ -116,12 +116,12 @@ envlist = {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.11,py3.12}-openai-base-v1.36.1 {py3.8,py3.11,py3.12}-openai-base-v1.71.0 - {py3.8,py3.12,py3.13}-openai-base-v1.106.1 + {py3.8,py3.12,py3.13}-openai-base-v1.107.0 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.36.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.71.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.106.1 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.107.0 {py3.9,py3.12,py3.13}-langgraph-v0.6.7 {py3.10,py3.12,py3.13}-langgraph-v1.0.0a3 @@ -130,8 +130,8 @@ envlist = {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 {py3.10,py3.12,py3.13}-openai_agents-v0.2.11 - {py3.8,py3.10,py3.11}-huggingface_hub-v0.22.2 - {py3.8,py3.11,py3.12}-huggingface_hub-v0.26.5 + {py3.8,py3.10,py3.11}-huggingface_hub-v0.24.7 + {py3.8,py3.12,py3.13}-huggingface_hub-v0.27.1 {py3.8,py3.12,py3.13}-huggingface_hub-v0.30.2 {py3.8,py3.12,py3.13}-huggingface_hub-v0.34.4 {py3.8,py3.12,py3.13}-huggingface_hub-v0.35.0rc0 @@ -141,7 +141,7 @@ envlist = {py3.6,py3.7}-boto3-v1.12.49 {py3.6,py3.9,py3.10}-boto3-v1.20.54 {py3.7,py3.11,py3.12}-boto3-v1.28.85 - {py3.9,py3.12,py3.13}-boto3-v1.40.25 + {py3.9,py3.12,py3.13}-boto3-v1.40.26 {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.6,py3.7,py3.8}-chalice-v1.21.9 @@ -487,7 +487,7 @@ deps = openai-base-v1.0.1: openai==1.0.1 openai-base-v1.36.1: openai==1.36.1 openai-base-v1.71.0: openai==1.71.0 - openai-base-v1.106.1: openai==1.106.1 + openai-base-v1.107.0: openai==1.107.0 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 @@ -496,7 +496,7 @@ deps = openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.36.1: openai==1.36.1 openai-notiktoken-v1.71.0: openai==1.71.0 - openai-notiktoken-v1.106.1: openai==1.106.1 + openai-notiktoken-v1.107.0: openai==1.107.0 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.36.1: httpx<0.28 @@ -509,18 +509,19 @@ deps = openai_agents-v0.2.11: openai-agents==0.2.11 openai_agents: pytest-asyncio - huggingface_hub-v0.22.2: huggingface_hub==0.22.2 - huggingface_hub-v0.26.5: huggingface_hub==0.26.5 + huggingface_hub-v0.24.7: huggingface_hub==0.24.7 + huggingface_hub-v0.27.1: huggingface_hub==0.27.1 huggingface_hub-v0.30.2: huggingface_hub==0.30.2 huggingface_hub-v0.34.4: huggingface_hub==0.34.4 huggingface_hub-v0.35.0rc0: huggingface_hub==0.35.0rc0 + huggingface_hub: responses # ~~~ Cloud ~~~ boto3-v1.12.49: boto3==1.12.49 boto3-v1.20.54: boto3==1.20.54 boto3-v1.28.85: boto3==1.28.85 - boto3-v1.40.25: boto3==1.40.25 + boto3-v1.40.26: boto3==1.40.26 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 From 94a92d8b6475905a247472ced68911320a4126c4 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 10 Sep 2025 15:02:02 +0200 Subject: [PATCH 39/88] Add input and output to `invoke_agent` span. (#4785) Add the original input for the agent and the final output of the agent to the `invoke_agent` span. fixes #4512 fixes PY-1740 --- .../openai_agents/patches/agent_run.py | 8 +-- .../openai_agents/spans/invoke_agent.py | 53 +++++++++++++++++-- .../openai_agents/test_openai_agents.py | 16 ++++++ 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/sentry_sdk/integrations/openai_agents/patches/agent_run.py index 29002f6619..5473915b48 100644 --- a/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ b/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -26,12 +26,12 @@ def _patch_agent_run(): original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs original_execute_final_output = agents._run_impl.RunImpl.execute_final_output - def _start_invoke_agent_span(context_wrapper, agent): - # type: (agents.RunContextWrapper, agents.Agent) -> None + def _start_invoke_agent_span(context_wrapper, agent, kwargs): + # type: (agents.RunContextWrapper, agents.Agent, dict[str, Any]) -> None """Start an agent invocation span""" # Store the agent on the context wrapper so we can access it later context_wrapper._sentry_current_agent = agent - invoke_agent_span(context_wrapper, agent) + invoke_agent_span(context_wrapper, agent, kwargs) def _end_invoke_agent_span(context_wrapper, agent, output=None): # type: (agents.RunContextWrapper, agents.Agent, Optional[Any]) -> None @@ -72,7 +72,7 @@ async def patched_run_single_turn(cls, *args, **kwargs): if current_agent and current_agent != agent: _end_invoke_agent_span(context_wrapper, current_agent) - _start_invoke_agent_span(context_wrapper, agent) + _start_invoke_agent_span(context_wrapper, agent, kwargs) # Call original method with all the correct parameters result = await original_run_single_turn(*args, **kwargs) diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index 549ade1246..d76d39f338 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -1,5 +1,8 @@ import sentry_sdk +from sentry_sdk.ai.utils import set_data_normalized from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import safe_serialize from ..consts import SPAN_ORIGIN from ..utils import _set_agent_data @@ -11,8 +14,8 @@ from typing import Any -def invoke_agent_span(context, agent): - # type: (agents.RunContextWrapper, agents.Agent) -> sentry_sdk.tracing.Span +def invoke_agent_span(context, agent, kwargs): + # type: (agents.RunContextWrapper, agents.Agent, dict[str, Any]) -> sentry_sdk.tracing.Span span = sentry_sdk.start_span( op=OP.GEN_AI_INVOKE_AGENT, name=f"invoke_agent {agent.name}", @@ -22,6 +25,40 @@ def invoke_agent_span(context, agent): span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + if should_send_default_pii(): + messages = [] + if agent.instructions: + message = ( + agent.instructions + if isinstance(agent.instructions, str) + else safe_serialize(agent.instructions) + ) + messages.append( + { + "content": [{"text": message, "type": "text"}], + "role": "system", + } + ) + + original_input = kwargs.get("original_input") + if original_input is not None: + message = ( + original_input + if isinstance(original_input, str) + else safe_serialize(original_input) + ) + messages.append( + { + "content": [{"text": message, "type": "text"}], + "role": "user", + } + ) + + if len(messages) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages, unpack=False + ) + _set_agent_data(span, agent) return span @@ -29,6 +66,12 @@ def invoke_agent_span(context, agent): def update_invoke_agent_span(context, agent, output): # type: (agents.RunContextWrapper, agents.Agent, Any) -> None - current_span = sentry_sdk.get_current_span() - if current_span: - current_span.__exit__(None, None, None) + span = sentry_sdk.get_current_span() + + if span: + if should_send_default_pii(): + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False + ) + + span.__exit__(None, None, None) diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index fab8d9e13f..047b919213 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -115,6 +115,7 @@ async def test_agent_invocation_span( sentry_init( integrations=[OpenAIAgentsIntegration()], traces_sample_rate=1.0, + send_default_pii=True, ) events = capture_events() @@ -134,6 +135,21 @@ async def test_agent_invocation_span( assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents" assert invoke_agent_span["description"] == "invoke_agent test_agent" + assert invoke_agent_span["data"]["gen_ai.request.messages"] == safe_serialize( + [ + { + "content": [ + {"text": "You are a helpful test assistant.", "type": "text"} + ], + "role": "system", + }, + {"content": [{"text": "Test input", "type": "text"}], "role": "user"}, + ] + ) + assert ( + invoke_agent_span["data"]["gen_ai.response.text"] + == "Hello, how can I help you?" + ) assert invoke_agent_span["data"]["gen_ai.operation.name"] == "invoke_agent" assert invoke_agent_span["data"]["gen_ai.system"] == "openai" assert invoke_agent_span["data"]["gen_ai.agent.name"] == "test_agent" From a6a2f930ff95807d64ff5bfb892435b3d06cdd2e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 11 Sep 2025 15:57:58 +0200 Subject: [PATCH 40/88] Add log message when `DedupeIntegration` is dropping an error. (#4788) Make it clearer for the user what is happening. resolves: PY-1840 --- sentry_sdk/integrations/dedupe.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index a115e35292..eab2764fcd 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -1,5 +1,5 @@ import sentry_sdk -from sentry_sdk.utils import ContextVar +from sentry_sdk.utils import ContextVar, logger from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor @@ -37,7 +37,9 @@ def processor(event, hint): exc = exc_info[1] if integration._last_seen.get(None) is exc: + logger.info("DedupeIntegration dropped duplicated error event %s", exc) return None + integration._last_seen.set(exc) return event From bd311b20c686778a7e83394dfa06970af2bcc90f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:12:12 +0000 Subject: [PATCH 41/88] build(deps): bump actions/setup-python from 5 to 6 (#4774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
Release notes

Sourced from actions/setup-python's releases.

v6.0.0

What's Changed

Breaking Changes

Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. See Release Notes

Enhancements:

Bug fixes:

Dependency updates:

New Contributors

Full Changelog: https://github.com/actions/setup-python/compare/v5...v6.0.0

v5.6.0

What's Changed

Full Changelog: https://github.com/actions/setup-python/compare/v5...v5.6.0

v5.5.0

What's Changed

Enhancements:

Bug fixes:

... (truncated)

Commits
  • e797f83 Upgrade to node 24 (#1164)
  • 3d1e2d2 Revert "Enhance cache-dependency-path handling to support files outside the w...
  • 65b0712 Clarify pythonLocation behavior for PyPy and GraalPy in environment variables...
  • 5b668cf Bump actions/checkout from 4 to 5 (#1181)
  • f62a0e2 Change missing cache directory error to warning (#1182)
  • 9322b3c Upgrade setuptools to 78.1.1 to fix path traversal vulnerability in PackageIn...
  • fbeb884 Bump form-data to fix critical vulnerabilities #182 & #183 (#1163)
  • 03bb615 Bump idna from 2.9 to 3.7 in /tests/data (#843)
  • 36da51d Add version parsing from Pipfile (#1067)
  • 3c6f142 update documentation (#1156)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Anton Pirker --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/test-integrations-ai.yml | 2 +- .github/workflows/test-integrations-cloud.yml | 2 +- .github/workflows/test-integrations-common.yml | 2 +- .github/workflows/test-integrations-dbs.yml | 4 ++-- .github/workflows/test-integrations-flags.yml | 2 +- .github/workflows/test-integrations-gevent.yml | 2 +- .github/workflows/test-integrations-graphql.yml | 2 +- .github/workflows/test-integrations-misc.yml | 2 +- .github/workflows/test-integrations-network.yml | 4 ++-- .github/workflows/test-integrations-tasks.yml | 4 ++-- .github/workflows/test-integrations-web-1.yml | 2 +- .github/workflows/test-integrations-web-2.yml | 4 ++-- scripts/split_tox_gh_actions/templates/test_group.jinja | 2 +- 14 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffc0a741fc..67b4fd3546 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.12 @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.12 @@ -59,7 +59,7 @@ jobs: steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.12 - name: Setup build cache @@ -90,7 +90,7 @@ jobs: steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.12 diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index 26a8bdb8bb..f65ee87ec3 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -39,7 +39,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-cloud.yml b/.github/workflows/test-integrations-cloud.yml index 62e70d759d..92c7d40ff4 100644 --- a/.github/workflows/test-integrations-cloud.yml +++ b/.github/workflows/test-integrations-cloud.yml @@ -43,7 +43,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-common.yml b/.github/workflows/test-integrations-common.yml index 1c0c9b80d2..ef1fab573c 100644 --- a/.github/workflows/test-integrations-common.yml +++ b/.github/workflows/test-integrations-common.yml @@ -39,7 +39,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index 2d6af43bc3..f22487eb54 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -57,7 +57,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} @@ -156,7 +156,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-flags.yml b/.github/workflows/test-integrations-flags.yml index f744f514ee..d7acf0670d 100644 --- a/.github/workflows/test-integrations-flags.yml +++ b/.github/workflows/test-integrations-flags.yml @@ -39,7 +39,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-gevent.yml b/.github/workflows/test-integrations-gevent.yml index 382e6a5f15..c32102df8c 100644 --- a/.github/workflows/test-integrations-gevent.yml +++ b/.github/workflows/test-integrations-gevent.yml @@ -39,7 +39,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-graphql.yml b/.github/workflows/test-integrations-graphql.yml index 93675fb4fe..578b7d65bf 100644 --- a/.github/workflows/test-integrations-graphql.yml +++ b/.github/workflows/test-integrations-graphql.yml @@ -39,7 +39,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index e8937708bc..c2673350b2 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -39,7 +39,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-network.yml b/.github/workflows/test-integrations-network.yml index 867681d3a3..9520d8ef4d 100644 --- a/.github/workflows/test-integrations-network.yml +++ b/.github/workflows/test-integrations-network.yml @@ -39,7 +39,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} @@ -106,7 +106,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-tasks.yml b/.github/workflows/test-integrations-tasks.yml index f842683285..051567b92b 100644 --- a/.github/workflows/test-integrations-tasks.yml +++ b/.github/workflows/test-integrations-tasks.yml @@ -39,7 +39,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} @@ -133,7 +133,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-web-1.yml b/.github/workflows/test-integrations-web-1.yml index ba802faa01..6131ff4250 100644 --- a/.github/workflows/test-integrations-web-1.yml +++ b/.github/workflows/test-integrations-web-1.yml @@ -57,7 +57,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-web-2.yml b/.github/workflows/test-integrations-web-2.yml index 22200f8ae1..c59553a88a 100644 --- a/.github/workflows/test-integrations-web-2.yml +++ b/.github/workflows/test-integrations-web-2.yml @@ -39,7 +39,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} @@ -134,7 +134,7 @@ jobs: container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 if: ${{ matrix.python-version != '3.6' }} with: python-version: ${{ matrix.python-version }} diff --git a/scripts/split_tox_gh_actions/templates/test_group.jinja b/scripts/split_tox_gh_actions/templates/test_group.jinja index 4ac0d03eb2..28e18c501b 100644 --- a/scripts/split_tox_gh_actions/templates/test_group.jinja +++ b/scripts/split_tox_gh_actions/templates/test_group.jinja @@ -43,7 +43,7 @@ {% raw %}container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }}{% endraw %} steps: - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 {% raw %}if: ${{ matrix.python-version != '3.6' }}{% endraw %} with: python-version: {% raw %}${{ matrix.python-version }}{% endraw %} From 18d38996c7fba2ddde885d98a5cd808eae1cae26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:15:24 +0000 Subject: [PATCH 42/88] build(deps): bump codecov/codecov-action from 5.5.0 to 5.5.1 (#4773) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.0 to 5.5.1.
Release notes

Sourced from codecov/codecov-action's releases.

v5.5.1

What's Changed

New Contributors

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.5.0...v5.5.1

Changelog

Sourced from codecov/codecov-action's changelog.

v5.5.1

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.5.0..v5.5.1

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov/codecov-action&package-manager=github_actions&previous-version=5.5.0&new-version=5.5.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Anton Pirker --- .github/workflows/test-integrations-ai.yml | 2 +- .github/workflows/test-integrations-cloud.yml | 2 +- .github/workflows/test-integrations-common.yml | 2 +- .github/workflows/test-integrations-dbs.yml | 4 ++-- .github/workflows/test-integrations-flags.yml | 2 +- .github/workflows/test-integrations-gevent.yml | 2 +- .github/workflows/test-integrations-graphql.yml | 2 +- .github/workflows/test-integrations-misc.yml | 2 +- .github/workflows/test-integrations-network.yml | 4 ++-- .github/workflows/test-integrations-tasks.yml | 4 ++-- .github/workflows/test-integrations-web-1.yml | 2 +- .github/workflows/test-integrations-web-2.yml | 4 ++-- scripts/split_tox_gh_actions/templates/test_group.jinja | 2 +- 13 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index f65ee87ec3..972df704e0 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -99,7 +99,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-cloud.yml b/.github/workflows/test-integrations-cloud.yml index 92c7d40ff4..6aeaea8c3a 100644 --- a/.github/workflows/test-integrations-cloud.yml +++ b/.github/workflows/test-integrations-cloud.yml @@ -87,7 +87,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-common.yml b/.github/workflows/test-integrations-common.yml index ef1fab573c..b682428dd1 100644 --- a/.github/workflows/test-integrations-common.yml +++ b/.github/workflows/test-integrations-common.yml @@ -67,7 +67,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index f22487eb54..efa9f8db39 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -107,7 +107,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -206,7 +206,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-flags.yml b/.github/workflows/test-integrations-flags.yml index d7acf0670d..d7baeeb870 100644 --- a/.github/workflows/test-integrations-flags.yml +++ b/.github/workflows/test-integrations-flags.yml @@ -79,7 +79,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-gevent.yml b/.github/workflows/test-integrations-gevent.yml index c32102df8c..9af6b4d7af 100644 --- a/.github/workflows/test-integrations-gevent.yml +++ b/.github/workflows/test-integrations-gevent.yml @@ -67,7 +67,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-graphql.yml b/.github/workflows/test-integrations-graphql.yml index 578b7d65bf..5c306dff3f 100644 --- a/.github/workflows/test-integrations-graphql.yml +++ b/.github/workflows/test-integrations-graphql.yml @@ -79,7 +79,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index c2673350b2..005e8395a2 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -87,7 +87,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-network.yml b/.github/workflows/test-integrations-network.yml index 9520d8ef4d..e34706ff09 100644 --- a/.github/workflows/test-integrations-network.yml +++ b/.github/workflows/test-integrations-network.yml @@ -75,7 +75,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -142,7 +142,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-tasks.yml b/.github/workflows/test-integrations-tasks.yml index 051567b92b..0038f1d050 100644 --- a/.github/workflows/test-integrations-tasks.yml +++ b/.github/workflows/test-integrations-tasks.yml @@ -102,7 +102,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -196,7 +196,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-web-1.yml b/.github/workflows/test-integrations-web-1.yml index 6131ff4250..4b22db6155 100644 --- a/.github/workflows/test-integrations-web-1.yml +++ b/.github/workflows/test-integrations-web-1.yml @@ -97,7 +97,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-web-2.yml b/.github/workflows/test-integrations-web-2.yml index c59553a88a..6b7fe58815 100644 --- a/.github/workflows/test-integrations-web-2.yml +++ b/.github/workflows/test-integrations-web-2.yml @@ -103,7 +103,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -198,7 +198,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/scripts/split_tox_gh_actions/templates/test_group.jinja b/scripts/split_tox_gh_actions/templates/test_group.jinja index 28e18c501b..f020a44b84 100644 --- a/scripts/split_tox_gh_actions/templates/test_group.jinja +++ b/scripts/split_tox_gh_actions/templates/test_group.jinja @@ -100,7 +100,7 @@ - name: Upload coverage to Codecov if: {% raw %}${{ !cancelled() }}{% endraw %} - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %} files: coverage.xml From 73e2b7e2d4a5c1bd833314a7d40606e98debae13 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Thu, 11 Sep 2025 16:30:06 +0200 Subject: [PATCH 43/88] feat(ai): Create transaction in AI agents framworks, when no transaction is running. (#4758) This includes: - nthropic: `message.create` - langchain: `invoke_agent` spans - openai_agent: `invoke_agent` spans + agent workflow (which was already like that) Closes https://linear.app/getsentry/issue/TET-1048/auto-wrap-gen-aiinvoke-agent-if-no-transaction-in-scope Co-authored-by: Anton Pirker --- sentry_sdk/ai/utils.py | 12 +++++++++++- sentry_sdk/integrations/anthropic.py | 4 ++-- sentry_sdk/integrations/langchain.py | 8 +++++--- .../openai_agents/spans/agent_workflow.py | 4 ++-- .../integrations/openai_agents/spans/invoke_agent.py | 5 +++-- sentry_sdk/integrations/openai_agents/utils.py | 10 ---------- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/sentry_sdk/ai/utils.py b/sentry_sdk/ai/utils.py index 2dc0de4ef3..41f89f5623 100644 --- a/sentry_sdk/ai/utils.py +++ b/sentry_sdk/ai/utils.py @@ -3,9 +3,10 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any + from typing import Any, Callable from sentry_sdk.tracing import Span +import sentry_sdk from sentry_sdk.utils import logger @@ -37,3 +38,12 @@ def set_data_normalized(span, key, value, unpack=True): span.set_data(key, normalized) else: span.set_data(key, json.dumps(normalized)) + + +def get_start_span_function(): + # type: () -> Callable[..., Any] + current_span = sentry_sdk.get_current_span() + transaction_exists = ( + current_span is not None and current_span.containing_transaction == current_span + ) + return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 05d45ef62f..ff3d9a3388 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import set_data_normalized +from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii @@ -194,7 +194,7 @@ def _sentry_patched_create_common(f, *args, **kwargs): model = kwargs.get("model", "") - span = sentry_sdk.start_span( + span = get_start_span_function()( op=OP.GEN_AI_CHAT, name=f"chat {model}".strip(), origin=AnthropicIntegration.origin, diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index e14dd619fe..1401be06e1 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.ai.monitoring import set_ai_pipeline_name -from sentry_sdk.ai.utils import set_data_normalized +from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii @@ -716,8 +716,9 @@ def new_invoke(self, *args, **kwargs): return f(self, *args, **kwargs) agent_name, tools = _get_request_data(self, args, kwargs) + start_span_function = get_start_span_function() - with sentry_sdk.start_span( + with start_span_function( op=OP.GEN_AI_INVOKE_AGENT, name=f"invoke_agent {agent_name}" if agent_name else "invoke_agent", origin=LangchainIntegration.origin, @@ -767,8 +768,9 @@ def new_stream(self, *args, **kwargs): return f(self, *args, **kwargs) agent_name, tools = _get_request_data(self, args, kwargs) + start_span_function = get_start_span_function() - span = sentry_sdk.start_span( + span = start_span_function( op=OP.GEN_AI_INVOKE_AGENT, name=f"invoke_agent {agent_name}".strip(), origin=LangchainIntegration.origin, diff --git a/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py b/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py index de2f28d41e..ef69b856e3 100644 --- a/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +++ b/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py @@ -1,7 +1,7 @@ import sentry_sdk +from sentry_sdk.ai.utils import get_start_span_function from ..consts import SPAN_ORIGIN -from ..utils import _get_start_span_function from typing import TYPE_CHECKING @@ -13,7 +13,7 @@ def agent_workflow_span(agent): # type: (agents.Agent) -> sentry_sdk.tracing.Span # Create a transaction or a span if an transaction is already active - span = _get_start_span_function()( + span = get_start_span_function()( name=f"{agent.name} workflow", origin=SPAN_ORIGIN, ) diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index d76d39f338..cf06120625 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -1,5 +1,5 @@ import sentry_sdk -from sentry_sdk.ai.utils import set_data_normalized +from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import safe_serialize @@ -16,7 +16,8 @@ def invoke_agent_span(context, agent, kwargs): # type: (agents.RunContextWrapper, agents.Agent, dict[str, Any]) -> sentry_sdk.tracing.Span - span = sentry_sdk.start_span( + start_span_function = get_start_span_function() + span = start_span_function( op=OP.GEN_AI_INVOKE_AGENT, name=f"invoke_agent {agent.name}", origin=SPAN_ORIGIN, diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 44b260d4bc..a0487e0e3a 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -9,7 +9,6 @@ if TYPE_CHECKING: from typing import Any - from typing import Callable from agents import Usage try: @@ -29,15 +28,6 @@ def _capture_exception(exc): sentry_sdk.capture_event(event, hint=hint) -def _get_start_span_function(): - # type: () -> Callable[..., Any] - current_span = sentry_sdk.get_current_span() - transaction_exists = ( - current_span is not None and current_span.containing_transaction == current_span - ) - return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction - - def _set_agent_data(span, agent): # type: (sentry_sdk.tracing.Span, agents.Agent) -> None span.set_data( From dcefe3840181e0aa534040b3932657f0ca1c36b8 Mon Sep 17 00:00:00 2001 From: Vadim Markovtsev Date: Thu, 11 Sep 2025 16:36:12 +0200 Subject: [PATCH 44/88] Avoid reporting false-positive StopAsyncIteration in the asyncio integration (#4741) If a coroutine exits an async loop by raising `AsyncStopIteration`, Sentry reports it as an error. There is no error in that case. Co-authored-by: Anton Pirker --- sentry_sdk/integrations/asyncio.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index ae580ca038..66742fe6e4 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -51,6 +51,8 @@ async def _task_with_sentry_span_creation(): ): try: result = await coro + except StopAsyncIteration as e: + raise e from None except Exception: reraise(*_capture_exception()) From 007058d0e66a6156c258d2acef76d43efad49223 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 12 Sep 2025 09:10:03 +0200 Subject: [PATCH 45/88] ref(tracing): Use float for sample rand (#4677) Closes https://github.com/getsentry/sentry-python/issues/4270 --------- Co-authored-by: Anton Pirker --- sentry_sdk/tracing.py | 3 +- sentry_sdk/tracing_utils.py | 40 +++++++++---------- sentry_sdk/utils.py | 6 +++ tests/integrations/aiohttp/test_aiohttp.py | 2 +- tests/integrations/celery/test_celery.py | 4 +- tests/integrations/httpx/test_httpx.py | 4 +- tests/integrations/stdlib/test_httplib.py | 2 +- tests/test_dsc.py | 2 +- tests/test_monitor.py | 2 +- tests/test_propagationcontext.py | 17 ++++---- tests/tracing/test_integration_tests.py | 2 +- tests/tracing/test_sample_rand.py | 37 +---------------- tests/tracing/test_sample_rand_propagation.py | 6 +-- 13 files changed, 49 insertions(+), 78 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 0d1fcc45da..fc43a33dc7 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,4 +1,3 @@ -from decimal import Decimal import uuid import warnings from datetime import datetime, timedelta, timezone @@ -1251,7 +1250,7 @@ def _set_initial_sampling_decision(self, sampling_context): return # Now we roll the dice. - self.sampled = self._sample_rand < Decimal.from_float(self.sample_rate) + self.sampled = self._sample_rand < self.sample_rate if self.sampled: logger.debug( diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index b31d3d85c5..c1cfde293b 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -6,7 +6,6 @@ import sys from collections.abc import Mapping from datetime import timedelta -from decimal import ROUND_DOWN, Decimal, DefaultContext, localcontext from random import Random from urllib.parse import quote, unquote import uuid @@ -502,7 +501,7 @@ def _fill_sample_rand(self): return sample_rand = try_convert( - Decimal, self.dynamic_sampling_context.get("sample_rand") + float, self.dynamic_sampling_context.get("sample_rand") ) if sample_rand is not None and 0 <= sample_rand < 1: # sample_rand is present and valid, so don't overwrite it @@ -650,7 +649,7 @@ def populate_from_transaction(cls, transaction): options = client.options or {} sentry_items["trace_id"] = transaction.trace_id - sentry_items["sample_rand"] = str(transaction._sample_rand) + sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231 if options.get("environment"): sentry_items["environment"] = options["environment"] @@ -724,15 +723,15 @@ def strip_sentry_baggage(header): ) def _sample_rand(self): - # type: () -> Optional[Decimal] + # type: () -> Optional[float] """Convenience method to get the sample_rand value from the sentry_items. - We validate the value and parse it as a Decimal before returning it. The value is considered - valid if it is a Decimal in the range [0, 1). + We validate the value and parse it as a float before returning it. The value is considered + valid if it is a float in the range [0, 1). """ - sample_rand = try_convert(Decimal, self.sentry_items.get("sample_rand")) + sample_rand = try_convert(float, self.sentry_items.get("sample_rand")) - if sample_rand is not None and Decimal(0) <= sample_rand < Decimal(1): + if sample_rand is not None and 0.0 <= sample_rand < 1.0: return sample_rand return None @@ -898,7 +897,7 @@ def _generate_sample_rand( *, interval=(0.0, 1.0), # type: tuple[float, float] ): - # type: (...) -> Decimal + # type: (...) -> float """Generate a sample_rand value from a trace ID. The generated value will be pseudorandomly chosen from the provided @@ -913,19 +912,16 @@ def _generate_sample_rand( raise ValueError("Invalid interval: lower must be less than upper") rng = Random(trace_id) - sample_rand = upper - while sample_rand >= upper: - sample_rand = rng.uniform(lower, upper) - - # Round down to exactly six decimal-digit precision. - # Setting the context is needed to avoid an InvalidOperation exception - # in case the user has changed the default precision or set traps. - with localcontext(DefaultContext) as ctx: - ctx.prec = 6 - return Decimal(sample_rand).quantize( - Decimal("0.000001"), - rounding=ROUND_DOWN, - ) + lower_scaled = int(lower * 1_000_000) + upper_scaled = int(upper * 1_000_000) + try: + sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled) + except ValueError: + # In some corner cases it might happen that the range is too small + # In that case, just take the lower bound + sample_rand_scaled = lower_scaled + + return sample_rand_scaled / 1_000_000 def _sample_rand_range(parent_sampled, sample_rate): diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index b0f3fa4a4c..3fe3ac3eec 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1934,6 +1934,12 @@ def try_convert(convert_func, value): given function. Return None if the conversion fails, i.e. if the function raises an exception. """ + try: + if isinstance(value, convert_func): # type: ignore + return value + except TypeError: + pass + try: return convert_func(value) except Exception: diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index dbb4286370..267ce08fdd 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -618,7 +618,7 @@ async def handler(request): raw_server = await aiohttp_raw_server(handler) - with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5): + with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000): with start_transaction( name="/interactions/other-dogs/new-dog", op="greeting.sniff", diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index ce2e693143..80b4a423cb 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -518,8 +518,8 @@ def test_baggage_propagation(init_celery): def dummy_task(self, x, y): return _get_headers(self) - # patch random.uniform to return a predictable sample_rand value - with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5): + # patch random.randrange to return a predictable sample_rand value + with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000): with start_transaction() as transaction: result = dummy_task.apply_async( args=(1, 0), diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index 5a35b68076..ba2575ce59 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -170,8 +170,8 @@ def test_outgoing_trace_headers_append_to_baggage( url = "http://example.com/" - # patch random.uniform to return a predictable sample_rand value - with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5): + # patch random.randrange to return a predictable sample_rand value + with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000): with start_transaction( name="/interactions/other-dogs/new-dog", op="greeting.sniff", diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index f6735d0e74..b8d46d0558 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -236,7 +236,7 @@ def test_outgoing_trace_headers_head_sdk(sentry_init, monkeypatch): monkeypatch.setattr(HTTPSConnection, "send", mock_send) sentry_init(traces_sample_rate=0.5, release="foo") - with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.25): + with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=250000): transaction = Transaction.continue_from_headers({}) with start_transaction(transaction=transaction, name="Head SDK tx") as transaction: diff --git a/tests/test_dsc.py b/tests/test_dsc.py index 8e549d0cf8..6097af7f95 100644 --- a/tests/test_dsc.py +++ b/tests/test_dsc.py @@ -175,7 +175,7 @@ def my_traces_sampler(sampling_context): } # We continue the incoming trace and start a new transaction - with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.125): + with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=125000): transaction = sentry_sdk.continue_trace(incoming_http_headers) with sentry_sdk.start_transaction(transaction, name="foo"): pass diff --git a/tests/test_monitor.py b/tests/test_monitor.py index b48d9f6282..9ffc943bed 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -73,7 +73,7 @@ def test_transaction_uses_downsampled_rate( assert monitor.downsample_factor == 1 # make sure we don't sample the transaction - with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.75): + with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=750000): with sentry_sdk.start_transaction(name="foobar") as transaction: assert transaction.sampled is False assert transaction.sample_rate == 0.5 diff --git a/tests/test_propagationcontext.py b/tests/test_propagationcontext.py index a0ce1094fa..078a69c72b 100644 --- a/tests/test_propagationcontext.py +++ b/tests/test_propagationcontext.py @@ -136,13 +136,13 @@ def test_sample_rand_filled(parent_sampled, sample_rate, expected_interval): else: sample_rate_str = "" - # for convenience, we'll just return the lower bound of the interval - mock_uniform = mock.Mock(return_value=expected_interval[0]) + # for convenience, we'll just return the lower bound of the interval as an integer + mock_randrange = mock.Mock(return_value=int(expected_interval[0] * 1000000)) def mock_random_class(seed): assert seed == "00000000000000000000000000000000", "seed should be the trace_id" rv = Mock() - rv.uniform = mock_uniform + rv.randrange = mock_randrange return rv with mock.patch("sentry_sdk.tracing_utils.Random", mock_random_class): @@ -158,17 +158,20 @@ def mock_random_class(seed): ctx.dynamic_sampling_context["sample_rand"] == f"{expected_interval[0]:.6f}" # noqa: E231 ) - assert mock_uniform.call_count == 1 - assert mock_uniform.call_args[0] == expected_interval + assert mock_randrange.call_count == 1 + assert mock_randrange.call_args[0] == ( + int(expected_interval[0] * 1000000), + int(expected_interval[1] * 1000000), + ) def test_sample_rand_rounds_down(): # Mock value that should round down to 0.999_999 - mock_uniform = mock.Mock(return_value=0.999_999_9) + mock_randrange = mock.Mock(return_value=999999) def mock_random_class(_): rv = Mock() - rv.uniform = mock_uniform + rv.randrange = mock_randrange return rv with mock.patch("sentry_sdk.tracing_utils.Random", mock_random_class): diff --git a/tests/tracing/test_integration_tests.py b/tests/tracing/test_integration_tests.py index 61ef14b7d0..8b5659b694 100644 --- a/tests/tracing/test_integration_tests.py +++ b/tests/tracing/test_integration_tests.py @@ -169,7 +169,7 @@ def test_dynamic_sampling_head_sdk_creates_dsc( envelopes = capture_envelopes() # make sure transaction is sampled for both cases - with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.25): + with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=250000): transaction = Transaction.continue_from_headers({}, name="Head SDK tx") # will create empty mutable baggage diff --git a/tests/tracing/test_sample_rand.py b/tests/tracing/test_sample_rand.py index f9c10aa04e..4a74950b30 100644 --- a/tests/tracing/test_sample_rand.py +++ b/tests/tracing/test_sample_rand.py @@ -1,5 +1,3 @@ -import decimal -from decimal import Inexact, FloatOperation from unittest import mock import pytest @@ -20,7 +18,8 @@ def test_deterministic_sampled(sentry_init, capture_events, sample_rate, sample_ events = capture_events() with mock.patch( - "sentry_sdk.tracing_utils.Random.uniform", return_value=sample_rand + "sentry_sdk.tracing_utils.Random.randrange", + return_value=int(sample_rand * 1000000), ): with sentry_sdk.start_transaction() as transaction: assert ( @@ -55,35 +54,3 @@ def test_transaction_uses_incoming_sample_rand( # Transaction event captured if sample_rand < sample_rate, indicating that # sample_rand is used to make the sampling decision. assert len(events) == int(sample_rand < sample_rate) - - -def test_decimal_context(sentry_init, capture_events): - """ - Ensure that having a user altered decimal context with a precision below 6 - does not cause an InvalidOperation exception. - """ - sentry_init(traces_sample_rate=1.0) - events = capture_events() - - old_prec = decimal.getcontext().prec - old_inexact = decimal.getcontext().traps[Inexact] - old_float_operation = decimal.getcontext().traps[FloatOperation] - - decimal.getcontext().prec = 2 - decimal.getcontext().traps[Inexact] = True - decimal.getcontext().traps[FloatOperation] = True - - try: - with mock.patch( - "sentry_sdk.tracing_utils.Random.uniform", return_value=0.123456789 - ): - with sentry_sdk.start_transaction() as transaction: - assert ( - transaction.get_baggage().sentry_items["sample_rand"] == "0.123456" - ) - finally: - decimal.getcontext().prec = old_prec - decimal.getcontext().traps[Inexact] = old_inexact - decimal.getcontext().traps[FloatOperation] = old_float_operation - - assert len(events) == 1 diff --git a/tests/tracing/test_sample_rand_propagation.py b/tests/tracing/test_sample_rand_propagation.py index ea3ea548ff..e6f3e99510 100644 --- a/tests/tracing/test_sample_rand_propagation.py +++ b/tests/tracing/test_sample_rand_propagation.py @@ -35,9 +35,9 @@ def test_continue_trace_missing_sample_rand(): "baggage": "sentry-placeholder=asdf", } - mock_uniform = Mock(return_value=0.5) - - with mock.patch("sentry_sdk.tracing_utils.Random.uniform", mock_uniform): + with mock.patch( + "sentry_sdk.tracing_utils.Random.randrange", Mock(return_value=500000) + ): transaction = sentry_sdk.continue_trace(headers) assert transaction.get_baggage().sentry_items["sample_rand"] == "0.500000" From ca12bbf547559d21319ac676b2e754df712de0f7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 12 Sep 2025 10:47:59 +0200 Subject: [PATCH 46/88] ci: Fix mypy, gevent (#4790) - mypy: looks like the error we're ignoring is now called differently - gevent: zope.interface pushed out a new major recently which broke old python tests --- scripts/populate_tox/tox.jinja | 1 + sentry_sdk/integrations/threading.py | 2 +- tox.ini | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index ef2e89c88c..4a4bd96c52 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -134,6 +134,7 @@ deps = {py3.8,py3.9,py3.10,py3.11,py3.12}-gevent: pytest gevent: pytest-asyncio {py3.10,py3.11}-gevent: zope.event<5.0.0 + {py3.10,py3.11}-gevent: zope.interface<8.0 # === Integrations === diff --git a/sentry_sdk/integrations/threading.py b/sentry_sdk/integrations/threading.py index fc4f539228..c031c51f50 100644 --- a/sentry_sdk/integrations/threading.py +++ b/sentry_sdk/integrations/threading.py @@ -52,7 +52,7 @@ def setup_once(): try: from django import VERSION as django_version # noqa: N811 - import channels # type: ignore[import-not-found] + import channels # type: ignore[import-untyped] channels_version = channels.__version__ except ImportError: diff --git a/tox.ini b/tox.ini index 1bc9757b9a..39ef4785b3 100644 --- a/tox.ini +++ b/tox.ini @@ -358,6 +358,7 @@ deps = {py3.8,py3.9,py3.10,py3.11,py3.12}-gevent: pytest gevent: pytest-asyncio {py3.10,py3.11}-gevent: zope.event<5.0.0 + {py3.10,py3.11}-gevent: zope.interface<8.0 # === Integrations === From b19e08642e093c2f7945849bc26fd0d5f735bed9 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 12 Sep 2025 10:58:01 +0200 Subject: [PATCH 47/88] Correctly check for a running transaction (#4791) Fix check for the existence of a currently transaction. --- sentry_sdk/ai/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/ai/utils.py b/sentry_sdk/ai/utils.py index 41f89f5623..d0ccf1bed3 100644 --- a/sentry_sdk/ai/utils.py +++ b/sentry_sdk/ai/utils.py @@ -44,6 +44,6 @@ def get_start_span_function(): # type: () -> Callable[..., Any] current_span = sentry_sdk.get_current_span() transaction_exists = ( - current_span is not None and current_span.containing_transaction == current_span + current_span is not None and current_span.containing_transaction is not None ) return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction From 398b7c4f4486e52493116f9dc469fbeef14f6bc7 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 12 Sep 2025 11:31:44 +0200 Subject: [PATCH 48/88] feat(anthropic): Add proper tool calling data to Anthropic integration (#4769) - Format the response of the LLM (`gen_ai.response.text`) correctly. Not using the JSON but only use the actual text that was returned. - Add responses for tool calls (`gen_ai.response.tool_calls`) to the LLM spans. - Add results of tool calls to the request (`gen_ai.request.messages`). Before: Screenshot 2025-09-12 at 10 43 32 After: Screenshot 2025-09-12 at 10 45 11 --- sentry_sdk/integrations/anthropic.py | 55 +++++++++++++++---- .../integrations/anthropic/test_anthropic.py | 41 ++++++-------- 2 files changed, 61 insertions(+), 35 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index ff3d9a3388..4f4c0b1a2a 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -1,5 +1,4 @@ from functools import wraps -import json from typing import TYPE_CHECKING import sentry_sdk @@ -117,8 +116,29 @@ def _set_input_data(span, kwargs, integration): and should_send_default_pii() and integration.include_prompts ): + normalized_messages = [] + for message in messages: + if ( + message.get("role") == "user" + and "content" in message + and isinstance(message["content"], (list, tuple)) + ): + for item in message["content"]: + if item.get("type") == "tool_result": + normalized_messages.append( + { + "role": "tool", + "content": { + "tool_use_id": item.get("tool_use_id"), + "output": item.get("content"), + }, + } + ) + else: + normalized_messages.append(message) + set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, safe_serialize(messages) + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, normalized_messages, unpack=False ) set_data_normalized( @@ -159,12 +179,29 @@ def _set_output_data( Set output data for the span based on the AI response.""" span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, model) if should_send_default_pii() and integration.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - json.dumps(content_blocks), - unpack=False, - ) + output_messages = { + "response": [], + "tool": [], + } # type: (dict[str, list[Any]]) + + for output in content_blocks: + if output["type"] == "text": + output_messages["response"].append(output["text"]) + elif output["type"] == "tool_use": + output_messages["tool"].append(output) + + if len(output_messages["tool"]) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + output_messages["tool"], + unpack=False, + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) record_token_usage( span, @@ -172,8 +209,6 @@ def _set_output_data( output_tokens=output_tokens, ) - # TODO: GEN_AI_RESPONSE_TOOL_CALLS ? - if finish_span: span.__exit__(None, None, None) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index eba07a1df6..3893626026 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -1,6 +1,6 @@ +import pytest from unittest import mock - try: from unittest.mock import AsyncMock except ImportError: @@ -10,7 +10,6 @@ async def __call__(self, *args, **kwargs): return super(AsyncMock, self).__call__(*args, **kwargs) -import pytest from anthropic import Anthropic, AnthropicError, AsyncAnthropic, AsyncStream, Stream from anthropic.types import MessageDeltaUsage, TextDelta, Usage from anthropic.types.content_block_delta_event import ContentBlockDeltaEvent @@ -20,9 +19,6 @@ async def __call__(self, *args, **kwargs): from anthropic.types.message_delta_event import MessageDeltaEvent from anthropic.types.message_start_event import MessageStartEvent -from sentry_sdk.integrations.anthropic import _set_output_data, _collect_ai_data -from sentry_sdk.utils import package_version - try: from anthropic.types import InputJSONDelta except ImportError: @@ -46,9 +42,16 @@ async def __call__(self, *args, **kwargs): from sentry_sdk import start_transaction, start_span from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.anthropic import AnthropicIntegration +from sentry_sdk.integrations.anthropic import ( + AnthropicIntegration, + _set_output_data, + _collect_ai_data, +) +from sentry_sdk.utils import package_version + ANTHROPIC_VERSION = package_version("anthropic") + EXAMPLE_MESSAGE = Message( id="id", model="model", @@ -121,10 +124,7 @@ def test_nonstreaming_create_message( span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert ( - span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - == '[{"text": "Hi, I\'m Claude.", "type": "text"}]' - ) + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude." else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] @@ -193,10 +193,7 @@ async def test_nonstreaming_create_message_async( span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert ( - span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - == '[{"text": "Hi, I\'m Claude.", "type": "text"}]' - ) + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude." else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] @@ -296,10 +293,7 @@ def test_streaming_create_message( span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert ( - span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - == '[{"text": "Hi! I\'m Claude!", "type": "text"}]' - ) + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!" else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -403,10 +397,7 @@ async def test_streaming_create_message_async( span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert ( - span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - == '[{"text": "Hi! I\'m Claude!", "type": "text"}]' - ) + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!" else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -539,7 +530,7 @@ def test_streaming_create_message_with_input_json_delta( ) assert ( span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - == '[{"text": "{\'location\': \'San Francisco, CA\'}", "type": "text"}]' + == "{'location': 'San Francisco, CA'}" ) else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -679,7 +670,7 @@ async def test_streaming_create_message_with_input_json_delta_async( ) assert ( span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - == '[{"text": "{\'location\': \'San Francisco, CA\'}", "type": "text"}]' + == "{'location': 'San Francisco, CA'}" ) else: @@ -835,7 +826,7 @@ def test_set_output_data_with_input_json_delta(sentry_init): assert ( span._data.get(SPANDATA.GEN_AI_RESPONSE_TEXT) - == "[{\"text\": \"{'test': 'data','more': 'json'}\", \"type\": \"text\"}]" + == "{'test': 'data','more': 'json'}" ) assert span._data.get(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS) == 10 assert span._data.get(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS) == 20 From 5a122b56fc39b841cf01243a622733bf43133403 Mon Sep 17 00:00:00 2001 From: "ZhengYu, Xu" Date: Fri, 12 Sep 2025 17:34:40 +0800 Subject: [PATCH 49/88] chore: Reexport module `profiler` (#4535) The example provided by sentry causes pylance to report `"profiler" is not a known attribute of module "sentry_sdk"` image --------- Co-authored-by: Anton Pirker Co-authored-by: Anton Pirker --- sentry_sdk/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index a37b52ff4e..1939be0510 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -1,10 +1,10 @@ +from sentry_sdk import profiler from sentry_sdk.scope import Scope from sentry_sdk.transport import Transport, HttpTransport from sentry_sdk.client import Client from sentry_sdk.api import * # noqa - -from sentry_sdk.consts import VERSION # noqa +from sentry_sdk.consts import VERSION __all__ = [ # noqa "Hub", @@ -12,6 +12,7 @@ "Client", "Transport", "HttpTransport", + "VERSION", "integrations", # From sentry_sdk.api "init", @@ -47,6 +48,7 @@ "trace", "monitor", "logger", + "profiler", "start_session", "end_session", "set_transaction_name", From 16f2c3df628ef1b0e8ecdaac272ab6e94931eec1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:50:39 +0200 Subject: [PATCH 50/88] build(deps): bump actions/create-github-app-token from 2.1.1 to 2.1.4 (#4795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2.1.1 to 2.1.4.
Release notes

Sourced from actions/create-github-app-token's releases.

v2.1.4

2.1.4 (2025-09-13)

Bug Fixes

  • deps: bump @​octokit/auth-app from 7.2.1 to 8.0.1 (#257) (bef1eaf)

v2.1.3

2.1.3 (2025-09-13)

Bug Fixes

  • deps: bump undici from 7.8.0 to 7.10.0 in the production-dependencies group (#254) (f3d5ec2)

v2.1.2

2.1.2 (2025-09-12)

Bug Fixes

  • deps: bump @​octokit/request from 9.2.3 to 10.0.2 (#256) (5d7307b)
Commits
  • 6701853 build(release): 2.1.4 [skip ci]
  • bef1eaf fix(deps): bump @​octokit/auth-app from 7.2.1 to 8.0.1 (#257)
  • 1526738 build(release): 2.1.3 [skip ci]
  • f3d5ec2 fix(deps): bump undici from 7.8.0 to 7.10.0 in the production-dependencies gr...
  • def152b build(release): 2.1.2 [skip ci]
  • 5d7307b fix(deps): bump @​octokit/request from 9.2.3 to 10.0.2 (#256)
  • 525760a build(deps): bump stefanzweifel/git-auto-commit-action from 5.2.0 to 6.0.1 (#...
  • 8ab05a8 Add beta branch support for releases (#282)
  • d00315e build(deps): bump actions/checkout from 4 to 5 (#279)
  • fcc6c28 build(deps-dev): bump dotenv from 16.5.0 to 17.2.1 (#269)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/create-github-app-token&package-manager=github_actions&previous-version=2.1.1&new-version=2.1.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5e952d0de..68aeebf2b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From 5747863128a3bbb382d37ea94d104a7d9b358441 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 15 Sep 2025 09:10:23 +0200 Subject: [PATCH 51/88] feat(integrations): Support gql 4.0-style execute (#4779) gql 4.0 [changed](https://github.com/graphql-python/gql/pull/556/files) the signature of the `execute` function which we were patching. Instead of a `DocumentNode` it now gets a `GraphQLRequest` (which contains the `document` attribute with the `DocumentNode`). This means we need to update the way we're extracting additional data in the event processor. --- sentry_sdk/integrations/gql.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index 5f4436f5b2..8c378060b7 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -18,6 +18,13 @@ ) from gql.transport import Transport, AsyncTransport # type: ignore[import-not-found] from gql.transport.exceptions import TransportQueryError # type: ignore[import-not-found] + + try: + # gql 4.0+ + from gql import GraphQLRequest + except ImportError: + GraphQLRequest = None + except ImportError: raise DidNotEnable("gql is not installed") @@ -92,13 +99,13 @@ def _patch_execute(): real_execute = gql.Client.execute @ensure_integration_enabled(GQLIntegration, real_execute) - def sentry_patched_execute(self, document, *args, **kwargs): + def sentry_patched_execute(self, document_or_request, *args, **kwargs): # type: (gql.Client, DocumentNode, Any, Any) -> Any scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_make_gql_event_processor(self, document)) + scope.add_event_processor(_make_gql_event_processor(self, document_or_request)) try: - return real_execute(self, document, *args, **kwargs) + return real_execute(self, document_or_request, *args, **kwargs) except TransportQueryError as e: event, hint = event_from_exception( e, @@ -112,8 +119,8 @@ def sentry_patched_execute(self, document, *args, **kwargs): gql.Client.execute = sentry_patched_execute -def _make_gql_event_processor(client, document): - # type: (gql.Client, DocumentNode) -> EventProcessor +def _make_gql_event_processor(client, document_or_request): + # type: (gql.Client, Union[DocumentNode, gql.GraphQLRequest]) -> EventProcessor def processor(event, hint): # type: (Event, dict[str, Any]) -> Event try: @@ -130,6 +137,16 @@ def processor(event, hint): ) if should_send_default_pii(): + if GraphQLRequest is not None and isinstance( + document_or_request, GraphQLRequest + ): + # In v4.0.0, gql moved to using GraphQLRequest instead of + # DocumentNode in execute + # https://github.com/graphql-python/gql/pull/556 + document = document_or_request.document + else: + document = document_or_request + request["data"] = _data_from_document(document) contexts = event.setdefault("contexts", {}) response = contexts.setdefault("response", {}) From 0df7f4508ceb45d146143f2ff95d37c0c54e7b74 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 15 Sep 2025 14:34:43 +0200 Subject: [PATCH 52/88] fix(logs): Expect `log_item` as rate limit category (#4798) The data category for rate limiting logs is `log_item`, not `log`. Closes https://github.com/getsentry/sentry-python/issues/4797 --- sentry_sdk/_types.py | 2 +- sentry_sdk/envelope.py | 2 +- tests/test_envelope.py | 23 ++++++++++++++++++++++- tests/test_transport.py | 39 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 8336617a8d..b28c7260ce 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -269,7 +269,7 @@ class SDKInfo(TypedDict): "metric_bucket", "monitor", "span", - "log", + "log_item", ] SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index 5f7220bf21..7dbbdec5c8 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -273,7 +273,7 @@ def data_category(self): elif ty == "event": return "error" elif ty == "log": - return "log" + return "log_item" elif ty == "client_report": return "internal" elif ty == "profile": diff --git a/tests/test_envelope.py b/tests/test_envelope.py index d1bc668f05..06f8971dc3 100644 --- a/tests/test_envelope.py +++ b/tests/test_envelope.py @@ -1,4 +1,4 @@ -from sentry_sdk.envelope import Envelope +from sentry_sdk.envelope import Envelope, Item, PayloadRef from sentry_sdk.session import Session from sentry_sdk import capture_event import sentry_sdk.client @@ -239,3 +239,24 @@ def test_envelope_without_headers(): assert len(items) == 1 assert items[0].payload.get_bytes() == b'{"started": "2020-02-07T14:16:00Z"}' + + +def test_envelope_item_data_category_mapping(): + """Test that envelope items map to correct data categories for rate limiting.""" + test_cases = [ + ("event", "error"), + ("transaction", "transaction"), + ("log", "log_item"), + ("session", "session"), + ("attachment", "attachment"), + ("client_report", "internal"), + ("profile", "profile"), + ("profile_chunk", "profile_chunk"), + ("statsd", "metric_bucket"), + ("check_in", "monitor"), + ("unknown_type", "default"), + ] + + for item_type, expected_category in test_cases: + item = Item(payload=PayloadRef(json={"test": "data"}), type=item_type) + assert item.data_category == expected_category diff --git a/tests/test_transport.py b/tests/test_transport.py index c6a1a0a7a7..e493515e9a 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -611,7 +611,7 @@ def test_metric_bucket_limits(capturing_server, response_code, make_client): assert capturing_server.captured[0].path == "/api/132/envelope/" capturing_server.clear_captured() - assert set(client.transport._disabled_until) == set(["metric_bucket"]) + assert set(client.transport._disabled_until) == {"metric_bucket"} client.transport.capture_envelope(envelope) client.capture_event({"type": "transaction"}) @@ -629,6 +629,43 @@ def test_metric_bucket_limits(capturing_server, response_code, make_client): ] +@pytest.mark.parametrize("response_code", [200, 429]) +def test_log_item_limits(capturing_server, response_code, make_client): + client = make_client() + capturing_server.respond_with( + code=response_code, + headers={ + "X-Sentry-Rate-Limits": "4711:log_item:organization:quota_exceeded:custom" + }, + ) + + envelope = Envelope() + envelope.add_item(Item(payload=b"{}", type="log")) + client.transport.capture_envelope(envelope) + client.flush() + + assert len(capturing_server.captured) == 1 + assert capturing_server.captured[0].path == "/api/132/envelope/" + capturing_server.clear_captured() + + assert set(client.transport._disabled_until) == {"log_item"} + + client.transport.capture_envelope(envelope) + client.capture_event({"type": "transaction"}) + client.flush() + + assert len(capturing_server.captured) == 2 + + envelope = capturing_server.captured[0].envelope + assert envelope.items[0].type == "transaction" + envelope = capturing_server.captured[1].envelope + assert envelope.items[0].type == "client_report" + report = parse_json(envelope.items[0].get_bytes()) + assert report["discarded_events"] == [ + {"category": "log_item", "reason": "ratelimit_backoff", "quantity": 1}, + ] + + @pytest.mark.parametrize("response_code", [200, 429]) def test_metric_bucket_limits_with_namespace( capturing_server, response_code, make_client From 36ae7c4386a900874e8d5423fda1793ebaaf0e73 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 15 Sep 2025 15:26:01 +0200 Subject: [PATCH 53/88] tests: Update tox.ini (#4799) Regular update --- tox.ini | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/tox.ini b/tox.ini index 39ef4785b3..5fe52a1e2b 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-09T08:24:12.875177+00:00 +# Last generated: 2025-09-15T12:28:26.599446+00:00 [tox] requires = @@ -98,12 +98,12 @@ envlist = {py3.8,py3.11,py3.12}-anthropic-v0.16.0 {py3.8,py3.11,py3.12}-anthropic-v0.33.1 {py3.8,py3.11,py3.12}-anthropic-v0.50.0 - {py3.8,py3.12,py3.13}-anthropic-v0.66.0 + {py3.8,py3.12,py3.13}-anthropic-v0.67.0 {py3.9,py3.10,py3.11}-cohere-v5.4.0 {py3.9,py3.11,py3.12}-cohere-v5.9.4 {py3.9,py3.11,py3.12}-cohere-v5.13.12 - {py3.9,py3.11,py3.12}-cohere-v5.17.0 + {py3.9,py3.11,py3.12}-cohere-v5.18.0 {py3.9,py3.11,py3.12}-langchain-base-v0.1.20 {py3.9,py3.11,py3.12}-langchain-base-v0.2.17 @@ -116,12 +116,12 @@ envlist = {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.11,py3.12}-openai-base-v1.36.1 {py3.8,py3.11,py3.12}-openai-base-v1.71.0 - {py3.8,py3.12,py3.13}-openai-base-v1.107.0 + {py3.8,py3.12,py3.13}-openai-base-v1.107.2 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.36.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.71.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.107.0 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.107.2 {py3.9,py3.12,py3.13}-langgraph-v0.6.7 {py3.10,py3.12,py3.13}-langgraph-v1.0.0a3 @@ -129,6 +129,7 @@ envlist = {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 {py3.10,py3.12,py3.13}-openai_agents-v0.2.11 + {py3.10,py3.12,py3.13}-openai_agents-v0.3.0 {py3.8,py3.10,py3.11}-huggingface_hub-v0.24.7 {py3.8,py3.12,py3.13}-huggingface_hub-v0.27.1 @@ -141,7 +142,7 @@ envlist = {py3.6,py3.7}-boto3-v1.12.49 {py3.6,py3.9,py3.10}-boto3-v1.20.54 {py3.7,py3.11,py3.12}-boto3-v1.28.85 - {py3.9,py3.12,py3.13}-boto3-v1.40.26 + {py3.9,py3.12,py3.13}-boto3-v1.40.30 {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.6,py3.7,py3.8}-chalice-v1.21.9 @@ -160,7 +161,7 @@ envlist = {py3.6}-pymongo-v3.5.1 {py3.6,py3.10,py3.11}-pymongo-v3.13.0 {py3.6,py3.9,py3.10}-pymongo-v4.0.2 - {py3.9,py3.12,py3.13}-pymongo-v4.14.1 + {py3.9,py3.12,py3.13}-pymongo-v4.15.0 {py3.6}-redis_py_cluster_legacy-v1.3.6 {py3.6,py3.7}-redis_py_cluster_legacy-v2.0.0 @@ -183,7 +184,7 @@ envlist = {py3.7,py3.12,py3.13}-statsig-v0.55.3 {py3.7,py3.12,py3.13}-statsig-v0.58.4 {py3.7,py3.12,py3.13}-statsig-v0.61.0 - {py3.7,py3.12,py3.13}-statsig-v0.63.0 + {py3.7,py3.12,py3.13}-statsig-v0.64.0 {py3.8,py3.12,py3.13}-unleash-v6.0.1 {py3.8,py3.12,py3.13}-unleash-v6.1.0 @@ -233,6 +234,7 @@ envlist = {py3.6,py3.7,py3.8}-celery-v4.4.7 {py3.6,py3.7,py3.8}-celery-v5.0.5 {py3.8,py3.12,py3.13}-celery-v5.5.3 + {py3.8,py3.12,py3.13}-celery-v5.6.0b1 {py3.6,py3.7}-dramatiq-v1.9.0 {py3.6,py3.8,py3.9}-dramatiq-v1.12.3 @@ -263,9 +265,9 @@ envlist = {py3.9,py3.12,py3.13}-flask-v3.1.2 {py3.6,py3.9,py3.10}-starlette-v0.16.0 - {py3.7,py3.10,py3.11}-starlette-v0.26.1 - {py3.8,py3.11,py3.12}-starlette-v0.36.3 - {py3.9,py3.12,py3.13}-starlette-v0.47.3 + {py3.7,py3.10,py3.11}-starlette-v0.27.0 + {py3.8,py3.12,py3.13}-starlette-v0.38.6 + {py3.9,py3.12,py3.13}-starlette-v0.48.0 {py3.6,py3.9,py3.10}-fastapi-v0.79.1 {py3.7,py3.10,py3.11}-fastapi-v0.91.0 @@ -460,7 +462,7 @@ deps = anthropic-v0.16.0: anthropic==0.16.0 anthropic-v0.33.1: anthropic==0.33.1 anthropic-v0.50.0: anthropic==0.50.0 - anthropic-v0.66.0: anthropic==0.66.0 + anthropic-v0.67.0: anthropic==0.67.0 anthropic: pytest-asyncio anthropic-v0.16.0: httpx<0.28.0 anthropic-v0.33.1: httpx<0.28.0 @@ -468,7 +470,7 @@ deps = cohere-v5.4.0: cohere==5.4.0 cohere-v5.9.4: cohere==5.9.4 cohere-v5.13.12: cohere==5.13.12 - cohere-v5.17.0: cohere==5.17.0 + cohere-v5.18.0: cohere==5.18.0 langchain-base-v0.1.20: langchain==0.1.20 langchain-base-v0.2.17: langchain==0.2.17 @@ -488,7 +490,7 @@ deps = openai-base-v1.0.1: openai==1.0.1 openai-base-v1.36.1: openai==1.36.1 openai-base-v1.71.0: openai==1.71.0 - openai-base-v1.107.0: openai==1.107.0 + openai-base-v1.107.2: openai==1.107.2 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 @@ -497,7 +499,7 @@ deps = openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.36.1: openai==1.36.1 openai-notiktoken-v1.71.0: openai==1.71.0 - openai-notiktoken-v1.107.0: openai==1.107.0 + openai-notiktoken-v1.107.2: openai==1.107.2 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.36.1: httpx<0.28 @@ -508,6 +510,7 @@ deps = openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.1.0: openai-agents==0.1.0 openai_agents-v0.2.11: openai-agents==0.2.11 + openai_agents-v0.3.0: openai-agents==0.3.0 openai_agents: pytest-asyncio huggingface_hub-v0.24.7: huggingface_hub==0.24.7 @@ -522,7 +525,7 @@ deps = boto3-v1.12.49: boto3==1.12.49 boto3-v1.20.54: boto3==1.20.54 boto3-v1.28.85: boto3==1.28.85 - boto3-v1.40.26: boto3==1.40.26 + boto3-v1.40.30: boto3==1.40.30 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 @@ -544,7 +547,7 @@ deps = pymongo-v3.5.1: pymongo==3.5.1 pymongo-v3.13.0: pymongo==3.13.0 pymongo-v4.0.2: pymongo==4.0.2 - pymongo-v4.14.1: pymongo==4.14.1 + pymongo-v4.15.0: pymongo==4.15.0 pymongo: mockupdb redis_py_cluster_legacy-v1.3.6: redis-py-cluster==1.3.6 @@ -568,7 +571,7 @@ deps = statsig-v0.55.3: statsig==0.55.3 statsig-v0.58.4: statsig==0.58.4 statsig-v0.61.0: statsig==0.61.0 - statsig-v0.63.0: statsig==0.63.0 + statsig-v0.64.0: statsig==0.64.0 statsig: typing_extensions unleash-v6.0.1: UnleashClient==6.0.1 @@ -639,6 +642,7 @@ deps = celery-v4.4.7: celery==4.4.7 celery-v5.0.5: celery==5.0.5 celery-v5.5.3: celery==5.5.3 + celery-v5.6.0b1: celery==5.6.0b1 celery: newrelic<10.17.0 celery: redis {py3.7}-celery: importlib-metadata<5.0 @@ -698,9 +702,9 @@ deps = flask-v1.1.4: markupsafe<2.1.0 starlette-v0.16.0: starlette==0.16.0 - starlette-v0.26.1: starlette==0.26.1 - starlette-v0.36.3: starlette==0.36.3 - starlette-v0.47.3: starlette==0.47.3 + starlette-v0.27.0: starlette==0.27.0 + starlette-v0.38.6: starlette==0.38.6 + starlette-v0.48.0: starlette==0.48.0 starlette: pytest-asyncio starlette: python-multipart starlette: requests @@ -708,8 +712,7 @@ deps = starlette: jinja2 starlette: httpx starlette-v0.16.0: httpx<0.28.0 - starlette-v0.26.1: httpx<0.28.0 - starlette-v0.36.3: httpx<0.28.0 + starlette-v0.27.0: httpx<0.28.0 {py3.6}-starlette: aiocontextvars fastapi-v0.79.1: fastapi==0.79.1 From 7ecb39b486231788ba3f18b547d7cb3ded25952e Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 15 Sep 2025 13:35:20 +0000 Subject: [PATCH 54/88] release: 2.38.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28c4882414..6ceda55626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 2.38.0 + +### Various fixes & improvements + +- tests: Update tox.ini (#4799) by @sentrivana +- fix(logs): Expect `log_item` as rate limit category (#4798) by @sentrivana +- feat(integrations): Support gql 4.0-style execute (#4779) by @sentrivana +- build(deps): bump actions/create-github-app-token from 2.1.1 to 2.1.4 (#4795) by @dependabot +- chore: Reexport module `profiler` (#4535) by @zen-xu +- feat(anthropic): Add proper tool calling data to Anthropic integration (#4769) by @antonpirker +- Correctly check for a running transaction (#4791) by @antonpirker +- ci: Fix mypy, gevent (#4790) by @sentrivana +- ref(tracing): Use float for sample rand (#4677) by @sentrivana +- Avoid reporting false-positive StopAsyncIteration in the asyncio integration (#4741) by @vmarkovtsev +- feat(ai): Create transaction in AI agents framworks, when no transaction is running. (#4758) by @constantinius +- build(deps): bump codecov/codecov-action from 5.5.0 to 5.5.1 (#4773) by @dependabot +- build(deps): bump actions/setup-python from 5 to 6 (#4774) by @dependabot +- Add log message when `DedupeIntegration` is dropping an error. (#4788) by @antonpirker +- Add input and output to `invoke_agent` span. (#4785) by @antonpirker +- Update HuggingFace Hub integration (#4746) by @antonpirker +- fix(profiling): Re-init continuous profiler (#4772) by @Zylphrex + ## 2.37.1 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index 28a49b7fa7..061b2bdfc8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.37.1" +release = "2.38.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index cc3c9b1612..91a1740526 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1331,4 +1331,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.37.1" +VERSION = "2.38.0" diff --git a/setup.py b/setup.py index 1b4d0063e4..58101aa65f 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.37.1", + version="2.38.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From 2e37b516aa626984a9b94dc15d9c5ff3459cefb2 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 15 Sep 2025 15:43:47 +0200 Subject: [PATCH 55/88] Updated Changelog --- CHANGELOG.md | 34 +++++++++++++++++----------------- sentry_sdk/tracing.py | 15 ++++++++++++--- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ceda55626..7abbed7218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,23 +4,23 @@ ### Various fixes & improvements -- tests: Update tox.ini (#4799) by @sentrivana -- fix(logs): Expect `log_item` as rate limit category (#4798) by @sentrivana -- feat(integrations): Support gql 4.0-style execute (#4779) by @sentrivana -- build(deps): bump actions/create-github-app-token from 2.1.1 to 2.1.4 (#4795) by @dependabot -- chore: Reexport module `profiler` (#4535) by @zen-xu -- feat(anthropic): Add proper tool calling data to Anthropic integration (#4769) by @antonpirker -- Correctly check for a running transaction (#4791) by @antonpirker -- ci: Fix mypy, gevent (#4790) by @sentrivana -- ref(tracing): Use float for sample rand (#4677) by @sentrivana -- Avoid reporting false-positive StopAsyncIteration in the asyncio integration (#4741) by @vmarkovtsev -- feat(ai): Create transaction in AI agents framworks, when no transaction is running. (#4758) by @constantinius -- build(deps): bump codecov/codecov-action from 5.5.0 to 5.5.1 (#4773) by @dependabot -- build(deps): bump actions/setup-python from 5 to 6 (#4774) by @dependabot -- Add log message when `DedupeIntegration` is dropping an error. (#4788) by @antonpirker -- Add input and output to `invoke_agent` span. (#4785) by @antonpirker -- Update HuggingFace Hub integration (#4746) by @antonpirker -- fix(profiling): Re-init continuous profiler (#4772) by @Zylphrex +- Feat(huggingface_hub): Update HuggingFace Hub integration (#4746) by @antonpirker +- Feat(Anthropic): Add proper tool calling data to Anthropic integration (#4769) by @antonpirker +- Feat(openai-agents): Add input and output to `invoke_agent` span. (#4785) by @antonpirker +- Feat(AI): Create transaction in AI agents framworks, when no transaction is running. (#4758) by @constantinius +- Feat(GraphQL): Support gql 4.0-style execute (#4779) by @sentrivana +- Fix(logs): Expect `log_item` as rate limit category (#4798) by @sentrivana +- Fix: CI for mypy, gevent (#4790) by @sentrivana +- Fix: Correctly check for a running transaction (#4791) by @antonpirker +- Fix: Use float for sample rand (#4677) by @sentrivana +- Fix: Avoid reporting false-positive StopAsyncIteration in the asyncio integration (#4741) by @vmarkovtsev +- Fix: Add log message when `DedupeIntegration` is dropping an error. (#4788) by @antonpirker +- Fix(profiling): Re-init continuous profiler (#4772) by @Zylphrex +- Chore: Reexport module `profiler` (#4535) by @zen-xu +- Tests: Update tox.ini (#4799) by @sentrivana +- Build(deps): bump actions/create-github-app-token from 2.1.1 to 2.1.4 (#4795) by @dependabot +- Build(deps): bump actions/setup-python from 5 to 6 (#4774) by @dependabot +- Build(deps): bump codecov/codecov-action from 5.5.0 to 5.5.1 (#4773) by @dependabot ## 2.37.1 diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index fc43a33dc7..76f91eebc5 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -708,10 +708,19 @@ def finish(self, scope=None, end_timestamp=None): end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) self.timestamp = end_timestamp else: - elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns - self.timestamp = self.start_timestamp + timedelta( - microseconds=elapsed / 1000 + print(f"In Span.finish for span {self}") + now = nanosecond_time() + elapsed = now - self._start_timestamp_monotonic_ns + print(f"NOW before assigment: {now}") + self.timestamp = ( + self.start_timestamp + + timedelta( # The assignment in this line is taking a lot of time + microseconds=elapsed / 1000 + ) ) + now2 = nanosecond_time() + print(f"NOW2 after assigment: {now2}") + print(f"Assignment duration: {now2 - now}") except AttributeError: self.timestamp = datetime.now(timezone.utc) From d94652a5527cd0e7810266f8cd30d9780e099a46 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 15 Sep 2025 16:43:25 +0200 Subject: [PATCH 56/88] removed accidental stuff --- sentry_sdk/tracing.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 76f91eebc5..fc43a33dc7 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -708,19 +708,10 @@ def finish(self, scope=None, end_timestamp=None): end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) self.timestamp = end_timestamp else: - print(f"In Span.finish for span {self}") - now = nanosecond_time() - elapsed = now - self._start_timestamp_monotonic_ns - print(f"NOW before assigment: {now}") - self.timestamp = ( - self.start_timestamp - + timedelta( # The assignment in this line is taking a lot of time - microseconds=elapsed / 1000 - ) + elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns + self.timestamp = self.start_timestamp + timedelta( + microseconds=elapsed / 1000 ) - now2 = nanosecond_time() - print(f"NOW2 after assigment: {now2}") - print(f"Assignment duration: {now2 - now}") except AttributeError: self.timestamp = datetime.now(timezone.utc) From 83e8d798af0d22626e3104e2c02cb125a3dbde10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= <114897+dingsdax@users.noreply.github.com> Date: Tue, 16 Sep 2025 11:20:42 +0200 Subject: [PATCH 57/88] meta: update pull request template (#4803) * resolves: #4796 * resolves: PY-1841 --- .github/PULL_REQUEST_TEMPLATE.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 12db62315a..dd7ef45832 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,14 @@ - +### Description + ---- +#### Issues + -Thank you for contributing to `sentry-python`! Please add tests to validate your changes, and lint your code using `tox -e linters`. - -Running the test suite on your PR might require maintainer approval. \ No newline at end of file +#### Reminders +- Please add tests to validate your changes, and lint your code using `tox -e linters`. +- Add GH Issue ID _&_ Linear ID (if applicable) +- PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) +- For external contributors: [CONTRIBUTING.md](../CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) From 7a905085087d5d11285037d609f6a91cec04b138 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 16 Sep 2025 11:24:56 +0200 Subject: [PATCH 58/88] tests: Another tox.ini update (#4801) More new releases. An initial RC of huggingface_hub v1.0 is also available and breaks our integration. Ignoring it in the tests for now. Follow up here: https://github.com/getsentry/sentry-python/issues/4802 --- scripts/populate_tox/config.py | 1 + tox.ini | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index bc20d531b3..8e35eaba4f 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -158,6 +158,7 @@ "deps": { "*": ["responses"], }, + "include": "<1.0", }, "langchain-base": { "package": "langchain", diff --git a/tox.ini b/tox.ini index 5fe52a1e2b..1fb8e9be40 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-15T12:28:26.599446+00:00 +# Last generated: 2025-09-16T07:18:41.732958+00:00 [tox] requires = @@ -116,12 +116,12 @@ envlist = {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.11,py3.12}-openai-base-v1.36.1 {py3.8,py3.11,py3.12}-openai-base-v1.71.0 - {py3.8,py3.12,py3.13}-openai-base-v1.107.2 + {py3.8,py3.12,py3.13}-openai-base-v1.107.3 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.36.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.71.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.107.2 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.107.3 {py3.9,py3.12,py3.13}-langgraph-v0.6.7 {py3.10,py3.12,py3.13}-langgraph-v1.0.0a3 @@ -134,7 +134,7 @@ envlist = {py3.8,py3.10,py3.11}-huggingface_hub-v0.24.7 {py3.8,py3.12,py3.13}-huggingface_hub-v0.27.1 {py3.8,py3.12,py3.13}-huggingface_hub-v0.30.2 - {py3.8,py3.12,py3.13}-huggingface_hub-v0.34.4 + {py3.8,py3.12,py3.13}-huggingface_hub-v0.34.5 {py3.8,py3.12,py3.13}-huggingface_hub-v0.35.0rc0 @@ -142,7 +142,7 @@ envlist = {py3.6,py3.7}-boto3-v1.12.49 {py3.6,py3.9,py3.10}-boto3-v1.20.54 {py3.7,py3.11,py3.12}-boto3-v1.28.85 - {py3.9,py3.12,py3.13}-boto3-v1.40.30 + {py3.9,py3.12,py3.13}-boto3-v1.40.31 {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.6,py3.7,py3.8}-chalice-v1.21.9 @@ -230,6 +230,7 @@ envlist = {py3.7,py3.8}-beam-v2.32.0 {py3.8,py3.10,py3.11}-beam-v2.50.0 {py3.9,py3.12,py3.13}-beam-v2.67.0 + {py3.9,py3.12,py3.13}-beam-v2.68.0rc2 {py3.6,py3.7,py3.8}-celery-v4.4.7 {py3.6,py3.7,py3.8}-celery-v5.0.5 @@ -322,8 +323,8 @@ envlist = {py3.6}-trytond-v4.8.18 {py3.6,py3.7,py3.8}-trytond-v5.8.16 {py3.8,py3.10,py3.11}-trytond-v6.8.17 - {py3.8,py3.11,py3.12}-trytond-v7.0.35 - {py3.9,py3.12,py3.13}-trytond-v7.6.6 + {py3.8,py3.11,py3.12}-trytond-v7.0.36 + {py3.9,py3.12,py3.13}-trytond-v7.6.7 {py3.7,py3.12,py3.13}-typer-v0.15.4 {py3.7,py3.12,py3.13}-typer-v0.16.1 @@ -490,7 +491,7 @@ deps = openai-base-v1.0.1: openai==1.0.1 openai-base-v1.36.1: openai==1.36.1 openai-base-v1.71.0: openai==1.71.0 - openai-base-v1.107.2: openai==1.107.2 + openai-base-v1.107.3: openai==1.107.3 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 @@ -499,7 +500,7 @@ deps = openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.36.1: openai==1.36.1 openai-notiktoken-v1.71.0: openai==1.71.0 - openai-notiktoken-v1.107.2: openai==1.107.2 + openai-notiktoken-v1.107.3: openai==1.107.3 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.36.1: httpx<0.28 @@ -516,7 +517,7 @@ deps = huggingface_hub-v0.24.7: huggingface_hub==0.24.7 huggingface_hub-v0.27.1: huggingface_hub==0.27.1 huggingface_hub-v0.30.2: huggingface_hub==0.30.2 - huggingface_hub-v0.34.4: huggingface_hub==0.34.4 + huggingface_hub-v0.34.5: huggingface_hub==0.34.5 huggingface_hub-v0.35.0rc0: huggingface_hub==0.35.0rc0 huggingface_hub: responses @@ -525,7 +526,7 @@ deps = boto3-v1.12.49: boto3==1.12.49 boto3-v1.20.54: boto3==1.20.54 boto3-v1.28.85: boto3==1.28.85 - boto3-v1.40.30: boto3==1.40.30 + boto3-v1.40.31: boto3==1.40.31 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 @@ -638,6 +639,7 @@ deps = beam-v2.32.0: apache-beam==2.32.0 beam-v2.50.0: apache-beam==2.50.0 beam-v2.67.0: apache-beam==2.67.0 + beam-v2.68.0rc2: apache-beam==2.68.0rc2 celery-v4.4.7: celery==4.4.7 celery-v5.0.5: celery==5.0.5 @@ -815,8 +817,8 @@ deps = trytond-v4.8.18: trytond==4.8.18 trytond-v5.8.16: trytond==5.8.16 trytond-v6.8.17: trytond==6.8.17 - trytond-v7.0.35: trytond==7.0.35 - trytond-v7.6.6: trytond==7.6.6 + trytond-v7.0.36: trytond==7.0.36 + trytond-v7.6.7: trytond==7.6.7 trytond: werkzeug trytond-v4.6.22: werkzeug<1.0 trytond-v4.8.18: werkzeug<1.0 From 875ed893cc2aeb61570123e556ac79fd1bed1ced Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 17 Sep 2025 09:17:36 +0200 Subject: [PATCH 59/88] feat: Add action to run toxgen periodically (#4805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description Run `scripts/generate-test-files.sh` in a GitHub action every Monday morning and create a PR with the changes. If there are already any toxgen PRs open, close them. Here's an example run of the action: https://github.com/sentrivana/sentry-python/actions/runs/17768312844 (from a fork of the repo so that I can test it) And an example PR: https://github.com/sentrivana/sentry-python/pull/6 Action shamelessly stolen and adapted from sentry-cli. 🥔 #### Issues Closes https://github.com/getsentry/sentry-python/issues/4050 #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](../CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --------- Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- .github/workflows/update-tox.yml | 111 +++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 .github/workflows/update-tox.yml diff --git a/.github/workflows/update-tox.yml b/.github/workflows/update-tox.yml new file mode 100644 index 0000000000..cfe98bbfe0 --- /dev/null +++ b/.github/workflows/update-tox.yml @@ -0,0 +1,111 @@ +name: Update test matrix + +on: + workflow_dispatch: + schedule: + # early Monday morning + - cron: '23 3 * * 1' + +jobs: + update-tox: + name: Update test matrix + runs-on: ubuntu-latest + timeout-minutes: 10 + + permissions: + contents: write + pull-requests: write + + steps: + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + - name: Checkout repo + uses: actions/checkout@v5.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + + - name: Run generate-test-files.sh + run: | + set -e + sh scripts/generate-test-files.sh + + - name: Create branch + id: create-branch + run: | + COMMIT_TITLE="ci: 🤖 Update test matrix with new releases" + DATE=`date +%m/%d` + BRANCH_NAME="toxgen/update" + + git checkout -B "$BRANCH_NAME" + git add --all + git commit -m "$COMMIT_TITLE" + git push origin "$BRANCH_NAME" --force + + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "commit_title=$COMMIT_TITLE" >> $GITHUB_OUTPUT + echo "date=$DATE" >> $GITHUB_OUTPUT + + - name: Create pull request + uses: actions/github-script@v8.0.0 + with: + script: | + const branchName = '${{ steps.create-branch.outputs.branch_name }}'; + const commitTitle = '${{ steps.create-branch.outputs.commit_title }}'; + const date = '${{ steps.create-branch.outputs.date }}'; + const prBody = `Update our test matrix with new releases of integrated frameworks and libraries. + + ## How it works + - Scan PyPI for all supported releases of all frameworks we have a dedicated test suite for. + - Pick a representative sample of releases to run our test suite against. We always test the latest and oldest supported version. + - Update [tox.ini](tox.ini) with the new releases. + + ## Action required + - If CI passes on this PR, it's safe to approve and merge. It means our integrations can handle new versions of frameworks that got pulled in. + - If CI doesn't pass on this PR, this points to an incompatibility of either our integration or our test setup with a new version of a framework. + - Check what the failures look like and either fix them, or update the [test config](scripts/populate_tox/config.py) and rerun [scripts/generate-test-files.sh](scripts/generate-test-files.sh). See [README.md](scripts/populate_tox/README.md) for what configuration options are available. + + _____________________ + + _🤖 This PR was automatically created using [a GitHub action](.github/workflows/update-tox.yml)._`.replace(/^ {16}/gm, '') + + // Close existing toxgen PRs as they're now obsolete + + const { data: existingPRs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: `${context.repo.owner}:${branchName}`, + state: 'open' + }); + + for (const pr of existingPRs) { + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + state: 'closed' + }) + }; + + const { data: pr } = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: commitTitle + ' (' + date + ')', + head: branchName, + base: '${{ github.ref_name }}', + body: prBody, + }); + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: ['Component: CI', 'Component: Tests'] + }); From bc990db9f02be3d9446bd5ed80587966bba9c2fb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:11:05 +0200 Subject: [PATCH 60/88] =?UTF-8?q?ci:=20=F0=9F=A4=96=20Update=20test=20matr?= =?UTF-8?q?ix=20with=20new=20releases=20(09/17)=20(#4806)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update our test matrix with new releases of integrated frameworks and libraries. ## How it works - Scan PyPI for all supported releases of all frameworks we have a dedicated test suite for. - Pick a representative sample of releases to run our test suite against. We always test the latest and oldest supported version. - Update [tox.ini](tox.ini) with the new releases. ## Action required - If CI passes on this PR, it's safe to approve and merge. It means our integrations can handle new versions of frameworks that got pulled in. - If CI doesn't pass on this PR, this points to an incompatibility of either our integration or our test setup with a new version of a framework. - Check what the failures look like and either fix them, or update the [test config](scripts/populate_tox/config.py) and rerun [scripts/generate-test-files.sh](scripts/generate-test-files.sh). See [README.md](scripts/populate_tox/README.md) for what configuration options are available. _____________________ _🤖 This PR was automatically created using [a GitHub action](.github/workflows/update-tox.yml)._ --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Ivana Kellyer --- tox.ini | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/tox.ini b/tox.ini index 1fb8e9be40..50ac22e886 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-16T07:18:41.732958+00:00 +# Last generated: 2025-09-17T07:20:17.058541+00:00 [tox] requires = @@ -132,17 +132,16 @@ envlist = {py3.10,py3.12,py3.13}-openai_agents-v0.3.0 {py3.8,py3.10,py3.11}-huggingface_hub-v0.24.7 - {py3.8,py3.12,py3.13}-huggingface_hub-v0.27.1 - {py3.8,py3.12,py3.13}-huggingface_hub-v0.30.2 - {py3.8,py3.12,py3.13}-huggingface_hub-v0.34.5 - {py3.8,py3.12,py3.13}-huggingface_hub-v0.35.0rc0 + {py3.8,py3.12,py3.13}-huggingface_hub-v0.28.1 + {py3.8,py3.12,py3.13}-huggingface_hub-v0.32.6 + {py3.8,py3.12,py3.13}-huggingface_hub-v0.35.0 # ~~~ Cloud ~~~ {py3.6,py3.7}-boto3-v1.12.49 {py3.6,py3.9,py3.10}-boto3-v1.20.54 {py3.7,py3.11,py3.12}-boto3-v1.28.85 - {py3.9,py3.12,py3.13}-boto3-v1.40.31 + {py3.9,py3.12,py3.13}-boto3-v1.40.32 {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.6,py3.7,py3.8}-chalice-v1.21.9 @@ -161,7 +160,7 @@ envlist = {py3.6}-pymongo-v3.5.1 {py3.6,py3.10,py3.11}-pymongo-v3.13.0 {py3.6,py3.9,py3.10}-pymongo-v4.0.2 - {py3.9,py3.12,py3.13}-pymongo-v4.15.0 + {py3.9,py3.12,py3.13}-pymongo-v4.15.1 {py3.6}-redis_py_cluster_legacy-v1.3.6 {py3.6,py3.7}-redis_py_cluster_legacy-v2.0.0 @@ -214,10 +213,9 @@ envlist = # ~~~ Network ~~~ {py3.7,py3.8}-grpc-v1.32.0 - {py3.7,py3.9,py3.10}-grpc-v1.46.5 - {py3.7,py3.11,py3.12}-grpc-v1.60.2 - {py3.9,py3.12,py3.13}-grpc-v1.74.0 - {py3.9,py3.12,py3.13}-grpc-v1.75.0rc1 + {py3.7,py3.9,py3.10}-grpc-v1.47.5 + {py3.7,py3.11,py3.12}-grpc-v1.62.3 + {py3.9,py3.12,py3.13}-grpc-v1.75.0 # ~~~ Tasks ~~~ @@ -273,7 +271,7 @@ envlist = {py3.6,py3.9,py3.10}-fastapi-v0.79.1 {py3.7,py3.10,py3.11}-fastapi-v0.91.0 {py3.7,py3.10,py3.11}-fastapi-v0.103.2 - {py3.8,py3.12,py3.13}-fastapi-v0.116.1 + {py3.8,py3.12,py3.13}-fastapi-v0.116.2 # ~~~ Web 2 ~~~ @@ -515,10 +513,9 @@ deps = openai_agents: pytest-asyncio huggingface_hub-v0.24.7: huggingface_hub==0.24.7 - huggingface_hub-v0.27.1: huggingface_hub==0.27.1 - huggingface_hub-v0.30.2: huggingface_hub==0.30.2 - huggingface_hub-v0.34.5: huggingface_hub==0.34.5 - huggingface_hub-v0.35.0rc0: huggingface_hub==0.35.0rc0 + huggingface_hub-v0.28.1: huggingface_hub==0.28.1 + huggingface_hub-v0.32.6: huggingface_hub==0.32.6 + huggingface_hub-v0.35.0: huggingface_hub==0.35.0 huggingface_hub: responses @@ -526,7 +523,7 @@ deps = boto3-v1.12.49: boto3==1.12.49 boto3-v1.20.54: boto3==1.20.54 boto3-v1.28.85: boto3==1.28.85 - boto3-v1.40.31: boto3==1.40.31 + boto3-v1.40.32: boto3==1.40.32 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 @@ -548,7 +545,7 @@ deps = pymongo-v3.5.1: pymongo==3.5.1 pymongo-v3.13.0: pymongo==3.13.0 pymongo-v4.0.2: pymongo==4.0.2 - pymongo-v4.15.0: pymongo==4.15.0 + pymongo-v4.15.1: pymongo==4.15.1 pymongo: mockupdb redis_py_cluster_legacy-v1.3.6: redis-py-cluster==1.3.6 @@ -615,10 +612,9 @@ deps = # ~~~ Network ~~~ grpc-v1.32.0: grpcio==1.32.0 - grpc-v1.46.5: grpcio==1.46.5 - grpc-v1.60.2: grpcio==1.60.2 - grpc-v1.74.0: grpcio==1.74.0 - grpc-v1.75.0rc1: grpcio==1.75.0rc1 + grpc-v1.47.5: grpcio==1.47.5 + grpc-v1.62.3: grpcio==1.62.3 + grpc-v1.75.0: grpcio==1.75.0 grpc: protobuf grpc: mypy-protobuf grpc: types-protobuf @@ -720,7 +716,7 @@ deps = fastapi-v0.79.1: fastapi==0.79.1 fastapi-v0.91.0: fastapi==0.91.0 fastapi-v0.103.2: fastapi==0.103.2 - fastapi-v0.116.1: fastapi==0.116.1 + fastapi-v0.116.2: fastapi==0.116.2 fastapi: httpx fastapi: pytest-asyncio fastapi: python-multipart From 62ff9ac68672dacaa9fe94df54de1999203d3e5e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 17 Sep 2025 12:13:27 +0200 Subject: [PATCH 61/88] fix: Fix link to CONTRIBUTING.md in PR template (#4808) ### Description The link to CONTRIBUTING.md in the PR template doesn't work (see below), updating it. #### Issues #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](../CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index dd7ef45832..79f27c30d8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,4 +11,4 @@ - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) -- For external contributors: [CONTRIBUTING.md](../CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) +- For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) From 6c2a99601a75227f39f36e636d3b3bc24017ae8e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 17 Sep 2025 12:13:37 +0200 Subject: [PATCH 62/88] fix: Fix links to files in toxgen PRs (#4807) ### Description The toxgen PR template contains links to files in the repo, but they were broken, fixing that here. #### Issues #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](../CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- .github/workflows/update-tox.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/update-tox.yml b/.github/workflows/update-tox.yml index cfe98bbfe0..11d143a2a9 100644 --- a/.github/workflows/update-tox.yml +++ b/.github/workflows/update-tox.yml @@ -65,16 +65,16 @@ jobs: ## How it works - Scan PyPI for all supported releases of all frameworks we have a dedicated test suite for. - Pick a representative sample of releases to run our test suite against. We always test the latest and oldest supported version. - - Update [tox.ini](tox.ini) with the new releases. + - Update [tox.ini](https://github.com/getsentry/sentry-python/blob/master/tox.ini) with the new releases. ## Action required - If CI passes on this PR, it's safe to approve and merge. It means our integrations can handle new versions of frameworks that got pulled in. - If CI doesn't pass on this PR, this points to an incompatibility of either our integration or our test setup with a new version of a framework. - - Check what the failures look like and either fix them, or update the [test config](scripts/populate_tox/config.py) and rerun [scripts/generate-test-files.sh](scripts/generate-test-files.sh). See [README.md](scripts/populate_tox/README.md) for what configuration options are available. + - Check what the failures look like and either fix them, or update the [test config](https://github.com/getsentry/sentry-python/blob/master/scripts/populate_tox/config.py) and rerun [scripts/generate-test-files.sh](https://github.com/getsentry/sentry-python/blob/master/scripts/generate-test-files.sh). See [scripts/populate_tox/README.md](https://github.com/getsentry/sentry-python/blob/master/scripts/populate_tox/README.md) for what configuration options are available. _____________________ - _🤖 This PR was automatically created using [a GitHub action](.github/workflows/update-tox.yml)._`.replace(/^ {16}/gm, '') + _🤖 This PR was automatically created using [a GitHub action](https://github.com/getsentry/sentry-python/blob/master/.github/workflows/update-tox.yml)._`.replace(/^ {16}/gm, '') // Close existing toxgen PRs as they're now obsolete From 43293832af8fd30f3fd18ef9206ba52c78fc5a52 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 17 Sep 2025 12:42:32 +0200 Subject: [PATCH 63/88] fix(Django): Avoid evaluating complex Django object in span.data/span.attributes (#4804) ### Description When rendering templates of the Django Admin in Django 5.0+ the template context includes also a `QuerySet` with lots of log items. In our `serialize()` function this was not properly serialized leading to evaluating the `QuerySet` (read: running the SQL query). This change updates the `serialize()` to also correctly serialize `QuerySet`s in `span.data` #### Issues * resolves: #4604 * resolves: PY-1781 --- sentry_sdk/serializer.py | 13 ++++++- tests/integrations/django/myapp/urls.py | 1 + tests/integrations/django/myapp/views.py | 23 +++++++++++++ tests/integrations/django/test_basic.py | 43 ++++++++++++++++++++++-- 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/serializer.py b/sentry_sdk/serializer.py index 04df9857bd..6bde5c08bd 100644 --- a/sentry_sdk/serializer.py +++ b/sentry_sdk/serializer.py @@ -187,6 +187,16 @@ def _is_databag(): return False + def _is_span_attribute(): + # type: () -> Optional[bool] + try: + if path[0] == "spans" and path[2] == "data": + return True + except IndexError: + return None + + return False + def _is_request_body(): # type: () -> Optional[bool] try: @@ -282,7 +292,8 @@ def _serialize_node_impl( ) return None - if is_databag and global_repr_processors: + is_span_attribute = _is_span_attribute() + if (is_databag or is_span_attribute) and global_repr_processors: hints = {"memo": memo, "remaining_depth": remaining_depth} for processor in global_repr_processors: result = processor(obj, hints) diff --git a/tests/integrations/django/myapp/urls.py b/tests/integrations/django/myapp/urls.py index 79dd4edd52..fbc9e6032e 100644 --- a/tests/integrations/django/myapp/urls.py +++ b/tests/integrations/django/myapp/urls.py @@ -58,6 +58,7 @@ def path(path, *args, **kwargs): path("template-test", views.template_test, name="template_test"), path("template-test2", views.template_test2, name="template_test2"), path("template-test3", views.template_test3, name="template_test3"), + path("template-test4", views.template_test4, name="template_test4"), path("postgres-select", views.postgres_select, name="postgres_select"), path("postgres-select-slow", views.postgres_select_orm, name="postgres_select_orm"), path( diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py index 5e8cc39053..9c14bc27d7 100644 --- a/tests/integrations/django/myapp/views.py +++ b/tests/integrations/django/myapp/views.py @@ -208,6 +208,29 @@ def template_test3(request, *args, **kwargs): return render(request, "trace_meta.html", {}) +@csrf_exempt +def template_test4(request, *args, **kwargs): + User.objects.create_user("john", "lennon@thebeatles.com", "johnpassword") + my_queryset = User.objects.all() # noqa + + template_context = { + "user_age": 25, + "complex_context": my_queryset, + "complex_list": [1, 2, 3, my_queryset], + "complex_dict": { + "a": 1, + "d": my_queryset, + }, + "none_context": None, + } + + return TemplateResponse( + request, + "user_name.html", + template_context, + ) + + @csrf_exempt def postgres_select(request, *args, **kwargs): from django.db import connections diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index e96cd09e4f..bbe29c7238 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -1,9 +1,10 @@ import inspect import json import os +import pytest import re import sys -import pytest + from functools import partial from unittest.mock import patch @@ -15,8 +16,8 @@ from django.core.management import execute_from_command_line from django.db.utils import OperationalError, ProgrammingError, DataError from django.http.request import RawPostDataException -from django.utils.functional import SimpleLazyObject from django.template.context import make_context +from django.utils.functional import SimpleLazyObject try: from django.urls import reverse @@ -956,6 +957,44 @@ def test_render_spans(sentry_init, client, capture_events, render_span_tree): assert expected_line in render_span_tree(transaction) +@pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9") +@pytest.mark.forked +@pytest_mark_django_db_decorator() +def test_render_spans_queryset_in_data(sentry_init, client, capture_events): + sentry_init( + integrations=[ + DjangoIntegration( + cache_spans=False, + middleware_spans=False, + signals_spans=False, + ) + ], + traces_sample_rate=1.0, + ) + events = capture_events() + + client.get(reverse("template_test4")) + + (transaction,) = events + template_context = transaction["spans"][-1]["data"]["context"] + + assert template_context["user_age"] == 25 + assert template_context["complex_context"].startswith( + "= (1, 10): EXPECTED_MIDDLEWARE_SPANS = """\ - op="http.server": description=null From 092b5cbd5f7e6fd83035ec646aebc54229b59f44 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 18 Sep 2025 08:20:53 +0200 Subject: [PATCH 64/88] tests: Move pure_eval under toxgen (#4815) ### Description - moved pure_eval under toxgen #### Issues Ref https://github.com/getsentry/sentry-python/issues/4506 #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- scripts/populate_tox/config.py | 3 +++ scripts/populate_tox/populate_tox.py | 1 - scripts/populate_tox/tox.jinja | 6 ------ tox.ini | 16 +++++++++------- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 8e35eaba4f..634aa62526 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -224,6 +224,9 @@ "openfeature": { "package": "openfeature-sdk", }, + "pure_eval": { + "package": "pure_eval", + }, "pymongo": { "package": "pymongo", "deps": { diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index e08c2d4b95..5734167a6a 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -70,7 +70,6 @@ # of these from the IGNORE list "gcp", "httpx", - "pure_eval", "ray", "redis", "requests", diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 4a4bd96c52..a433406bd8 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -61,9 +61,6 @@ envlist = # OpenTelemetry Experimental (POTel) {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # pure_eval - {py3.6,py3.12,py3.13}-pure_eval - # Ray {py3.10,py3.11}-ray-v{2.34} {py3.10,py3.11}-ray-latest @@ -177,9 +174,6 @@ deps = # OpenTelemetry Experimental (POTel) potel: -e .[opentelemetry-experimental] - # pure_eval - pure_eval: pure_eval - # Ray ray-v2.34: ray~=2.34.0 ray-latest: ray diff --git a/tox.ini b/tox.ini index 50ac22e886..d325d6071e 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-17T07:20:17.058541+00:00 +# Last generated: 2025-09-17T14:43:06.969007+00:00 [tox] requires = @@ -61,9 +61,6 @@ envlist = # OpenTelemetry Experimental (POTel) {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # pure_eval - {py3.6,py3.12,py3.13}-pure_eval - # Ray {py3.10,py3.11}-ray-v{2.34} {py3.10,py3.11}-ray-latest @@ -317,6 +314,10 @@ envlist = # ~~~ Misc ~~~ {py3.6,py3.12,py3.13}-loguru-v0.7.3 + {py3.6,py3.7,py3.8}-pure_eval-v0.0.3 + {py3.6,py3.8,py3.9}-pure_eval-v0.1.1 + {py3.7,py3.12,py3.13}-pure_eval-v0.2.3 + {py3.6}-trytond-v4.6.22 {py3.6}-trytond-v4.8.18 {py3.6,py3.7,py3.8}-trytond-v5.8.16 @@ -402,9 +403,6 @@ deps = # OpenTelemetry Experimental (POTel) potel: -e .[opentelemetry-experimental] - # pure_eval - pure_eval: pure_eval - # Ray ray-v2.34: ray~=2.34.0 ray-latest: ray @@ -809,6 +807,10 @@ deps = # ~~~ Misc ~~~ loguru-v0.7.3: loguru==0.7.3 + pure_eval-v0.0.3: pure_eval==0.0.3 + pure_eval-v0.1.1: pure_eval==0.1.1 + pure_eval-v0.2.3: pure_eval==0.2.3 + trytond-v4.6.22: trytond==4.6.22 trytond-v4.8.18: trytond==4.8.18 trytond-v5.8.16: trytond==5.8.16 From 7ae6866a650228a67a18abfa52b4d488b50d9be3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 06:54:02 +0000 Subject: [PATCH 65/88] =?UTF-8?q?ci:=20=F0=9F=A4=96=20Update=20test=20matr?= =?UTF-8?q?ix=20with=20new=20releases=20(09/18)=20(#4816)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update our test matrix with new releases of integrated frameworks and libraries. ## How it works - Scan PyPI for all supported releases of all frameworks we have a dedicated test suite for. - Pick a representative sample of releases to run our test suite against. We always test the latest and oldest supported version. - Update [tox.ini](https://github.com/getsentry/sentry-python/blob/master/tox.ini) with the new releases. ## Action required - If CI passes on this PR, it's safe to approve and merge. It means our integrations can handle new versions of frameworks that got pulled in. - If CI doesn't pass on this PR, this points to an incompatibility of either our integration or our test setup with a new version of a framework. - Check what the failures look like and either fix them, or update the [test config](https://github.com/getsentry/sentry-python/blob/master/scripts/populate_tox/config.py) and rerun [scripts/generate-test-files.sh](https://github.com/getsentry/sentry-python/blob/master/scripts/generate-test-files.sh). See [scripts/populate_tox/README.md](https://github.com/getsentry/sentry-python/blob/master/scripts/populate_tox/README.md) for what configuration options are available. _____________________ _🤖 This PR was automatically created using [a GitHub action](https://github.com/getsentry/sentry-python/blob/master/.github/workflows/update-tox.yml)._ Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scripts/populate_tox/populate_tox.py | 2 +- tox.ini | 42 +++++++++++++++------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 5734167a6a..0d5f5043ed 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -438,7 +438,7 @@ def _render_dependencies(integration: str, releases: list[Version]) -> list[str] for dep in deps: rendered.append(f"{{{constraint}}}-{integration}: {dep}") else: - restriction = SpecifierSet(constraint) + restriction = SpecifierSet(constraint, prereleases=True) for release in releases: if release in restriction: for dep in deps: diff --git a/tox.ini b/tox.ini index d325d6071e..dc289b4587 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-17T14:43:06.969007+00:00 +# Last generated: 2025-09-18T06:43:59.191429+00:00 [tox] requires = @@ -95,7 +95,7 @@ envlist = {py3.8,py3.11,py3.12}-anthropic-v0.16.0 {py3.8,py3.11,py3.12}-anthropic-v0.33.1 {py3.8,py3.11,py3.12}-anthropic-v0.50.0 - {py3.8,py3.12,py3.13}-anthropic-v0.67.0 + {py3.8,py3.12,py3.13}-anthropic-v0.68.0 {py3.9,py3.10,py3.11}-cohere-v5.4.0 {py3.9,py3.11,py3.12}-cohere-v5.9.4 @@ -111,14 +111,14 @@ envlist = {py3.9,py3.12,py3.13}-langchain-notiktoken-v0.3.27 {py3.8,py3.11,py3.12}-openai-base-v1.0.1 - {py3.8,py3.11,py3.12}-openai-base-v1.36.1 - {py3.8,py3.11,py3.12}-openai-base-v1.71.0 - {py3.8,py3.12,py3.13}-openai-base-v1.107.3 + {py3.8,py3.11,py3.12}-openai-base-v1.37.2 + {py3.8,py3.11,py3.12}-openai-base-v1.73.0 + {py3.8,py3.12,py3.13}-openai-base-v1.108.0 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 - {py3.8,py3.11,py3.12}-openai-notiktoken-v1.36.1 - {py3.8,py3.11,py3.12}-openai-notiktoken-v1.71.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.107.3 + {py3.8,py3.11,py3.12}-openai-notiktoken-v1.37.2 + {py3.8,py3.11,py3.12}-openai-notiktoken-v1.73.0 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.108.0 {py3.9,py3.12,py3.13}-langgraph-v0.6.7 {py3.10,py3.12,py3.13}-langgraph-v1.0.0a3 @@ -138,7 +138,7 @@ envlist = {py3.6,py3.7}-boto3-v1.12.49 {py3.6,py3.9,py3.10}-boto3-v1.20.54 {py3.7,py3.11,py3.12}-boto3-v1.28.85 - {py3.9,py3.12,py3.13}-boto3-v1.40.32 + {py3.9,py3.12,py3.13}-boto3-v1.40.33 {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.6,py3.7,py3.8}-chalice-v1.21.9 @@ -254,6 +254,7 @@ envlist = {py3.8,py3.11,py3.12}-django-v4.2.24 {py3.10,py3.11,py3.12}-django-v5.0.14 {py3.10,py3.12,py3.13}-django-v5.2.6 + {py3.12,py3.13}-django-v6.0a1 {py3.6,py3.7,py3.8}-flask-v1.1.4 {py3.8,py3.12,py3.13}-flask-v2.3.3 @@ -459,7 +460,7 @@ deps = anthropic-v0.16.0: anthropic==0.16.0 anthropic-v0.33.1: anthropic==0.33.1 anthropic-v0.50.0: anthropic==0.50.0 - anthropic-v0.67.0: anthropic==0.67.0 + anthropic-v0.68.0: anthropic==0.68.0 anthropic: pytest-asyncio anthropic-v0.16.0: httpx<0.28.0 anthropic-v0.33.1: httpx<0.28.0 @@ -485,21 +486,21 @@ deps = langchain-notiktoken-v0.3.27: langchain-community openai-base-v1.0.1: openai==1.0.1 - openai-base-v1.36.1: openai==1.36.1 - openai-base-v1.71.0: openai==1.71.0 - openai-base-v1.107.3: openai==1.107.3 + openai-base-v1.37.2: openai==1.37.2 + openai-base-v1.73.0: openai==1.73.0 + openai-base-v1.108.0: openai==1.108.0 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 - openai-base-v1.36.1: httpx<0.28 + openai-base-v1.37.2: httpx<0.28 openai-notiktoken-v1.0.1: openai==1.0.1 - openai-notiktoken-v1.36.1: openai==1.36.1 - openai-notiktoken-v1.71.0: openai==1.71.0 - openai-notiktoken-v1.107.3: openai==1.107.3 + openai-notiktoken-v1.37.2: openai==1.37.2 + openai-notiktoken-v1.73.0: openai==1.73.0 + openai-notiktoken-v1.108.0: openai==1.108.0 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 - openai-notiktoken-v1.36.1: httpx<0.28 + openai-notiktoken-v1.37.2: httpx<0.28 langgraph-v0.6.7: langgraph==0.6.7 langgraph-v1.0.0a3: langgraph==1.0.0a3 @@ -521,7 +522,7 @@ deps = boto3-v1.12.49: boto3==1.12.49 boto3-v1.20.54: boto3==1.20.54 boto3-v1.28.85: boto3==1.28.85 - boto3-v1.40.32: boto3==1.40.32 + boto3-v1.40.33: boto3==1.40.33 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 @@ -665,6 +666,7 @@ deps = django-v4.2.24: django==4.2.24 django-v5.0.14: django==5.0.14 django-v5.2.6: django==5.2.6 + django-v6.0a1: django==6.0a1 django: psycopg2-binary django: djangorestframework django: pytest-django @@ -674,11 +676,13 @@ deps = django-v4.2.24: channels[daphne] django-v5.0.14: channels[daphne] django-v5.2.6: channels[daphne] + django-v6.0a1: channels[daphne] django-v2.2.28: six django-v3.2.25: pytest-asyncio django-v4.2.24: pytest-asyncio django-v5.0.14: pytest-asyncio django-v5.2.6: pytest-asyncio + django-v6.0a1: pytest-asyncio django-v1.11.29: djangorestframework>=3.0,<4.0 django-v1.11.29: Werkzeug<2.1.0 django-v2.2.28: djangorestframework>=3.0,<4.0 From e1101516330033d04c158f02d1405b2d8d795697 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 18 Sep 2025 12:22:05 +0200 Subject: [PATCH 66/88] tests: Move sanic under toxgen & add option to control how many releases to pick (#4767) - Move sanic under toxgen - This gets rid of the `-latest` group for Web 2 as all Web 2 test suites are now governed by toxgen - Add `num_versions` as a new option to optionally slim down the number of tested versions (useful for sanic as it has a lot of "majors" due to calver) - When a package had multiple majors, we used to always pick the lowest as well as the highest release in the last major -- removed the lowest now to not over-test. All majors will still be tested by default, we just won't be testing two releases in the last major. Ref https://github.com/getsentry/sentry-python/issues/4506 --- .github/workflows/test-integrations-web-2.yml | 95 ------------------- scripts/populate_tox/README.md | 9 +- scripts/populate_tox/config.py | 10 ++ scripts/populate_tox/populate_tox.py | 75 +++++++++------ scripts/populate_tox/tox.jinja | 18 ---- tox.ini | 52 ++++------ 6 files changed, 82 insertions(+), 177 deletions(-) diff --git a/.github/workflows/test-integrations-web-2.yml b/.github/workflows/test-integrations-web-2.yml index 6b7fe58815..3dbe2e1168 100644 --- a/.github/workflows/test-integrations-web-2.yml +++ b/.github/workflows/test-integrations-web-2.yml @@ -22,101 +22,6 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-web_2-latest: - name: Web 2 (latest) - timeout-minutes: 30 - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - python-version: ["3.9","3.12","3.13"] - # python3.6 reached EOL and is no longer being supported on - # new versions of hosted runners on Github Actions - # ubuntu-20.04 is the last version that supported python3.6 - # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 - os: [ubuntu-22.04] - # Use Docker container only for Python 3.6 - container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} - steps: - - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v6 - if: ${{ matrix.python-version != '3.6' }} - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - name: Setup Test Env - run: | - pip install "coverage[toml]" tox - - name: Erase coverage - run: | - coverage erase - - name: Test aiohttp latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-aiohttp-latest" - - name: Test asgi latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-asgi-latest" - - name: Test bottle latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-bottle-latest" - - name: Test falcon latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-falcon-latest" - - name: Test litestar latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-litestar-latest" - - name: Test pyramid latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-pyramid-latest" - - name: Test quart latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-quart-latest" - - name: Test sanic latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-sanic-latest" - - name: Test starlite latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-starlite-latest" - - name: Test tornado latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-tornado-latest" - - name: Generate coverage XML (Python 3.6) - if: ${{ !cancelled() && matrix.python-version == '3.6' }} - run: | - export COVERAGE_RCFILE=.coveragerc36 - coverage combine .coverage-sentry-* - coverage xml --ignore-errors - - name: Generate coverage XML - if: ${{ !cancelled() && matrix.python-version != '3.6' }} - run: | - coverage combine .coverage-sentry-* - coverage xml - - name: Upload coverage to Codecov - if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml - verbose: true test-web_2-pinned: name: Web 2 (pinned) timeout-minutes: 30 diff --git a/scripts/populate_tox/README.md b/scripts/populate_tox/README.md index c48d57734d..4acebd3259 100644 --- a/scripts/populate_tox/README.md +++ b/scripts/populate_tox/README.md @@ -18,8 +18,7 @@ then determining which versions make sense to test to get good coverage. The lowest supported and latest version of a framework are always tested, with a number of releases in between: -- If the package has majors, we pick the highest version of each major. For the - latest major, we also pick the lowest version in that major. +- If the package has majors, we pick the highest version of each major. - If the package doesn't have multiple majors, we pick two versions in between lowest and highest. @@ -46,6 +45,8 @@ integration_name: { }, "python": python_version_specifier, "include": package_version_specifier, + "integration_name": integration_name, + "num_versions": int, } ``` @@ -161,6 +162,10 @@ of which are actually testing the `openai` integration. If this is the case, you Linking an integration to a test suite allows the script to access integration configuration like for example the minimum version defined in `sentry_sdk/integrations/__init__.py`. +### `num_versions` + +With this option you can tweak the default version picking behavior by specifying how many package versions should be tested. It accepts an integer equal to or greater than 2, as the oldest and latest supported versions will always be picked. Additionally, if there is a recent prerelease, it'll also always be picked (this doesn't count towards `num_versions`). + ## How-Tos diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 634aa62526..ce9fdefe48 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -259,6 +259,16 @@ "requests": { "package": "requests", }, + "sanic": { + "package": "sanic", + "deps": { + "*": ["websockets<11.0", "aiohttp"], + ">=22": ["sanic-testing"], + "py3.6": ["aiocontextvars==0.2.1"], + "py3.8": ["tracerite<1.1.2"], + }, + "num_versions": 4, + }, "spark": { "package": "pyspark", "python": ">=3.8", diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 0d5f5043ed..a545eed330 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -74,7 +74,6 @@ "redis", "requests", "rq", - "sanic", } @@ -258,47 +257,48 @@ def _supports_lowest(release: Version) -> bool: def pick_releases_to_test( - releases: list[Version], last_prerelease: Optional[Version] + integration: str, releases: list[Version], last_prerelease: Optional[Version] ) -> list[Version]: """Pick a handful of releases to test from a sorted list of supported releases.""" # If the package has majors (or major-like releases, even if they don't do - # semver), we want to make sure we're testing them all. If not, we just pick - # the oldest, the newest, and a couple in between. + # semver), we want to make sure we're testing them all (unless there's too + # many). If not, we just pick the oldest, the newest, and a couple + # in between. # # If there is a relevant prerelease, also test that in addition to the above. - has_majors = len(set([v.major for v in releases])) > 1 + num_versions = TEST_SUITE_CONFIG[integration].get("num_versions") + if num_versions is not None and ( + not isinstance(num_versions, int) or num_versions < 2 + ): + print(" Integration has invalid `num_versions`: must be an int >= 2") + num_versions = None + + has_majors = len({v.major for v in releases}) > 1 filtered_releases = set() if has_majors: # Always check the very first supported release filtered_releases.add(releases[0]) - # Find out the min and max release by each major + # Find out the max release by each major releases_by_major = {} for release in releases: - if release.major not in releases_by_major: - releases_by_major[release.major] = [release, release] - if release < releases_by_major[release.major][0]: - releases_by_major[release.major][0] = release - if release > releases_by_major[release.major][1]: - releases_by_major[release.major][1] = release - - for i, (min_version, max_version) in enumerate(releases_by_major.values()): + if ( + release.major not in releases_by_major + or release > releases_by_major[release.major] + ): + releases_by_major[release.major] = release + + # Add the highest release in each major + for max_version in releases_by_major.values(): filtered_releases.add(max_version) - if i == len(releases_by_major) - 1: - # If this is the latest major release, also check the lowest - # version of this version - filtered_releases.add(min_version) + + # If num_versions was provided, slim down the selection + if num_versions is not None: + filtered_releases = _pick_releases(sorted(filtered_releases), num_versions) else: - filtered_releases = { - releases[0], # oldest version supported - releases[len(releases) // 3], - releases[ - len(releases) // 3 * 2 - ], # two releases in between, roughly evenly spaced - releases[-1], # latest - } + filtered_releases = _pick_releases(releases, num_versions) filtered_releases = sorted(filtered_releases) if last_prerelease is not None: @@ -307,6 +307,25 @@ def pick_releases_to_test( return filtered_releases +def _pick_releases( + releases: list[Version], num_versions: Optional[int] +) -> set[Version]: + num_versions = num_versions or 4 + + versions = { + releases[0], # oldest version supported + releases[-1], # latest + } + + for i in range(1, num_versions - 1): + try: + versions.add(releases[len(releases) // (num_versions - 1) * i]) + except IndexError: + pass + + return versions + + def supported_python_versions( package_python_versions: Union[SpecifierSet, list[Version]], custom_supported_versions: Optional[SpecifierSet] = None, @@ -631,7 +650,9 @@ def main(fail_on_changes: bool = False) -> None: # Pick a handful of the supported releases to actually test against # and fetch the PyPI data for each to determine which Python versions # to test it on - test_releases = pick_releases_to_test(releases, latest_prerelease) + test_releases = pick_releases_to_test( + integration, releases, latest_prerelease + ) for release in test_releases: _add_python_versions_to_release(integration, package, release) diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index a433406bd8..0522c60231 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -81,12 +81,6 @@ envlist = {py3.7,py3.11,py3.12}-rq-v{1.15,1.16} {py3.7,py3.12,py3.13}-rq-latest - # Sanic - {py3.6,py3.7}-sanic-v{0.8} - {py3.6,py3.8}-sanic-v{20} - {py3.8,py3.11,py3.12}-sanic-v{24.6} - {py3.9,py3.12,py3.13}-sanic-latest - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. @@ -210,18 +204,6 @@ deps = rq-v1.16: rq~=1.16.0 rq-latest: rq - # Sanic - sanic: websockets<11.0 - sanic: aiohttp - sanic-v{24.6}: sanic_testing - sanic-latest: sanic_testing - {py3.6}-sanic: aiocontextvars==0.2.1 - {py3.8}-sanic: tracerite<1.1.2 - sanic-v0.8: sanic~=0.8.0 - sanic-v20: sanic~=20.0 - sanic-v24.6: sanic~=24.6.0 - sanic-latest: sanic - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. diff --git a/tox.ini b/tox.ini index dc289b4587..e2898ad8f3 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-18T06:43:59.191429+00:00 +# Last generated: 2025-09-18T08:05:49.500134+00:00 [tox] requires = @@ -81,12 +81,6 @@ envlist = {py3.7,py3.11,py3.12}-rq-v{1.15,1.16} {py3.7,py3.12,py3.13}-rq-latest - # Sanic - {py3.6,py3.7}-sanic-v{0.8} - {py3.6,py3.8}-sanic-v{20} - {py3.8,py3.11,py3.12}-sanic-v{24.6} - {py3.9,py3.12,py3.13}-sanic-latest - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. @@ -156,11 +150,9 @@ envlist = {py3.6}-pymongo-v3.5.1 {py3.6,py3.10,py3.11}-pymongo-v3.13.0 - {py3.6,py3.9,py3.10}-pymongo-v4.0.2 {py3.9,py3.12,py3.13}-pymongo-v4.15.1 {py3.6}-redis_py_cluster_legacy-v1.3.6 - {py3.6,py3.7}-redis_py_cluster_legacy-v2.0.0 {py3.6,py3.7,py3.8}-redis_py_cluster_legacy-v2.1.3 {py3.6,py3.8,py3.9}-sqlalchemy-v1.3.24 @@ -228,7 +220,6 @@ envlist = {py3.9,py3.12,py3.13}-beam-v2.68.0rc2 {py3.6,py3.7,py3.8}-celery-v4.4.7 - {py3.6,py3.7,py3.8}-celery-v5.0.5 {py3.8,py3.12,py3.13}-celery-v5.5.3 {py3.8,py3.12,py3.13}-celery-v5.6.0b1 @@ -252,13 +243,11 @@ envlist = {py3.6,py3.8,py3.9}-django-v2.2.28 {py3.6,py3.9,py3.10}-django-v3.2.25 {py3.8,py3.11,py3.12}-django-v4.2.24 - {py3.10,py3.11,py3.12}-django-v5.0.14 {py3.10,py3.12,py3.13}-django-v5.2.6 {py3.12,py3.13}-django-v6.0a1 {py3.6,py3.7,py3.8}-flask-v1.1.4 {py3.8,py3.12,py3.13}-flask-v2.3.3 - {py3.8,py3.12,py3.13}-flask-v3.0.3 {py3.9,py3.12,py3.13}-flask-v3.1.2 {py3.6,py3.9,py3.10}-starlette-v0.16.0 @@ -284,7 +273,6 @@ envlist = {py3.6}-falcon-v1.4.1 {py3.6,py3.7}-falcon-v2.0.0 {py3.6,py3.11,py3.12}-falcon-v3.1.3 - {py3.8,py3.11,py3.12}-falcon-v4.0.2 {py3.8,py3.11,py3.12}-falcon-v4.1.0 {py3.8,py3.10,py3.11}-litestar-v2.0.1 @@ -301,6 +289,11 @@ envlist = {py3.7,py3.10,py3.11}-quart-v0.18.4 {py3.9,py3.12,py3.13}-quart-v0.20.0 + {py3.6}-sanic-v0.8.3 + {py3.6,py3.8,py3.9}-sanic-v20.12.7 + {py3.8,py3.10,py3.11}-sanic-v23.12.2 + {py3.9,py3.12,py3.13}-sanic-v25.3.0 + {py3.8,py3.10,py3.11}-starlite-v1.48.1 {py3.8,py3.10,py3.11}-starlite-v1.49.0 {py3.8,py3.10,py3.11}-starlite-v1.50.2 @@ -323,7 +316,6 @@ envlist = {py3.6}-trytond-v4.8.18 {py3.6,py3.7,py3.8}-trytond-v5.8.16 {py3.8,py3.10,py3.11}-trytond-v6.8.17 - {py3.8,py3.11,py3.12}-trytond-v7.0.36 {py3.9,py3.12,py3.13}-trytond-v7.6.7 {py3.7,py3.12,py3.13}-typer-v0.15.4 @@ -440,18 +432,6 @@ deps = rq-v1.16: rq~=1.16.0 rq-latest: rq - # Sanic - sanic: websockets<11.0 - sanic: aiohttp - sanic-v{24.6}: sanic_testing - sanic-latest: sanic_testing - {py3.6}-sanic: aiocontextvars==0.2.1 - {py3.8}-sanic: tracerite<1.1.2 - sanic-v0.8: sanic~=0.8.0 - sanic-v20: sanic~=20.0 - sanic-v24.6: sanic~=24.6.0 - sanic-latest: sanic - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. @@ -543,12 +523,10 @@ deps = pymongo-v3.5.1: pymongo==3.5.1 pymongo-v3.13.0: pymongo==3.13.0 - pymongo-v4.0.2: pymongo==4.0.2 pymongo-v4.15.1: pymongo==4.15.1 pymongo: mockupdb redis_py_cluster_legacy-v1.3.6: redis-py-cluster==1.3.6 - redis_py_cluster_legacy-v2.0.0: redis-py-cluster==2.0.0 redis_py_cluster_legacy-v2.1.3: redis-py-cluster==2.1.3 sqlalchemy-v1.3.24: sqlalchemy==1.3.24 @@ -637,7 +615,6 @@ deps = beam-v2.68.0rc2: apache-beam==2.68.0rc2 celery-v4.4.7: celery==4.4.7 - celery-v5.0.5: celery==5.0.5 celery-v5.5.3: celery==5.5.3 celery-v5.6.0b1: celery==5.6.0b1 celery: newrelic<10.17.0 @@ -664,7 +641,6 @@ deps = django-v2.2.28: django==2.2.28 django-v3.2.25: django==3.2.25 django-v4.2.24: django==4.2.24 - django-v5.0.14: django==5.0.14 django-v5.2.6: django==5.2.6 django-v6.0a1: django==6.0a1 django: psycopg2-binary @@ -674,13 +650,11 @@ deps = django-v2.2.28: channels[daphne] django-v3.2.25: channels[daphne] django-v4.2.24: channels[daphne] - django-v5.0.14: channels[daphne] django-v5.2.6: channels[daphne] django-v6.0a1: channels[daphne] django-v2.2.28: six django-v3.2.25: pytest-asyncio django-v4.2.24: pytest-asyncio - django-v5.0.14: pytest-asyncio django-v5.2.6: pytest-asyncio django-v6.0a1: pytest-asyncio django-v1.11.29: djangorestframework>=3.0,<4.0 @@ -694,7 +668,6 @@ deps = flask-v1.1.4: flask==1.1.4 flask-v2.3.3: flask==2.3.3 - flask-v3.0.3: flask==3.0.3 flask-v3.1.2: flask==3.1.2 flask: flask-login flask: werkzeug @@ -746,7 +719,6 @@ deps = falcon-v1.4.1: falcon==1.4.1 falcon-v2.0.0: falcon==2.0.0 falcon-v3.1.3: falcon==3.1.3 - falcon-v4.0.2: falcon==4.0.2 falcon-v4.1.0: falcon==4.1.0 litestar-v2.0.1: litestar==2.0.1 @@ -787,6 +759,17 @@ deps = quart-v0.18.4: hypercorn<0.15.0 {py3.8}-quart: taskgroup==0.0.0a4 + sanic-v0.8.3: sanic==0.8.3 + sanic-v20.12.7: sanic==20.12.7 + sanic-v23.12.2: sanic==23.12.2 + sanic-v25.3.0: sanic==25.3.0 + sanic: websockets<11.0 + sanic: aiohttp + sanic-v23.12.2: sanic-testing + sanic-v25.3.0: sanic-testing + {py3.6}-sanic: aiocontextvars==0.2.1 + {py3.8}-sanic: tracerite<1.1.2 + starlite-v1.48.1: starlite==1.48.1 starlite-v1.49.0: starlite==1.49.0 starlite-v1.50.2: starlite==1.50.2 @@ -819,7 +802,6 @@ deps = trytond-v4.8.18: trytond==4.8.18 trytond-v5.8.16: trytond==5.8.16 trytond-v6.8.17: trytond==6.8.17 - trytond-v7.0.36: trytond==7.0.36 trytond-v7.6.7: trytond==7.6.7 trytond: werkzeug trytond-v4.6.22: werkzeug<1.0 From 78843dcbd47228adc4dc84788880d09ac81a1e40 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 18 Sep 2025 12:41:11 +0200 Subject: [PATCH 67/88] tests: Move ray under toxgen (#4810) ### Description - moving the Ray test suite under toxgen - removed `@pytest.mark.forked` and instead: - added a fixture to ensure ray is shut down after each test - gave each test that was looking at logs its own temporary directory to ensure logs don't get mixed up - we were only testing Ray on Python 3.10 and 3.11 -- fixing that #### Issues Ref https://github.com/getsentry/sentry-python/issues/4506 Closes https://github.com/getsentry/sentry-python/issues/4811 #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- .github/workflows/test-integrations-tasks.yml | 2 +- scripts/populate_tox/config.py | 5 + scripts/populate_tox/populate_tox.py | 1 - scripts/populate_tox/tox.jinja | 8 - tests/integrations/ray/test_ray.py | 154 +++++++++++------- tox.ini | 16 +- 6 files changed, 105 insertions(+), 81 deletions(-) diff --git a/.github/workflows/test-integrations-tasks.yml b/.github/workflows/test-integrations-tasks.yml index 0038f1d050..d84789b767 100644 --- a/.github/workflows/test-integrations-tasks.yml +++ b/.github/workflows/test-integrations-tasks.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7","3.10","3.11","3.12","3.13"] + python-version: ["3.7","3.12","3.13"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index ce9fdefe48..346cabc2db 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -253,6 +253,11 @@ "py3.8": ["taskgroup==0.0.0a4"], }, }, + "ray": { + "package": "ray", + "python": ">=3.9", + "num_versions": 2, + }, "redis_py_cluster_legacy": { "package": "redis-py-cluster", }, diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index a545eed330..a3f1ec8fea 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -70,7 +70,6 @@ # of these from the IGNORE list "gcp", "httpx", - "ray", "redis", "requests", "rq", diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 0522c60231..9c511d3f1e 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -61,10 +61,6 @@ envlist = # OpenTelemetry Experimental (POTel) {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # Ray - {py3.10,py3.11}-ray-v{2.34} - {py3.10,py3.11}-ray-latest - # Redis {py3.6,py3.8}-redis-v{3} {py3.7,py3.8,py3.11}-redis-v{4} @@ -168,10 +164,6 @@ deps = # OpenTelemetry Experimental (POTel) potel: -e .[opentelemetry-experimental] - # Ray - ray-v2.34: ray~=2.34.0 - ray-latest: ray - # Redis redis: fakeredis!=1.7.4 redis: pytest<8.0.0 diff --git a/tests/integrations/ray/test_ray.py b/tests/integrations/ray/test_ray.py index b5bdd473c4..f4e67df038 100644 --- a/tests/integrations/ray/test_ray.py +++ b/tests/integrations/ray/test_ray.py @@ -1,6 +1,8 @@ import json import os import pytest +import shutil +import uuid import ray @@ -10,6 +12,12 @@ from tests.conftest import TestTransport +@pytest.fixture(autouse=True) +def shutdown_ray(tmpdir): + yield + ray.shutdown() + + class RayTestTransport(TestTransport): def __init__(self): self.envelopes = [] @@ -20,9 +28,6 @@ def capture_envelope(self, envelope: Envelope) -> None: class RayLoggingTransport(TestTransport): - def __init__(self): - super().__init__() - def capture_envelope(self, envelope: Envelope) -> None: print(envelope.serialize().decode("utf-8", "replace")) @@ -39,13 +44,24 @@ def setup_sentry(transport=None): ) -def read_error_from_log(job_id): - log_dir = "/tmp/ray/session_latest/logs/" +def read_error_from_log(job_id, ray_temp_dir): + # Find the actual session directory that Ray created + session_dirs = [d for d in os.listdir(ray_temp_dir) if d.startswith("session_")] + if not session_dirs: + raise FileNotFoundError(f"No session directory found in {ray_temp_dir}") + + session_dir = os.path.join(ray_temp_dir, session_dirs[0]) + log_dir = os.path.join(session_dir, "logs") + + if not os.path.exists(log_dir): + raise FileNotFoundError(f"No logs directory found at {log_dir}") + log_file = [ f for f in os.listdir(log_dir) if "worker" in f and job_id in f and f.endswith(".out") ][0] + with open(os.path.join(log_dir, log_file), "r") as file: lines = file.readlines() @@ -58,7 +74,6 @@ def read_error_from_log(job_id): return error -@pytest.mark.forked @pytest.mark.parametrize( "task_options", [{}, {"num_cpus": 0, "memory": 1024 * 1024 * 10}] ) @@ -124,40 +139,47 @@ def example_task(): ) -@pytest.mark.forked def test_errors_in_ray_tasks(): setup_sentry_with_logging_transport() - ray.init( - runtime_env={ - "worker_process_setup_hook": setup_sentry_with_logging_transport, - "working_dir": "./", - } - ) + ray_temp_dir = os.path.join("/tmp", f"ray_test_{uuid.uuid4().hex[:8]}") + os.makedirs(ray_temp_dir, exist_ok=True) - # Setup ray task - @ray.remote - def example_task(): - 1 / 0 + try: + ray.init( + runtime_env={ + "worker_process_setup_hook": setup_sentry_with_logging_transport, + "working_dir": "./", + }, + _temp_dir=ray_temp_dir, + ) - with sentry_sdk.start_transaction(op="task", name="ray test transaction"): - with pytest.raises(ZeroDivisionError): - future = example_task.remote() - ray.get(future) + # Setup ray task + @ray.remote + def example_task(): + 1 / 0 - job_id = future.job_id().hex() - error = read_error_from_log(job_id) + with sentry_sdk.start_transaction(op="task", name="ray test transaction"): + with pytest.raises(ZeroDivisionError): + future = example_task.remote() + ray.get(future) - assert error["level"] == "error" - assert ( - error["transaction"] - == "tests.integrations.ray.test_ray.test_errors_in_ray_tasks..example_task" - ) - assert error["exception"]["values"][0]["mechanism"]["type"] == "ray" - assert not error["exception"]["values"][0]["mechanism"]["handled"] + job_id = future.job_id().hex() + error = read_error_from_log(job_id, ray_temp_dir) + + assert error["level"] == "error" + assert ( + error["transaction"] + == "tests.integrations.ray.test_ray.test_errors_in_ray_tasks..example_task" + ) + assert error["exception"]["values"][0]["mechanism"]["type"] == "ray" + assert not error["exception"]["values"][0]["mechanism"]["handled"] + + finally: + if os.path.exists(ray_temp_dir): + shutil.rmtree(ray_temp_dir, ignore_errors=True) -@pytest.mark.forked def test_tracing_in_ray_actors(): setup_sentry() @@ -194,37 +216,45 @@ def increment(self): assert worker_envelopes == [] -@pytest.mark.forked def test_errors_in_ray_actors(): setup_sentry_with_logging_transport() - ray.init( - runtime_env={ - "worker_process_setup_hook": setup_sentry_with_logging_transport, - "working_dir": "./", - } - ) - - # Setup ray actor - @ray.remote - class Counter: - def __init__(self): - self.n = 0 - - def increment(self): - with sentry_sdk.start_span(op="task", name="example actor execution"): - 1 / 0 - - return sentry_sdk.get_client().transport.envelopes - - with sentry_sdk.start_transaction(op="task", name="ray test transaction"): - with pytest.raises(ZeroDivisionError): - counter = Counter.remote() - future = counter.increment.remote() - ray.get(future) - - job_id = future.job_id().hex() - error = read_error_from_log(job_id) - - # We do not capture errors in ray actors yet - assert error is None + ray_temp_dir = os.path.join("/tmp", f"ray_test_{uuid.uuid4().hex[:8]}") + os.makedirs(ray_temp_dir, exist_ok=True) + + try: + ray.init( + runtime_env={ + "worker_process_setup_hook": setup_sentry_with_logging_transport, + "working_dir": "./", + }, + _temp_dir=ray_temp_dir, + ) + + # Setup ray actor + @ray.remote + class Counter: + def __init__(self): + self.n = 0 + + def increment(self): + with sentry_sdk.start_span(op="task", name="example actor execution"): + 1 / 0 + + return sentry_sdk.get_client().transport.envelopes + + with sentry_sdk.start_transaction(op="task", name="ray test transaction"): + with pytest.raises(ZeroDivisionError): + counter = Counter.remote() + future = counter.increment.remote() + ray.get(future) + + job_id = future.job_id().hex() + error = read_error_from_log(job_id, ray_temp_dir) + + # We do not capture errors in ray actors yet + assert error is None + + finally: + if os.path.exists(ray_temp_dir): + shutil.rmtree(ray_temp_dir, ignore_errors=True) diff --git a/tox.ini b/tox.ini index e2898ad8f3..15192bc0ad 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-18T08:05:49.500134+00:00 +# Last generated: 2025-09-18T10:26:22.484602+00:00 [tox] requires = @@ -61,10 +61,6 @@ envlist = # OpenTelemetry Experimental (POTel) {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # Ray - {py3.10,py3.11}-ray-v{2.34} - {py3.10,py3.11}-ray-latest - # Redis {py3.6,py3.8}-redis-v{3} {py3.7,py3.8,py3.11}-redis-v{4} @@ -233,6 +229,9 @@ envlist = {py3.6,py3.7}-huey-v2.3.2 {py3.6,py3.11,py3.12}-huey-v2.5.3 + {py3.9,py3.10}-ray-v2.7.2 + {py3.9,py3.12,py3.13}-ray-v2.49.1 + {py3.8,py3.9}-spark-v3.0.3 {py3.8,py3.10,py3.11}-spark-v3.5.6 {py3.9,py3.12,py3.13}-spark-v4.0.1 @@ -396,10 +395,6 @@ deps = # OpenTelemetry Experimental (POTel) potel: -e .[opentelemetry-experimental] - # Ray - ray-v2.34: ray~=2.34.0 - ray-latest: ray - # Redis redis: fakeredis!=1.7.4 redis: pytest<8.0.0 @@ -631,6 +626,9 @@ deps = huey-v2.3.2: huey==2.3.2 huey-v2.5.3: huey==2.5.3 + ray-v2.7.2: ray==2.7.2 + ray-v2.49.1: ray==2.49.1 + spark-v3.0.3: pyspark==3.0.3 spark-v3.5.6: pyspark==3.5.6 spark-v4.0.1: pyspark==4.0.1 From 704803355a599bf1973758ef006a5bbaa1bb3967 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 18 Sep 2025 12:53:18 +0200 Subject: [PATCH 68/88] tests: Move httpx under toxgen (#4780) * Move httpx under toxgen * This is not straightforward since we depend on `pytest-httpx` which has a very weird strict relationship to which httpx versions it supports * In addition to that, some `pytest-httpx` versions will not install on Python 3.8 despite the corresponding `httpx` versions being compatible -- added a new way to filter out these scenarios * This also removes the -latest Networking group as the httpx test suite was the last Networking test suite that wasn't completely pinned * Also move gcp to the permanent part of the ignore list as it doesn't fit the toxgen mold Ref https://github.com/getsentry/sentry-python/issues/4506 --- .../workflows/test-integrations-network.yml | 67 ------------------- scripts/populate_tox/README.md | 34 ++++++++-- scripts/populate_tox/config.py | 19 ++++++ scripts/populate_tox/populate_tox.py | 56 +++++++++++++--- scripts/populate_tox/tox.jinja | 28 -------- sentry_sdk/integrations/__init__.py | 1 + tox.ini | 45 +++++-------- 7 files changed, 109 insertions(+), 141 deletions(-) diff --git a/.github/workflows/test-integrations-network.yml b/.github/workflows/test-integrations-network.yml index e34706ff09..b2f88d261c 100644 --- a/.github/workflows/test-integrations-network.yml +++ b/.github/workflows/test-integrations-network.yml @@ -22,73 +22,6 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-network-latest: - name: Network (latest) - timeout-minutes: 30 - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - python-version: ["3.9","3.12","3.13"] - # python3.6 reached EOL and is no longer being supported on - # new versions of hosted runners on Github Actions - # ubuntu-20.04 is the last version that supported python3.6 - # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 - os: [ubuntu-22.04] - # Use Docker container only for Python 3.6 - container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} - steps: - - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v6 - if: ${{ matrix.python-version != '3.6' }} - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - name: Setup Test Env - run: | - pip install "coverage[toml]" tox - - name: Erase coverage - run: | - coverage erase - - name: Test grpc latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-grpc-latest" - - name: Test httpx latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-httpx-latest" - - name: Test requests latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-requests-latest" - - name: Generate coverage XML (Python 3.6) - if: ${{ !cancelled() && matrix.python-version == '3.6' }} - run: | - export COVERAGE_RCFILE=.coveragerc36 - coverage combine .coverage-sentry-* - coverage xml --ignore-errors - - name: Generate coverage XML - if: ${{ !cancelled() && matrix.python-version != '3.6' }} - run: | - coverage combine .coverage-sentry-* - coverage xml - - name: Upload coverage to Codecov - if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml - verbose: true test-network-pinned: name: Network (pinned) timeout-minutes: 30 diff --git a/scripts/populate_tox/README.md b/scripts/populate_tox/README.md index 4acebd3259..8014acff3f 100644 --- a/scripts/populate_tox/README.md +++ b/scripts/populate_tox/README.md @@ -107,9 +107,14 @@ This key is optional. ### `python` Sometimes, the whole test suite should only run on specific Python versions. -This can be achieved via the `python` key, which expects a version specifier. +This can be achieved via the `python` key. -For example, if you want AIOHTTP tests to only run on Python 3.7+, you can say: +There are two variants how to define the Python versions to run the test suite +on. + +If you want the test suite to only be run on specific Python versions, you can +set `python` to a version specifier. For example, if you want AIOHTTP tests to +only run on Python 3.7+, you can say: ```python "aiohttp": { @@ -118,12 +123,27 @@ For example, if you want AIOHTTP tests to only run on Python 3.7+, you can say: } ``` +If the Python version to use is dependent on the version of the package under +test, you can use the more expressive dictionary variant. For instance, while +HTTPX v0.28 supports Python 3.8, a test dependency of ours, `pytest-httpx`, +doesn't. If you want to specify that HTTPX test suite should not be run on +a Python version older than 3.9 if the HTTPX version is 0.28 or higher, you can +say: + +```python +"httpx": { + "python": { + # run the test suite for httpx v0.28+ on Python 3.9+ only + ">=0.28": ">=3.9", + }, +} +``` + The `python` key is optional, and when possible, it should be omitted. The script -should automatically detect which Python versions the package supports. -However, if a package has broken -metadata or the SDK is explicitly not supporting some packages on specific -Python versions (because of, for example, broken context vars), the `python` -key can be used. +should automatically detect which Python versions the package supports. However, +if a package has broken metadata or the SDK is explicitly not supporting some +packages on specific Python versions (because of, for example, broken context +vars), the `python` key can be used. ### `include` diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 346cabc2db..cf8b9ae9dc 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -150,6 +150,25 @@ }, "python": ">=3.7", }, + "httpx": { + "package": "httpx", + "deps": { + "*": ["anyio<4.0.0"], + ">=0.16,<0.17": ["pytest-httpx==0.10.0"], + ">=0.17,<0.19": ["pytest-httpx==0.12.0"], + ">=0.19,<0.21": ["pytest-httpx==0.14.0"], + ">=0.21,<0.23": ["pytest-httpx==0.19.0"], + ">=0.23,<0.24": ["pytest-httpx==0.21.0"], + ">=0.24,<0.25": ["pytest-httpx==0.22.0"], + ">=0.25,<0.26": ["pytest-httpx==0.25.0"], + ">=0.26,<0.27": ["pytest-httpx==0.28.0"], + ">=0.27,<0.28": ["pytest-httpx==0.30.0"], + ">=0.28,<0.29": ["pytest-httpx==0.35.0"], + }, + "python": { + ">=0.28": ">=3.9", + }, + }, "huey": { "package": "huey", }, diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index a3f1ec8fea..afcef05438 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -63,13 +63,12 @@ "aws_lambda", "cloud_resource_context", "common", + "gcp", "gevent", "opentelemetry", "potel", # Integrations that can be migrated -- we should eventually remove all # of these from the IGNORE list - "gcp", - "httpx", "redis", "requests", "rq", @@ -240,9 +239,9 @@ def _supports_lowest(release: Version) -> bool: sys.exit(1) py_versions = determine_python_versions(pypi_data) - target_python_versions = TEST_SUITE_CONFIG[integration].get("python") - if target_python_versions: - target_python_versions = SpecifierSet(target_python_versions) + target_python_versions = _transform_target_python_versions( + TEST_SUITE_CONFIG[integration].get("python") + ) return bool(supported_python_versions(py_versions, target_python_versions)) if not _supports_lowest(releases[0]): @@ -327,7 +326,10 @@ def _pick_releases( def supported_python_versions( package_python_versions: Union[SpecifierSet, list[Version]], - custom_supported_versions: Optional[SpecifierSet] = None, + custom_supported_versions: Optional[ + Union[SpecifierSet, dict[SpecifierSet, SpecifierSet]] + ] = None, + version: Optional[Version] = None, ) -> list[Version]: """ Get the intersection of Python versions supported by the package and the SDK. @@ -354,9 +356,25 @@ def supported_python_versions( curr = MIN_PYTHON_VERSION while curr <= MAX_PYTHON_VERSION: if curr in package_python_versions: - if not custom_supported_versions or curr in custom_supported_versions: + if not custom_supported_versions: supported.append(curr) + else: + if isinstance(custom_supported_versions, SpecifierSet): + if curr in custom_supported_versions: + supported.append(curr) + + elif version is not None and isinstance( + custom_supported_versions, dict + ): + for v, py in custom_supported_versions.items(): + if version in v: + if curr in py: + supported.append(curr) + break + else: + supported.append(curr) + # Construct the next Python version (i.e., bump the minor) next = [int(v) for v in str(curr).split(".")] next[1] += 1 @@ -535,20 +553,38 @@ def _add_python_versions_to_release( time.sleep(PYPI_COOLDOWN) # give PYPI some breathing room - target_python_versions = TEST_SUITE_CONFIG[integration].get("python") - if target_python_versions: - target_python_versions = SpecifierSet(target_python_versions) + target_python_versions = _transform_target_python_versions( + TEST_SUITE_CONFIG[integration].get("python") + ) release.python_versions = pick_python_versions_to_test( supported_python_versions( determine_python_versions(release_pypi_data), target_python_versions, + release, ) ) release.rendered_python_versions = _render_python_versions(release.python_versions) +def _transform_target_python_versions( + python_versions: Union[str, dict[str, str], None] +) -> Union[SpecifierSet, dict[SpecifierSet, SpecifierSet], None]: + """Wrap the contents of the `python` key in SpecifierSets.""" + if not python_versions: + return None + + if isinstance(python_versions, str): + return SpecifierSet(python_versions) + + if isinstance(python_versions, dict): + updated = {} + for key, value in python_versions.items(): + updated[SpecifierSet(key)] = SpecifierSet(value) + return updated + + def get_file_hash() -> str: """Calculate a hash of the tox.ini file.""" hasher = hashlib.md5() diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 9c511d3f1e..40980197ab 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -48,13 +48,6 @@ envlist = # GCP {py3.7}-gcp - # HTTPX - {py3.6,py3.9}-httpx-v{0.16,0.18} - {py3.6,py3.10}-httpx-v{0.20,0.22} - {py3.7,py3.11,py3.12}-httpx-v{0.23,0.24} - {py3.9,py3.11,py3.12}-httpx-v{0.25,0.27} - {py3.9,py3.12,py3.13}-httpx-latest - # OpenTelemetry (OTel) {py3.7,py3.9,py3.12,py3.13}-opentelemetry @@ -137,27 +130,6 @@ deps = aws_lambda: requests aws_lambda: uvicorn - # HTTPX - httpx-v0.16: pytest-httpx==0.10.0 - httpx-v0.18: pytest-httpx==0.12.0 - httpx-v0.20: pytest-httpx==0.14.0 - httpx-v0.22: pytest-httpx==0.19.0 - httpx-v0.23: pytest-httpx==0.21.0 - httpx-v0.24: pytest-httpx==0.22.0 - httpx-v0.25: pytest-httpx==0.25.0 - httpx: pytest-httpx - # anyio is a dep of httpx - httpx: anyio<4.0.0 - httpx-v0.16: httpx~=0.16.0 - httpx-v0.18: httpx~=0.18.0 - httpx-v0.20: httpx~=0.20.0 - httpx-v0.22: httpx~=0.22.0 - httpx-v0.23: httpx~=0.23.0 - httpx-v0.24: httpx~=0.24.0 - httpx-v0.25: httpx~=0.25.0 - httpx-v0.27: httpx~=0.27.0 - httpx-latest: httpx - # OpenTelemetry (OTel) opentelemetry: opentelemetry-distro diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 2f5a1f397e..e397c9986a 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -141,6 +141,7 @@ def iter_default_integrations(with_auto_enabling_integrations): "gql": (3, 4, 1), "graphene": (3, 3), "grpc": (1, 32, 0), # grpcio + "httpx": (0, 16, 0), "huggingface_hub": (0, 24, 7), "langchain": (0, 1, 0), "langgraph": (0, 6, 6), diff --git a/tox.ini b/tox.ini index 15192bc0ad..b646228b7f 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-18T10:26:22.484602+00:00 +# Last generated: 2025-09-18T10:42:56.677852+00:00 [tox] requires = @@ -48,13 +48,6 @@ envlist = # GCP {py3.7}-gcp - # HTTPX - {py3.6,py3.9}-httpx-v{0.16,0.18} - {py3.6,py3.10}-httpx-v{0.20,0.22} - {py3.7,py3.11,py3.12}-httpx-v{0.23,0.24} - {py3.9,py3.11,py3.12}-httpx-v{0.25,0.27} - {py3.9,py3.12,py3.13}-httpx-latest - # OpenTelemetry (OTel) {py3.7,py3.9,py3.12,py3.13}-opentelemetry @@ -202,6 +195,11 @@ envlist = {py3.7,py3.11,py3.12}-grpc-v1.62.3 {py3.9,py3.12,py3.13}-grpc-v1.75.0 + {py3.6,py3.8,py3.9}-httpx-v0.16.1 + {py3.6,py3.9,py3.10}-httpx-v0.20.0 + {py3.7,py3.10,py3.11}-httpx-v0.24.1 + {py3.9,py3.11,py3.12}-httpx-v0.28.1 + # ~~~ Tasks ~~~ {py3.7,py3.9,py3.10}-arq-v0.23 @@ -368,27 +366,6 @@ deps = aws_lambda: requests aws_lambda: uvicorn - # HTTPX - httpx-v0.16: pytest-httpx==0.10.0 - httpx-v0.18: pytest-httpx==0.12.0 - httpx-v0.20: pytest-httpx==0.14.0 - httpx-v0.22: pytest-httpx==0.19.0 - httpx-v0.23: pytest-httpx==0.21.0 - httpx-v0.24: pytest-httpx==0.22.0 - httpx-v0.25: pytest-httpx==0.25.0 - httpx: pytest-httpx - # anyio is a dep of httpx - httpx: anyio<4.0.0 - httpx-v0.16: httpx~=0.16.0 - httpx-v0.18: httpx~=0.18.0 - httpx-v0.20: httpx~=0.20.0 - httpx-v0.22: httpx~=0.22.0 - httpx-v0.23: httpx~=0.23.0 - httpx-v0.24: httpx~=0.24.0 - httpx-v0.25: httpx~=0.25.0 - httpx-v0.27: httpx~=0.27.0 - httpx-latest: httpx - # OpenTelemetry (OTel) opentelemetry: opentelemetry-distro @@ -592,6 +569,16 @@ deps = grpc: types-protobuf grpc: pytest-asyncio + httpx-v0.16.1: httpx==0.16.1 + httpx-v0.20.0: httpx==0.20.0 + httpx-v0.24.1: httpx==0.24.1 + httpx-v0.28.1: httpx==0.28.1 + httpx: anyio<4.0.0 + httpx-v0.16.1: pytest-httpx==0.10.0 + httpx-v0.20.0: pytest-httpx==0.14.0 + httpx-v0.24.1: pytest-httpx==0.22.0 + httpx-v0.28.1: pytest-httpx==0.35.0 + # ~~~ Tasks ~~~ arq-v0.23: arq==0.23 From d5fb1bbf07d2b9b954c96f0036af7597571c61b2 Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Thu, 18 Sep 2025 11:48:09 -0400 Subject: [PATCH 69/88] test(spark): Improve `test_spark` speed (#4822) Fixes GH-4623 ### Description I'm trying to improve the `test_spark.py` speed using the same Spark connection in the entire test session. In my local it has passed from image to image --- tests/integrations/spark/test_spark.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/integrations/spark/test_spark.py b/tests/integrations/spark/test_spark.py index 91882a0b8f..0ea2770b89 100644 --- a/tests/integrations/spark/test_spark.py +++ b/tests/integrations/spark/test_spark.py @@ -28,11 +28,12 @@ def sentry_init_with_reset(sentry_init): _processed_integrations.discard("spark") -@pytest.fixture(scope="function") +@pytest.fixture(scope="session") def create_spark_context(): conf = SparkConf().set("spark.driver.bindAddress", "127.0.0.1") - yield lambda: SparkContext(conf=conf, appName="Testing123") - SparkContext._active_spark_context.stop() + sc = SparkContext(conf=conf, appName="Testing123") + yield lambda: sc + sc.stop() def test_set_app_properties(create_spark_context): @@ -61,12 +62,18 @@ def test_start_sentry_listener(create_spark_context): def test_initialize_spark_integration_before_spark_context_init( mock_patch_spark_context_init, sentry_init_with_reset, - create_spark_context, ): - sentry_init_with_reset() - create_spark_context() - - mock_patch_spark_context_init.assert_called_once() + # As we are using the same SparkContext connection for the whole session, + # we clean it during this test. + original_context = SparkContext._active_spark_context + SparkContext._active_spark_context = None + + try: + sentry_init_with_reset() + mock_patch_spark_context_init.assert_called_once() + finally: + # Restore the original one. + SparkContext._active_spark_context = original_context @patch("sentry_sdk.integrations.spark.spark_driver._activate_integration") From f3b3c70dcd31b285c4248fa0c1413a52a03c3326 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 19 Sep 2025 13:30:52 +0200 Subject: [PATCH 70/88] tests: Move redis under toxgen (#4824) ### Description Moved redis under toxgen, effectively moving the whole DBs group away from the old system. #### Issues Ref https://github.com/getsentry/sentry-python/issues/4506 #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- .github/workflows/test-integrations-dbs.yml | 99 --------------------- scripts/populate_tox/config.py | 9 ++ scripts/populate_tox/populate_tox.py | 1 - scripts/populate_tox/tox.jinja | 17 ---- tox.ini | 46 +++++----- 5 files changed, 33 insertions(+), 139 deletions(-) diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index efa9f8db39..d5edba3105 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -22,105 +22,6 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-dbs-latest: - name: DBs (latest) - timeout-minutes: 30 - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - python-version: ["3.7","3.12","3.13"] - # python3.6 reached EOL and is no longer being supported on - # new versions of hosted runners on Github Actions - # ubuntu-20.04 is the last version that supported python3.6 - # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 - os: [ubuntu-22.04] - 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 - 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 - # Use Docker container only for Python 3.6 - container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} - steps: - - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v6 - if: ${{ matrix.python-version != '3.6' }} - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - name: "Setup ClickHouse Server" - uses: getsentry/action-clickhouse-in-ci@v1.6 - - name: Setup Test Env - run: | - pip install "coverage[toml]" tox - - name: Erase coverage - run: | - coverage erase - - name: Test asyncpg latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-asyncpg-latest" - - name: Test clickhouse_driver latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-clickhouse_driver-latest" - - name: Test pymongo latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-pymongo-latest" - - name: Test redis latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-redis-latest" - - name: Test redis_py_cluster_legacy latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-redis_py_cluster_legacy-latest" - - name: Test sqlalchemy latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-sqlalchemy-latest" - - name: Generate coverage XML (Python 3.6) - if: ${{ !cancelled() && matrix.python-version == '3.6' }} - run: | - export COVERAGE_RCFILE=.coveragerc36 - coverage combine .coverage-sentry-* - coverage xml --ignore-errors - - name: Generate coverage XML - if: ${{ !cancelled() && matrix.python-version != '3.6' }} - run: | - coverage combine .coverage-sentry-* - coverage xml - - name: Upload coverage to Codecov - if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml - verbose: true test-dbs-pinned: name: DBs (pinned) timeout-minutes: 30 diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index cf8b9ae9dc..87f181c6e3 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -277,6 +277,15 @@ "python": ">=3.9", "num_versions": 2, }, + "redis": { + "package": "redis", + "deps": { + "*": ["fakeredis!=1.7.4", "pytest<8.0.0"], + ">=4.0,<5.0": ["fakeredis<2.31.0"], + "py3.6,py3.7,py3.8": ["fakeredis<2.26.0"], + "py3.7,py3.8,py3.9,py3.10,py3.11,py3.12,py3.13": ["pytest-asyncio"], + }, + }, "redis_py_cluster_legacy": { "package": "redis-py-cluster", }, diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index afcef05438..5a44baec24 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -69,7 +69,6 @@ "potel", # Integrations that can be migrated -- we should eventually remove all # of these from the IGNORE list - "redis", "requests", "rq", } diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 40980197ab..f8112b7ff5 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -54,12 +54,6 @@ envlist = # OpenTelemetry Experimental (POTel) {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # Redis - {py3.6,py3.8}-redis-v{3} - {py3.7,py3.8,py3.11}-redis-v{4} - {py3.7,py3.11,py3.12}-redis-v{5} - {py3.7,py3.12,py3.13}-redis-latest - # Requests {py3.6,py3.8,py3.12,py3.13}-requests @@ -136,17 +130,6 @@ deps = # OpenTelemetry Experimental (POTel) potel: -e .[opentelemetry-experimental] - # Redis - redis: fakeredis!=1.7.4 - redis: pytest<8.0.0 - {py3.6,py3.7,py3.8}-redis: fakeredis<2.26.0 - {py3.7,py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-redis: pytest-asyncio - redis-v3: redis~=3.0 - redis-v4: redis~=4.0 - redis-v4: fakeredis<2.31.0 - redis-v5: redis~=5.0 - redis-latest: redis - # Requests requests: requests>=2.0 diff --git a/tox.ini b/tox.ini index b646228b7f..9e864dd76d 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-18T10:42:56.677852+00:00 +# Last generated: 2025-09-19T10:59:45.339459+00:00 [tox] requires = @@ -54,12 +54,6 @@ envlist = # OpenTelemetry Experimental (POTel) {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # Redis - {py3.6,py3.8}-redis-v{3} - {py3.7,py3.8,py3.11}-redis-v{4} - {py3.7,py3.11,py3.12}-redis-v{5} - {py3.7,py3.12,py3.13}-redis-latest - # Requests {py3.6,py3.8,py3.12,py3.13}-requests @@ -109,7 +103,7 @@ envlist = {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 {py3.10,py3.12,py3.13}-openai_agents-v0.2.11 - {py3.10,py3.12,py3.13}-openai_agents-v0.3.0 + {py3.10,py3.12,py3.13}-openai_agents-v0.3.1 {py3.8,py3.10,py3.11}-huggingface_hub-v0.24.7 {py3.8,py3.12,py3.13}-huggingface_hub-v0.28.1 @@ -121,7 +115,7 @@ envlist = {py3.6,py3.7}-boto3-v1.12.49 {py3.6,py3.9,py3.10}-boto3-v1.20.54 {py3.7,py3.11,py3.12}-boto3-v1.28.85 - {py3.9,py3.12,py3.13}-boto3-v1.40.33 + {py3.9,py3.12,py3.13}-boto3-v1.40.34 {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.6,py3.7,py3.8}-chalice-v1.21.9 @@ -141,6 +135,13 @@ envlist = {py3.6,py3.10,py3.11}-pymongo-v3.13.0 {py3.9,py3.12,py3.13}-pymongo-v4.15.1 + {py3.6}-redis-v2.10.6 + {py3.6,py3.7,py3.8}-redis-v3.5.3 + {py3.7,py3.10,py3.11}-redis-v4.6.0 + {py3.8,py3.11,py3.12}-redis-v5.3.1 + {py3.9,py3.12,py3.13}-redis-v6.4.0 + {py3.9,py3.12,py3.13}-redis-v7.0.0b1 + {py3.6}-redis_py_cluster_legacy-v1.3.6 {py3.6,py3.7,py3.8}-redis_py_cluster_legacy-v2.1.3 @@ -372,17 +373,6 @@ deps = # OpenTelemetry Experimental (POTel) potel: -e .[opentelemetry-experimental] - # Redis - redis: fakeredis!=1.7.4 - redis: pytest<8.0.0 - {py3.6,py3.7,py3.8}-redis: fakeredis<2.26.0 - {py3.7,py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-redis: pytest-asyncio - redis-v3: redis~=3.0 - redis-v4: redis~=4.0 - redis-v4: fakeredis<2.31.0 - redis-v5: redis~=5.0 - redis-latest: redis - # Requests requests: requests>=2.0 @@ -460,7 +450,7 @@ deps = openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.1.0: openai-agents==0.1.0 openai_agents-v0.2.11: openai-agents==0.2.11 - openai_agents-v0.3.0: openai-agents==0.3.0 + openai_agents-v0.3.1: openai-agents==0.3.1 openai_agents: pytest-asyncio huggingface_hub-v0.24.7: huggingface_hub==0.24.7 @@ -474,7 +464,7 @@ deps = boto3-v1.12.49: boto3==1.12.49 boto3-v1.20.54: boto3==1.20.54 boto3-v1.28.85: boto3==1.28.85 - boto3-v1.40.33: boto3==1.40.33 + boto3-v1.40.34: boto3==1.40.34 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 @@ -498,6 +488,18 @@ deps = pymongo-v4.15.1: pymongo==4.15.1 pymongo: mockupdb + redis-v2.10.6: redis==2.10.6 + redis-v3.5.3: redis==3.5.3 + redis-v4.6.0: redis==4.6.0 + redis-v5.3.1: redis==5.3.1 + redis-v6.4.0: redis==6.4.0 + redis-v7.0.0b1: redis==7.0.0b1 + redis: fakeredis!=1.7.4 + redis: pytest<8.0.0 + redis-v4.6.0: fakeredis<2.31.0 + {py3.6,py3.7,py3.8}-redis: fakeredis<2.26.0 + {py3.7,py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-redis: pytest-asyncio + redis_py_cluster_legacy-v1.3.6: redis-py-cluster==1.3.6 redis_py_cluster_legacy-v2.1.3: redis-py-cluster==2.1.3 From 222badbbf6549c97dfe23ec655594e502d4cabbf Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 19 Sep 2025 13:50:15 +0200 Subject: [PATCH 71/88] tests: Move rq under toxgen (#4818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description - moved rq under toxgen 🎉 it needed quite some fiddling around with fakeredis versions but seems to work now #### Issues Ref https://github.com/getsentry/sentry-python/issues/4506 #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- .github/workflows/test-integrations-tasks.yml | 94 ------------------- scripts/populate_tox/config.py | 11 +++ scripts/populate_tox/populate_tox.py | 1 - scripts/populate_tox/tox.jinja | 25 ----- tox.ini | 42 ++++----- 5 files changed, 27 insertions(+), 146 deletions(-) diff --git a/.github/workflows/test-integrations-tasks.yml b/.github/workflows/test-integrations-tasks.yml index d84789b767..f4c3632261 100644 --- a/.github/workflows/test-integrations-tasks.yml +++ b/.github/workflows/test-integrations-tasks.yml @@ -22,100 +22,6 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-tasks-latest: - name: Tasks (latest) - timeout-minutes: 30 - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - python-version: ["3.7","3.12","3.13"] - # python3.6 reached EOL and is no longer being supported on - # new versions of hosted runners on Github Actions - # ubuntu-20.04 is the last version that supported python3.6 - # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 - os: [ubuntu-22.04] - # Use Docker container only for Python 3.6 - container: ${{ matrix.python-version == '3.6' && 'python:3.6' || null }} - steps: - - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v6 - if: ${{ matrix.python-version != '3.6' }} - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - name: Start Redis - uses: supercharge/redis-github-action@1.8.0 - - name: Install Java - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '21' - - name: Setup Test Env - run: | - pip install "coverage[toml]" tox - - name: Erase coverage - run: | - coverage erase - - name: Test arq latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-arq-latest" - - name: Test beam latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-beam-latest" - - name: Test celery latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-celery-latest" - - name: Test dramatiq latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-dramatiq-latest" - - name: Test huey latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-huey-latest" - - name: Test ray latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-ray-latest" - - name: Test rq latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-rq-latest" - - name: Test spark latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-spark-latest" - - name: Generate coverage XML (Python 3.6) - if: ${{ !cancelled() && matrix.python-version == '3.6' }} - run: | - export COVERAGE_RCFILE=.coveragerc36 - coverage combine .coverage-sentry-* - coverage xml --ignore-errors - - name: Generate coverage XML - if: ${{ !cancelled() && matrix.python-version != '3.6' }} - run: | - coverage combine .coverage-sentry-* - coverage xml - - name: Upload coverage to Codecov - if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.5.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - # make sure no plugins alter our coverage reports - plugins: noop - verbose: true - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: .junitxml - verbose: true test-tasks-pinned: name: Tasks (pinned) timeout-minutes: 30 diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 87f181c6e3..7d6a519b6b 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -292,6 +292,17 @@ "requests": { "package": "requests", }, + "rq": { + "package": "rq", + "deps": { + # https://github.com/jamesls/fakeredis/issues/245 + # https://github.com/cunla/fakeredis-py/issues/341 + "*": ["fakeredis<2.28.0"], + "<0.9": ["fakeredis<1.0", "redis<3.2.2"], + ">=0.9,<0.14": ["fakeredis>=1.0,<1.7.4"], + "py3.6,py3.7": ["fakeredis!=2.26.0"], + }, + }, "sanic": { "package": "sanic", "deps": { diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 5a44baec24..1ca9319c2e 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -70,7 +70,6 @@ # Integrations that can be migrated -- we should eventually remove all # of these from the IGNORE list "requests", - "rq", } diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index f8112b7ff5..6931b64cfc 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -57,13 +57,6 @@ envlist = # Requests {py3.6,py3.8,py3.12,py3.13}-requests - # RQ (Redis Queue) - {py3.6}-rq-v{0.6} - {py3.6,py3.9}-rq-v{0.13,1.0} - {py3.6,py3.11}-rq-v{1.5,1.10} - {py3.7,py3.11,py3.12}-rq-v{1.15,1.16} - {py3.7,py3.12,py3.13}-rq-latest - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. @@ -133,24 +126,6 @@ deps = # Requests requests: requests>=2.0 - # RQ (Redis Queue) - # https://github.com/jamesls/fakeredis/issues/245 - rq-v{0.6}: fakeredis<1.0 - rq-v{0.6}: redis<3.2.2 - rq-v{0.13,1.0,1.5,1.10}: fakeredis>=1.0,<1.7.4 - rq-v{1.15,1.16}: fakeredis<2.28.0 - {py3.6,py3.7}-rq-v{1.15,1.16}: fakeredis!=2.26.0 # https://github.com/cunla/fakeredis-py/issues/341 - rq-latest: fakeredis<2.28.0 - {py3.6,py3.7}-rq-latest: fakeredis!=2.26.0 # https://github.com/cunla/fakeredis-py/issues/341 - rq-v0.6: rq~=0.6.0 - rq-v0.13: rq~=0.13.0 - rq-v1.0: rq~=1.0.0 - rq-v1.5: rq~=1.5.0 - rq-v1.10: rq~=1.10.0 - rq-v1.15: rq~=1.15.0 - rq-v1.16: rq~=1.16.0 - rq-latest: rq - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. diff --git a/tox.ini b/tox.ini index 9e864dd76d..c8ec198810 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-19T10:59:45.339459+00:00 +# Last generated: 2025-09-19T11:39:21.755227+00:00 [tox] requires = @@ -57,13 +57,6 @@ envlist = # Requests {py3.6,py3.8,py3.12,py3.13}-requests - # RQ (Redis Queue) - {py3.6}-rq-v{0.6} - {py3.6,py3.9}-rq-v{0.13,1.0} - {py3.6,py3.11}-rq-v{1.5,1.10} - {py3.7,py3.11,py3.12}-rq-v{1.15,1.16} - {py3.7,py3.12,py3.13}-rq-latest - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. @@ -231,6 +224,11 @@ envlist = {py3.9,py3.10}-ray-v2.7.2 {py3.9,py3.12,py3.13}-ray-v2.49.1 + {py3.6}-rq-v0.8.2 + {py3.6,py3.7}-rq-v0.13.0 + {py3.7,py3.11,py3.12}-rq-v1.16.2 + {py3.9,py3.12,py3.13}-rq-v2.6.0 + {py3.8,py3.9}-spark-v3.0.3 {py3.8,py3.10,py3.11}-spark-v3.5.6 {py3.9,py3.12,py3.13}-spark-v4.0.1 @@ -376,24 +374,6 @@ deps = # Requests requests: requests>=2.0 - # RQ (Redis Queue) - # https://github.com/jamesls/fakeredis/issues/245 - rq-v{0.6}: fakeredis<1.0 - rq-v{0.6}: redis<3.2.2 - rq-v{0.13,1.0,1.5,1.10}: fakeredis>=1.0,<1.7.4 - rq-v{1.15,1.16}: fakeredis<2.28.0 - {py3.6,py3.7}-rq-v{1.15,1.16}: fakeredis!=2.26.0 # https://github.com/cunla/fakeredis-py/issues/341 - rq-latest: fakeredis<2.28.0 - {py3.6,py3.7}-rq-latest: fakeredis!=2.26.0 # https://github.com/cunla/fakeredis-py/issues/341 - rq-v0.6: rq~=0.6.0 - rq-v0.13: rq~=0.13.0 - rq-v1.0: rq~=1.0.0 - rq-v1.5: rq~=1.5.0 - rq-v1.10: rq~=1.10.0 - rq-v1.15: rq~=1.15.0 - rq-v1.16: rq~=1.16.0 - rq-latest: rq - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. @@ -618,6 +598,16 @@ deps = ray-v2.7.2: ray==2.7.2 ray-v2.49.1: ray==2.49.1 + rq-v0.8.2: rq==0.8.2 + rq-v0.13.0: rq==0.13.0 + rq-v1.16.2: rq==1.16.2 + rq-v2.6.0: rq==2.6.0 + rq: fakeredis<2.28.0 + rq-v0.8.2: fakeredis<1.0 + rq-v0.8.2: redis<3.2.2 + rq-v0.13.0: fakeredis>=1.0,<1.7.4 + {py3.6,py3.7}-rq: fakeredis!=2.26.0 + spark-v3.0.3: pyspark==3.0.3 spark-v3.5.6: pyspark==3.5.6 spark-v4.0.1: pyspark==4.0.1 From 97a5c0896d9673c75d76ab5ee3c2c1608dde91f4 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 19 Sep 2025 14:20:28 +0200 Subject: [PATCH 72/88] tests: Move requests under toxgen (#4825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description - Move requests under toxgen - This was the last test suite to port! 🎉 #### Issues ref https://github.com/getsentry/sentry-python/issues/4506 #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- scripts/populate_tox/config.py | 1 + scripts/populate_tox/populate_tox.py | 3 --- scripts/populate_tox/tox.jinja | 6 ------ tox.ini | 14 +++++++------- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 7d6a519b6b..70cecd60d9 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -291,6 +291,7 @@ }, "requests": { "package": "requests", + "num_versions": 2, }, "rq": { "package": "rq", diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 1ca9319c2e..17fced9b4f 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -67,9 +67,6 @@ "gevent", "opentelemetry", "potel", - # Integrations that can be migrated -- we should eventually remove all - # of these from the IGNORE list - "requests", } diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 6931b64cfc..98e14adb20 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -54,9 +54,6 @@ envlist = # OpenTelemetry Experimental (POTel) {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # Requests - {py3.6,py3.8,py3.12,py3.13}-requests - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. @@ -123,9 +120,6 @@ deps = # OpenTelemetry Experimental (POTel) potel: -e .[opentelemetry-experimental] - # Requests - requests: requests>=2.0 - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. diff --git a/tox.ini b/tox.ini index c8ec198810..662394de59 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-19T11:39:21.755227+00:00 +# Last generated: 2025-09-19T11:53:09.269997+00:00 [tox] requires = @@ -54,9 +54,6 @@ envlist = # OpenTelemetry Experimental (POTel) {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # Requests - {py3.6,py3.8,py3.12,py3.13}-requests - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. @@ -194,6 +191,9 @@ envlist = {py3.7,py3.10,py3.11}-httpx-v0.24.1 {py3.9,py3.11,py3.12}-httpx-v0.28.1 + {py3.6}-requests-v2.12.5 + {py3.9,py3.12,py3.13}-requests-v2.32.5 + # ~~~ Tasks ~~~ {py3.7,py3.9,py3.10}-arq-v0.23 @@ -371,9 +371,6 @@ deps = # OpenTelemetry Experimental (POTel) potel: -e .[opentelemetry-experimental] - # Requests - requests: requests>=2.0 - # === Integrations - Auto-generated === # These come from the populate_tox.py script. Eventually we should move all # integration tests there. @@ -561,6 +558,9 @@ deps = httpx-v0.24.1: pytest-httpx==0.22.0 httpx-v0.28.1: pytest-httpx==0.35.0 + requests-v2.12.5: requests==2.12.5 + requests-v2.32.5: requests==2.32.5 + # ~~~ Tasks ~~~ arq-v0.23: arq==0.23 From 4c8c92551ffe7e8c7431fe928de3358317afe8cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 07:58:40 +0000 Subject: [PATCH 73/88] =?UTF-8?q?ci:=20=F0=9F=A4=96=20Update=20test=20matr?= =?UTF-8?q?ix=20with=20new=20releases=20(09/22)=20(#4831)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update our test matrix with new releases of integrated frameworks and libraries. ## How it works - Scan PyPI for all supported releases of all frameworks we have a dedicated test suite for. - Pick a representative sample of releases to run our test suite against. We always test the latest and oldest supported version. - Update [tox.ini](https://github.com/getsentry/sentry-python/blob/master/tox.ini) with the new releases. ## Action required - If CI passes on this PR, it's safe to approve and merge. It means our integrations can handle new versions of frameworks that got pulled in. - If CI doesn't pass on this PR, this points to an incompatibility of either our integration or our test setup with a new version of a framework. - Check what the failures look like and either fix them, or update the [test config](https://github.com/getsentry/sentry-python/blob/master/scripts/populate_tox/config.py) and rerun [scripts/generate-test-files.sh](https://github.com/getsentry/sentry-python/blob/master/scripts/generate-test-files.sh). See [scripts/populate_tox/README.md](https://github.com/getsentry/sentry-python/blob/master/scripts/populate_tox/README.md) for what configuration options are available. _____________________ _🤖 This PR was automatically created using [a GitHub action](https://github.com/getsentry/sentry-python/blob/master/.github/workflows/update-tox.yml)._ --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Ivana Kellyer --- tox.ini | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/tox.ini b/tox.ini index 662394de59..906d075e2b 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-19T11:53:09.269997+00:00 +# Last generated: 2025-09-22T03:40:00.010016+00:00 [tox] requires = @@ -80,12 +80,12 @@ envlist = {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.11,py3.12}-openai-base-v1.37.2 {py3.8,py3.11,py3.12}-openai-base-v1.73.0 - {py3.8,py3.12,py3.13}-openai-base-v1.108.0 + {py3.8,py3.12,py3.13}-openai-base-v1.108.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.37.2 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.73.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.108.0 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.108.1 {py3.9,py3.12,py3.13}-langgraph-v0.6.7 {py3.10,py3.12,py3.13}-langgraph-v1.0.0a3 @@ -105,7 +105,7 @@ envlist = {py3.6,py3.7}-boto3-v1.12.49 {py3.6,py3.9,py3.10}-boto3-v1.20.54 {py3.7,py3.11,py3.12}-boto3-v1.28.85 - {py3.9,py3.12,py3.13}-boto3-v1.40.34 + {py3.9,py3.12,py3.13}-boto3-v1.40.35 {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.6,py3.7,py3.8}-chalice-v1.21.9 @@ -147,7 +147,7 @@ envlist = {py3.9,py3.12,py3.13}-launchdarkly-v9.12.0 {py3.8,py3.12,py3.13}-openfeature-v0.7.5 - {py3.9,py3.12,py3.13}-openfeature-v0.8.2 + {py3.9,py3.12,py3.13}-openfeature-v0.8.3 {py3.7,py3.12,py3.13}-statsig-v0.55.3 {py3.7,py3.12,py3.13}-statsig-v0.58.4 @@ -222,7 +222,7 @@ envlist = {py3.6,py3.11,py3.12}-huey-v2.5.3 {py3.9,py3.10}-ray-v2.7.2 - {py3.9,py3.12,py3.13}-ray-v2.49.1 + {py3.9,py3.12,py3.13}-ray-v2.49.2 {py3.6}-rq-v0.8.2 {py3.6,py3.7}-rq-v0.13.0 @@ -252,9 +252,9 @@ envlist = {py3.9,py3.12,py3.13}-starlette-v0.48.0 {py3.6,py3.9,py3.10}-fastapi-v0.79.1 - {py3.7,py3.10,py3.11}-fastapi-v0.91.0 - {py3.7,py3.10,py3.11}-fastapi-v0.103.2 - {py3.8,py3.12,py3.13}-fastapi-v0.116.2 + {py3.7,py3.10,py3.11}-fastapi-v0.92.0 + {py3.8,py3.10,py3.11}-fastapi-v0.105.0 + {py3.8,py3.12,py3.13}-fastapi-v0.117.1 # ~~~ Web 2 ~~~ @@ -316,7 +316,8 @@ envlist = {py3.7,py3.12,py3.13}-typer-v0.15.4 {py3.7,py3.12,py3.13}-typer-v0.16.1 - {py3.7,py3.12,py3.13}-typer-v0.17.4 + {py3.7,py3.12,py3.13}-typer-v0.17.5 + {py3.7,py3.12,py3.13}-typer-v0.19.1 @@ -407,7 +408,7 @@ deps = openai-base-v1.0.1: openai==1.0.1 openai-base-v1.37.2: openai==1.37.2 openai-base-v1.73.0: openai==1.73.0 - openai-base-v1.108.0: openai==1.108.0 + openai-base-v1.108.1: openai==1.108.1 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 @@ -416,7 +417,7 @@ deps = openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.37.2: openai==1.37.2 openai-notiktoken-v1.73.0: openai==1.73.0 - openai-notiktoken-v1.108.0: openai==1.108.0 + openai-notiktoken-v1.108.1: openai==1.108.1 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.37.2: httpx<0.28 @@ -441,7 +442,7 @@ deps = boto3-v1.12.49: boto3==1.12.49 boto3-v1.20.54: boto3==1.20.54 boto3-v1.28.85: boto3==1.28.85 - boto3-v1.40.34: boto3==1.40.34 + boto3-v1.40.35: boto3==1.40.35 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 @@ -492,7 +493,7 @@ deps = launchdarkly-v9.12.0: launchdarkly-server-sdk==9.12.0 openfeature-v0.7.5: openfeature-sdk==0.7.5 - openfeature-v0.8.2: openfeature-sdk==0.8.2 + openfeature-v0.8.3: openfeature-sdk==0.8.3 statsig-v0.55.3: statsig==0.55.3 statsig-v0.58.4: statsig==0.58.4 @@ -596,7 +597,7 @@ deps = huey-v2.5.3: huey==2.5.3 ray-v2.7.2: ray==2.7.2 - ray-v2.49.1: ray==2.49.1 + ray-v2.49.2: ray==2.49.2 rq-v0.8.2: rq==0.8.2 rq-v0.13.0: rq==0.13.0 @@ -666,17 +667,17 @@ deps = {py3.6}-starlette: aiocontextvars fastapi-v0.79.1: fastapi==0.79.1 - fastapi-v0.91.0: fastapi==0.91.0 - fastapi-v0.103.2: fastapi==0.103.2 - fastapi-v0.116.2: fastapi==0.116.2 + fastapi-v0.92.0: fastapi==0.92.0 + fastapi-v0.105.0: fastapi==0.105.0 + fastapi-v0.117.1: fastapi==0.117.1 fastapi: httpx fastapi: pytest-asyncio fastapi: python-multipart fastapi: requests fastapi: anyio<4 fastapi-v0.79.1: httpx<0.28.0 - fastapi-v0.91.0: httpx<0.28.0 - fastapi-v0.103.2: httpx<0.28.0 + fastapi-v0.92.0: httpx<0.28.0 + fastapi-v0.105.0: httpx<0.28.0 {py3.6}-fastapi: aiocontextvars @@ -786,7 +787,8 @@ deps = typer-v0.15.4: typer==0.15.4 typer-v0.16.1: typer==0.16.1 - typer-v0.17.4: typer==0.17.4 + typer-v0.17.5: typer==0.17.5 + typer-v0.19.1: typer==0.19.1 From a92664022dc205434734bd8aa042baa1173a9ce6 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 22 Sep 2025 12:13:19 +0200 Subject: [PATCH 74/88] tests: Remove old system for checking for regressions (#4826) ### Description With all eligible test suites now moved to toxgen, we can remove our old way of testing for regressions in new versions of packages. For context, before toxgen we had two groups of test targets for each test suite: pinned and latest. Pinned targets used to be a small selection of hand-picked pinned versions of the package to test. Latest always pulled the newest release, to check for incompatibilities. There was a lot of code to cater to having these two different categories. With the switch to toxgen, all targets are now pinned, and we make sure we pick a representative sample of versions to test against, always including the latest available version to detect regressions. I'm removing the code dealing with the `latest` group entirely, and removing references to anything `pinned` as this is now simply the default. #### Issues #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- .github/workflows/test-integrations-ai.yml | 46 ++++++++-------- .github/workflows/test-integrations-cloud.yml | 30 +++++------ .../workflows/test-integrations-common.yml | 14 ++--- .github/workflows/test-integrations-dbs.yml | 34 ++++++------ .github/workflows/test-integrations-flags.yml | 26 ++++----- .../workflows/test-integrations-gevent.yml | 14 ++--- .../workflows/test-integrations-graphql.yml | 26 ++++----- .github/workflows/test-integrations-misc.yml | 34 ++++++------ .../workflows/test-integrations-network.yml | 22 ++++---- .github/workflows/test-integrations-tasks.yml | 42 +++++++-------- .github/workflows/test-integrations-web-1.yml | 26 ++++----- .github/workflows/test-integrations-web-2.yml | 50 ++++++++--------- scripts/populate_tox/populate_tox.py | 12 +---- scripts/runtox.sh | 18 +------ .../split_tox_gh_actions.py | 53 ++++++------------- .../split_tox_gh_actions/templates/base.jinja | 3 -- .../templates/check_required.jinja | 8 ++- .../templates/test_group.jinja | 14 ++--- 18 files changed, 210 insertions(+), 262 deletions(-) diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index 972df704e0..cf21720ff1 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-ai-pinned: - name: AI (pinned) + test-ai: + name: AI timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -50,42 +50,42 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test anthropic pinned + - name: Test anthropic run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-anthropic" - - name: Test cohere pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-anthropic" + - name: Test cohere run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-cohere" - - name: Test langchain-base pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-cohere" + - name: Test langchain-base run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-langchain-base" - - name: Test langchain-notiktoken pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-langchain-base" + - name: Test langchain-notiktoken run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-langchain-notiktoken" - - name: Test openai-base pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-langchain-notiktoken" + - name: Test openai-base run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai-base" - - name: Test openai-notiktoken pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-openai-base" + - name: Test openai-notiktoken run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai-notiktoken" - - name: Test langgraph pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-openai-notiktoken" + - name: Test langgraph run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-langgraph" - - name: Test openai_agents pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-langgraph" + - name: Test openai_agents run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openai_agents" - - name: Test huggingface_hub pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-openai_agents" + - name: Test huggingface_hub run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-huggingface_hub" + ./scripts/runtox.sh "py${{ matrix.python-version }}-huggingface_hub" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -114,13 +114,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned AI tests passed - needs: test-ai-pinned + name: All AI tests passed + needs: test-ai # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-ai-pinned.result != 'success' + if: needs.test-ai.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-cloud.yml b/.github/workflows/test-integrations-cloud.yml index 6aeaea8c3a..48a2881ffc 100644 --- a/.github/workflows/test-integrations-cloud.yml +++ b/.github/workflows/test-integrations-cloud.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-cloud-pinned: - name: Cloud (pinned) + test-cloud: + name: Cloud timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -54,26 +54,26 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test aws_lambda pinned + - name: Test aws_lambda run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-aws_lambda" - - name: Test boto3 pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-aws_lambda" + - name: Test boto3 run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-boto3" - - name: Test chalice pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-boto3" + - name: Test chalice run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-chalice" - - name: Test cloud_resource_context pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-chalice" + - name: Test cloud_resource_context run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-cloud_resource_context" - - name: Test gcp pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-cloud_resource_context" + - name: Test gcp run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-gcp" + ./scripts/runtox.sh "py${{ matrix.python-version }}-gcp" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -102,13 +102,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned Cloud tests passed - needs: test-cloud-pinned + name: All Cloud tests passed + needs: test-cloud # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-cloud-pinned.result != 'success' + if: needs.test-cloud.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-common.yml b/.github/workflows/test-integrations-common.yml index b682428dd1..eb5dde8b17 100644 --- a/.github/workflows/test-integrations-common.yml +++ b/.github/workflows/test-integrations-common.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-common-pinned: - name: Common (pinned) + test-common: + name: Common timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -50,10 +50,10 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test common pinned + - name: Test common run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-common" + ./scripts/runtox.sh "py${{ matrix.python-version }}-common" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -82,13 +82,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned Common tests passed - needs: test-common-pinned + name: All Common tests passed + needs: test-common # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-common-pinned.result != 'success' + if: needs.test-common.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index d5edba3105..a778ce86d5 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-dbs-pinned: - name: DBs (pinned) + test-dbs: + name: DBs timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -70,30 +70,30 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test asyncpg pinned + - name: Test asyncpg run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-asyncpg" - - name: Test clickhouse_driver pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-asyncpg" + - name: Test clickhouse_driver run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-clickhouse_driver" - - name: Test pymongo pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-clickhouse_driver" + - name: Test pymongo run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-pymongo" - - name: Test redis pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-pymongo" + - name: Test redis run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-redis" - - name: Test redis_py_cluster_legacy pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-redis" + - name: Test redis_py_cluster_legacy run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-redis_py_cluster_legacy" - - name: Test sqlalchemy pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-redis_py_cluster_legacy" + - name: Test sqlalchemy run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-sqlalchemy" + ./scripts/runtox.sh "py${{ matrix.python-version }}-sqlalchemy" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -122,13 +122,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned DBs tests passed - needs: test-dbs-pinned + name: All DBs tests passed + needs: test-dbs # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-dbs-pinned.result != 'success' + if: needs.test-dbs.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-flags.yml b/.github/workflows/test-integrations-flags.yml index d7baeeb870..ce622d4258 100644 --- a/.github/workflows/test-integrations-flags.yml +++ b/.github/workflows/test-integrations-flags.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-flags-pinned: - name: Flags (pinned) + test-flags: + name: Flags timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -50,22 +50,22 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test launchdarkly pinned + - name: Test launchdarkly run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-launchdarkly" - - name: Test openfeature pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-launchdarkly" + - name: Test openfeature run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openfeature" - - name: Test statsig pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-openfeature" + - name: Test statsig run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-statsig" - - name: Test unleash pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-statsig" + - name: Test unleash run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-unleash" + ./scripts/runtox.sh "py${{ matrix.python-version }}-unleash" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -94,13 +94,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned Flags tests passed - needs: test-flags-pinned + name: All Flags tests passed + needs: test-flags # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-flags-pinned.result != 'success' + if: needs.test-flags.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-gevent.yml b/.github/workflows/test-integrations-gevent.yml index 9af6b4d7af..8b077f357c 100644 --- a/.github/workflows/test-integrations-gevent.yml +++ b/.github/workflows/test-integrations-gevent.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-gevent-pinned: - name: Gevent (pinned) + test-gevent: + name: Gevent timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -50,10 +50,10 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test gevent pinned + - name: Test gevent run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-gevent" + ./scripts/runtox.sh "py${{ matrix.python-version }}-gevent" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -82,13 +82,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned Gevent tests passed - needs: test-gevent-pinned + name: All Gevent tests passed + needs: test-gevent # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-gevent-pinned.result != 'success' + if: needs.test-gevent.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-graphql.yml b/.github/workflows/test-integrations-graphql.yml index 5c306dff3f..81d809c94e 100644 --- a/.github/workflows/test-integrations-graphql.yml +++ b/.github/workflows/test-integrations-graphql.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-graphql-pinned: - name: GraphQL (pinned) + test-graphql: + name: GraphQL timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -50,22 +50,22 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test ariadne pinned + - name: Test ariadne run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-ariadne" - - name: Test gql pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-ariadne" + - name: Test gql run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-gql" - - name: Test graphene pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-gql" + - name: Test graphene run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-graphene" - - name: Test strawberry pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-graphene" + - name: Test strawberry run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-strawberry" + ./scripts/runtox.sh "py${{ matrix.python-version }}-strawberry" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -94,13 +94,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned GraphQL tests passed - needs: test-graphql-pinned + name: All GraphQL tests passed + needs: test-graphql # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-graphql-pinned.result != 'success' + if: needs.test-graphql.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index 005e8395a2..2aaf2f07fe 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-misc-pinned: - name: Misc (pinned) + test-misc: + name: Misc timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -50,30 +50,30 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test loguru pinned + - name: Test loguru run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-loguru" - - name: Test opentelemetry pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-loguru" + - name: Test opentelemetry run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-opentelemetry" - - name: Test potel pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-opentelemetry" + - name: Test potel run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-potel" - - name: Test pure_eval pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-potel" + - name: Test pure_eval run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-pure_eval" - - name: Test trytond pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-pure_eval" + - name: Test trytond run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-trytond" - - name: Test typer pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-trytond" + - name: Test typer run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-typer" + ./scripts/runtox.sh "py${{ matrix.python-version }}-typer" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -102,13 +102,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned Misc tests passed - needs: test-misc-pinned + name: All Misc tests passed + needs: test-misc # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-misc-pinned.result != 'success' + if: needs.test-misc.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-network.yml b/.github/workflows/test-integrations-network.yml index b2f88d261c..e8b5b36c98 100644 --- a/.github/workflows/test-integrations-network.yml +++ b/.github/workflows/test-integrations-network.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-network-pinned: - name: Network (pinned) + test-network: + name: Network timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -50,18 +50,18 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test grpc pinned + - name: Test grpc run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-grpc" - - name: Test httpx pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-grpc" + - name: Test httpx run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-httpx" - - name: Test requests pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-httpx" + - name: Test requests run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-requests" + ./scripts/runtox.sh "py${{ matrix.python-version }}-requests" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -90,13 +90,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned Network tests passed - needs: test-network-pinned + name: All Network tests passed + needs: test-network # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-network-pinned.result != 'success' + if: needs.test-network.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-tasks.yml b/.github/workflows/test-integrations-tasks.yml index f4c3632261..2dfae9c9a3 100644 --- a/.github/workflows/test-integrations-tasks.yml +++ b/.github/workflows/test-integrations-tasks.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-tasks-pinned: - name: Tasks (pinned) + test-tasks: + name: Tasks timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -57,38 +57,38 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test arq pinned + - name: Test arq run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-arq" - - name: Test beam pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-arq" + - name: Test beam run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-beam" - - name: Test celery pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-beam" + - name: Test celery run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-celery" - - name: Test dramatiq pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-celery" + - name: Test dramatiq run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-dramatiq" - - name: Test huey pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-dramatiq" + - name: Test huey run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-huey" - - name: Test ray pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-huey" + - name: Test ray run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-ray" - - name: Test rq pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-ray" + - name: Test rq run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-rq" - - name: Test spark pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-rq" + - name: Test spark run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-spark" + ./scripts/runtox.sh "py${{ matrix.python-version }}-spark" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -117,13 +117,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned Tasks tests passed - needs: test-tasks-pinned + name: All Tasks tests passed + needs: test-tasks # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-tasks-pinned.result != 'success' + if: needs.test-tasks.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-web-1.yml b/.github/workflows/test-integrations-web-1.yml index 4b22db6155..455f15723a 100644 --- a/.github/workflows/test-integrations-web-1.yml +++ b/.github/workflows/test-integrations-web-1.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-web_1-pinned: - name: Web 1 (pinned) + test-web_1: + name: Web 1 timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -68,22 +68,22 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test django pinned + - name: Test django run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-django" - - name: Test flask pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-django" + - name: Test flask run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-flask" - - name: Test starlette pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-flask" + - name: Test starlette run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-starlette" - - name: Test fastapi pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-starlette" + - name: Test fastapi run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-fastapi" + ./scripts/runtox.sh "py${{ matrix.python-version }}-fastapi" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -112,13 +112,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned Web 1 tests passed - needs: test-web_1-pinned + name: All Web 1 tests passed + needs: test-web_1 # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-web_1-pinned.result != 'success' + if: needs.test-web_1.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-web-2.yml b/.github/workflows/test-integrations-web-2.yml index 3dbe2e1168..98cf4456e8 100644 --- a/.github/workflows/test-integrations-web-2.yml +++ b/.github/workflows/test-integrations-web-2.yml @@ -22,8 +22,8 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless jobs: - test-web_2-pinned: - name: Web 2 (pinned) + test-web_2: + name: Web 2 timeout-minutes: 30 runs-on: ${{ matrix.os }} strategy: @@ -50,46 +50,46 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test aiohttp pinned + - name: Test aiohttp run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-aiohttp" - - name: Test asgi pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-aiohttp" + - name: Test asgi run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-asgi" - - name: Test bottle pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-asgi" + - name: Test bottle run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-bottle" - - name: Test falcon pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-bottle" + - name: Test falcon run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-falcon" - - name: Test litestar pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-falcon" + - name: Test litestar run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-litestar" - - name: Test pyramid pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-litestar" + - name: Test pyramid run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-pyramid" - - name: Test quart pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-pyramid" + - name: Test quart run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-quart" - - name: Test sanic pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-quart" + - name: Test sanic run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-sanic" - - name: Test starlite pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-sanic" + - name: Test starlite run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-starlite" - - name: Test tornado pinned + ./scripts/runtox.sh "py${{ matrix.python-version }}-starlite" + - name: Test tornado run: | set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-tornado" + ./scripts/runtox.sh "py${{ matrix.python-version }}-tornado" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -118,13 +118,13 @@ jobs: files: .junitxml verbose: true check_required_tests: - name: All pinned Web 2 tests passed - needs: test-web_2-pinned + name: All Web 2 tests passed + needs: test-web_2 # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-web_2-pinned.result != 'success' + if: needs.test-web_2.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 17fced9b4f..37ddfb9daf 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -49,16 +49,8 @@ IGNORE = { # Do not try auto-generating the tox entries for these. They will be - # hardcoded in tox.ini. - # - # This set should be getting smaller over time as we migrate more test - # suites over to this script. Some entries will probably stay forever - # as they don't fit the mold (e.g. common, asgi, which don't have a 3rd party - # pypi package to install in different versions). - # - # Test suites that will have to remain hardcoded since they don't fit the - # toxgen usecase (there is no one package that should be tested in different - # versions) + # hardcoded in tox.ini since they don't fit the toxgen usecase (there is no + # one package that should be tested in different versions). "asgi", "aws_lambda", "cloud_resource_context", diff --git a/scripts/runtox.sh b/scripts/runtox.sh index 6acf4406fb..bd6e954947 100755 --- a/scripts/runtox.sh +++ b/scripts/runtox.sh @@ -13,25 +13,9 @@ else TOXPATH=./.venv/bin/tox fi -excludelatest=false -for arg in "$@" -do - if [ "$arg" = "--exclude-latest" ]; then - excludelatest=true - shift - break - fi -done - searchstring="$1" -if $excludelatest; then - echo "Excluding latest" - ENV="$($TOXPATH -l | grep -- "$searchstring" | grep -v -- '-latest' | tr $'\n' ',')" -else - echo "Including latest" - ENV="$($TOXPATH -l | grep -- "$searchstring" | tr $'\n' ',')" -fi +ENV="$($TOXPATH -l | grep -- "$searchstring" | tr $'\n' ',')" if [ -z "${ENV}" ]; then echo "No targets found. Skipping." 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 51ee614d04..a7b7c394b1 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -29,11 +29,9 @@ TOXENV_REGEX = re.compile( r""" {?(?P(py\d+\.\d+,?)+)}? - -(?P[a-z](?:[a-z_]|-(?!v{?\d|latest))*[a-z0-9]) + -(?P[a-z](?:[a-z_]|-(?!v{?\d))*[a-z0-9]) (?:-( (v{?(?P[0-9.]+[0-9a-z,.]*}?)) - | - (?Platest) ))? """, re.VERBOSE, @@ -167,13 +165,11 @@ def main(fail_on_changes): old_hash = get_files_hash() print("Parsing tox.ini...") - py_versions_pinned, py_versions_latest = parse_tox() + py_versions = parse_tox() if fail_on_changes: print("Checking if all frameworks belong in a group...") - missing_frameworks = find_frameworks_missing_from_groups( - py_versions_pinned, py_versions_latest - ) + missing_frameworks = find_frameworks_missing_from_groups(py_versions) if missing_frameworks: raise RuntimeError( "Please add the following frameworks to the corresponding group " @@ -183,9 +179,7 @@ def main(fail_on_changes): print("Rendering templates...") for group, frameworks in GROUPS.items(): - contents = render_template( - group, frameworks, py_versions_pinned, py_versions_latest - ) + contents = render_template(group, frameworks, py_versions) filename = write_file(contents, group) print(f"Created {filename}") @@ -213,8 +207,7 @@ def parse_tox(): if line.strip() and not line.strip().startswith("#") ] - py_versions_pinned = defaultdict(set) - py_versions_latest = defaultdict(set) + py_versions = defaultdict(set) parsed_correctly = True @@ -232,14 +225,10 @@ def parse_tox(): groups = parsed.groupdict() raw_python_versions = groups["py_versions"] framework = groups["framework"] - framework_versions_latest = groups.get("framework_versions_latest") or False # collect python versions to test the framework in raw_python_versions = set(raw_python_versions.split(",")) - if framework_versions_latest: - py_versions_latest[framework] |= raw_python_versions - else: - py_versions_pinned[framework] |= raw_python_versions + py_versions[framework] |= raw_python_versions except Exception: print(f"ERROR reading line {line}") @@ -248,15 +237,14 @@ def parse_tox(): if not parsed_correctly: raise RuntimeError("Failed to parse tox.ini") - py_versions_pinned = _normalize_py_versions(py_versions_pinned) - py_versions_latest = _normalize_py_versions(py_versions_latest) + py_versions = _normalize_py_versions(py_versions) - return py_versions_pinned, py_versions_latest + return py_versions -def find_frameworks_missing_from_groups(py_versions_pinned, py_versions_latest): +def find_frameworks_missing_from_groups(py_versions): frameworks_in_a_group = _union(GROUPS.values()) - all_frameworks = set(py_versions_pinned.keys()) | set(py_versions_latest.keys()) + all_frameworks = set(py_versions) return all_frameworks - frameworks_in_a_group @@ -296,33 +284,26 @@ def _union(seq): return reduce(lambda x, y: set(x) | set(y), seq) -def render_template(group, frameworks, py_versions_pinned, py_versions_latest): +def render_template(group, frameworks, py_versions): template = ENV.get_template("base.jinja") - categories = set() - py_versions = defaultdict(set) + py_versions_final = set() for framework in frameworks: - if py_versions_pinned[framework]: - categories.add("pinned") - py_versions["pinned"] |= set(py_versions_pinned[framework]) - if py_versions_latest[framework]: - categories.add("latest") - py_versions["latest"] |= set(py_versions_latest[framework]) + py_versions_final |= set(py_versions[framework]) context = { "group": group, "frameworks": frameworks, - "categories": sorted(categories), "needs_clickhouse": bool(set(frameworks) & FRAMEWORKS_NEEDING_CLICKHOUSE), "needs_docker": bool(set(frameworks) & FRAMEWORKS_NEEDING_DOCKER), "needs_postgres": bool(set(frameworks) & FRAMEWORKS_NEEDING_POSTGRES), "needs_redis": bool(set(frameworks) & FRAMEWORKS_NEEDING_REDIS), "needs_java": bool(set(frameworks) & FRAMEWORKS_NEEDING_JAVA), - "py_versions": { - category: [f'"{version}"' for version in _normalize_py_versions(versions)] - for category, versions in py_versions.items() - }, + "py_versions": [ + f'"{version}"' for version in _normalize_py_versions(py_versions_final) + ], } + rendered = template.render(context) rendered = postprocess_template(rendered) return rendered diff --git a/scripts/split_tox_gh_actions/templates/base.jinja b/scripts/split_tox_gh_actions/templates/base.jinja index 75c988e32a..5d59e63348 100644 --- a/scripts/split_tox_gh_actions/templates/base.jinja +++ b/scripts/split_tox_gh_actions/templates/base.jinja @@ -30,9 +30,6 @@ env: {% raw %}${{ github.workspace }}/dist-serverless{% endraw %} jobs: -{% for category in categories %} {% include "test_group.jinja" %} -{% endfor %} - {% include "check_required.jinja" %} {% endwith %} diff --git a/scripts/split_tox_gh_actions/templates/check_required.jinja b/scripts/split_tox_gh_actions/templates/check_required.jinja index 9a2bbed830..37d4f8fd78 100644 --- a/scripts/split_tox_gh_actions/templates/check_required.jinja +++ b/scripts/split_tox_gh_actions/templates/check_required.jinja @@ -1,13 +1,11 @@ check_required_tests: - name: All pinned {{ group }} tests passed - {% if "pinned" in categories %} - needs: test-{{ group | replace(" ", "_") | lower }}-pinned - {% endif %} + name: All {{ group }} tests passed + needs: test-{{ group | replace(" ", "_") | lower }} # Always run this, even if a dependent job failed if: always() runs-on: ubuntu-22.04 steps: - name: Check for failures - if: needs.test-{{ lowercase_group }}-pinned.result != 'success' + if: needs.test-{{ lowercase_group }}.result != 'success' run: | echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/scripts/split_tox_gh_actions/templates/test_group.jinja b/scripts/split_tox_gh_actions/templates/test_group.jinja index f020a44b84..59369265b3 100644 --- a/scripts/split_tox_gh_actions/templates/test_group.jinja +++ b/scripts/split_tox_gh_actions/templates/test_group.jinja @@ -1,11 +1,11 @@ - test-{{ lowercase_group }}-{{ category }}: - name: {{ group }} ({{ category }}) + test-{{ lowercase_group }}: + name: {{ group }} timeout-minutes: 30 runs-on: {% raw %}${{ matrix.os }}{% endraw %} strategy: fail-fast: false matrix: - python-version: [{{ py_versions.get(category)|join(",") }}] + python-version: [{{ py_versions|join(",") }}] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 @@ -75,14 +75,10 @@ coverage erase {% for framework in frameworks %} - - name: Test {{ framework }} {{ category }} + - name: Test {{ framework }} run: | set -x # print commands that are executed - {% if category == "pinned" %} - ./scripts/runtox.sh --exclude-latest "{% raw %}py${{ matrix.python-version }}{% endraw %}-{{ framework }}" - {% elif category == "latest" %} - ./scripts/runtox.sh "{% raw %}py${{ matrix.python-version }}{% endraw %}-{{ framework }}-latest" - {% endif %} + ./scripts/runtox.sh "{% raw %}py${{ matrix.python-version }}{% endraw %}-{{ framework }}" {% endfor %} - name: Generate coverage XML (Python 3.6) From 34a35809492cc6cd1413ab0a5da06da261863844 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Mon, 22 Sep 2025 14:02:58 +0200 Subject: [PATCH 75/88] Use weakref in dedupe where possible (#4834) We cannot use `weakref` always since built-in exception types like `ZeroDivisionError` raise a `TypeError`. This makes the memory usage at least a little better for custom exception types. #### Issues * resolves: #3165 * resolves: PY-17 --- sentry_sdk/integrations/dedupe.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index eab2764fcd..99ac6ce164 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -1,3 +1,5 @@ +import weakref + import sentry_sdk from sentry_sdk.utils import ContextVar, logger from sentry_sdk.integrations import Integration @@ -35,12 +37,24 @@ def processor(event, hint): if exc_info is None: return event + last_seen = integration._last_seen.get(None) + if last_seen is not None: + # last_seen is either a weakref or the original instance + last_seen = ( + last_seen() if isinstance(last_seen, weakref.ref) else last_seen + ) + exc = exc_info[1] - if integration._last_seen.get(None) is exc: + if last_seen is exc: logger.info("DedupeIntegration dropped duplicated error event %s", exc) return None - integration._last_seen.set(exc) + # we can only weakref non builtin types + try: + integration._last_seen.set(weakref.ref(exc)) + except TypeError: + integration._last_seen.set(exc) + return event @staticmethod From 1b9f0cfe03fb9fa3ae9a399ae99da726b16189a2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 22 Sep 2025 16:41:45 +0200 Subject: [PATCH 76/88] fix: Pin shibuya (#4839) ### Description Apidocs build started [failing](https://github.com/getsentry/sentry-python/actions/runs/17918383503/job/50946544869?pr=4837) a couple hours ago. A [new version](https://pypi.org/project/shibuya/2025.9.22/) of the shibuya theme was released recently, which seems to be the culprit. Pinning for now to unblock, will investigate whether we can just adapt. #### Issues #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 81e04ba3ef..b98ad4834a 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ gevent -shibuya +shibuya<2025.9.22 sphinx<8.2 sphinx-autodoc-typehints[type_comments]>=1.8.0 typing-extensions From 3f4e2a8497ae942b380ce4d2459cac7bb364506e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:10:08 +0000 Subject: [PATCH 77/88] build(deps): bump actions/setup-python from 5 to 6 (#4830) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
Release notes

Sourced from actions/setup-python's releases.

v6.0.0

What's Changed

Breaking Changes

Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. See Release Notes

Enhancements:

Bug fixes:

Dependency updates:

New Contributors

Full Changelog: https://github.com/actions/setup-python/compare/v5...v6.0.0

v5.6.0

What's Changed

Full Changelog: https://github.com/actions/setup-python/compare/v5...v5.6.0

v5.5.0

What's Changed

Enhancements:

Bug fixes:

... (truncated)

Commits
  • e797f83 Upgrade to node 24 (#1164)
  • 3d1e2d2 Revert "Enhance cache-dependency-path handling to support files outside the w...
  • 65b0712 Clarify pythonLocation behavior for PyPy and GraalPy in environment variables...
  • 5b668cf Bump actions/checkout from 4 to 5 (#1181)
  • f62a0e2 Change missing cache directory error to warning (#1182)
  • 9322b3c Upgrade setuptools to 78.1.1 to fix path traversal vulnerability in PackageIn...
  • fbeb884 Bump form-data to fix critical vulnerabilities #182 & #183 (#1163)
  • 03bb615 Bump idna from 2.9 to 3.7 in /tests/data (#843)
  • 36da51d Add version parsing from Pipfile (#1067)
  • 3c6f142 update documentation (#1156)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ivana Kellyer --- .github/workflows/update-tox.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-tox.yml b/.github/workflows/update-tox.yml index 11d143a2a9..0d2640bb47 100644 --- a/.github/workflows/update-tox.yml +++ b/.github/workflows/update-tox.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.13 From e52c3b117a404fb7755feced02680d29e421b0ee Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Wed, 24 Sep 2025 09:37:26 +0200 Subject: [PATCH 78/88] fix(langchain): don't record tool call output if not include_prompt / should_send_default_pii (#4836) --- sentry_sdk/integrations/langchain.py | 17 ++++----- .../integrations/langchain/test_langchain.py | 36 +++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 1401be06e1..b92f6983a1 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -322,14 +322,15 @@ def on_llm_end(self, response, *, run_id, **kwargs): pass try: - tool_calls = getattr(generation.message, "tool_calls", None) - if tool_calls is not None and tool_calls != []: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) + if should_send_default_pii() and self.include_prompts: + tool_calls = getattr(generation.message, "tool_calls", None) + if tool_calls is not None and tool_calls != []: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) except AttributeError: pass diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index b6b432c523..2a40945413 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -221,6 +221,19 @@ def test_langchain_agent( in chat_spans[1]["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] ) assert "5" in chat_spans[1]["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + + # Verify tool calls are recorded when PII is enabled + assert SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS in chat_spans[0].get( + "data", {} + ), "Tool calls should be recorded when send_default_pii=True and include_prompts=True" + tool_calls_data = chat_spans[0]["data"][SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS] + assert isinstance(tool_calls_data, (list, str)) # Could be serialized + if isinstance(tool_calls_data, str): + assert "get_word_length" in tool_calls_data + elif isinstance(tool_calls_data, list) and len(tool_calls_data) > 0: + # Check if tool calls contain expected function name + tool_call_str = str(tool_calls_data) + assert "get_word_length" in tool_call_str else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in chat_spans[0].get("data", {}) assert SPANDATA.GEN_AI_RESPONSE_TEXT not in chat_spans[0].get("data", {}) @@ -229,6 +242,29 @@ def test_langchain_agent( assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in tool_exec_span.get("data", {}) assert SPANDATA.GEN_AI_RESPONSE_TEXT not in tool_exec_span.get("data", {}) + # Verify tool calls are NOT recorded when PII is disabled + assert SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS not in chat_spans[0].get( + "data", {} + ), ( + f"Tool calls should NOT be recorded when send_default_pii={send_default_pii} " + f"and include_prompts={include_prompts}" + ) + assert SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS not in chat_spans[1].get( + "data", {} + ), ( + f"Tool calls should NOT be recorded when send_default_pii={send_default_pii} " + f"and include_prompts={include_prompts}" + ) + + # Verify that available tools are always recorded regardless of PII settings + for chat_span in chat_spans: + span_data = chat_span.get("data", {}) + if SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS in span_data: + tools_data = span_data[SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS] + assert ( + tools_data is not None + ), "Available tools should always be recorded regardless of PII settings" + def test_langchain_error(sentry_init, capture_events): sentry_init( From 808c1805c0253fe18a21156fb57a219ef2f71acd Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 24 Sep 2025 09:51:49 +0200 Subject: [PATCH 79/88] fix(AI): Make agents integrations set the span status in case of error (#4820) ### Description Make all AI related integrations set the `span.status` to `"error"` and also the `status` of the transaction/root_span to `"error"` in case of an error. This is also the OTel semantic convention for failing spans. #### Issues * resolves: #4752 * resolves: PY-1825 --- sentry_sdk/consts.py | 4 +- sentry_sdk/integrations/anthropic.py | 21 ++++++-- sentry_sdk/integrations/cohere.py | 4 ++ sentry_sdk/integrations/huggingface_hub.py | 5 +- sentry_sdk/integrations/langchain.py | 6 +-- sentry_sdk/integrations/openai.py | 3 ++ .../openai_agents/spans/execute_tool.py | 2 +- .../integrations/openai_agents/utils.py | 3 ++ sentry_sdk/tracing.py | 3 +- sentry_sdk/tracing_utils.py | 15 +++++- .../integrations/anthropic/test_anthropic.py | 51 ++++++++++++++++++- tests/integrations/cohere/test_cohere.py | 18 +++++++ .../huggingface_hub/test_huggingface_hub.py | 19 +++++++ .../integrations/langchain/test_langchain.py | 47 ++++++++++++++++- tests/integrations/openai/test_openai.py | 20 ++++++++ .../openai_agents/test_openai_agents.py | 26 ++++++++++ 16 files changed, 231 insertions(+), 16 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 91a1740526..606cf804b6 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -765,11 +765,12 @@ class SPANSTATUS: CANCELLED = "cancelled" DATA_LOSS = "data_loss" DEADLINE_EXCEEDED = "deadline_exceeded" + ERROR = "error" # OTel status code: https://opentelemetry.io/docs/concepts/signals/traces/#span-status FAILED_PRECONDITION = "failed_precondition" INTERNAL_ERROR = "internal_error" INVALID_ARGUMENT = "invalid_argument" NOT_FOUND = "not_found" - OK = "ok" + OK = "ok" # HTTP 200 and OTel status code: https://opentelemetry.io/docs/concepts/signals/traces/#span-status OUT_OF_RANGE = "out_of_range" PERMISSION_DENIED = "permission_denied" RESOURCE_EXHAUSTED = "resource_exhausted" @@ -777,6 +778,7 @@ class SPANSTATUS: UNAVAILABLE = "unavailable" UNIMPLEMENTED = "unimplemented" UNKNOWN_ERROR = "unknown_error" + UNSET = "unset" # OTel status code: https://opentelemetry.io/docs/concepts/signals/traces/#span-status class OP: diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 4f4c0b1a2a..d9898fa1d1 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -4,9 +4,10 @@ import sentry_sdk from sentry_sdk.ai.monitoring import record_token_usage from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function -from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import set_span_errored from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, @@ -52,6 +53,8 @@ def setup_once(): def _capture_exception(exc): # type: (Any) -> None + set_span_errored() + event, hint = event_from_exception( exc, client_options=sentry_sdk.get_client().options, @@ -357,7 +360,13 @@ def _sentry_patched_create_sync(*args, **kwargs): integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) kwargs["integration"] = integration - return _execute_sync(f, *args, **kwargs) + try: + return _execute_sync(f, *args, **kwargs) + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + with capture_internal_exceptions(): + span.__exit__(None, None, None) return _sentry_patched_create_sync @@ -390,6 +399,12 @@ async def _sentry_patched_create_async(*args, **kwargs): integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) kwargs["integration"] = integration - return await _execute_async(f, *args, **kwargs) + try: + return await _execute_async(f, *args, **kwargs) + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + with capture_internal_exceptions(): + span.__exit__(None, None, None) return _sentry_patched_create_async diff --git a/sentry_sdk/integrations/cohere.py b/sentry_sdk/integrations/cohere.py index 57ffdb908a..3445900c80 100644 --- a/sentry_sdk/integrations/cohere.py +++ b/sentry_sdk/integrations/cohere.py @@ -7,6 +7,8 @@ from typing import TYPE_CHECKING +from sentry_sdk.tracing_utils import set_span_errored + if TYPE_CHECKING: from typing import Any, Callable, Iterator from sentry_sdk.tracing import Span @@ -84,6 +86,8 @@ def setup_once(): def _capture_exception(exc): # type: (Any) -> None + set_span_errored() + event, hint = event_from_exception( exc, client_options=sentry_sdk.get_client().options, diff --git a/sentry_sdk/integrations/huggingface_hub.py b/sentry_sdk/integrations/huggingface_hub.py index cb76ccf507..2e2b382abd 100644 --- a/sentry_sdk/integrations/huggingface_hub.py +++ b/sentry_sdk/integrations/huggingface_hub.py @@ -7,6 +7,7 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import set_span_errored from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, @@ -52,6 +53,8 @@ def setup_once(): def _capture_exception(exc): # type: (Any) -> None + set_span_errored() + event, hint = event_from_exception( exc, client_options=sentry_sdk.get_client().options, @@ -127,8 +130,6 @@ def new_huggingface_task(*args, **kwargs): try: res = f(*args, **kwargs) except Exception as e: - # Error Handling - span.set_status("error") _capture_exception(e) span.__exit__(None, None, None) raise e from None diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index b92f6983a1..6bc3ceb93e 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -8,8 +8,7 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import _get_value +from sentry_sdk.tracing_utils import _get_value, set_span_errored from sentry_sdk.utils import logger, capture_internal_exceptions from typing import TYPE_CHECKING @@ -26,6 +25,7 @@ Union, ) from uuid import UUID + from sentry_sdk.tracing import Span try: @@ -116,7 +116,7 @@ def _handle_error(self, run_id, error): span_data = self.span_map[run_id] span = span_data.span - span.set_status("unknown") + set_span_errored(span) sentry_sdk.capture_exception(error, span.scope) diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 467116c8f4..4d72ec366c 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -7,6 +7,7 @@ from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import set_span_errored from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, @@ -83,6 +84,8 @@ def _capture_exception(exc, manual_span_cleanup=True): # Close an eventually open span # We need to do this by hand because we are not using the start_span context manager current_span = sentry_sdk.get_current_span() + set_span_errored(current_span) + if manual_span_cleanup and current_span is not None: current_span.__exit__(None, None, None) diff --git a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py index 5f9e4cb340..ad70762cd0 100644 --- a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py @@ -42,7 +42,7 @@ def update_execute_tool_span(span, agent, tool, result): if isinstance(result, str) and result.startswith( "An error occurred while running the tool" ): - span.set_status(SPANSTATUS.INTERNAL_ERROR) + span.set_status(SPANSTATUS.ERROR) if should_send_default_pii(): span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, result) diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index a0487e0e3a..73d2858e7f 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -3,6 +3,7 @@ from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import set_span_errored from sentry_sdk.utils import event_from_exception, safe_serialize from typing import TYPE_CHECKING @@ -20,6 +21,8 @@ def _capture_exception(exc): # type: (Any) -> None + set_span_errored() + event, hint = event_from_exception( exc, client_options=sentry_sdk.get_client().options, diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index fc43a33dc7..4edda21075 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -416,7 +416,8 @@ def __enter__(self): def __exit__(self, ty, value, tb): # type: (Optional[Any], Optional[Any], Optional[Any]) -> None if value is not None and should_be_treated_as_error(ty, value): - self.set_status(SPANSTATUS.INTERNAL_ERROR) + if self.status != SPANSTATUS.ERROR: + self.set_status(SPANSTATUS.INTERNAL_ERROR) with capture_internal_exceptions(): scope, old_span = self._context_manager_state diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index c1cfde293b..2f3e334e3f 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -11,7 +11,7 @@ import uuid import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANTEMPLATE +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS, SPANTEMPLATE from sentry_sdk.utils import ( capture_internal_exceptions, filename_for_module, @@ -892,6 +892,19 @@ def get_current_span(scope=None): return current_span +def set_span_errored(span=None): + # type: (Optional[Span]) -> None + """ + Set the status of the current or given span to ERROR. + Also sets the status of the transaction (root span) to ERROR. + """ + span = span or get_current_span() + if span is not None: + span.set_status(SPANSTATUS.ERROR) + if span.containing_transaction is not None: + span.containing_transaction.set_status(SPANSTATUS.ERROR) + + def _generate_sample_rand( trace_id, # type: Optional[str] *, diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 3893626026..04ff12eb8b 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -698,8 +698,54 @@ def test_exception_message_create(sentry_init, capture_events): max_tokens=1024, ) - (event,) = events + (event, transaction) = events assert event["level"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + +def test_span_status_error(sentry_init, capture_events): + sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) + events = capture_events() + + with start_transaction(name="anthropic"): + client = Anthropic(api_key="z") + client.messages._post = mock.Mock( + side_effect=AnthropicError("API rate limit reached") + ) + with pytest.raises(AnthropicError): + client.messages.create( + model="some-model", + messages=[{"role": "system", "content": "I'm throwing an exception"}], + max_tokens=1024, + ) + + (error, transaction) = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + +@pytest.mark.asyncio +async def test_span_status_error_async(sentry_init, capture_events): + sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) + events = capture_events() + + with start_transaction(name="anthropic"): + client = AsyncAnthropic(api_key="z") + client.messages._post = AsyncMock( + side_effect=AnthropicError("API rate limit reached") + ) + with pytest.raises(AnthropicError): + await client.messages.create( + model="some-model", + messages=[{"role": "system", "content": "I'm throwing an exception"}], + max_tokens=1024, + ) + + (error, transaction) = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" @pytest.mark.asyncio @@ -718,8 +764,9 @@ async def test_exception_message_create_async(sentry_init, capture_events): max_tokens=1024, ) - (event,) = events + (event, transaction) = events assert event["level"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" def test_span_origin(sentry_init, capture_events): diff --git a/tests/integrations/cohere/test_cohere.py b/tests/integrations/cohere/test_cohere.py index ee876172d1..a97d2befae 100644 --- a/tests/integrations/cohere/test_cohere.py +++ b/tests/integrations/cohere/test_cohere.py @@ -167,6 +167,24 @@ def test_bad_chat(sentry_init, capture_events): assert event["level"] == "error" +def test_span_status_error(sentry_init, capture_events): + sentry_init(integrations=[CohereIntegration()], traces_sample_rate=1.0) + events = capture_events() + + with start_transaction(name="test"): + client = Client(api_key="z") + HTTPXClient.request = mock.Mock( + side_effect=httpx.HTTPError("API rate limit reached") + ) + with pytest.raises(httpx.HTTPError): + client.chat(model="some-model", message="hello") + + (error, transaction) = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + @pytest.mark.parametrize( "send_default_pii, include_prompts", [(True, True), (True, False), (False, True), (False, False)], diff --git a/tests/integrations/huggingface_hub/test_huggingface_hub.py b/tests/integrations/huggingface_hub/test_huggingface_hub.py index 86f9c10109..5aa3928a67 100644 --- a/tests/integrations/huggingface_hub/test_huggingface_hub.py +++ b/tests/integrations/huggingface_hub/test_huggingface_hub.py @@ -654,6 +654,25 @@ def test_chat_completion_api_error( assert span["data"] == expected_data +def test_span_status_error(sentry_init, capture_events, mock_hf_api_with_errors): + # type: (Any, Any, Any) -> None + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + client = InferenceClient(model="test-model") + + with sentry_sdk.start_transaction(name="test"): + with pytest.raises(HfHubHTTPError): + client.chat_completion( + messages=[{"role": "user", "content": "Hello!"}], + ) + + (error, transaction) = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + @pytest.mark.parametrize("send_default_pii", [True, False]) @pytest.mark.parametrize("include_prompts", [True, False]) def test_chat_completion_with_tools( diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 2a40945413..af4b6b8c56 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -285,7 +285,7 @@ def test_langchain_error(sentry_init, capture_events): ] ) global stream_result_mock - stream_result_mock = Mock(side_effect=Exception("API rate limit error")) + stream_result_mock = Mock(side_effect=ValueError("API rate limit error")) llm = MockOpenAI( model_name="gpt-3.5-turbo", temperature=0, @@ -295,13 +295,56 @@ def test_langchain_error(sentry_init, capture_events): agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True) - with start_transaction(), pytest.raises(Exception): + with start_transaction(), pytest.raises(ValueError): list(agent_executor.stream({"input": "How many letters in the word eudca"})) error = events[0] assert error["level"] == "error" +def test_span_status_error(sentry_init, capture_events): + global llm_type + llm_type = "acme-llm" + + sentry_init( + integrations=[LangchainIntegration(include_prompts=True)], + traces_sample_rate=1.0, + ) + events = capture_events() + + with start_transaction(name="test"): + prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + "You are very powerful assistant, but don't know current events", + ), + ("user", "{input}"), + MessagesPlaceholder(variable_name="agent_scratchpad"), + ] + ) + global stream_result_mock + stream_result_mock = Mock(side_effect=ValueError("API rate limit error")) + llm = MockOpenAI( + model_name="gpt-3.5-turbo", + temperature=0, + openai_api_key="badkey", + ) + agent = create_openai_tools_agent(llm, [get_word_length], prompt) + + agent_executor = AgentExecutor( + agent=agent, tools=[get_word_length], verbose=True + ) + + with pytest.raises(ValueError): + list(agent_executor.stream({"input": "How many letters in the word eudca"})) + + (error, transaction) = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + def test_span_origin(sentry_init, capture_events): sentry_init( integrations=[LangchainIntegration()], diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 18968fb36a..e7fbf8a7d8 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -416,6 +416,26 @@ def test_bad_chat_completion(sentry_init, capture_events): assert event["level"] == "error" +def test_span_status_error(sentry_init, capture_events): + sentry_init(integrations=[OpenAIIntegration()], traces_sample_rate=1.0) + events = capture_events() + + with start_transaction(name="test"): + client = OpenAI(api_key="z") + client.chat.completions._post = mock.Mock( + side_effect=OpenAIError("API rate limit reached") + ) + with pytest.raises(OpenAIError): + client.chat.completions.create( + model="some-model", messages=[{"role": "system", "content": "hello"}] + ) + + (error, transaction) = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + @pytest.mark.asyncio async def test_bad_chat_completion_async(sentry_init, capture_events): sentry_init(integrations=[OpenAIIntegration()], traces_sample_rate=1.0) diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 047b919213..bd7f15faff 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -657,6 +657,32 @@ async def test_error_handling(sentry_init, capture_events, test_agent): assert ai_client_span["tags"]["status"] == "internal_error" +@pytest.mark.asyncio +async def test_span_status_error(sentry_init, capture_events, test_agent): + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + with patch( + "agents.models.openai_responses.OpenAIResponsesModel.get_response" + ) as mock_get_response: + mock_get_response.side_effect = ValueError("Model Error") + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + ) + + events = capture_events() + + with pytest.raises(ValueError, match="Model Error"): + await agents.Runner.run( + test_agent, "Test input", run_config=test_run_config + ) + + (error, transaction) = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + @pytest.mark.asyncio async def test_multiple_agents_asyncio( sentry_init, capture_events, test_agent, mock_model_response From 13a8ae1f6891503663a1cfc1d07e498f48bbc9f0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 24 Sep 2025 10:20:09 +0200 Subject: [PATCH 80/88] feat(toxgen): Cache release data (#4835) ### Description We're fetching the same release data from PyPI everytime toxgen is run. Cache it instead to avoid unnecessary network requests. This makes toxgen about twice as fast. I'm sorting the cache file to hopefully avoid merge conflicts (at least it should make them less likely compared to if we'd only append to the file). Let's see how this works, if the cache makes it harder to merge stuff because of merge conflicts, we can get rid of it. Ease of use is more important than runtime. #### Issues Closes https://github.com/getsentry/sentry-python/issues/4817 #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- scripts/populate_tox/populate_tox.py | 79 ++++++++- scripts/populate_tox/releases.jsonl | 236 +++++++++++++++++++++++++++ tox.ini | 24 ++- 3 files changed, 323 insertions(+), 16 deletions(-) create mode 100644 scripts/populate_tox/releases.jsonl diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 37ddfb9daf..28a9fe8334 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -4,6 +4,7 @@ import functools import hashlib +import json import os import sys import time @@ -34,18 +35,20 @@ # CUTOFF = datetime.now(tz=timezone.utc) - timedelta(days=365 * 5) TOX_FILE = Path(__file__).resolve().parent.parent.parent / "tox.ini" +RELEASES_CACHE_FILE = Path(__file__).resolve().parent / "releases.jsonl" ENV = Environment( loader=FileSystemLoader(Path(__file__).resolve().parent), trim_blocks=True, lstrip_blocks=True, ) -PYPI_COOLDOWN = 0.1 # seconds to wait between requests to PyPI +PYPI_COOLDOWN = 0.05 # seconds to wait between requests to PyPI PYPI_PROJECT_URL = "https://pypi.python.org/pypi/{project}/json" PYPI_VERSION_URL = "https://pypi.python.org/pypi/{project}/{version}/json" CLASSIFIER_PREFIX = "Programming Language :: Python :: " +CACHE = defaultdict(dict) IGNORE = { # Do not try auto-generating the tox entries for these. They will be @@ -94,9 +97,32 @@ def fetch_package(package: str) -> Optional[dict]: @functools.cache def fetch_release(package: str, version: Version) -> Optional[dict]: - """Fetch release metadata from PyPI.""" + """Fetch release metadata from cache or, failing that, PyPI.""" + release = _fetch_from_cache(package, version) + if release is not None: + return release + url = PYPI_VERSION_URL.format(project=package, version=version) - return fetch_url(url) + release = fetch_url(url) + if release is not None: + _save_to_cache(package, version, release) + return release + + +def _fetch_from_cache(package: str, version: Version) -> Optional[dict]: + package = _normalize_name(package) + if package in CACHE and str(version) in CACHE[package]: + CACHE[package][str(version)]["_accessed"] = True + return CACHE[package][str(version)] + + return None + + +def _save_to_cache(package: str, version: Version, release: Optional[dict]) -> None: + with open(RELEASES_CACHE_FILE, "a") as releases_cache: + releases_cache.write(json.dumps(_normalize_release(release)) + "\n") + + CACHE[_normalize_name(package)][str(version)] = release def _prefilter_releases( @@ -600,6 +626,24 @@ def get_last_updated() -> Optional[datetime]: return timestamp +def _normalize_name(package: str) -> str: + return package.lower().replace("-", "_") + + +def _normalize_release(release: dict) -> dict: + """Filter out unneeded parts of the release JSON.""" + normalized = { + "info": { + "classifiers": release["info"]["classifiers"], + "name": release["info"]["name"], + "requires_python": release["info"]["requires_python"], + "version": release["info"]["version"], + "yanked": release["info"]["yanked"], + }, + } + return normalized + + def main(fail_on_changes: bool = False) -> None: """ Generate tox.ini from the tox.jinja template. @@ -636,6 +680,20 @@ def main(fail_on_changes: bool = False) -> None: f"The SDK supports Python versions {MIN_PYTHON_VERSION} - {MAX_PYTHON_VERSION}." ) + # Load file cache + global CACHE + + with open(RELEASES_CACHE_FILE) as releases_cache: + for line in releases_cache: + release = json.loads(line) + name = _normalize_name(release["info"]["name"]) + version = release["info"]["version"] + CACHE[name][version] = release + CACHE[name][version][ + "_accessed" + ] = False # for cleaning up unused cache entries + + # Process packages packages = defaultdict(list) for group, integrations in GROUPS.items(): @@ -701,6 +759,21 @@ def main(fail_on_changes: bool = False) -> None: packages, update_timestamp=not fail_on_changes, last_updated=last_updated ) + # Sort the release cache file + releases = [] + with open(RELEASES_CACHE_FILE) as releases_cache: + releases = [json.loads(line) for line in releases_cache] + releases.sort(key=lambda r: (r["info"]["name"], r["info"]["version"])) + with open(RELEASES_CACHE_FILE, "w") as releases_cache: + for release in releases: + if ( + CACHE[_normalize_name(release["info"]["name"])][ + release["info"]["version"] + ]["_accessed"] + is True + ): + releases_cache.write(json.dumps(release) + "\n") + if fail_on_changes: new_file_hash = get_file_hash() if old_file_hash != new_file_hash: diff --git a/scripts/populate_tox/releases.jsonl b/scripts/populate_tox/releases.jsonl new file mode 100644 index 0000000000..814d0c15b9 --- /dev/null +++ b/scripts/populate_tox/releases.jsonl @@ -0,0 +1,236 @@ +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": "", "version": "1.10.8", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": "", "version": "1.11.29", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": "", "version": "1.8.19", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.4", "version": "2.0.13", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.5", "version": "2.2.28", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.6", "version": "3.1.14", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.6", "version": "3.2.25", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.8", "version": "4.2.24", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.10", "version": "5.2.6", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Django", "requires_python": ">=3.12", "version": "6.0a1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Flask", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", "version": "1.1.4", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "Flask", "requires_python": ">=3.8", "version": "2.3.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", "Typing :: Typed"], "name": "Flask", "requires_python": ">=3.9", "version": "3.1.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Quart", "requires_python": ">=3.7", "version": "0.16.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Quart", "requires_python": ">=3.7", "version": "0.17.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Quart", "requires_python": ">=3.7", "version": "0.18.4", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Application Frameworks", "Typing :: Typed"], "name": "Quart", "requires_python": ">=3.9", "version": "0.20.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": "", "version": "1.2.19", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "version": "1.3.24", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7", "version": "1.4.54", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": ">=3.7", "version": "2.0.43", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed"], "name": "UnleashClient", "requires_python": ">=3.8", "version": "6.0.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed"], "name": "UnleashClient", "requires_python": ">=3.8", "version": "6.1.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed"], "name": "UnleashClient", "requires_python": ">=3.8", "version": "6.2.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed"], "name": "UnleashClient", "requires_python": ">=3.8", "version": "6.3.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "aiohttp", "requires_python": ">=3.8", "version": "3.10.11", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "aiohttp", "requires_python": ">=3.9", "version": "3.12.15", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet :: WWW/HTTP"], "name": "aiohttp", "requires_python": ">=3.5.3", "version": "3.4.4", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "aiohttp", "requires_python": ">=3.6", "version": "3.7.4", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "anthropic", "requires_python": ">=3.7", "version": "0.16.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "anthropic", "requires_python": ">=3.7", "version": "0.33.1", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "anthropic", "requires_python": ">=3.8", "version": "0.50.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "anthropic", "requires_python": ">=3.8", "version": "0.68.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.12.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.13.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.14.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.15.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.19.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.6", "version": "2.26.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.6", "version": "2.32.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.7", "version": "2.40.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.8", "version": "2.50.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.9", "version": "2.68.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": "", "version": "0.20.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": "", "version": "0.22", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": null, "version": "0.24.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": ">=3.9", "version": "0.26.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "arq", "requires_python": ">=3.6", "version": "0.23", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "arq", "requires_python": ">=3.7", "version": "0.24.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "arq", "requires_python": ">=3.7", "version": "0.25.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "arq", "requires_python": ">=3.8", "version": "0.26.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database :: Front-Ends"], "name": "asyncpg", "requires_python": ">=3.5.0", "version": "0.23.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database :: Front-Ends"], "name": "asyncpg", "requires_python": ">=3.6.0", "version": "0.25.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database :: Front-Ends"], "name": "asyncpg", "requires_python": ">=3.7.0", "version": "0.27.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database :: Front-Ends"], "name": "asyncpg", "requires_python": ">=3.8.0", "version": "0.30.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "boto3", "requires_python": "", "version": "1.12.49", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "boto3", "requires_python": ">= 3.6", "version": "1.20.54", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "boto3", "requires_python": ">= 3.7", "version": "1.28.85", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9"], "name": "boto3", "requires_python": ">=3.9", "version": "1.40.36", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", "Topic :: Internet :: WWW/HTTP :: WSGI :: Server", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "bottle", "requires_python": "", "version": "0.12.25", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", "Topic :: Internet :: WWW/HTTP :: WSGI :: Server", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "bottle", "requires_python": null, "version": "0.13.4", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Object Brokering", "Topic :: System :: Distributed Computing"], "name": "celery", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", "version": "4.4.7", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Celery", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Object Brokering", "Topic :: System :: Distributed Computing"], "name": "celery", "requires_python": ">=3.8", "version": "5.5.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Celery", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Object Brokering", "Topic :: System :: Distributed Computing"], "name": "celery", "requires_python": ">=3.8", "version": "5.6.0b1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "chalice", "requires_python": "", "version": "1.16.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "chalice", "requires_python": "", "version": "1.21.9", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "chalice", "requires_python": "", "version": "1.26.6", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "chalice", "requires_python": null, "version": "1.32.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: SQL", "Topic :: Database", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "clickhouse-driver", "requires_python": "<4,>=3.7", "version": "0.2.9", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "cohere", "requires_python": "<4.0,>=3.9", "version": "5.13.12", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "cohere", "requires_python": "<4.0,>=3.9", "version": "5.18.0", "yanked": false}} +{"info": {"classifiers": ["Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "cohere", "requires_python": "<4.0,>=3.8", "version": "5.4.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "cohere", "requires_python": "<4.0,>=3.8", "version": "5.9.4", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: System :: Distributed Computing"], "name": "dramatiq", "requires_python": ">=3.6", "version": "1.12.3", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: System :: Distributed Computing"], "name": "dramatiq", "requires_python": ">=3.7", "version": "1.15.0", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: System :: Distributed Computing"], "name": "dramatiq", "requires_python": ">=3.9", "version": "1.18.0", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: System :: Distributed Computing"], "name": "dramatiq", "requires_python": ">=3.5", "version": "1.9.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: Jython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "falcon", "requires_python": "", "version": "1.4.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "falcon", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "version": "2.0.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Cython", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "falcon", "requires_python": ">=3.5", "version": "3.1.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Cython", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks", "Typing :: Typed"], "name": "falcon", "requires_python": ">=3.8", "version": "4.1.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 1", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.8", "version": "0.105.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 1", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.8", "version": "0.117.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.6.1", "version": "0.79.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.7", "version": "0.92.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": "", "version": "3.4.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": null, "version": "3.5.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": ">=3.8.1", "version": "4.0.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": ">=3.8.1", "version": "4.2.0b0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries"], "name": "graphene", "requires_python": "", "version": "3.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries"], "name": "graphene", "requires_python": null, "version": "3.4.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "grpcio", "requires_python": "", "version": "1.32.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "grpcio", "requires_python": ">=3.6", "version": "1.47.5", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "grpcio", "requires_python": ">=3.7", "version": "1.62.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "grpcio", "requires_python": ">=3.9", "version": "1.75.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Trio", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "httpx", "requires_python": ">=3.6", "version": "0.16.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Trio", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "httpx", "requires_python": ">=3.6", "version": "0.20.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Trio", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "httpx", "requires_python": ">=3.6", "version": "0.22.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Trio", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "httpx", "requires_python": ">=3.7", "version": "0.24.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Trio", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "httpx", "requires_python": ">=3.8", "version": "0.26.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Trio", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "httpx", "requires_python": ">=3.8", "version": "0.28.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python"], "name": "huey", "requires_python": null, "version": "0.1.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python"], "name": "huey", "requires_python": "", "version": "1.10.5", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python"], "name": "huey", "requires_python": "", "version": "1.7.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python"], "name": "huey", "requires_python": "", "version": "2.0.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "huey", "requires_python": "", "version": "2.1.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "huey", "requires_python": "", "version": "2.2.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "huey", "requires_python": "", "version": "2.3.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "huey", "requires_python": null, "version": "2.5.3", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.24.7", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.28.1", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.32.6", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.35.0", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.9"], "name": "langchain", "requires_python": "<4.0,>=3.8.1", "version": "0.1.20", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.9"], "name": "langchain", "requires_python": "<4.0,>=3.8.1", "version": "0.2.17", "yanked": false}} +{"info": {"classifiers": [], "name": "langchain", "requires_python": "<4.0,>=3.9", "version": "0.3.27", "yanked": false}} +{"info": {"classifiers": [], "name": "langgraph", "requires_python": ">=3.9", "version": "0.6.7", "yanked": false}} +{"info": {"classifiers": [], "name": "langgraph", "requires_python": ">=3.10", "version": "1.0.0a3", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries"], "name": "launchdarkly-server-sdk", "requires_python": ">=3.8", "version": "9.10.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries"], "name": "launchdarkly-server-sdk", "requires_python": ">=3.9", "version": "9.12.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries"], "name": "launchdarkly-server-sdk", "requires_python": ">=3.8", "version": "9.8.1", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries"], "name": "launchdarkly-server-sdk", "requires_python": ">=3.8", "version": "9.9.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Pydantic", "Framework :: Pydantic :: 1", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": ">=3.8,<4.0", "version": "2.0.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": "<4.0,>=3.8", "version": "2.12.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": "<4.0,>=3.8", "version": "2.17.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": "<4.0,>=3.8", "version": "2.6.4", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: System :: Logging"], "name": "loguru", "requires_python": "<4.0,>=3.5", "version": "0.7.3", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.7.1", "version": "1.0.1", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.108.2", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.7.1", "version": "1.37.2", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.73.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.0.19", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.1.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.2.11", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.3.1", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3"], "name": "openfeature-sdk", "requires_python": ">=3.8", "version": "0.7.5", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3"], "name": "openfeature-sdk", "requires_python": ">=3.9", "version": "0.8.3", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "pure-eval", "requires_python": "", "version": "0.0.3", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "pure-eval", "requires_python": "", "version": "0.1.1", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "pure-eval", "requires_python": null, "version": "0.2.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Topic :: Database"], "name": "pymongo", "requires_python": null, "version": "0.6", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.4", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.1", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: Jython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": null, "version": "2.8.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": "", "version": "3.13.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": null, "version": "3.2.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": "", "version": "3.4.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": "", "version": "3.5.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": "", "version": "3.6.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": ">=3.6", "version": "4.0.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database", "Typing :: Typed"], "name": "pymongo", "requires_python": ">=3.9", "version": "4.15.1", "yanked": false}} +{"info": {"classifiers": ["Framework :: Pylons", "Intended Audience :: Developers", "License :: Repoze Public License", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI"], "name": "pyramid", "requires_python": null, "version": "1.0.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 6 - Mature", "Framework :: Pyramid", "Intended Audience :: Developers", "License :: Repoze Public License", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI"], "name": "pyramid", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", "version": "1.10.8", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 6 - Mature", "Framework :: Pyramid", "Intended Audience :: Developers", "License :: Repoze Public License", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI"], "name": "pyramid", "requires_python": "", "version": "1.6.5", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 6 - Mature", "Framework :: Pyramid", "Intended Audience :: Developers", "License :: Repoze Public License", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI"], "name": "pyramid", "requires_python": "", "version": "1.7.6", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 6 - Mature", "Framework :: Pyramid", "Intended Audience :: Developers", "License :: Repoze Public License", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI"], "name": "pyramid", "requires_python": "", "version": "1.8.6", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 6 - Mature", "Framework :: Pyramid", "Intended Audience :: Developers", "License :: Repoze Public License", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI"], "name": "pyramid", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", "version": "1.9.4", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 6 - Mature", "Framework :: Pyramid", "Intended Audience :: Developers", "License :: Repoze Public License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI"], "name": "pyramid", "requires_python": ">=3.6", "version": "2.0.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "pyspark", "requires_python": "", "version": "2.1.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "pyspark", "requires_python": "", "version": "2.3.4", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "pyspark", "requires_python": "", "version": "2.4.8", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "pyspark", "requires_python": "", "version": "3.0.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.6", "version": "3.1.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.8", "version": "3.5.6", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.9", "version": "4.0.1", "yanked": false}} +{"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": ">=3.9", "version": "2.49.2", "yanked": false}} +{"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": "", "version": "2.7.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python"], "name": "redis", "requires_python": null, "version": "0.6.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6"], "name": "redis", "requires_python": "", "version": "2.10.6", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3"], "name": "redis", "requires_python": null, "version": "2.9.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6"], "name": "redis", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "version": "3.0.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "redis", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "version": "3.2.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "redis", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", "version": "3.5.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "redis", "requires_python": ">=3.6", "version": "4.0.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "redis", "requires_python": ">=3.7", "version": "4.6.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "redis", "requires_python": ">=3.8", "version": "5.3.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "redis", "requires_python": ">=3.9", "version": "6.4.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "redis", "requires_python": ">=3.9", "version": "7.0.0b1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4"], "name": "redis-py-cluster", "requires_python": null, "version": "0.1.0", "yanked": false}} +{"info": {"classifiers": [], "name": "redis-py-cluster", "requires_python": null, "version": "1.1.0", "yanked": false}} +{"info": {"classifiers": [], "name": "redis-py-cluster", "requires_python": null, "version": "1.2.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6"], "name": "redis-py-cluster", "requires_python": "", "version": "1.3.6", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "redis-py-cluster", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "version": "2.0.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "redis-py-cluster", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4", "version": "2.1.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3"], "name": "requests", "requires_python": null, "version": "2.0.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "requests", "requires_python": null, "version": "2.10.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "requests", "requires_python": null, "version": "2.11.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "requests", "requires_python": "", "version": "2.12.5", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "requests", "requires_python": "", "version": "2.16.5", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries"], "name": "requests", "requires_python": ">=3.9", "version": "2.32.5", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5"], "name": "requests", "requires_python": null, "version": "2.8.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": "", "version": "0.10.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": "", "version": "0.13.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": "", "version": "0.6.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": "", "version": "0.7.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": "", "version": "0.8.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": ">=2.7", "version": "1.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": ">=3.7", "version": "1.16.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": ">=3.5", "version": "1.8.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "rq", "requires_python": ">=3.9", "version": "2.6.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6"], "name": "sanic", "requires_python": "", "version": "0.8.3", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "sanic", "requires_python": ">=3.6", "version": "20.12.7", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "sanic", "requires_python": ">=3.8", "version": "23.12.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "sanic", "requires_python": ">=3.8", "version": "25.3.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.6", "version": "0.16.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AnyIO", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.7", "version": "0.27.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AnyIO", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.8", "version": "0.38.6", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AnyIO", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.9", "version": "0.48.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Typing :: Typed"], "name": "starlite", "requires_python": ">=3.8,<4.0", "version": "1.48.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Typing :: Typed"], "name": "starlite", "requires_python": ">=3.8,<4.0", "version": "1.49.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Typing :: Typed"], "name": "starlite", "requires_python": ">=3.8,<4.0", "version": "1.50.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Typing :: Typed"], "name": "starlite", "requires_python": "<4.0,>=3.8", "version": "1.51.16", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries"], "name": "statsig", "requires_python": ">=3.7", "version": "0.55.3", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries"], "name": "statsig", "requires_python": ">=3.7", "version": "0.58.4", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries"], "name": "statsig", "requires_python": ">=3.7", "version": "0.61.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries"], "name": "statsig", "requires_python": ">=3.7", "version": "0.64.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "strawberry-graphql", "requires_python": ">=3.8,<4.0", "version": "0.209.8", "yanked": false}} +{"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.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "strawberry-graphql", "requires_python": "<4.0,>=3.8", "version": "0.233.3", "yanked": false}} +{"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.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "strawberry-graphql", "requires_python": "<4.0,>=3.9", "version": "0.257.0", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "strawberry-graphql", "requires_python": "<4.0,>=3.9", "version": "0.282.0", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "tornado", "requires_python": ">= 3.5", "version": "6.0.4", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "tornado", "requires_python": ">= 3.7", "version": "6.2", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "tornado", "requires_python": ">=3.8", "version": "6.4.2", "yanked": false}} +{"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "tornado", "requires_python": ">=3.9", "version": "6.5.2", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License (GPL)", "Natural Language :: English", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Spanish", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": null, "version": "1.2.10", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Italian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": "", "version": "4.4.27", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Italian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": "", "version": "4.6.22", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": "", "version": "4.8.18", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Operating System :: OS Independent", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.4", "version": "5.0.63", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: Finnish", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Indonesian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Natural Language :: Turkish", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.5", "version": "5.4.20", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: Finnish", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Indonesian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Natural Language :: Turkish", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.6", "version": "5.8.16", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: Finnish", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Indonesian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Romanian", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Natural Language :: Turkish", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.6", "version": "6.2.14", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: Finnish", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Indonesian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Romanian", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Natural Language :: Turkish", "Natural Language :: Ukrainian", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.8", "version": "6.8.17", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: Finnish", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Indonesian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Romanian", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Natural Language :: Turkish", "Natural Language :: Ukrainian", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.9", "version": "7.6.7", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "typer", "requires_python": ">=3.7", "version": "0.15.4", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "typer", "requires_python": ">=3.7", "version": "0.16.1", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "typer", "requires_python": ">=3.7", "version": "0.17.5", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "typer", "requires_python": ">=3.8", "version": "0.19.2", "yanked": false}} diff --git a/tox.ini b/tox.ini index 906d075e2b..f80c86b51e 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-22T03:40:00.010016+00:00 +# Last generated: 2025-09-23T13:41:50.551689+00:00 [tox] requires = @@ -80,12 +80,12 @@ envlist = {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.11,py3.12}-openai-base-v1.37.2 {py3.8,py3.11,py3.12}-openai-base-v1.73.0 - {py3.8,py3.12,py3.13}-openai-base-v1.108.1 + {py3.8,py3.12,py3.13}-openai-base-v1.108.2 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.37.2 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.73.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.108.1 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.108.2 {py3.9,py3.12,py3.13}-langgraph-v0.6.7 {py3.10,py3.12,py3.13}-langgraph-v1.0.0a3 @@ -105,7 +105,7 @@ envlist = {py3.6,py3.7}-boto3-v1.12.49 {py3.6,py3.9,py3.10}-boto3-v1.20.54 {py3.7,py3.11,py3.12}-boto3-v1.28.85 - {py3.9,py3.12,py3.13}-boto3-v1.40.35 + {py3.9,py3.12,py3.13}-boto3-v1.40.36 {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.6,py3.7,py3.8}-chalice-v1.21.9 @@ -204,8 +204,7 @@ envlist = {py3.7}-beam-v2.14.0 {py3.7,py3.8}-beam-v2.32.0 {py3.8,py3.10,py3.11}-beam-v2.50.0 - {py3.9,py3.12,py3.13}-beam-v2.67.0 - {py3.9,py3.12,py3.13}-beam-v2.68.0rc2 + {py3.9,py3.12,py3.13}-beam-v2.68.0 {py3.6,py3.7,py3.8}-celery-v4.4.7 {py3.8,py3.12,py3.13}-celery-v5.5.3 @@ -317,7 +316,7 @@ envlist = {py3.7,py3.12,py3.13}-typer-v0.15.4 {py3.7,py3.12,py3.13}-typer-v0.16.1 {py3.7,py3.12,py3.13}-typer-v0.17.5 - {py3.7,py3.12,py3.13}-typer-v0.19.1 + {py3.8,py3.12,py3.13}-typer-v0.19.2 @@ -408,7 +407,7 @@ deps = openai-base-v1.0.1: openai==1.0.1 openai-base-v1.37.2: openai==1.37.2 openai-base-v1.73.0: openai==1.73.0 - openai-base-v1.108.1: openai==1.108.1 + openai-base-v1.108.2: openai==1.108.2 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 @@ -417,7 +416,7 @@ deps = openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.37.2: openai==1.37.2 openai-notiktoken-v1.73.0: openai==1.73.0 - openai-notiktoken-v1.108.1: openai==1.108.1 + openai-notiktoken-v1.108.2: openai==1.108.2 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.37.2: httpx<0.28 @@ -442,7 +441,7 @@ deps = boto3-v1.12.49: boto3==1.12.49 boto3-v1.20.54: boto3==1.20.54 boto3-v1.28.85: boto3==1.28.85 - boto3-v1.40.35: boto3==1.40.35 + boto3-v1.40.36: boto3==1.40.36 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 @@ -576,8 +575,7 @@ deps = beam-v2.14.0: apache-beam==2.14.0 beam-v2.32.0: apache-beam==2.32.0 beam-v2.50.0: apache-beam==2.50.0 - beam-v2.67.0: apache-beam==2.67.0 - beam-v2.68.0rc2: apache-beam==2.68.0rc2 + beam-v2.68.0: apache-beam==2.68.0 celery-v4.4.7: celery==4.4.7 celery-v5.5.3: celery==5.5.3 @@ -788,7 +786,7 @@ deps = typer-v0.15.4: typer==0.15.4 typer-v0.16.1: typer==0.16.1 typer-v0.17.5: typer==0.17.5 - typer-v0.19.1: typer==0.19.1 + typer-v0.19.2: typer==0.19.2 From c3d367202427233757f5275271873b4f4e60a825 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 24 Sep 2025 10:37:40 +0200 Subject: [PATCH 81/88] chore: Clean up toxgen (#4855) ### Description Polish toxgen a bit, improving wording, examples, formatting, removing obsolete parts. #### Issues #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- scripts/populate_tox/README.md | 64 +++++++++++----------------- scripts/populate_tox/populate_tox.py | 49 ++++++++++++--------- scripts/populate_tox/releases.jsonl | 10 ++--- scripts/populate_tox/tox.jinja | 17 +------- tox.ini | 43 +++++++------------ 5 files changed, 76 insertions(+), 107 deletions(-) diff --git a/scripts/populate_tox/README.md b/scripts/populate_tox/README.md index 8014acff3f..68e72a8ca7 100644 --- a/scripts/populate_tox/README.md +++ b/scripts/populate_tox/README.md @@ -16,8 +16,8 @@ The `populate_tox.py` script fills out the auto-generated part of that template. It does this by querying PyPI for each framework's package and its metadata and then determining which versions make sense to test to get good coverage. -The lowest supported and latest version of a framework are always tested, with -a number of releases in between: +By default, the lowest supported and latest version of a framework are always +tested, with a number of releases in between: - If the package has majors, we pick the highest version of each major. - If the package doesn't have multiple majors, we pick two versions in between lowest and highest. @@ -34,7 +34,8 @@ the main package (framework, library) to test with; any additional test dependencies, optionally gated behind specific conditions; and optionally the Python versions to test on. -Constraints are defined using the format specified below. The following sections describe each key. +Constraints are defined using the format specified below. The following sections +describe each key. ``` integration_name: { @@ -43,7 +44,7 @@ integration_name: { rule1: [package1, package2, ...], rule2: [package3, package4, ...], }, - "python": python_version_specifier, + "python": python_version_specifier | dict[package_version_specifier, python_version_specifier], "include": package_version_specifier, "integration_name": integration_name, "num_versions": int, @@ -102,15 +103,14 @@ Python versions, you can say: ... } ``` + This key is optional. ### `python` Sometimes, the whole test suite should only run on specific Python versions. -This can be achieved via the `python` key. - -There are two variants how to define the Python versions to run the test suite -on. +This can be achieved via the `python` key. There are two variants how to define +the Python versions to run the test suite on. If you want the test suite to only be run on specific Python versions, you can set `python` to a version specifier. For example, if you want AIOHTTP tests to @@ -142,18 +142,17 @@ say: The `python` key is optional, and when possible, it should be omitted. The script should automatically detect which Python versions the package supports. However, if a package has broken metadata or the SDK is explicitly not supporting some -packages on specific Python versions (because of, for example, broken context -vars), the `python` key can be used. +packages on specific Python versions, the `python` key can be used. ### `include` -Sometimes we only want to consider testing some specific versions of packages. -For example, the Starlite package has two alpha prereleases of version 2.0.0, but -we do not want to test these, since Starlite 2.0 was renamed to Litestar. +Sometimes we only want to test specific versions of packages. For example, the +Starlite package has two alpha prereleases of version 2.0.0, but we do not want +to test these, since Starlite 2.0 was renamed to Litestar. The value of the `include` key expects a version specifier defining which versions should be considered for testing. For example, since we only want to test -versions below 2.x in Starlite, we can use +versions below 2.x in Starlite, we can use: ```python "starlite": { @@ -178,13 +177,21 @@ be expressed like so: Sometimes, the name of the test suite doesn't match the name of the integration. For example, we have the `openai_base` and `openai_notiktoken` test suites, both -of which are actually testing the `openai` integration. If this is the case, you can use the `integration_name` key to define the name of the integration. If not provided, it will default to the name of the test suite. +of which are actually testing the `openai` integration. If this is the case, you +can use the `integration_name` key to define the name of the integration. If not +provided, it will default to the name of the test suite. -Linking an integration to a test suite allows the script to access integration configuration like for example the minimum version defined in `sentry_sdk/integrations/__init__.py`. +Linking an integration to a test suite allows the script to access integration +configuration like, for example, the minimum supported version defined in +`sentry_sdk/integrations/__init__.py`. ### `num_versions` -With this option you can tweak the default version picking behavior by specifying how many package versions should be tested. It accepts an integer equal to or greater than 2, as the oldest and latest supported versions will always be picked. Additionally, if there is a recent prerelease, it'll also always be picked (this doesn't count towards `num_versions`). +With this option you can tweak the default version picking behavior by specifying +how many package versions should be tested. It accepts an integer equal to or +greater than 2, as the oldest and latest supported versions will always be +picked. Additionally, if there is a recent prerelease, it'll also always be +picked (this doesn't count towards `num_versions`). ## How-Tos @@ -202,26 +209,3 @@ With this option you can tweak the default version picking behavior by specifyin `scripts/split_tox_gh_actions/split_tox_gh_actions.py`. 4. Add the `TESTPATH` for the test suite in `tox.jinja`'s `setenv` section. 5. Run `scripts/generate-test-files.sh` and commit the changes. - -### Migrate a test suite to populate_tox.py - -A handful of integration test suites are still hardcoded. The goal is to migrate -them all to `populate_tox.py` over time. - -1. Remove the integration from the `IGNORE` list in `populate_tox.py`. -2. Remove the hardcoded entries for the integration from the `envlist` and `deps` sections of `tox.jinja`. -3. Run `scripts/generate-test-files.sh`. -4. Run the test suite, either locally or by creating a PR. -5. Address any test failures that happen. - -You might have to introduce additional version bounds on the dependencies of the -package. Try to determine the source of the failure and address it. - -Common scenarios: -- An old version of the tested package installs a dependency without defining - an upper version bound on it. A new version of the dependency is installed that - is incompatible with the package. In this case you need to determine which - versions of the dependency don't contain the breaking change and restrict this - in `TEST_SUITE_CONFIG`. -- Tests are failing on an old Python version. In this case first double-check - whether we were even testing them on that version in the original `tox.ini`. diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 28a9fe8334..070ce82492 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -1,5 +1,7 @@ """ This script populates tox.ini automatically using release data from PyPI. + +See scripts/populate_tox/README.md for more info. """ import functools @@ -123,6 +125,7 @@ def _save_to_cache(package: str, version: Version, release: Optional[dict]) -> N releases_cache.write(json.dumps(_normalize_release(release)) + "\n") CACHE[_normalize_name(package)][str(version)] = release + CACHE[_normalize_name(package)][str(version)]["_accessed"] = True def _prefilter_releases( @@ -150,7 +153,8 @@ def _prefilter_releases( min_supported = Version(".".join(map(str, min_supported))) else: print( - f" {integration} doesn't have a minimum version defined in sentry_sdk/integrations/__init__.py. Consider defining one" + f" {integration} doesn't have a minimum version defined in " + f"sentry_sdk/integrations/__init__.py. Consider defining one" ) include_versions = None @@ -225,7 +229,8 @@ def get_supported_releases( Get a list of releases that are currently supported by the SDK. This takes into account a handful of parameters (Python support, the lowest - version we've defined for the framework, the date of the release). + supported version we've defined for the framework, optionally the date + of the release). We return the list of supported releases and optionally also the newest prerelease, if it should be tested (meaning it's for a version higher than @@ -272,9 +277,9 @@ def pick_releases_to_test( ) -> list[Version]: """Pick a handful of releases to test from a sorted list of supported releases.""" # If the package has majors (or major-like releases, even if they don't do - # semver), we want to make sure we're testing them all (unless there's too - # many). If not, we just pick the oldest, the newest, and a couple - # in between. + # semver), we want to make sure we're testing them all. If it doesn't have + # multiple majors, we just pick the oldest, the newest, and a couple of + # releases in between. # # If there is a relevant prerelease, also test that in addition to the above. num_versions = TEST_SUITE_CONFIG[integration].get("num_versions") @@ -342,7 +347,7 @@ def supported_python_versions( custom_supported_versions: Optional[ Union[SpecifierSet, dict[SpecifierSet, SpecifierSet]] ] = None, - version: Optional[Version] = None, + release_version: Optional[Version] = None, ) -> list[Version]: """ Get the intersection of Python versions supported by the package and the SDK. @@ -362,6 +367,12 @@ def supported_python_versions( on Python 3.7, so we can provide this as `custom_supported_versions`. The result of this function will then by the intersection of all three, i.e., [3.7]. + - The Python SDK supports Python 3.6-3.13. The package supports 3.5-3.8. + Additionally, we have a limitation in place to only test this framework on + Python 3.5 if the framework version is <2.0. `custom_supported_versions` + will contain this restriction, and `release_version` will contain the + version of the package we're currently looking at, to determine whether the + <2.0 restriction applies in this case. """ supported = [] @@ -377,11 +388,11 @@ def supported_python_versions( if curr in custom_supported_versions: supported.append(curr) - elif version is not None and isinstance( + elif release_version is not None and isinstance( custom_supported_versions, dict ): for v, py in custom_supported_versions.items(): - if version in v: + if release_version in v: if curr in py: supported.append(curr) break @@ -435,8 +446,10 @@ def _parse_python_versions_from_classifiers(classifiers: list[str]) -> list[Vers def determine_python_versions(pypi_data: dict) -> Union[SpecifierSet, list[Version]]: """ - Given data from PyPI's release endpoint, determine the Python versions supported by the package - from the Python version classifiers, when present, or from `requires_python` if there are no classifiers. + Determine the Python versions supported by the package from PyPI data. + + We're looking at Python version classifiers, if present, and + `requires_python` if there are no classifiers. """ try: classifiers = pypi_data["info"]["classifiers"] @@ -453,7 +466,7 @@ def determine_python_versions(pypi_data: dict) -> Union[SpecifierSet, list[Versi # We only use `requires_python` if there are no classifiers. This is because # `requires_python` doesn't tell us anything about the upper bound, which - # depends on when the release first came out + # implicitly depends on when the release first came out. try: requires_python = pypi_data["info"]["requires_python"] except (AttributeError, KeyError): @@ -666,7 +679,8 @@ def main(fail_on_changes: bool = False) -> None: # timestamp so that we don't fail CI on a PR just because a new package # version was released, leading to unrelated changes in tox.ini. print( - f"Since we're in fail_on_changes mode, we're only considering releases before the last tox.ini update at {last_updated.isoformat()}." + f"Since we're in fail_on_changes mode, we're only considering " + f"releases before the last tox.ini update at {last_updated.isoformat()}." ) global MIN_PYTHON_VERSION, MAX_PYTHON_VERSION @@ -789,19 +803,16 @@ def main(fail_on_changes: bool = False) -> None: Please don't make manual changes to `tox.ini`. Instead, make the changes to the `tox.jinja` template and/or the `populate_tox.py` - script (as applicable) and regenerate the `tox.ini` file with: - - python -m venv toxgen.env - . toxgen.env/bin/activate - pip install -r scripts/populate_tox/requirements.txt - python scripts/populate_tox/populate_tox.py + script (as applicable) and regenerate the `tox.ini` file by + running scripts/generate-test-files.sh """ ) ) print("Done checking tox.ini. Looking good!") else: print( - "Done generating tox.ini. Make sure to also update the CI YAML files to reflect the new test targets." + "Done generating tox.ini. Make sure to also update the CI YAML " + "files to reflect the new test targets." ) diff --git a/scripts/populate_tox/releases.jsonl b/scripts/populate_tox/releases.jsonl index 814d0c15b9..5940eb28c1 100644 --- a/scripts/populate_tox/releases.jsonl +++ b/scripts/populate_tox/releases.jsonl @@ -56,7 +56,7 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "boto3", "requires_python": "", "version": "1.12.49", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "boto3", "requires_python": ">= 3.6", "version": "1.20.54", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "boto3", "requires_python": ">= 3.7", "version": "1.28.85", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9"], "name": "boto3", "requires_python": ">=3.9", "version": "1.40.36", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9"], "name": "boto3", "requires_python": ">=3.9", "version": "1.40.37", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", "Topic :: Internet :: WWW/HTTP :: WSGI :: Server", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "bottle", "requires_python": "", "version": "0.12.25", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", "Topic :: Internet :: WWW/HTTP :: WSGI :: Server", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "bottle", "requires_python": null, "version": "0.13.4", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Object Brokering", "Topic :: System :: Distributed Computing"], "name": "celery", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", "version": "4.4.7", "yanked": false}} @@ -110,7 +110,7 @@ {"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.24.7", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.28.1", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.32.6", "yanked": false}} -{"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.35.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.35.1", "yanked": false}} {"info": {"classifiers": ["License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.9"], "name": "langchain", "requires_python": "<4.0,>=3.8.1", "version": "0.1.20", "yanked": false}} {"info": {"classifiers": ["License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.9"], "name": "langchain", "requires_python": "<4.0,>=3.8.1", "version": "0.2.17", "yanked": false}} {"info": {"classifiers": [], "name": "langchain", "requires_python": "<4.0,>=3.9", "version": "0.3.27", "yanked": false}} @@ -126,13 +126,13 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": "<4.0,>=3.8", "version": "2.6.4", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: System :: Logging"], "name": "loguru", "requires_python": "<4.0,>=3.5", "version": "0.7.3", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.7.1", "version": "1.0.1", "yanked": false}} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.108.2", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.109.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.7.1", "version": "1.37.2", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.73.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.0.19", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.1.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.2.11", "yanked": false}} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.3.1", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.3.2", "yanked": false}} {"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3"], "name": "openfeature-sdk", "requires_python": ">=3.8", "version": "0.7.5", "yanked": false}} {"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3"], "name": "openfeature-sdk", "requires_python": ">=3.9", "version": "0.8.3", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "pure-eval", "requires_python": "", "version": "0.0.3", "yanked": false}} @@ -159,7 +159,7 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "pyspark", "requires_python": "", "version": "2.4.8", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "pyspark", "requires_python": "", "version": "3.0.3", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.6", "version": "3.1.3", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.8", "version": "3.5.6", "yanked": false}} +{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.8", "version": "3.5.7", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed"], "name": "pyspark", "requires_python": ">=3.9", "version": "4.0.1", "yanked": false}} {"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": ">=3.9", "version": "2.49.2", "yanked": false}} {"info": {"classifiers": ["Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "ray", "requires_python": "", "version": "2.7.2", "yanked": false}} diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 98e14adb20..9aa942ebe4 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -24,17 +24,6 @@ envlist = {py3.6,py3.8,py3.10,py3.11,py3.12}-gevent # === Integrations === - # General format is {pythonversion}-{integrationname}-v{frameworkversion} - # 1 blank line between different integrations - # Each framework version should only be mentioned once. I.e: - # {py3.7,py3.10}-django-v{3.2} - # {py3.10}-django-v{4.0} - # instead of: - # {py3.7}-django-v{3.2} - # {py3.7,py3.10}-django-v{3.2,4.0} - # - # At a minimum, we should test against at least the lowest - # and the latest supported version of a framework. # Asgi {py3.7,py3.12,py3.13}-asgi @@ -55,8 +44,7 @@ envlist = {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel # === Integrations - Auto-generated === - # These come from the populate_tox.py script. Eventually we should move all - # integration tests there. + # These come from the populate_tox.py script. {% for group, integrations in groups.items() %} # ~~~ {{ group }} ~~~ @@ -121,8 +109,7 @@ deps = potel: -e .[opentelemetry-experimental] # === Integrations - Auto-generated === - # These come from the populate_tox.py script. Eventually we should move all - # integration tests there. + # These come from the populate_tox.py script. {% for group, integrations in groups.items() %} # ~~~ {{ group }} ~~~ diff --git a/tox.ini b/tox.ini index f80c86b51e..7520a4cb1f 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-23T13:41:50.551689+00:00 +# Last generated: 2025-09-24T08:25:49.003370+00:00 [tox] requires = @@ -24,17 +24,6 @@ envlist = {py3.6,py3.8,py3.10,py3.11,py3.12}-gevent # === Integrations === - # General format is {pythonversion}-{integrationname}-v{frameworkversion} - # 1 blank line between different integrations - # Each framework version should only be mentioned once. I.e: - # {py3.7,py3.10}-django-v{3.2} - # {py3.10}-django-v{4.0} - # instead of: - # {py3.7}-django-v{3.2} - # {py3.7,py3.10}-django-v{3.2,4.0} - # - # At a minimum, we should test against at least the lowest - # and the latest supported version of a framework. # Asgi {py3.7,py3.12,py3.13}-asgi @@ -55,8 +44,7 @@ envlist = {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel # === Integrations - Auto-generated === - # These come from the populate_tox.py script. Eventually we should move all - # integration tests there. + # These come from the populate_tox.py script. # ~~~ AI ~~~ {py3.8,py3.11,py3.12}-anthropic-v0.16.0 @@ -80,12 +68,12 @@ envlist = {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.11,py3.12}-openai-base-v1.37.2 {py3.8,py3.11,py3.12}-openai-base-v1.73.0 - {py3.8,py3.12,py3.13}-openai-base-v1.108.2 + {py3.8,py3.12,py3.13}-openai-base-v1.109.0 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.37.2 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.73.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.108.2 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.109.0 {py3.9,py3.12,py3.13}-langgraph-v0.6.7 {py3.10,py3.12,py3.13}-langgraph-v1.0.0a3 @@ -93,19 +81,19 @@ envlist = {py3.10,py3.11,py3.12}-openai_agents-v0.0.19 {py3.10,py3.12,py3.13}-openai_agents-v0.1.0 {py3.10,py3.12,py3.13}-openai_agents-v0.2.11 - {py3.10,py3.12,py3.13}-openai_agents-v0.3.1 + {py3.10,py3.12,py3.13}-openai_agents-v0.3.2 {py3.8,py3.10,py3.11}-huggingface_hub-v0.24.7 {py3.8,py3.12,py3.13}-huggingface_hub-v0.28.1 {py3.8,py3.12,py3.13}-huggingface_hub-v0.32.6 - {py3.8,py3.12,py3.13}-huggingface_hub-v0.35.0 + {py3.8,py3.12,py3.13}-huggingface_hub-v0.35.1 # ~~~ Cloud ~~~ {py3.6,py3.7}-boto3-v1.12.49 {py3.6,py3.9,py3.10}-boto3-v1.20.54 {py3.7,py3.11,py3.12}-boto3-v1.28.85 - {py3.9,py3.12,py3.13}-boto3-v1.40.36 + {py3.9,py3.12,py3.13}-boto3-v1.40.37 {py3.6,py3.7,py3.8}-chalice-v1.16.0 {py3.6,py3.7,py3.8}-chalice-v1.21.9 @@ -229,7 +217,7 @@ envlist = {py3.9,py3.12,py3.13}-rq-v2.6.0 {py3.8,py3.9}-spark-v3.0.3 - {py3.8,py3.10,py3.11}-spark-v3.5.6 + {py3.8,py3.10,py3.11}-spark-v3.5.7 {py3.9,py3.12,py3.13}-spark-v4.0.1 @@ -372,8 +360,7 @@ deps = potel: -e .[opentelemetry-experimental] # === Integrations - Auto-generated === - # These come from the populate_tox.py script. Eventually we should move all - # integration tests there. + # These come from the populate_tox.py script. # ~~~ AI ~~~ anthropic-v0.16.0: anthropic==0.16.0 @@ -407,7 +394,7 @@ deps = openai-base-v1.0.1: openai==1.0.1 openai-base-v1.37.2: openai==1.37.2 openai-base-v1.73.0: openai==1.73.0 - openai-base-v1.108.2: openai==1.108.2 + openai-base-v1.109.0: openai==1.109.0 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 @@ -416,7 +403,7 @@ deps = openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.37.2: openai==1.37.2 openai-notiktoken-v1.73.0: openai==1.73.0 - openai-notiktoken-v1.108.2: openai==1.108.2 + openai-notiktoken-v1.109.0: openai==1.109.0 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.37.2: httpx<0.28 @@ -427,13 +414,13 @@ deps = openai_agents-v0.0.19: openai-agents==0.0.19 openai_agents-v0.1.0: openai-agents==0.1.0 openai_agents-v0.2.11: openai-agents==0.2.11 - openai_agents-v0.3.1: openai-agents==0.3.1 + openai_agents-v0.3.2: openai-agents==0.3.2 openai_agents: pytest-asyncio huggingface_hub-v0.24.7: huggingface_hub==0.24.7 huggingface_hub-v0.28.1: huggingface_hub==0.28.1 huggingface_hub-v0.32.6: huggingface_hub==0.32.6 - huggingface_hub-v0.35.0: huggingface_hub==0.35.0 + huggingface_hub-v0.35.1: huggingface_hub==0.35.1 huggingface_hub: responses @@ -441,7 +428,7 @@ deps = boto3-v1.12.49: boto3==1.12.49 boto3-v1.20.54: boto3==1.20.54 boto3-v1.28.85: boto3==1.28.85 - boto3-v1.40.36: boto3==1.40.36 + boto3-v1.40.37: boto3==1.40.37 {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 @@ -608,7 +595,7 @@ deps = {py3.6,py3.7}-rq: fakeredis!=2.26.0 spark-v3.0.3: pyspark==3.0.3 - spark-v3.5.6: pyspark==3.5.6 + spark-v3.5.7: pyspark==3.5.7 spark-v4.0.1: pyspark==4.0.1 From d9811feda0867beca09a4b92fc142e2ea12702bb Mon Sep 17 00:00:00 2001 From: Alex Alderman Webb Date: Wed, 24 Sep 2025 12:36:56 +0200 Subject: [PATCH 82/88] docs: Update contributing guidelines with instructions to run tests with tox (#4857) Updating instructions to run unit tests with a little background about `tox` and some commands you can use to run the tests. Closes https://github.com/getsentry/sentry-python/issues/4548 --- CONTRIBUTING.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 313910fe56..89330087d9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,19 +74,37 @@ That's it. You should be ready to make changes, run tests, and make commits! If ## Running Tests -You can run all tests with the following command: +We test against a number of Python language and library versions, which are automatically generated and stored in the [tox.ini](tox.ini) file. The `envlist` defines the environments you can choose from when running tests, and correspond to package versions and environment variables. The `TESTPATH` environment variable, in turn, determines which tests are run. + +The tox CLI tool is required to run the tests locally. Follow [the installation instructions](https://tox.wiki/en/latest/installation.html) for tox. Dependencies are installed for you when you run the command below, but _you_ need to bring an appropriate Python interpreter. + +[Pyenv](https://github.com/pyenv/pyenv) is a cross-platform utility for managing Python versions. You can also use a conventional package manager, but not all versions may be distributed in the package manager of your choice. For macOS, Versions 3.8 and up can be installed with Homebrew. + +An environment consists of the Python major and minor version and the library name and version. The exception to the rule is that you can provide `common` instead of the library information. The environments tied to a specific library usually run the corresponding test suite, while `common` targets all tests but skips those that require uninstalled dependencies. + +To run Celery tests for version v5.5.3 of its Python library using a 3.12 interpreter, use ```bash -pytest tests/ +tox -p auto -o -e py3.12-celery-v5.5.3 ``` -If you would like to run the tests for a specific integration, use a command similar to the one below: +or to use the `common` environment, run ```bash -pytest -rs tests/integrations/flask/ # Replace "flask" with the specific integration you wish to test +tox -p auto -o -e py3.12-common ``` -**Hint:** Tests of integrations need additional dependencies. The switch `-rs` will show you why tests were skipped and what dependencies you need to install for the tests to run. (You can also consult the [tox.ini](tox.ini) file to see what dependencies are installed for each integration) +To select specific tests, you can forward arguments to `pytest` like so + +```bash +tox -p auto -o -e py3.12-celery-v5.5.3 -- -k test_transaction_events +``` + +In general, you use + +```bash +tox -p auto -o -e -- +``` ## Adding a New Integration From 4e1b96caa049ce3a9ec7ea0d18462337c771761a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 24 Sep 2025 15:10:01 +0200 Subject: [PATCH 83/88] chore: Slim down test matrix (#4856) ### Description Only test the lowest and highest supported version of some of our smaller integrations. #### Issues #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- .../workflows/test-integrations-graphql.yml | 2 +- scripts/populate_tox/config.py | 20 +++++ scripts/populate_tox/releases.jsonl | 30 -------- scripts/populate_tox/tox.jinja | 2 +- tox.ini | 75 +------------------ 5 files changed, 24 insertions(+), 105 deletions(-) diff --git a/.github/workflows/test-integrations-graphql.yml b/.github/workflows/test-integrations-graphql.yml index 81d809c94e..f1a43dc77d 100644 --- a/.github/workflows/test-integrations-graphql.yml +++ b/.github/workflows/test-integrations-graphql.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6","3.7","3.8","3.9","3.10","3.11","3.12","3.13"] + python-version: ["3.6","3.8","3.9","3.10","3.11","3.12","3.13"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 70cecd60d9..34ae680fad 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -28,6 +28,7 @@ "*": ["fastapi", "flask", "httpx"], }, "python": ">=3.8", + "num_versions": 2, }, "arq": { "package": "arq", @@ -35,6 +36,7 @@ "*": ["async-timeout", "pytest-asyncio", "fakeredis>=2.2.0,<2.8"], "<=0.23": ["pydantic<2"], }, + "num_versions": 2, }, "asyncpg": { "package": "asyncpg", @@ -46,6 +48,7 @@ "beam": { "package": "apache-beam", "python": ">=3.7", + "num_versions": 2, }, "boto3": { "package": "boto3", @@ -71,9 +74,11 @@ "deps": { "*": ["pytest-chalice"], }, + "num_versions": 2, }, "clickhouse_driver": { "package": "clickhouse-driver", + "num_versions": 2, }, "cohere": { "package": "cohere", @@ -100,6 +105,7 @@ }, "dramatiq": { "package": "dramatiq", + "num_versions": 2, }, "falcon": { "package": "falcon", @@ -135,6 +141,7 @@ }, "gql": { "package": "gql[all]", + "num_versions": 2, }, "graphene": { "package": "graphene", @@ -171,6 +178,7 @@ }, "huey": { "package": "huey", + "num_versions": 2, }, "huggingface_hub": { "package": "huggingface_hub", @@ -204,6 +212,7 @@ }, "launchdarkly": { "package": "launchdarkly-server-sdk", + "num_versions": 2, }, "litestar": { "package": "litestar", @@ -214,6 +223,7 @@ }, "loguru": { "package": "loguru", + "num_versions": 2, }, "openai-base": { "package": "openai", @@ -242,9 +252,11 @@ }, "openfeature": { "package": "openfeature-sdk", + "num_versions": 2, }, "pure_eval": { "package": "pure_eval", + "num_versions": 2, }, "pymongo": { "package": "pymongo", @@ -271,6 +283,7 @@ ], "py3.8": ["taskgroup==0.0.0a4"], }, + "num_versions": 2, }, "ray": { "package": "ray", @@ -288,6 +301,7 @@ }, "redis_py_cluster_legacy": { "package": "redis-py-cluster", + "num_versions": 2, }, "requests": { "package": "requests", @@ -352,12 +366,14 @@ }, "python": "<=3.11", "include": "!=2.0.0a1,!=2.0.0a2", # these are not relevant as there will never be a stable 2.0 release (starlite continues as litestar) + "num_versions": 2, }, "statsig": { "package": "statsig", "deps": { "*": ["typing_extensions"], }, + "num_versions": 2, }, "strawberry": { "package": "strawberry-graphql[fastapi,flask]", @@ -365,6 +381,7 @@ "*": ["httpx"], "<=0.262.5": ["pydantic<2.11"], }, + "num_versions": 2, }, "tornado": { "package": "tornado", @@ -375,6 +392,7 @@ ], # https://github.com/tornadoweb/tornado/pull/3382 "py3.6": ["aiocontextvars"], }, + "num_versions": 2, }, "trytond": { "package": "trytond", @@ -385,8 +403,10 @@ }, "typer": { "package": "typer", + "num_versions": 2, }, "unleash": { "package": "UnleashClient", + "num_versions": 2, }, } diff --git a/scripts/populate_tox/releases.jsonl b/scripts/populate_tox/releases.jsonl index 5940eb28c1..fa82e6b1cd 100644 --- a/scripts/populate_tox/releases.jsonl +++ b/scripts/populate_tox/releases.jsonl @@ -12,16 +12,12 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "Flask", "requires_python": ">=3.8", "version": "2.3.3", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", "Typing :: Typed"], "name": "Flask", "requires_python": ">=3.9", "version": "3.1.2", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Quart", "requires_python": ">=3.7", "version": "0.16.3", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Quart", "requires_python": ">=3.7", "version": "0.17.0", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "Quart", "requires_python": ">=3.7", "version": "0.18.4", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Application Frameworks", "Typing :: Typed"], "name": "Quart", "requires_python": ">=3.9", "version": "0.20.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": "", "version": "1.2.19", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "version": "1.3.24", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7", "version": "1.4.54", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database :: Front-Ends"], "name": "SQLAlchemy", "requires_python": ">=3.7", "version": "2.0.43", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed"], "name": "UnleashClient", "requires_python": ">=3.8", "version": "6.0.1", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed"], "name": "UnleashClient", "requires_python": ">=3.8", "version": "6.1.0", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed"], "name": "UnleashClient", "requires_python": ">=3.8", "version": "6.2.2", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed"], "name": "UnleashClient", "requires_python": ">=3.8", "version": "6.3.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "aiohttp", "requires_python": ">=3.8", "version": "3.10.11", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "aiohttp", "requires_python": ">=3.9", "version": "3.12.15", "yanked": false}} @@ -37,17 +33,11 @@ {"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.15.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "version": "2.19.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.6", "version": "2.26.0", "yanked": false}} -{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.6", "version": "2.32.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.7", "version": "2.40.0", "yanked": false}} -{"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.8", "version": "2.50.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "apache-beam", "requires_python": ">=3.9", "version": "2.68.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": "", "version": "0.20.1", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": "", "version": "0.22", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": null, "version": "0.24.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "ariadne", "requires_python": ">=3.9", "version": "0.26.2", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "arq", "requires_python": ">=3.6", "version": "0.23", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "arq", "requires_python": ">=3.7", "version": "0.24.0", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "arq", "requires_python": ">=3.7", "version": "0.25.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration"], "name": "arq", "requires_python": ">=3.8", "version": "0.26.3", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database :: Front-Ends"], "name": "asyncpg", "requires_python": ">=3.5.0", "version": "0.23.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database :: Front-Ends"], "name": "asyncpg", "requires_python": ">=3.6.0", "version": "0.25.0", "yanked": false}} @@ -63,16 +53,12 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Celery", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Object Brokering", "Topic :: System :: Distributed Computing"], "name": "celery", "requires_python": ">=3.8", "version": "5.5.3", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Framework :: Celery", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Object Brokering", "Topic :: System :: Distributed Computing"], "name": "celery", "requires_python": ">=3.8", "version": "5.6.0b1", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "chalice", "requires_python": "", "version": "1.16.0", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "chalice", "requires_python": "", "version": "1.21.9", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "chalice", "requires_python": "", "version": "1.26.6", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9"], "name": "chalice", "requires_python": null, "version": "1.32.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: SQL", "Topic :: Database", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "clickhouse-driver", "requires_python": "<4,>=3.7", "version": "0.2.9", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "cohere", "requires_python": "<4.0,>=3.9", "version": "5.13.12", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "cohere", "requires_python": "<4.0,>=3.9", "version": "5.18.0", "yanked": false}} {"info": {"classifiers": ["Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "cohere", "requires_python": "<4.0,>=3.8", "version": "5.4.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "cohere", "requires_python": "<4.0,>=3.8", "version": "5.9.4", "yanked": false}} -{"info": {"classifiers": ["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: System :: Distributed Computing"], "name": "dramatiq", "requires_python": ">=3.6", "version": "1.12.3", "yanked": false}} -{"info": {"classifiers": ["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: System :: Distributed Computing"], "name": "dramatiq", "requires_python": ">=3.7", "version": "1.15.0", "yanked": false}} {"info": {"classifiers": ["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: System :: Distributed Computing"], "name": "dramatiq", "requires_python": ">=3.9", "version": "1.18.0", "yanked": false}} {"info": {"classifiers": ["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: System :: Distributed Computing"], "name": "dramatiq", "requires_python": ">=3.5", "version": "1.9.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: Jython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "falcon", "requires_python": "", "version": "1.4.1", "yanked": false}} @@ -84,7 +70,6 @@ {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.6.1", "version": "0.79.1", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "fastapi", "requires_python": ">=3.7", "version": "0.92.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": "", "version": "3.4.1", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": null, "version": "3.5.3", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": ">=3.8.1", "version": "4.0.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries"], "name": "gql", "requires_python": ">=3.8.1", "version": "4.2.0b0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries"], "name": "graphene", "requires_python": "", "version": "3.3", "yanked": false}} @@ -104,8 +89,6 @@ {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python"], "name": "huey", "requires_python": "", "version": "1.7.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python"], "name": "huey", "requires_python": "", "version": "2.0.1", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "huey", "requires_python": "", "version": "2.1.3", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "huey", "requires_python": "", "version": "2.2.0", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "name": "huey", "requires_python": "", "version": "2.3.2", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "huey", "requires_python": null, "version": "2.5.3", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.24.7", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "name": "huggingface-hub", "requires_python": ">=3.8.0", "version": "0.28.1", "yanked": false}} @@ -116,10 +99,8 @@ {"info": {"classifiers": [], "name": "langchain", "requires_python": "<4.0,>=3.9", "version": "0.3.27", "yanked": false}} {"info": {"classifiers": [], "name": "langgraph", "requires_python": ">=3.9", "version": "0.6.7", "yanked": false}} {"info": {"classifiers": [], "name": "langgraph", "requires_python": ">=3.10", "version": "1.0.0a3", "yanked": false}} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries"], "name": "launchdarkly-server-sdk", "requires_python": ">=3.8", "version": "9.10.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries"], "name": "launchdarkly-server-sdk", "requires_python": ">=3.9", "version": "9.12.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries"], "name": "launchdarkly-server-sdk", "requires_python": ">=3.8", "version": "9.8.1", "yanked": false}} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries"], "name": "launchdarkly-server-sdk", "requires_python": ">=3.8", "version": "9.9.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Pydantic", "Framework :: Pydantic :: 1", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": ">=3.8,<4.0", "version": "2.0.1", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": "<4.0,>=3.8", "version": "2.12.1", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": "<4.0,>=3.8", "version": "2.17.0", "yanked": false}} @@ -136,7 +117,6 @@ {"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3"], "name": "openfeature-sdk", "requires_python": ">=3.8", "version": "0.7.5", "yanked": false}} {"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3"], "name": "openfeature-sdk", "requires_python": ">=3.9", "version": "0.8.3", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8"], "name": "pure-eval", "requires_python": "", "version": "0.0.3", "yanked": false}} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "pure-eval", "requires_python": "", "version": "0.1.1", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9"], "name": "pure-eval", "requires_python": null, "version": "0.2.3", "yanked": false}} {"info": {"classifiers": ["Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Topic :: Database"], "name": "pymongo", "requires_python": null, "version": "0.6", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.4", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.1", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: Jython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database"], "name": "pymongo", "requires_python": null, "version": "2.8.1", "yanked": false}} @@ -205,20 +185,12 @@ {"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AnyIO", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.8", "version": "0.38.6", "yanked": false}} {"info": {"classifiers": ["Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AnyIO", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP"], "name": "starlette", "requires_python": ">=3.9", "version": "0.48.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Typing :: Typed"], "name": "starlite", "requires_python": ">=3.8,<4.0", "version": "1.48.1", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Typing :: Typed"], "name": "starlite", "requires_python": ">=3.8,<4.0", "version": "1.49.0", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Typing :: Typed"], "name": "starlite", "requires_python": ">=3.8,<4.0", "version": "1.50.2", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Typing :: Typed"], "name": "starlite", "requires_python": "<4.0,>=3.8", "version": "1.51.16", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries"], "name": "statsig", "requires_python": ">=3.7", "version": "0.55.3", "yanked": false}} -{"info": {"classifiers": ["Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries"], "name": "statsig", "requires_python": ">=3.7", "version": "0.58.4", "yanked": false}} -{"info": {"classifiers": ["Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries"], "name": "statsig", "requires_python": ">=3.7", "version": "0.61.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries"], "name": "statsig", "requires_python": ">=3.7", "version": "0.64.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "strawberry-graphql", "requires_python": ">=3.8,<4.0", "version": "0.209.8", "yanked": false}} -{"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.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "strawberry-graphql", "requires_python": "<4.0,>=3.8", "version": "0.233.3", "yanked": false}} -{"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.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "strawberry-graphql", "requires_python": "<4.0,>=3.9", "version": "0.257.0", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules"], "name": "strawberry-graphql", "requires_python": "<4.0,>=3.9", "version": "0.282.0", "yanked": false}} {"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "tornado", "requires_python": ">= 3.5", "version": "6.0.4", "yanked": false}} -{"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "tornado", "requires_python": ">= 3.7", "version": "6.2", "yanked": false}} -{"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "tornado", "requires_python": ">=3.8", "version": "6.4.2", "yanked": false}} {"info": {"classifiers": ["License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy"], "name": "tornado", "requires_python": ">=3.9", "version": "6.5.2", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License (GPL)", "Natural Language :: English", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Spanish", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": null, "version": "1.2.10", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Italian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": "", "version": "4.4.27", "yanked": false}} @@ -231,6 +203,4 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: Finnish", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Indonesian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Romanian", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Natural Language :: Turkish", "Natural Language :: Ukrainian", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.8", "version": "6.8.17", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: No Input/Output (Daemon)", "Framework :: Tryton", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Natural Language :: Bulgarian", "Natural Language :: Catalan", "Natural Language :: Chinese (Simplified)", "Natural Language :: Czech", "Natural Language :: Dutch", "Natural Language :: English", "Natural Language :: Finnish", "Natural Language :: French", "Natural Language :: German", "Natural Language :: Hungarian", "Natural Language :: Indonesian", "Natural Language :: Italian", "Natural Language :: Persian", "Natural Language :: Polish", "Natural Language :: Portuguese (Brazilian)", "Natural Language :: Romanian", "Natural Language :: Russian", "Natural Language :: Slovenian", "Natural Language :: Spanish", "Natural Language :: Turkish", "Natural Language :: Ukrainian", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Application Frameworks"], "name": "trytond", "requires_python": ">=3.9", "version": "7.6.7", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "typer", "requires_python": ">=3.7", "version": "0.15.4", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "typer", "requires_python": ">=3.7", "version": "0.16.1", "yanked": false}} -{"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "typer", "requires_python": ">=3.7", "version": "0.17.5", "yanked": false}} {"info": {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "typer", "requires_python": ">=3.8", "version": "0.19.2", "yanked": false}} diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 9aa942ebe4..f47d5db692 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -7,7 +7,7 @@ # by "scripts/populate_tox/populate_tox.py". Any changes to the file should # be made in the template (if you want to change a hardcoded part of the file) # or in the script (if you want to change the auto-generated part). -# The file (and all resulting CI YAMLs) then need to be regenerated via +# The file (and all resulting CI YAMLs) then needs to be regenerated via # "scripts/generate-test-files.sh". # # Last generated: {{ updated }} diff --git a/tox.ini b/tox.ini index 7520a4cb1f..e0295707ab 100644 --- a/tox.ini +++ b/tox.ini @@ -7,10 +7,10 @@ # by "scripts/populate_tox/populate_tox.py". Any changes to the file should # be made in the template (if you want to change a hardcoded part of the file) # or in the script (if you want to change the auto-generated part). -# The file (and all resulting CI YAMLs) then need to be regenerated via +# The file (and all resulting CI YAMLs) then needs to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-09-24T08:25:49.003370+00:00 +# Last generated: 2025-09-24T10:52:00.519014+00:00 [tox] requires = @@ -96,8 +96,6 @@ envlist = {py3.9,py3.12,py3.13}-boto3-v1.40.37 {py3.6,py3.7,py3.8}-chalice-v1.16.0 - {py3.6,py3.7,py3.8}-chalice-v1.21.9 - {py3.6,py3.8,py3.9}-chalice-v1.26.6 {py3.9,py3.12,py3.13}-chalice-v1.32.0 @@ -130,32 +128,23 @@ envlist = # ~~~ Flags ~~~ {py3.8,py3.12,py3.13}-launchdarkly-v9.8.1 - {py3.8,py3.12,py3.13}-launchdarkly-v9.9.0 - {py3.8,py3.12,py3.13}-launchdarkly-v9.10.0 {py3.9,py3.12,py3.13}-launchdarkly-v9.12.0 {py3.8,py3.12,py3.13}-openfeature-v0.7.5 {py3.9,py3.12,py3.13}-openfeature-v0.8.3 {py3.7,py3.12,py3.13}-statsig-v0.55.3 - {py3.7,py3.12,py3.13}-statsig-v0.58.4 - {py3.7,py3.12,py3.13}-statsig-v0.61.0 {py3.7,py3.12,py3.13}-statsig-v0.64.0 {py3.8,py3.12,py3.13}-unleash-v6.0.1 - {py3.8,py3.12,py3.13}-unleash-v6.1.0 - {py3.8,py3.12,py3.13}-unleash-v6.2.2 {py3.8,py3.12,py3.13}-unleash-v6.3.0 # ~~~ GraphQL ~~~ {py3.8,py3.10,py3.11}-ariadne-v0.20.1 - {py3.8,py3.11,py3.12}-ariadne-v0.22 - {py3.8,py3.11,py3.12}-ariadne-v0.24.0 {py3.9,py3.12,py3.13}-ariadne-v0.26.2 {py3.6,py3.9,py3.10}-gql-v3.4.1 - {py3.7,py3.11,py3.12}-gql-v3.5.3 {py3.9,py3.12,py3.13}-gql-v4.0.0 {py3.9,py3.12,py3.13}-gql-v4.2.0b0 @@ -163,8 +152,6 @@ envlist = {py3.8,py3.12,py3.13}-graphene-v3.4.3 {py3.8,py3.10,py3.11}-strawberry-v0.209.8 - {py3.8,py3.11,py3.12}-strawberry-v0.233.3 - {py3.9,py3.12,py3.13}-strawberry-v0.257.0 {py3.9,py3.12,py3.13}-strawberry-v0.282.0 @@ -185,13 +172,9 @@ envlist = # ~~~ Tasks ~~~ {py3.7,py3.9,py3.10}-arq-v0.23 - {py3.7,py3.10,py3.11}-arq-v0.24.0 - {py3.7,py3.10,py3.11}-arq-v0.25.0 {py3.8,py3.11,py3.12}-arq-v0.26.3 {py3.7}-beam-v2.14.0 - {py3.7,py3.8}-beam-v2.32.0 - {py3.8,py3.10,py3.11}-beam-v2.50.0 {py3.9,py3.12,py3.13}-beam-v2.68.0 {py3.6,py3.7,py3.8}-celery-v4.4.7 @@ -199,13 +182,9 @@ envlist = {py3.8,py3.12,py3.13}-celery-v5.6.0b1 {py3.6,py3.7}-dramatiq-v1.9.0 - {py3.6,py3.8,py3.9}-dramatiq-v1.12.3 - {py3.7,py3.10,py3.11}-dramatiq-v1.15.0 {py3.9,py3.12,py3.13}-dramatiq-v1.18.0 {py3.6,py3.7}-huey-v2.1.3 - {py3.6,py3.7}-huey-v2.2.0 - {py3.6,py3.7}-huey-v2.3.2 {py3.6,py3.11,py3.12}-huey-v2.5.3 {py3.9,py3.10}-ray-v2.7.2 @@ -268,8 +247,6 @@ envlist = {py3.6,py3.10,py3.11}-pyramid-v2.0.2 {py3.7,py3.9,py3.10}-quart-v0.16.3 - {py3.7,py3.9,py3.10}-quart-v0.17.0 - {py3.7,py3.10,py3.11}-quart-v0.18.4 {py3.9,py3.12,py3.13}-quart-v0.20.0 {py3.6}-sanic-v0.8.3 @@ -278,13 +255,9 @@ envlist = {py3.9,py3.12,py3.13}-sanic-v25.3.0 {py3.8,py3.10,py3.11}-starlite-v1.48.1 - {py3.8,py3.10,py3.11}-starlite-v1.49.0 - {py3.8,py3.10,py3.11}-starlite-v1.50.2 {py3.8,py3.10,py3.11}-starlite-v1.51.16 {py3.6,py3.7,py3.8}-tornado-v6.0.4 - {py3.7,py3.9,py3.10}-tornado-v6.2 - {py3.8,py3.10,py3.11}-tornado-v6.4.2 {py3.9,py3.12,py3.13}-tornado-v6.5.2 @@ -292,7 +265,6 @@ envlist = {py3.6,py3.12,py3.13}-loguru-v0.7.3 {py3.6,py3.7,py3.8}-pure_eval-v0.0.3 - {py3.6,py3.8,py3.9}-pure_eval-v0.1.1 {py3.7,py3.12,py3.13}-pure_eval-v0.2.3 {py3.6}-trytond-v4.6.22 @@ -302,8 +274,6 @@ envlist = {py3.9,py3.12,py3.13}-trytond-v7.6.7 {py3.7,py3.12,py3.13}-typer-v0.15.4 - {py3.7,py3.12,py3.13}-typer-v0.16.1 - {py3.7,py3.12,py3.13}-typer-v0.17.5 {py3.8,py3.12,py3.13}-typer-v0.19.2 @@ -432,8 +402,6 @@ deps = {py3.7,py3.8}-boto3: urllib3<2.0.0 chalice-v1.16.0: chalice==1.16.0 - chalice-v1.21.9: chalice==1.21.9 - chalice-v1.26.6: chalice==1.26.6 chalice-v1.32.0: chalice==1.32.0 chalice: pytest-chalice @@ -474,36 +442,27 @@ deps = # ~~~ Flags ~~~ launchdarkly-v9.8.1: launchdarkly-server-sdk==9.8.1 - launchdarkly-v9.9.0: launchdarkly-server-sdk==9.9.0 - launchdarkly-v9.10.0: launchdarkly-server-sdk==9.10.0 launchdarkly-v9.12.0: launchdarkly-server-sdk==9.12.0 openfeature-v0.7.5: openfeature-sdk==0.7.5 openfeature-v0.8.3: openfeature-sdk==0.8.3 statsig-v0.55.3: statsig==0.55.3 - statsig-v0.58.4: statsig==0.58.4 - statsig-v0.61.0: statsig==0.61.0 statsig-v0.64.0: statsig==0.64.0 statsig: typing_extensions unleash-v6.0.1: UnleashClient==6.0.1 - unleash-v6.1.0: UnleashClient==6.1.0 - unleash-v6.2.2: UnleashClient==6.2.2 unleash-v6.3.0: UnleashClient==6.3.0 # ~~~ GraphQL ~~~ ariadne-v0.20.1: ariadne==0.20.1 - ariadne-v0.22: ariadne==0.22 - ariadne-v0.24.0: ariadne==0.24.0 ariadne-v0.26.2: ariadne==0.26.2 ariadne: fastapi ariadne: flask ariadne: httpx gql-v3.4.1: gql[all]==3.4.1 - gql-v3.5.3: gql[all]==3.5.3 gql-v4.0.0: gql[all]==4.0.0 gql-v4.2.0b0: gql[all]==4.2.0b0 @@ -516,13 +475,9 @@ deps = {py3.6}-graphene: aiocontextvars strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8 - strawberry-v0.233.3: strawberry-graphql[fastapi,flask]==0.233.3 - strawberry-v0.257.0: strawberry-graphql[fastapi,flask]==0.257.0 strawberry-v0.282.0: strawberry-graphql[fastapi,flask]==0.282.0 strawberry: httpx strawberry-v0.209.8: pydantic<2.11 - strawberry-v0.233.3: pydantic<2.11 - strawberry-v0.257.0: pydantic<2.11 # ~~~ Network ~~~ @@ -551,8 +506,6 @@ deps = # ~~~ Tasks ~~~ arq-v0.23: arq==0.23 - arq-v0.24.0: arq==0.24.0 - arq-v0.25.0: arq==0.25.0 arq-v0.26.3: arq==0.26.3 arq: async-timeout arq: pytest-asyncio @@ -560,8 +513,6 @@ deps = arq-v0.23: pydantic<2 beam-v2.14.0: apache-beam==2.14.0 - beam-v2.32.0: apache-beam==2.32.0 - beam-v2.50.0: apache-beam==2.50.0 beam-v2.68.0: apache-beam==2.68.0 celery-v4.4.7: celery==4.4.7 @@ -572,13 +523,9 @@ deps = {py3.7}-celery: importlib-metadata<5.0 dramatiq-v1.9.0: dramatiq==1.9.0 - dramatiq-v1.12.3: dramatiq==1.12.3 - dramatiq-v1.15.0: dramatiq==1.15.0 dramatiq-v1.18.0: dramatiq==1.18.0 huey-v2.1.3: huey==2.1.3 - huey-v2.2.0: huey==2.2.0 - huey-v2.3.2: huey==2.3.2 huey-v2.5.3: huey==2.5.3 ray-v2.7.2: ray==2.7.2 @@ -701,8 +648,6 @@ deps = pyramid: werkzeug<2.1.0 quart-v0.16.3: quart==0.16.3 - quart-v0.17.0: quart==0.17.0 - quart-v0.18.4: quart==0.18.4 quart-v0.20.0: quart==0.20.0 quart: quart-auth quart: pytest-asyncio @@ -712,14 +657,6 @@ deps = quart-v0.16.3: jinja2<3.1.0 quart-v0.16.3: Werkzeug<2.3.0 quart-v0.16.3: hypercorn<0.15.0 - quart-v0.17.0: blinker<1.6 - quart-v0.17.0: jinja2<3.1.0 - quart-v0.17.0: Werkzeug<2.3.0 - quart-v0.17.0: hypercorn<0.15.0 - quart-v0.18.4: blinker<1.6 - quart-v0.18.4: jinja2<3.1.0 - quart-v0.18.4: Werkzeug<2.3.0 - quart-v0.18.4: hypercorn<0.15.0 {py3.8}-quart: taskgroup==0.0.0a4 sanic-v0.8.3: sanic==0.8.3 @@ -734,8 +671,6 @@ deps = {py3.8}-sanic: tracerite<1.1.2 starlite-v1.48.1: starlite==1.48.1 - starlite-v1.49.0: starlite==1.49.0 - starlite-v1.50.2: starlite==1.50.2 starlite-v1.51.16: starlite==1.51.16 starlite: pytest-asyncio starlite: python-multipart @@ -745,12 +680,9 @@ deps = starlite: httpx<0.28 tornado-v6.0.4: tornado==6.0.4 - tornado-v6.2: tornado==6.2 - tornado-v6.4.2: tornado==6.4.2 tornado-v6.5.2: tornado==6.5.2 tornado: pytest tornado-v6.0.4: pytest<8.2 - tornado-v6.2: pytest<8.2 {py3.6}-tornado: aiocontextvars @@ -758,7 +690,6 @@ deps = loguru-v0.7.3: loguru==0.7.3 pure_eval-v0.0.3: pure_eval==0.0.3 - pure_eval-v0.1.1: pure_eval==0.1.1 pure_eval-v0.2.3: pure_eval==0.2.3 trytond-v4.6.22: trytond==4.6.22 @@ -771,8 +702,6 @@ deps = trytond-v4.8.18: werkzeug<1.0 typer-v0.15.4: typer==0.15.4 - typer-v0.16.1: typer==0.16.1 - typer-v0.17.5: typer==0.17.5 typer-v0.19.2: typer==0.19.2 From b7780bab8e6b4e288f8782d9c8fcc9d8b9e40bad Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 24 Sep 2025 16:44:13 +0200 Subject: [PATCH 84/88] feat(toxgen): Remove timestamp prone to merge conflicts (#4860) ### Description Most of the merge conflicts concerning `tox.ini` come from the timestamp of the last update. If two PRs regenerate `tox.ini`, they'll be guaranteed to have a merge conflict that needs to be resolved by hand. This is time consuming and unnecessary. We still need some way of figuring out when the file was last updated. This is important for checking unwanted edits to `tox.ini` (instead of to the template it's generated from). In order to check for them, we need to regenerate the file on the current branch and compare it to the committed version. However, if there have been new package versions in the meantime, these would then pop up in the diff. One solution is to regenerate the file ignoring any new releases after the last update, which is what we were using the last updated timestamp for. In this PR, we simply figure out the timestamp from `git` history, removing the need for storing it explicitly anywhere. #### Issues #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- .github/workflows/ci.yml | 3 ++ scripts/populate_tox/populate_tox.py | 44 ++++++++++++---------------- scripts/populate_tox/releases.jsonl | 2 +- scripts/populate_tox/tox.jinja | 2 -- tox.ini | 10 +++---- 5 files changed, 27 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67b4fd3546..50ab1d39ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,9 @@ jobs: steps: - uses: actions/checkout@v5.0.0 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 - uses: actions/setup-python@v6 with: python-version: 3.12 diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 070ce82492..f13fa6d002 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -8,6 +8,7 @@ import hashlib import json import os +import subprocess import sys import time from bisect import bisect_left @@ -509,9 +510,7 @@ def _render_dependencies(integration: str, releases: list[Version]) -> list[str] return rendered -def write_tox_file( - packages: dict, update_timestamp: bool, last_updated: datetime -) -> None: +def write_tox_file(packages: dict) -> None: template = ENV.get_template("tox.jinja") context = {"groups": {}} @@ -530,11 +529,6 @@ def write_tox_file( } ) - if update_timestamp: - context["updated"] = datetime.now(tz=timezone.utc).isoformat() - else: - context["updated"] = last_updated.isoformat() - rendered = template.render(context) with open(TOX_FILE, "w") as file: @@ -623,19 +617,21 @@ def get_file_hash() -> str: def get_last_updated() -> Optional[datetime]: - timestamp = None - - with open(TOX_FILE, "r") as f: - for line in f: - if line.startswith("# Last generated:"): - timestamp = datetime.fromisoformat(line.strip().split()[-1]) - break - - if timestamp is None: - print( - "Failed to find out when tox.ini was last generated; the timestamp seems to be missing from the file." - ) - + repo_root = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + ).stdout.strip() + tox_ini_path = Path(repo_root) / "tox.ini" + + timestamp = subprocess.run( + ["git", "log", "-1", "--pretty=%ct", str(tox_ini_path)], + capture_output=True, + text=True, + ).stdout.strip() + + timestamp = datetime.fromtimestamp(int(timestamp), timezone.utc) + print(f"Last committed tox.ini update: {timestamp}") return timestamp @@ -675,7 +671,7 @@ def main(fail_on_changes: bool = False) -> None: print(f"Running in {'fail_on_changes' if fail_on_changes else 'normal'} mode.") last_updated = get_last_updated() if fail_on_changes: - # We need to make the script ignore any new releases after the `last_updated` + # We need to make the script ignore any new releases after the last updated # timestamp so that we don't fail CI on a PR just because a new package # version was released, leading to unrelated changes in tox.ini. print( @@ -769,9 +765,7 @@ def main(fail_on_changes: bool = False) -> None: if fail_on_changes: old_file_hash = get_file_hash() - write_tox_file( - packages, update_timestamp=not fail_on_changes, last_updated=last_updated - ) + write_tox_file(packages) # Sort the release cache file releases = [] diff --git a/scripts/populate_tox/releases.jsonl b/scripts/populate_tox/releases.jsonl index fa82e6b1cd..96eb2da8e0 100644 --- a/scripts/populate_tox/releases.jsonl +++ b/scripts/populate_tox/releases.jsonl @@ -107,7 +107,7 @@ {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "litestar", "requires_python": "<4.0,>=3.8", "version": "2.6.4", "yanked": false}} {"info": {"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: System :: Logging"], "name": "loguru", "requires_python": "<4.0,>=3.5", "version": "0.7.3", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.7.1", "version": "1.0.1", "yanked": false}} -{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.109.0", "yanked": false}} +{"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.109.1", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.7.1", "version": "1.37.2", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai", "requires_python": ">=3.8", "version": "1.73.0", "yanked": false}} {"info": {"classifiers": ["Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed"], "name": "openai-agents", "requires_python": ">=3.9", "version": "0.0.19", "yanked": false}} diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index f47d5db692..039b93c42e 100755 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -9,8 +9,6 @@ # or in the script (if you want to change the auto-generated part). # The file (and all resulting CI YAMLs) then needs to be regenerated via # "scripts/generate-test-files.sh". -# -# Last generated: {{ updated }} [tox] requires = diff --git a/tox.ini b/tox.ini index e0295707ab..8e4c6e5a36 100644 --- a/tox.ini +++ b/tox.ini @@ -9,8 +9,6 @@ # or in the script (if you want to change the auto-generated part). # The file (and all resulting CI YAMLs) then needs to be regenerated via # "scripts/generate-test-files.sh". -# -# Last generated: 2025-09-24T10:52:00.519014+00:00 [tox] requires = @@ -68,12 +66,12 @@ envlist = {py3.8,py3.11,py3.12}-openai-base-v1.0.1 {py3.8,py3.11,py3.12}-openai-base-v1.37.2 {py3.8,py3.11,py3.12}-openai-base-v1.73.0 - {py3.8,py3.12,py3.13}-openai-base-v1.109.0 + {py3.8,py3.12,py3.13}-openai-base-v1.109.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.0.1 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.37.2 {py3.8,py3.11,py3.12}-openai-notiktoken-v1.73.0 - {py3.8,py3.12,py3.13}-openai-notiktoken-v1.109.0 + {py3.8,py3.12,py3.13}-openai-notiktoken-v1.109.1 {py3.9,py3.12,py3.13}-langgraph-v0.6.7 {py3.10,py3.12,py3.13}-langgraph-v1.0.0a3 @@ -364,7 +362,7 @@ deps = openai-base-v1.0.1: openai==1.0.1 openai-base-v1.37.2: openai==1.37.2 openai-base-v1.73.0: openai==1.73.0 - openai-base-v1.109.0: openai==1.109.0 + openai-base-v1.109.1: openai==1.109.1 openai-base: pytest-asyncio openai-base: tiktoken openai-base-v1.0.1: httpx<0.28 @@ -373,7 +371,7 @@ deps = openai-notiktoken-v1.0.1: openai==1.0.1 openai-notiktoken-v1.37.2: openai==1.37.2 openai-notiktoken-v1.73.0: openai==1.73.0 - openai-notiktoken-v1.109.0: openai==1.109.0 + openai-notiktoken-v1.109.1: openai==1.109.1 openai-notiktoken: pytest-asyncio openai-notiktoken-v1.0.1: httpx<0.28 openai-notiktoken-v1.37.2: httpx<0.28 From 6325924c2b8b0e4d7facdc49dd9aa7bdade9a08c Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Wed, 24 Sep 2025 17:35:11 +0200 Subject: [PATCH 85/88] Don't swallow userland exceptions in openai (#4861) #### Issues * resolves: #4853 * resolves: PY-1865 --- sentry_sdk/integrations/openai.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 4d72ec366c..e8b3b30ab2 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -282,9 +282,9 @@ def _set_output_data(span, response, kwargs, integration, finish_span=True): def new_iterator(): # type: () -> Iterator[ChatCompletionChunk] - with capture_internal_exceptions(): - count_tokens_manually = True - for x in old_iterator: + count_tokens_manually = True + for x in old_iterator: + with capture_internal_exceptions(): # OpenAI chat completion API if hasattr(x, "choices"): choice_index = 0 @@ -315,8 +315,9 @@ def new_iterator(): ) count_tokens_manually = False - yield x + yield x + with capture_internal_exceptions(): if len(data_buf) > 0: all_responses = ["".join(chunk) for chunk in data_buf] if should_send_default_pii() and integration.include_prompts: @@ -337,9 +338,9 @@ def new_iterator(): async def new_iterator_async(): # type: () -> AsyncIterator[ChatCompletionChunk] - with capture_internal_exceptions(): - count_tokens_manually = True - async for x in old_iterator: + count_tokens_manually = True + async for x in old_iterator: + with capture_internal_exceptions(): # OpenAI chat completion API if hasattr(x, "choices"): choice_index = 0 @@ -370,8 +371,9 @@ async def new_iterator_async(): ) count_tokens_manually = False - yield x + yield x + with capture_internal_exceptions(): if len(data_buf) > 0: all_responses = ["".join(chunk) for chunk in data_buf] if should_send_default_pii() and integration.include_prompts: From 01fc851b7a805816fddfd252d2cd93dfd0484b69 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 25 Sep 2025 08:51:53 +0000 Subject: [PATCH 86/88] release: 2.39.0 --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7abbed7218..2c159f8afe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 2.39.0 + +### Various fixes & improvements + +- Don't swallow userland exceptions in openai (#4861) by @sl0thentr0py +- feat(toxgen): Remove timestamp prone to merge conflicts (#4860) by @sentrivana +- chore: Slim down test matrix (#4856) by @sentrivana +- docs: Update contributing guidelines with instructions to run tests with tox (#4857) by @alexander-alderman-webb +- chore: Clean up toxgen (#4855) by @sentrivana +- feat(toxgen): Cache release data (#4835) by @sentrivana +- fix(AI): Make agents integrations set the span status in case of error (#4820) by @antonpirker +- fix(langchain): don't record tool call output if not include_prompt / should_send_default_pii (#4836) by @shellmayr +- build(deps): bump actions/setup-python from 5 to 6 (#4830) by @dependabot +- fix: Pin shibuya (#4839) by @sentrivana +- Use weakref in dedupe where possible (#4834) by @sl0thentr0py +- tests: Remove old system for checking for regressions (#4826) by @sentrivana +- ci: 🤖 Update test matrix with new releases (09/22) (#4831) by @github-actions +- tests: Move requests under toxgen (#4825) by @sentrivana +- tests: Move rq under toxgen (#4818) by @sentrivana +- tests: Move redis under toxgen (#4824) by @sentrivana +- test(spark): Improve `test_spark` speed (#4822) by @mgaligniana +- tests: Move httpx under toxgen (#4780) by @sentrivana +- tests: Move ray under toxgen (#4810) by @sentrivana +- tests: Move sanic under toxgen & add option to control how many releases to pick (#4767) by @sentrivana +- ci: 🤖 Update test matrix with new releases (09/18) (#4816) by @github-actions +- tests: Move pure_eval under toxgen (#4815) by @sentrivana +- fix(Django): Avoid evaluating complex Django object in span.data/span.attributes (#4804) by @antonpirker +- fix: Fix links to files in toxgen PRs (#4807) by @sentrivana + +_Plus 5 more_ + ## 2.38.0 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index 061b2bdfc8..292e0971e2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.38.0" +release = "2.39.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 606cf804b6..5b8e840f13 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1333,4 +1333,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.38.0" +VERSION = "2.39.0" diff --git a/setup.py b/setup.py index 58101aa65f..7119e20e90 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.38.0", + version="2.39.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From ff527644c5446674c0870cb830a122f7b8fe3ac2 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Sep 2025 10:59:46 +0200 Subject: [PATCH 87/88] Updated changelog --- CHANGELOG.md | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c159f8afe..b9095279f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,32 +4,32 @@ ### Various fixes & improvements -- Don't swallow userland exceptions in openai (#4861) by @sl0thentr0py -- feat(toxgen): Remove timestamp prone to merge conflicts (#4860) by @sentrivana -- chore: Slim down test matrix (#4856) by @sentrivana -- docs: Update contributing guidelines with instructions to run tests with tox (#4857) by @alexander-alderman-webb -- chore: Clean up toxgen (#4855) by @sentrivana -- feat(toxgen): Cache release data (#4835) by @sentrivana -- fix(AI): Make agents integrations set the span status in case of error (#4820) by @antonpirker -- fix(langchain): don't record tool call output if not include_prompt / should_send_default_pii (#4836) by @shellmayr -- build(deps): bump actions/setup-python from 5 to 6 (#4830) by @dependabot -- fix: Pin shibuya (#4839) by @sentrivana -- Use weakref in dedupe where possible (#4834) by @sl0thentr0py -- tests: Remove old system for checking for regressions (#4826) by @sentrivana -- ci: 🤖 Update test matrix with new releases (09/22) (#4831) by @github-actions -- tests: Move requests under toxgen (#4825) by @sentrivana -- tests: Move rq under toxgen (#4818) by @sentrivana -- tests: Move redis under toxgen (#4824) by @sentrivana -- test(spark): Improve `test_spark` speed (#4822) by @mgaligniana -- tests: Move httpx under toxgen (#4780) by @sentrivana -- tests: Move ray under toxgen (#4810) by @sentrivana -- tests: Move sanic under toxgen & add option to control how many releases to pick (#4767) by @sentrivana -- ci: 🤖 Update test matrix with new releases (09/18) (#4816) by @github-actions -- tests: Move pure_eval under toxgen (#4815) by @sentrivana -- fix(Django): Avoid evaluating complex Django object in span.data/span.attributes (#4804) by @antonpirker -- fix: Fix links to files in toxgen PRs (#4807) by @sentrivana - -_Plus 5 more_ +- Feat(toxgen): Cache release data (#4835) by @sentrivana +- Feat(toxgen): Remove timestamp prone to merge conflicts (#4860) by @sentrivana +- Fix: Pin shibuya (#4839) by @sentrivana +- Fix: Links to files in toxgen PRs (#4807) by @sentrivana +- Fix(AI): Make agents integrations set the span status in case of error (#4820) by @antonpirker +- Fix(dedupe): Use weakref in dedupe where possible (#4834) by @sl0thentr0py +- Fix(Django): Avoid evaluating complex Django object in span.data/span.attributes (#4804) by @antonpirker +- Fix(Dangchain): don't record tool call output if not include_prompt / should_send_default_pii (#4836) by @shellmayr +- Fix(OpenAI): Don't swallow userland exceptions in openai (#4861) by @sl0thentr0py +- Docs: Update contributing guidelines with instructions to run tests with tox (#4857) by @alexander-alderman-webb +- Test(Spark): Improve `test_spark` speed (#4822) by @mgaligniana +- Tests: Remove old system for checking for regressions (#4826) by @sentrivana +- Tests: Move requests under toxgen (#4825) by @sentrivana +- Tests: Move rq under toxgen (#4818) by @sentrivana +- Tests: Move redis under toxgen (#4824) by @sentrivana +- Tests: Move httpx under toxgen (#4780) by @sentrivana +- Tests: Move ray under toxgen (#4810) by @sentrivana +- Tests: Move sanic under toxgen & add option to control how many releases to pick (#4767) by @sentrivana +- Tests: Move pure_eval under toxgen (#4815) by @sentrivana +- Chore: Clean up toxgen (#4855) by @sentrivana +- Chore: Slim down test matrix (#4856) by @sentrivana +- Note: This is my last release. So long, and thanks for all the fish! by @antonpirker +- Build(deps): bump actions/setup-python from 5 to 6 (#4830) by @dependabot +- CI: 🤖 Update test matrix with new releases (09/18) (#4816) by @github-actions +- CI: 🤖 Update test matrix with new releases (09/22) (#4831) by @github-actions + ## 2.38.0 From 0f99a91e7b5103382c8198a7fcae70ab2933d85d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 25 Sep 2025 11:05:18 +0200 Subject: [PATCH 88/88] Update CHANGELOG.md --- CHANGELOG.md | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9095279f7..941aec99e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,31 +4,15 @@ ### Various fixes & improvements -- Feat(toxgen): Cache release data (#4835) by @sentrivana -- Feat(toxgen): Remove timestamp prone to merge conflicts (#4860) by @sentrivana -- Fix: Pin shibuya (#4839) by @sentrivana -- Fix: Links to files in toxgen PRs (#4807) by @sentrivana - Fix(AI): Make agents integrations set the span status in case of error (#4820) by @antonpirker - Fix(dedupe): Use weakref in dedupe where possible (#4834) by @sl0thentr0py - Fix(Django): Avoid evaluating complex Django object in span.data/span.attributes (#4804) by @antonpirker -- Fix(Dangchain): don't record tool call output if not include_prompt / should_send_default_pii (#4836) by @shellmayr +- Fix(Langchain): Don't record tool call output if not include_prompt / should_send_default_pii (#4836) by @shellmayr - Fix(OpenAI): Don't swallow userland exceptions in openai (#4861) by @sl0thentr0py - Docs: Update contributing guidelines with instructions to run tests with tox (#4857) by @alexander-alderman-webb - Test(Spark): Improve `test_spark` speed (#4822) by @mgaligniana -- Tests: Remove old system for checking for regressions (#4826) by @sentrivana -- Tests: Move requests under toxgen (#4825) by @sentrivana -- Tests: Move rq under toxgen (#4818) by @sentrivana -- Tests: Move redis under toxgen (#4824) by @sentrivana -- Tests: Move httpx under toxgen (#4780) by @sentrivana -- Tests: Move ray under toxgen (#4810) by @sentrivana -- Tests: Move sanic under toxgen & add option to control how many releases to pick (#4767) by @sentrivana -- Tests: Move pure_eval under toxgen (#4815) by @sentrivana -- Chore: Clean up toxgen (#4855) by @sentrivana -- Chore: Slim down test matrix (#4856) by @sentrivana -- Note: This is my last release. So long, and thanks for all the fish! by @antonpirker -- Build(deps): bump actions/setup-python from 5 to 6 (#4830) by @dependabot -- CI: 🤖 Update test matrix with new releases (09/18) (#4816) by @github-actions -- CI: 🤖 Update test matrix with new releases (09/22) (#4831) by @github-actions + +Note: This is my last release. So long, and thanks for all the fish! by @antonpirker ## 2.38.0