diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index f0002fe486..12db62315a 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -4,4 +4,4 @@
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. The AWS Lambda tests additionally require a maintainer to add a special label, and they will fail until this label is added.
+Running the test suite on your PR might require maintainer approval.
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4d8c060f6a..c1861ce182 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@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
+ uses: actions/create-github-app-token@21cfef2b496dd8ef5b904c159339626a10ad380e # v1.11.6
with:
app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}
diff --git a/.github/workflows/scripts/trigger_tests_on_label.py b/.github/workflows/scripts/trigger_tests_on_label.py
deleted file mode 100644
index f6039fd16a..0000000000
--- a/.github/workflows/scripts/trigger_tests_on_label.py
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/env python3
-import argparse
-import json
-import os
-from urllib.parse import quote
-from urllib.request import Request, urlopen
-
-LABEL = "Trigger: tests using secrets"
-
-
-def _has_write(repo_id: int, username: str, *, token: str) -> bool:
- req = Request(
- f"https://api.github.com/repositories/{repo_id}/collaborators/{username}/permission",
- headers={"Authorization": f"token {token}"},
- )
- contents = json.load(urlopen(req, timeout=10))
-
- return contents["permission"] in {"admin", "write"}
-
-
-def _remove_label(repo_id: int, pr: int, label: str, *, token: str) -> None:
- quoted_label = quote(label)
- req = Request(
- f"https://api.github.com/repositories/{repo_id}/issues/{pr}/labels/{quoted_label}",
- method="DELETE",
- headers={"Authorization": f"token {token}"},
- )
- urlopen(req)
-
-
-def main() -> int:
- parser = argparse.ArgumentParser()
- parser.add_argument("--repo-id", type=int, required=True)
- parser.add_argument("--pr", type=int, required=True)
- parser.add_argument("--event", required=True)
- parser.add_argument("--username", required=True)
- parser.add_argument("--label-names", type=json.loads, required=True)
- args = parser.parse_args()
-
- token = os.environ["GITHUB_TOKEN"]
-
- write_permission = _has_write(args.repo_id, args.username, token=token)
-
- if (
- not write_permission
- # `reopened` is included here due to close => push => reopen
- and args.event in {"synchronize", "reopened"}
- and LABEL in args.label_names
- ):
- print(f"Invalidating label [{LABEL}] due to code change...")
- _remove_label(args.repo_id, args.pr, LABEL, token=token)
- args.label_names.remove(LABEL)
-
- if write_permission or LABEL in args.label_names:
- print("Permissions passed!")
- print(f"- has write permission: {write_permission}")
- print(f"- has [{LABEL}] label: {LABEL in args.label_names}")
- return 0
- else:
- print("Permissions failed!")
- print(f"- has write permission: {write_permission}")
- print(f"- has [{LABEL}] label: {LABEL in args.label_names}")
- print(f"- args.label_names: {args.label_names}")
- print(
- f"Please have a collaborator add the [{LABEL}] label once they "
- f"have reviewed the code to trigger tests."
- )
- return 1
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml
index a0f3e26a04..3e728988b5 100644
--- a/.github/workflows/test-integrations-ai.yml
+++ b/.github/workflows/test-integrations-ai.yml
@@ -70,7 +70,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -132,7 +132,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
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 450825e7f6..62c45f0464 100644
--- a/.github/workflows/test-integrations-cloud.yml
+++ b/.github/workflows/test-integrations-cloud.yml
@@ -31,6 +31,10 @@ jobs:
matrix:
python-version: ["3.8","3.11","3.12","3.13"]
os: [ubuntu-22.04]
+ services:
+ docker:
+ image: docker:dind # Required for Docker network management
+ options: --privileged # Required for Docker-in-Docker operations
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/setup-python@v5
@@ -43,6 +47,10 @@ jobs:
- 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
@@ -66,7 +74,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -87,8 +95,12 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.7","3.9","3.11","3.12","3.13"]
+ python-version: ["3.7","3.8","3.9","3.11","3.12","3.13"]
os: [ubuntu-22.04]
+ services:
+ docker:
+ image: docker:dind # Required for Docker network management
+ options: --privileged # Required for Docker-in-Docker operations
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/setup-python@v5
@@ -101,6 +113,10 @@ jobs:
- name: Erase coverage
run: |
coverage erase
+ - name: Test aws_lambda pinned
+ run: |
+ set -x # print commands that are executed
+ ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-aws_lambda"
- name: Test boto3 pinned
run: |
set -x # print commands that are executed
@@ -124,7 +140,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
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 cb33dddf17..f755c6db69 100644
--- a/.github/workflows/test-integrations-common.yml
+++ b/.github/workflows/test-integrations-common.yml
@@ -54,7 +54,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
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 156e14d03d..10576d0ade 100644
--- a/.github/workflows/test-integrations-dbs.yml
+++ b/.github/workflows/test-integrations-dbs.yml
@@ -94,7 +94,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -180,7 +180,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
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 6e4033389e..be2d970362 100644
--- a/.github/workflows/test-integrations-flags.yml
+++ b/.github/workflows/test-integrations-flags.yml
@@ -66,7 +66,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
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 bb371d1717..c9746bba5c 100644
--- a/.github/workflows/test-integrations-gevent.yml
+++ b/.github/workflows/test-integrations-gevent.yml
@@ -54,7 +54,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
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 2fdd46422b..0145b06a12 100644
--- a/.github/workflows/test-integrations-graphql.yml
+++ b/.github/workflows/test-integrations-graphql.yml
@@ -66,7 +66,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
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 b70d37702d..0ce38f016f 100644
--- a/.github/workflows/test-integrations-misc.yml
+++ b/.github/workflows/test-integrations-misc.yml
@@ -74,7 +74,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
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 bea1a553bd..a2455a28b1 100644
--- a/.github/workflows/test-integrations-network.yml
+++ b/.github/workflows/test-integrations-network.yml
@@ -62,7 +62,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -116,7 +116,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
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 a727058bbc..226ba3ac06 100644
--- a/.github/workflows/test-integrations-tasks.yml
+++ b/.github/workflows/test-integrations-tasks.yml
@@ -84,7 +84,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -160,7 +160,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
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 0f4399d439..2b84ff7b08 100644
--- a/.github/workflows/test-integrations-web-1.yml
+++ b/.github/workflows/test-integrations-web-1.yml
@@ -84,7 +84,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -160,7 +160,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
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 9706cbca3e..fa012b2c98 100644
--- a/.github/workflows/test-integrations-web-2.yml
+++ b/.github/workflows/test-integrations-web-2.yml
@@ -90,7 +90,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -172,7 +172,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.gitignore b/.gitignore
index c9724e80d5..4401dd6bfc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,6 @@ relay
pip-wheel-metadata
.mypy_cache
.vscode/
+
+# for running AWS Lambda tests using AWS SAM
+sam.template.yaml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 939a612bc0..2bf4da0e29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,49 @@
# Changelog
+## 2.23.1
+
+### Various fixes & improvements
+
+- Fix import problem in release 2.23.0 (#4140) by @antonpirker
+
+## 2.23.0
+
+### Various fixes & improvements
+
+- Feat(profiling): Add new functions to start/stop continuous profiler (#4056) by @Zylphrex
+- Feat(profiling): Export start/stop profile session (#4079) by @Zylphrex
+- Feat(tracing): Backfill missing `sample_rand` on `PropagationContext` (#4038) by @szokeasaurusrex
+- Feat(logs): Add alpha version of Sentry logs (#4126) by @colin-sentry
+- Security(gha): fix potential for shell injection (#4099) by @mdtro
+- Docs: Add `init()` parameters to ApiDocs. (#4100) by @antonpirker
+- Docs: Document that caller must check `mutable` (#4010) by @szokeasaurusrex
+- Fix(Anthropic): Add partial json support to streams (#3674)
+- Fix(ASGI): Fix KeyError if transaction does not exist (#4095) by @kevinji
+- Fix(asyncio): Improve asyncio integration error handling. (#4129) by @antonpirker
+- Fix(AWS Lambda): Fix capturing errors during AWS Lambda INIT phase (#3943)
+- Fix(Bottle): Prevent internal error on 404 (#4131) by @sentrivana
+- Fix(CI): Fix API doc failure in CI (#4075) by @sentrivana
+- Fix(ClickHouse) ClickHouse in test suite (#4087) by @antonpirker
+- Fix(cloudresourcecontext): Added timeout to HTTP requests in CloudResourceContextIntegration (#4120) by @antonpirker
+- Fix(crons): Fixed bug when `cron_jobs` is set to `None` in arq integration (#4115) by @antonpirker
+- Fix(debug): Take into account parent handlers for debug logger (#4133) by @sentrivana
+- Fix(FastAPI/Starlette): Fix middleware with positional arguments. (#4118) by @antonpirker
+- Fix(featureflags): add LRU update/dedupe test coverage (#4082)
+- Fix(logging): Coerce None values into strings in logentry params. (#4121) by @antonpirker
+- Fix(pyspark): Grab `attemptId` more defensively (#4130) by @sentrivana
+- Fix(Quart): Support `quart_flask_patch` (#4132) by @sentrivana
+- Fix(tests): A way to locally run AWS Lambda functions (#4128) by @antonpirker
+- Fix(tests): Add concurrency testcase for arq (#4125) by @sentrivana
+- Fix(tests): Add fail_on_changes to toxgen by @sentrivana
+- Fix(tests): Run AWS Lambda tests locally (#3988) by @antonpirker
+- Fix(tests): Test relevant prereleases and allow to ignore releases
+- Fix(tracing): Move `TRANSACTION_SOURCE_*` constants to `Enum` (#3889) by @mgaligniana
+- Fix(typing): Add more typing info to Scope.update_from_kwargs's "contexts" (#4080)
+- Fix(typing): Set correct type for `set_context` everywhere (#4123) by @sentrivana
+- Chore(tests): Regenerate tox.ini (#4108) by @sentrivana
+- Build(deps): bump actions/create-github-app-token from 1.11.5 to 1.11.6 (#4113) by @dependabot
+- Build(deps): bump codecov/codecov-action from 5.3.1 to 5.4.0 (#4112) by @dependabot
+
## 2.22.0
### Various fixes & improvements
diff --git a/README.md b/README.md
index 29501064f3..10bc8eb2ed 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,32 @@
+
+_Bad software is everywhere, and we're tired of it. Sentry is on a mission to help developers write better software faster, so we can get back to enjoying technology. If you want to join us
+[
**Check out our open positions**](https://sentry.io/careers/)_.
+
+[](https://discord.gg/wdNEHETs87)
+[](https://twitter.com/intent/follow?screen_name=getsentry)
+[](https://pypi.python.org/pypi/sentry-sdk)
+

+[](https://github.com/getsentry/sentry-python/actions/workflows/ci.yml)
+
+
+
+
-_Bad software is everywhere, and we're tired of it. Sentry is on a mission to help developers write better software faster, so we can get back to enjoying technology. If you want to join us, [**check out our open positions**](https://sentry.io/careers/)_.
# Official Sentry SDK for Python
-[](https://github.com/getsentry/sentry-python/actions/workflows/ci.yml)
-[](https://pypi.python.org/pypi/sentry-sdk)
-[](https://discord.gg/cWnMQeA)
+Welcome to the official Python SDK for **[Sentry](http://sentry.io/)**.
+
+
+## π¦ Getting Started
-Welcome to the official Python SDK for **[Sentry](http://sentry.io/)**!
+### Prerequisites
-## Getting Started
+You need a Sentry [account](https://sentry.io/signup/) and [project](https://docs.sentry.io/product/projects/).
### Installation
@@ -25,7 +38,7 @@ pip install --upgrade sentry-sdk
### Basic Configuration
-Hereβs a quick configuration example to get Sentry up and running:
+Here's a quick configuration example to get Sentry up and running:
```python
import sentry_sdk
@@ -34,7 +47,7 @@ sentry_sdk.init(
"https://12927b5f211046b575ee51fd8b1ac34f@o1.ingest.sentry.io/1", # Your DSN here
# Set traces_sample_rate to 1.0 to capture 100%
- # of transactions for performance monitoring.
+ # of traces for performance monitoring.
traces_sample_rate=1.0,
)
```
@@ -46,36 +59,26 @@ With this configuration, Sentry will monitor for exceptions and performance issu
To generate some events that will show up in Sentry, you can log messages or capture errors:
```python
-from sentry_sdk import capture_message
-capture_message("Hello Sentry!") # You'll see this in your Sentry dashboard.
+import sentry_sdk
+sentry_sdk.init(...) # same as above
+
+sentry_sdk.capture_message("Hello Sentry!") # You'll see this in your Sentry dashboard.
raise ValueError("Oops, something went wrong!") # This will create an error event in Sentry.
```
-#### Explore the Docs
-
-For more details on advanced usage, integrations, and customization, check out the full documentation:
-
-- [Official SDK Docs](https://docs.sentry.io/platforms/python/)
-- [API Reference](https://getsentry.github.io/sentry-python/)
-## Integrations
+## π Documentation
-Sentry integrates with many popular Python libraries and frameworks, including:
+For more details on advanced usage, integrations, and customization, check out the full documentation on [https://docs.sentry.io](https://docs.sentry.io/).
-- [Django](https://docs.sentry.io/platforms/python/integrations/django/)
-- [Flask](https://docs.sentry.io/platforms/python/integrations/flask/)
-- [FastAPI](https://docs.sentry.io/platforms/python/integrations/fastapi/)
-- [Celery](https://docs.sentry.io/platforms/python/integrations/celery/)
-- [AWS Lambda](https://docs.sentry.io/platforms/python/integrations/aws-lambda/)
-Want more? [Check out the full list of integrations](https://docs.sentry.io/platforms/python/integrations/).
+## π§© Integrations
-### Rolling Your Own Integration?
+Sentry integrates with a ton of popular Python libraries and frameworks, including [FastAPI](https://docs.sentry.io/platforms/python/integrations/fastapi/), [Django](https://docs.sentry.io/platforms/python/integrations/django/), [Celery](https://docs.sentry.io/platforms/python/integrations/celery/), [OpenAI](https://docs.sentry.io/platforms/python/integrations/openai/) and many, many more. Check out the [full list of integrations](https://docs.sentry.io/platforms/python/integrations/) to get the full picture.
-If you want to create a new integration or improve an existing one, weβd welcome your contributions! Please read our [contributing guide](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md) before starting.
-## Migrating Between Versions?
+## π§ Migrating Between Versions?
### From `1.x` to `2.x`
@@ -85,30 +88,35 @@ If you're using the older `1.x` version of the SDK, now's the time to upgrade to
Using the legacy `raven-python` client? It's now in maintenance mode, and we recommend migrating to the new SDK for an improved experience. Get all the details in our [migration guide](https://docs.sentry.io/platforms/python/migration/raven-to-sentry-sdk/).
-## Want to Contribute?
-Weβd love your help in improving the Sentry SDK! Whether itβs fixing bugs, adding features, or enhancing documentation, every contribution is valuable.
+## π Want to Contribute?
-For details on how to contribute, please check out [CONTRIBUTING.md](CONTRIBUTING.md) and explore the [open issues](https://github.com/getsentry/sentry-python/issues).
+We'd love your help in improving the Sentry SDK! Whether it's fixing bugs, adding features, writing new integrations, or enhancing documentation, every contribution is valuable.
-## Need Help?
+For details on how to contribute, please read our [contribution guide](CONTRIBUTING.md) and explore the [open issues](https://github.com/getsentry/sentry-python/issues).
-If you encounter issues or need help setting up or configuring the SDK, donβt hesitate to reach out to the [Sentry Community on Discord](https://discord.com/invite/Ww9hbqr). There is a ton of great people there ready to help!
-## Resources
+## π Need Help?
-Here are additional resources to help you make the most of Sentry:
+If you encounter issues or need help setting up or configuring the SDK, don't hesitate to reach out to the [Sentry Community on Discord](https://discord.com/invite/Ww9hbqr). There is a ton of great people there ready to help!
-- [](https://docs.sentry.io/quickstart/) β Official documentation to get started.
-- [](https://discord.gg/Ww9hbqr) β Join our Discord community.
-- [](https://twitter.com/intent/follow?screen_name=getsentry) β Follow us on X (Twitter) for updates.
-- [](http://stackoverflow.com/questions/tagged/sentry) β Questions and answers related to Sentry.
-## License
+## π Resources
+
+Here are all resources to help you make the most of Sentry:
+
+- [Documentation](https://docs.sentry.io/platforms/python/) - Official documentation to get started.
+- [Discord](https://img.shields.io/discord/621778831602221064) - Join our Discord community.
+- [X/Twitter](https://twitter.com/intent/follow?screen_name=getsentry) - Follow us on X (Twitter) for updates.
+- [Stack Overflow](https://stackoverflow.com/questions/tagged/sentry) - Questions and answers related to Sentry.
+
+
+## π License
The SDK is open-source and available under the MIT license. Check out the [LICENSE](LICENSE) file for more information.
----
+
+## π Contributors
Thanks to everyone who has helped improve the SDK!
diff --git a/docs/api.rst b/docs/api.rst
index 73821d720d..95acc70455 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -5,6 +5,14 @@ Top Level API
This is the user facing API of the SDK. It's exposed as ``sentry_sdk``.
With this API you can implement a custom performance monitoring or error reporting solution.
+Initializing the SDK
+====================
+
+.. autoclass:: sentry_sdk.client.ClientConstructor
+ :members:
+ :undoc-members:
+ :special-members: __init__
+ :noindex:
Capturing Data
==============
diff --git a/docs/conf.py b/docs/conf.py
index c25e9706db..7c23d00a97 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -33,7 +33,7 @@
copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year)
author = "Sentry Team and Contributors"
-release = "2.22.0"
+release = "2.23.1"
version = ".".join(release.split(".")[:2]) # The short X.Y version.
diff --git a/requirements-testing.txt b/requirements-testing.txt
index 851620c1d3..e528ccf8de 100644
--- a/requirements-testing.txt
+++ b/requirements-testing.txt
@@ -15,3 +15,4 @@ httpcore[http2]
setuptools
freezegun
Brotli
+docker
diff --git a/scripts/aws-cleanup.sh b/scripts/aws-cleanup.sh
deleted file mode 100755
index 982835c283..0000000000
--- a/scripts/aws-cleanup.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/sh
-#
-# Helper script to clean up AWS Lambda functions created
-# by the test suite (tests/integrations/aws_lambda/test_aws.py).
-#
-# This will delete all Lambda functions named `test_function_*`.
-#
-
-export AWS_DEFAULT_REGION="us-east-1"
-export AWS_ACCESS_KEY_ID="$SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID"
-export AWS_SECRET_ACCESS_KEY="$SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY"
-
-for func in $(aws lambda list-functions --output text --query 'Functions[?starts_with(FunctionName, `test_`) == `true`].FunctionName'); do
- echo "Deleting $func"
- aws lambda delete-function --function-name "$func"
-done
-
-echo "All done! Have a nice day!"
diff --git a/scripts/aws-attach-layer-to-lambda-function.sh b/scripts/aws/aws-attach-layer-to-lambda-function.sh
similarity index 100%
rename from scripts/aws-attach-layer-to-lambda-function.sh
rename to scripts/aws/aws-attach-layer-to-lambda-function.sh
diff --git a/scripts/aws-delete-lambda-layer-versions.sh b/scripts/aws/aws-delete-lambda-layer-versions.sh
similarity index 95%
rename from scripts/aws-delete-lambda-layer-versions.sh
rename to scripts/aws/aws-delete-lambda-layer-versions.sh
index f467f9398b..dcbd2f9c65 100755
--- a/scripts/aws-delete-lambda-layer-versions.sh
+++ b/scripts/aws/aws-delete-lambda-layer-versions.sh
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
#
# Deletes all versions of the layer specified in LAYER_NAME in one region.
+# Use with caution!
#
set -euo pipefail
diff --git a/scripts/aws-deploy-local-layer.sh b/scripts/aws/aws-deploy-local-layer.sh
similarity index 81%
rename from scripts/aws-deploy-local-layer.sh
rename to scripts/aws/aws-deploy-local-layer.sh
index 56f2087596..ee7b3e45c0 100755
--- a/scripts/aws-deploy-local-layer.sh
+++ b/scripts/aws/aws-deploy-local-layer.sh
@@ -1,9 +1,8 @@
#!/usr/bin/env bash
#
-# Builds and deploys the Sentry AWS Lambda layer (including the Sentry SDK and the Sentry Lambda Extension)
+# Builds and deploys the `SentryPythonServerlessSDK-local-dev` AWS Lambda layer (containing the Sentry SDK)
#
# The currently checked out version of the SDK in your local directory is used.
-# The latest version of the Lambda Extension is fetched from the Sentry Release Registry.
#
set -euo pipefail
diff --git a/scripts/aws_lambda_functions/README.md b/scripts/aws_lambda_functions/README.md
deleted file mode 100644
index e07b445d5b..0000000000
--- a/scripts/aws_lambda_functions/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-aws_lambda_functions
-====================
-
-In this directory you can place AWS Lambda functions that are used for administrative tasks (or whatever)
\ No newline at end of file
diff --git a/scripts/aws_lambda_functions/sentryPythonDeleteTestFunctions/README.md b/scripts/aws_lambda_functions/sentryPythonDeleteTestFunctions/README.md
deleted file mode 100644
index a5cc1d8d42..0000000000
--- a/scripts/aws_lambda_functions/sentryPythonDeleteTestFunctions/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-sentryPythonDeleteTestFunctions
-===============================
-
-This AWS Lambda function deletes all AWS Lambda functions in the current AWS account that are prefixed with `test_`.
-The functions that are deleted are created by the Google Actions CI checks running on every PR of the `sentry-python` repository.
-
-The Lambda function has been deployed here:
-- AWS Account ID: `943013980633`
-- Region: `us-east-1`
-- Function ARN: `arn:aws:lambda:us-east-1:943013980633:function:sentryPythonDeleteTestFunctions`
-
-This function also emits Sentry Crons checkins to the `sentry-python` project in the `Sentry SDKs` organisation on Sentry.io:
-https://sentry-sdks.sentry.io/projects/sentry-python/?project=5461230
diff --git a/scripts/aws_lambda_functions/sentryPythonDeleteTestFunctions/lambda_function.py b/scripts/aws_lambda_functions/sentryPythonDeleteTestFunctions/lambda_function.py
deleted file mode 100644
index c365ec2aca..0000000000
--- a/scripts/aws_lambda_functions/sentryPythonDeleteTestFunctions/lambda_function.py
+++ /dev/null
@@ -1,50 +0,0 @@
-import boto3
-import sentry_sdk
-
-
-monitor_slug = "python-sdk-aws-lambda-tests-cleanup"
-monitor_config = {
- "schedule": {
- "type": "crontab",
- "value": "0 12 * * 0", # 12 o'clock on Sunday
- },
- "timezone": "UTC",
- "checkin_margin": 2,
- "max_runtime": 20,
- "failure_issue_threshold": 1,
- "recovery_threshold": 1,
-}
-
-
-@sentry_sdk.crons.monitor(monitor_slug=monitor_slug)
-def delete_lambda_functions(prefix="test_"):
- """
- Delete all AWS Lambda functions in the current account
- where the function name matches the prefix
- """
- client = boto3.client("lambda", region_name="us-east-1")
- functions_deleted = 0
-
- functions_paginator = client.get_paginator("list_functions")
- for functions_page in functions_paginator.paginate():
- for func in functions_page["Functions"]:
- function_name = func["FunctionName"]
- if function_name.startswith(prefix):
- try:
- response = client.delete_function(
- FunctionName=func["FunctionArn"],
- )
- functions_deleted += 1
- except Exception as ex:
- print(f"Got exception: {ex}")
-
- return functions_deleted
-
-
-def lambda_handler(event, context):
- functions_deleted = delete_lambda_functions()
-
- return {
- "statusCode": 200,
- "body": f"{functions_deleted} AWS Lambda functions deleted successfully.",
- }
diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja
index fdda6840db..9821a3e94c 100644
--- a/scripts/populate_tox/tox.jinja
+++ b/scripts/populate_tox/tox.jinja
@@ -57,10 +57,7 @@ envlist =
{py3.8,py3.11,py3.12}-asyncpg-latest
# AWS Lambda
- # The aws_lambda tests deploy to the real AWS and have their own
- # matrix of Python versions to run the test lambda function in.
- # see `lambda_runtime` fixture in tests/integrations/aws_lambda.py
- {py3.9}-aws_lambda
+ {py3.8,py3.9,py3.11,py3.13}-aws_lambda
# Beam
{py3.7}-beam-v{2.12}
@@ -249,7 +246,12 @@ deps =
asyncpg: pytest-asyncio
# AWS Lambda
+ aws_lambda: aws-cdk-lib
+ aws_lambda: aws-sam-cli
aws_lambda: boto3
+ aws_lambda: fastapi
+ aws_lambda: requests
+ aws_lambda: uvicorn
# Beam
beam-v2.12: apache-beam~=2.12.0
@@ -381,6 +383,7 @@ deps =
# 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
@@ -524,8 +527,6 @@ setenv =
socket: TESTPATH=tests/integrations/socket
passenv =
- SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID
- SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY
SENTRY_PYTHON_TEST_POSTGRES_HOST
SENTRY_PYTHON_TEST_POSTGRES_USER
SENTRY_PYTHON_TEST_POSTGRES_PASSWORD
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 cf6d769468..293af897c9 100755
--- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py
+++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py
@@ -43,11 +43,7 @@
"clickhouse_driver",
}
-FRAMEWORKS_NEEDING_AWS = {
- "aws_lambda",
-}
-
-FRAMEWORKS_NEEDING_GITHUB_SECRETS = {
+FRAMEWORKS_NEEDING_DOCKER = {
"aws_lambda",
}
@@ -58,9 +54,6 @@
"Common": [
"common",
],
- "Gevent": [
- "gevent",
- ],
"AI": [
"anthropic",
"cohere",
@@ -68,12 +61,8 @@
"openai",
"huggingface_hub",
],
- "AWS": [
- # this is separate from Cloud Computing because only this one test suite
- # needs to run with access to GitHub secrets
- "aws_lambda",
- ],
"Cloud": [
+ "aws_lambda",
"boto3",
"chalice",
"cloud_resource_context",
@@ -295,13 +284,10 @@ def render_template(group, frameworks, py_versions_pinned, py_versions_latest):
"group": group,
"frameworks": frameworks,
"categories": sorted(categories),
- "needs_aws_credentials": bool(set(frameworks) & FRAMEWORKS_NEEDING_AWS),
"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_github_secrets": bool(
- set(frameworks) & FRAMEWORKS_NEEDING_GITHUB_SECRETS
- ),
"py_versions": {
category: [f'"{version}"' for version in _normalize_py_versions(versions)]
for category, versions in py_versions.items()
diff --git a/scripts/split_tox_gh_actions/templates/base.jinja b/scripts/split_tox_gh_actions/templates/base.jinja
index e69b6f9134..75c988e32a 100644
--- a/scripts/split_tox_gh_actions/templates/base.jinja
+++ b/scripts/split_tox_gh_actions/templates/base.jinja
@@ -13,15 +13,7 @@ on:
- release/**
- potel-base
- {% if needs_github_secrets %}
- # XXX: We are using `pull_request_target` instead of `pull_request` because we want
- # this to run on forks with access to the secrets necessary to run the test suite.
- # Prefer to use `pull_request` when possible.
- pull_request_target:
- types: [labeled, opened, reopened, synchronize]
- {% else %}
pull_request:
- {% endif %}
# Cancel in progress workflows on pull_requests.
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
@@ -31,27 +23,13 @@ concurrency:
permissions:
contents: read
- {% if needs_github_secrets %}
- # `write` is needed to remove the `Trigger: tests using secrets` label
- pull-requests: write
- {% endif %}
env:
-{% if needs_aws_credentials %}
-{% raw %}
- SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID: ${{ secrets.SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID }}
- SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY: ${{ secrets.SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY }}
-{% endraw %}
-{% endif %}
BUILD_CACHE_KEY: {% raw %}${{ github.sha }}{% endraw %}
CACHED_BUILD_PATHS: |
{% raw %}${{ github.workspace }}/dist-serverless{% endraw %}
jobs:
-{% if needs_github_secrets %}
-{% include "check_permissions.jinja" %}
-{% endif %}
-
{% for category in categories %}
{% include "test_group.jinja" %}
{% endfor %}
diff --git a/scripts/split_tox_gh_actions/templates/check_permissions.jinja b/scripts/split_tox_gh_actions/templates/check_permissions.jinja
deleted file mode 100644
index 390f447856..0000000000
--- a/scripts/split_tox_gh_actions/templates/check_permissions.jinja
+++ /dev/null
@@ -1,30 +0,0 @@
- check-permissions:
- name: permissions check
- runs-on: ubuntu-20.04
- steps:
- - uses: actions/checkout@v4.2.2
- with:
- persist-credentials: false
-
- - name: Check permissions on PR
- if: github.event_name == 'pull_request_target'
- run: |
- {% raw %}
- python3 -uS .github/workflows/scripts/trigger_tests_on_label.py \
- --repo-id ${{ github.event.repository.id }} \
- --pr ${{ github.event.number }} \
- --event ${{ github.event.action }} \
- --username "$ARG_USERNAME" \
- --label-names "$ARG_LABEL_NAMES"
- {% endraw %}
- env:
- {% raw %}
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- # these can contain special characters
- ARG_USERNAME: ${{ github.event.pull_request.user.login }}
- ARG_LABEL_NAMES: ${{ toJSON(github.event.pull_request.labels.*.name) }}
- {% endraw %}
-
- - name: Check permissions on repo branch
- if: github.event_name == 'push'
- run: true
diff --git a/scripts/split_tox_gh_actions/templates/test_group.jinja b/scripts/split_tox_gh_actions/templates/test_group.jinja
index adf530b5ad..251e54d01d 100644
--- a/scripts/split_tox_gh_actions/templates/test_group.jinja
+++ b/scripts/split_tox_gh_actions/templates/test_group.jinja
@@ -8,10 +8,12 @@
python-version: [{{ py_versions.get(category)|join(",") }}]
os: [ubuntu-22.04]
- {% if needs_github_secrets %}
- needs: check-permissions
+ {% if needs_docker %}
+ services:
+ docker:
+ image: docker:dind # Required for Docker network management
+ options: --privileged # Required for Docker-in-Docker operations
{% endif %}
-
{% if needs_postgres %}
services:
postgres:
@@ -36,12 +38,6 @@
steps:
- uses: actions/checkout@v4.2.2
- {% if needs_github_secrets %}
- {% raw %}
- with:
- ref: ${{ github.event.pull_request.head.sha || github.ref }}
- {% endraw %}
- {% endif %}
- uses: actions/setup-python@v5
with:
python-version: {% raw %}${{ matrix.python-version }}{% endraw %}
@@ -82,7 +78,7 @@
- name: Upload coverage to Codecov
if: {% raw %}${{ !cancelled() }}{% endraw %}
- uses: codecov/codecov-action@v5.3.1
+ uses: codecov/codecov-action@v5.4.0
with:
token: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %}
files: coverage.xml
diff --git a/scripts/test-lambda-locally/.gitignore b/scripts/test-lambda-locally/.gitignore
new file mode 100644
index 0000000000..f9b7f4de58
--- /dev/null
+++ b/scripts/test-lambda-locally/.gitignore
@@ -0,0 +1,4 @@
+.envrc
+.venv/
+package/
+lambda_deployment_package.zip
diff --git a/scripts/test-lambda-locally/README.md b/scripts/test-lambda-locally/README.md
new file mode 100644
index 0000000000..115927cc2b
--- /dev/null
+++ b/scripts/test-lambda-locally/README.md
@@ -0,0 +1,28 @@
+# Test AWS Lambda functions locally
+
+An easy way to run an AWS Lambda function with the Sentry SDK locally.
+
+This is a small helper to create a AWS Lambda function that includes the
+currently checked out Sentry SDK and runs it in a local AWS Lambda environment.
+
+Currently only embedding the Sentry SDK into the Lambda function package
+is supported. Adding the SDK as Lambda Layer is not possible at the moment.
+
+## Prerequisites
+
+- Set `SENTRY_DSN` environment variable. The Lambda function will use this DSN.
+- You need to have Docker installed and running.
+
+## Run Lambda function
+
+- Update `lambda_function.py` to include your test code.
+- Run `./deploy-lambda-locally.sh`. This will:
+ - Install [AWS SAM](https://aws.amazon.com/serverless/sam/) in a virtual Python environment
+ - Create a lambda function package in `package/` that includes
+ - The currently checked out Sentry SDK
+ - All dependencies of the Sentry SDK (certifi and urllib3)
+ - The actual function defined in `lamdba_function.py`.
+ - Zip everything together into lambda_deployment_package.zip
+ - Run a local Lambda environment that serves that Lambda function.
+- Point your browser to `http://127.0.0.1:3000` to access your Lambda function.
+ - Currently GET and POST requests are possible. This is defined in `template.yaml`.
\ No newline at end of file
diff --git a/scripts/test-lambda-locally/deploy-lambda-locally.sh b/scripts/test-lambda-locally/deploy-lambda-locally.sh
new file mode 100755
index 0000000000..495c1259dc
--- /dev/null
+++ b/scripts/test-lambda-locally/deploy-lambda-locally.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+# exit on first error
+set -xeuo pipefail
+
+# Setup local AWS Lambda environment
+
+# Install uv if it's not installed
+if ! command -v uv &> /dev/null; then
+ curl -LsSf https://astral.sh/uv/install.sh | sh
+fi
+
+uv sync
+
+# Create a deployment package of the lambda function in `lambda_function.py`.
+rm -rf package && mkdir -p package
+pip install ../../../sentry-python -t package/ --upgrade
+cp lambda_function.py package/
+cd package && zip -r ../lambda_deployment_package.zip . && cd ..
+
+# Start the local Lambda server with the new function (defined in template.yaml)
+uv run sam local start-api \
+ --skip-pull-image \
+ --force-image-build \
+ --parameter-overrides SentryDsn=$SENTRY_DSN
diff --git a/scripts/test-lambda-locally/lambda_function.py b/scripts/test-lambda-locally/lambda_function.py
new file mode 100644
index 0000000000..ceab090499
--- /dev/null
+++ b/scripts/test-lambda-locally/lambda_function.py
@@ -0,0 +1,25 @@
+import logging
+import os
+import sentry_sdk
+
+from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
+from sentry_sdk.integrations.logging import LoggingIntegration
+
+def lambda_handler(event, context):
+ sentry_sdk.init(
+ dsn=os.environ.get("SENTRY_DSN"),
+ attach_stacktrace=True,
+ integrations=[
+ LoggingIntegration(level=logging.INFO, event_level=logging.ERROR),
+ AwsLambdaIntegration(timeout_warning=True)
+ ],
+ traces_sample_rate=1.0,
+ debug=True,
+ )
+
+ try:
+ my_dict = {"a" : "test"}
+ value = my_dict["b"] # This should raise exception
+ except:
+ logging.exception("Key Does not Exists")
+ raise
diff --git a/scripts/test-lambda-locally/pyproject.toml b/scripts/test-lambda-locally/pyproject.toml
new file mode 100644
index 0000000000..522e9620e8
--- /dev/null
+++ b/scripts/test-lambda-locally/pyproject.toml
@@ -0,0 +1,8 @@
+[project]
+name = "test-lambda-locally"
+version = "0"
+requires-python = ">=3.12"
+
+dependencies = [
+ "aws-sam-cli>=1.135.0",
+]
diff --git a/scripts/test-lambda-locally/template.yaml b/scripts/test-lambda-locally/template.yaml
new file mode 100644
index 0000000000..67b8f6e7da
--- /dev/null
+++ b/scripts/test-lambda-locally/template.yaml
@@ -0,0 +1,29 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Resources:
+ SentryLambdaFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda_deployment_package.zip
+ Handler: lambda_function.lambda_handler
+ Runtime: python3.12
+ Timeout: 30
+ Environment:
+ Variables:
+ SENTRY_DSN: !Ref SentryDsn
+ Events:
+ ApiEventGet:
+ Type: Api
+ Properties:
+ Path: /
+ Method: get
+ ApiEventPost:
+ Type: Api
+ Properties:
+ Path: /
+ Method: post
+
+Parameters:
+ SentryDsn:
+ Type: String
+ Default: ''
diff --git a/scripts/test-lambda-locally/uv.lock b/scripts/test-lambda-locally/uv.lock
new file mode 100644
index 0000000000..889ca8e62f
--- /dev/null
+++ b/scripts/test-lambda-locally/uv.lock
@@ -0,0 +1,1239 @@
+version = 1
+revision = 1
+requires-python = ">=3.12"
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
+]
+
+[[package]]
+name = "arrow"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+ { name = "types-python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419 },
+]
+
+[[package]]
+name = "attrs"
+version = "25.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 },
+]
+
+[[package]]
+name = "aws-lambda-builders"
+version = "1.53.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "setuptools" },
+ { name = "wheel" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/0a/09a966ac588a3eb3333348a5e13892889fe9531a491359b35bc5b7b13818/aws_lambda_builders-1.53.0.tar.gz", hash = "sha256:d08bfa947fff590f1bedd16c2f4ec7722cbb8869aae80764d99215a41ff284a1", size = 95491 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/28/8c/9cf80784437059db1999655a943eb950a0587793c3fddb56aee3c0f60ae3/aws_lambda_builders-1.53.0-py3-none-any.whl", hash = "sha256:ca9ddd99214aef8a113a3fcd7d7fe3951ef0e078478484f03c398a3bdee04ccb", size = 131138 },
+]
+
+[[package]]
+name = "aws-sam-cli"
+version = "1.135.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aws-lambda-builders" },
+ { name = "aws-sam-translator" },
+ { name = "boto3" },
+ { name = "boto3-stubs", extra = ["apigateway", "cloudformation", "ecr", "iam", "kinesis", "lambda", "s3", "schemas", "secretsmanager", "signer", "sqs", "stepfunctions", "sts", "xray"] },
+ { name = "cfn-lint" },
+ { name = "chevron" },
+ { name = "click" },
+ { name = "cookiecutter" },
+ { name = "dateparser" },
+ { name = "docker" },
+ { name = "flask" },
+ { name = "jmespath" },
+ { name = "jsonschema" },
+ { name = "pyopenssl" },
+ { name = "pyyaml" },
+ { name = "regex" },
+ { name = "requests" },
+ { name = "rich" },
+ { name = "ruamel-yaml" },
+ { name = "tomlkit" },
+ { name = "typing-extensions" },
+ { name = "tzlocal" },
+ { name = "watchdog" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/ff/92159d25b8c563de8605cb67b18c6d4ec68880d2dfd7eac689f0f4b80f57/aws_sam_cli-1.135.0.tar.gz", hash = "sha256:c630b351feeb4854ad5ecea6768920c61e7d331b3d040a677fa8744380f48808", size = 5792676 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/0f/f299f9ac27d946d7bf5fb11b3d01e7d1f5affd2ec9220449636949ccc39a/aws_sam_cli-1.135.0-py3-none-any.whl", hash = "sha256:473d30202b89a9624201e46b3ecb9ad5bcd05332c3d308a888464f002c29432b", size = 6077290 },
+]
+
+[[package]]
+name = "aws-sam-translator"
+version = "1.95.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "boto3" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/8c/4ea1c5fafdec02f2b3a91d60889219a42c18f5c3dd93ec13ef985e4249f6/aws_sam_translator-1.95.0.tar.gz", hash = "sha256:fd2b891fc4cbdde1e06130eaf2710de5cc74442a656b7859b3840691144494cf", size = 327484 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/5a/2edbe63d0b1c1e3c685a9b8464626f59c48bfbcc4e20142acae5ddea504c/aws_sam_translator-1.95.0-py3-none-any.whl", hash = "sha256:c9e0f22cbe83c768f7d20a3afb7e654bd6bfc087b387528bd48e98366b82ae40", size = 385846 },
+]
+
+[[package]]
+name = "binaryornot"
+version = "0.4.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "chardet" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a7/fe/7ebfec74d49f97fc55cd38240c7a7d08134002b1e14be8c3897c0dd5e49b/binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061", size = 371054 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/7e/f7b6f453e6481d1e233540262ccbfcf89adcd43606f44a028d7f5fae5eb2/binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4", size = 9006 },
+]
+
+[[package]]
+name = "blinker"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 },
+]
+
+[[package]]
+name = "boto3"
+version = "1.37.11"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "botocore" },
+ { name = "jmespath" },
+ { name = "s3transfer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/21/12/948ab48f2e2d4eda72f907352e67379334ded1a2a6d1ebbaac11e77dfca9/boto3-1.37.11.tar.gz", hash = "sha256:8eec08363ef5db05c2fbf58e89f0c0de6276cda2fdce01e76b3b5f423cd5c0f4", size = 111323 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/29/55/0afe0471e391f4aaa99e5216b5c9ce6493756c0b7a7d8f8ffe85ba83b7a0/boto3-1.37.11-py3-none-any.whl", hash = "sha256:da6c22fc8a7e9bca5d7fc465a877ac3d45b6b086d776bd1a6c55bdde60523741", size = 139553 },
+]
+
+[[package]]
+name = "boto3-stubs"
+version = "1.35.71"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "botocore-stubs" },
+ { name = "types-s3transfer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/85/86243ad2792f8506b567c645d97ece548258203c55bcc165fd5801f4372f/boto3_stubs-1.35.71.tar.gz", hash = "sha256:50e20fa74248c96b3e3498b2d81388585583e38b9f0609d2fa58257e49c986a5", size = 93776 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a6/d1/aedf5f4a92e1e74ee29a4d43084780f2d77aeef3d734e550aa2ab304e1fb/boto3_stubs-1.35.71-py3-none-any.whl", hash = "sha256:4abf357250bdb16d1a56489a59bfc385d132a43677956bd984f6578638d599c0", size = 62964 },
+]
+
+[package.optional-dependencies]
+apigateway = [
+ { name = "mypy-boto3-apigateway" },
+]
+cloudformation = [
+ { name = "mypy-boto3-cloudformation" },
+]
+ecr = [
+ { name = "mypy-boto3-ecr" },
+]
+iam = [
+ { name = "mypy-boto3-iam" },
+]
+kinesis = [
+ { name = "mypy-boto3-kinesis" },
+]
+lambda = [
+ { name = "mypy-boto3-lambda" },
+]
+s3 = [
+ { name = "mypy-boto3-s3" },
+]
+schemas = [
+ { name = "mypy-boto3-schemas" },
+]
+secretsmanager = [
+ { name = "mypy-boto3-secretsmanager" },
+]
+signer = [
+ { name = "mypy-boto3-signer" },
+]
+sqs = [
+ { name = "mypy-boto3-sqs" },
+]
+stepfunctions = [
+ { name = "mypy-boto3-stepfunctions" },
+]
+sts = [
+ { name = "mypy-boto3-sts" },
+]
+xray = [
+ { name = "mypy-boto3-xray" },
+]
+
+[[package]]
+name = "botocore"
+version = "1.37.11"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jmespath" },
+ { name = "python-dateutil" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/24/ce/b11d4405b8be900bfea15d9460376ff6f07dd0e1b1f8a47e2671bf6e5ca8/botocore-1.37.11.tar.gz", hash = "sha256:72eb3a9a58b064be26ba154e5e56373633b58f951941c340ace0d379590d98b5", size = 13640593 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/0d/b07e9b6cd8823e520f1782742730f2e68b68ad7444825ed8dd8fcdb98fcb/botocore-1.37.11-py3-none-any.whl", hash = "sha256:02505309b1235f9f15a6da79103ca224b3f3dc5f6a62f8630fbb2c6ed05e2da8", size = 13407367 },
+]
+
+[[package]]
+name = "botocore-stubs"
+version = "1.37.11"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "types-awscrt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/6f/710664aac77cf91a663dcb291c2bbdcfe796909115aa5bb03382521359b1/botocore_stubs-1.37.11.tar.gz", hash = "sha256:9b89ba9a98eb9f088a5f82c52488013858092777c17b56265574bbf2d21da422", size = 42119 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/89/c8a6497055f9ecd0af5c16434c277635a4b365793d54f2d8f2b28aeeb58e/botocore_stubs-1.37.11-py3-none-any.whl", hash = "sha256:bec458a0d054892cdf82466b4d075f30a36fa03ce34f9becbcace5f36ec674bf", size = 65384 },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.1.31"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
+ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
+ { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
+ { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
+ { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
+ { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
+ { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
+ { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
+]
+
+[[package]]
+name = "cfn-lint"
+version = "1.25.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aws-sam-translator" },
+ { name = "jsonpatch" },
+ { name = "networkx" },
+ { name = "pyyaml" },
+ { name = "regex" },
+ { name = "sympy" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4d/c0/a36a1bdc6ba1fd4a7e5f48cd23a1802ccaf745ffb5c79e3fdf800eb5ae90/cfn_lint-1.25.1.tar.gz", hash = "sha256:717012566c6034ffa7e60fcf1b350804d093ee37589a1e91a1fd867f33a930b7", size = 2837233 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/1c/b03940f2213f308f19318aaa8847adfe789b834e497f8839b2c9a876618b/cfn_lint-1.25.1-py3-none-any.whl", hash = "sha256:bbf6c2d95689da466dc427217ab7ed8f3a2a4a134df70876cc63e41aaad9385a", size = 4907033 },
+]
+
+[[package]]
+name = "chardet"
+version = "5.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
+ { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
+ { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
+ { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
+ { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
+ { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
+ { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
+ { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
+ { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
+ { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
+ { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
+ { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
+ { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
+ { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
+ { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
+ { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
+ { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
+ { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
+ { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
+ { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
+ { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
+ { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
+ { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
+ { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
+ { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
+ { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
+ { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
+]
+
+[[package]]
+name = "chevron"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/1f/ca74b65b19798895d63a6e92874162f44233467c9e7c1ed8afd19016ebe9/chevron-0.14.0.tar.gz", hash = "sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf", size = 11440 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/93/342cc62a70ab727e093ed98e02a725d85b746345f05d2b5e5034649f4ec8/chevron-0.14.0-py3-none-any.whl", hash = "sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443", size = 11595 },
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "cookiecutter"
+version = "2.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "arrow" },
+ { name = "binaryornot" },
+ { name = "click" },
+ { name = "jinja2" },
+ { name = "python-slugify" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/17/9f2cd228eb949a91915acd38d3eecdc9d8893dde353b603f0db7e9f6be55/cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c", size = 158767 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/d9/0137658a353168ffa9d0fc14b812d3834772040858ddd1cb6eeaf09f7a44/cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d", size = 39177 },
+]
+
+[[package]]
+name = "cryptography"
+version = "44.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 },
+ { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 },
+ { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 },
+ { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 },
+ { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 },
+ { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 },
+ { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 },
+ { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 },
+ { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 },
+ { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 },
+ { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 },
+ { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 },
+ { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 },
+ { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 },
+ { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 },
+ { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 },
+ { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 },
+ { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 },
+ { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 },
+ { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 },
+ { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 },
+ { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 },
+ { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 },
+]
+
+[[package]]
+name = "dateparser"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+ { name = "pytz" },
+ { name = "regex" },
+ { name = "tzlocal" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bd/3f/d3207a05f5b6a78c66d86631e60bfba5af163738a599a5b9aa2c2737a09e/dateparser-1.2.1.tar.gz", hash = "sha256:7e4919aeb48481dbfc01ac9683c8e20bfe95bb715a38c1e9f6af889f4f30ccc3", size = 309924 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cf/0a/981c438c4cd84147c781e4e96c1d72df03775deb1bc76c5a6ee8afa89c62/dateparser-1.2.1-py3-none-any.whl", hash = "sha256:bdcac262a467e6260030040748ad7c10d6bacd4f3b9cdb4cfd2251939174508c", size = 295658 },
+]
+
+[[package]]
+name = "docker"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "requests" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 },
+]
+
+[[package]]
+name = "flask"
+version = "3.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "blinker" },
+ { name = "click" },
+ { name = "itsdangerous" },
+ { name = "jinja2" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/89/50/dff6380f1c7f84135484e176e0cac8690af72fa90e932ad2a0a60e28c69b/flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", size = 680824 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136", size = 102979 },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+]
+
+[[package]]
+name = "itsdangerous"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
+]
+
+[[package]]
+name = "jmespath"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 },
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpointer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898 },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.23.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2024.10.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
+ { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
+ { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
+ { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
+ { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
+ { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
+ { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
+ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
+ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
+ { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
+ { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
+ { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
+ { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
+ { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
+ { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
+ { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
+ { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
+ { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
+ { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
+ { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
+ { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
+ { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
+ { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
+ { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
+ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
+ { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
+ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 },
+]
+
+[[package]]
+name = "mypy-boto3-apigateway"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/3d/c5dc7a750d9fdba2bf704d3d963be9ad4ed617fe5bb98e5c88374a3d8d69/mypy_boto3_apigateway-1.35.93.tar.gz", hash = "sha256:df90957c5f2c219663f825b905cb53b9f53fd7982e01bb21da65f5757c3d5d41", size = 44837 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/7d/89f26a626ab30283143222430bd39ec46cf8a2ae002e5b5c590e01ff3ad0/mypy_boto3_apigateway-1.35.93-py3-none-any.whl", hash = "sha256:a5649e9899209470c35249651f7f2faa7d6919aab6b4fcac7bd4a54c11e872bc", size = 50874 },
+]
+
+[[package]]
+name = "mypy-boto3-cloudformation"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/26/e59425e30fb1783aa718f1a8ac93cdc415e279e175c953ee0a72310f7490/mypy_boto3_cloudformation-1.35.93.tar.gz", hash = "sha256:57dc112ff3e2ddc1e9e621e428490b904c0da8c1532d30e9fa2a19aefde9f719", size = 54529 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/52/6e73adba190fc65c5cf89ed9394cc8a1acb073989f4eda87f80f451c9b15/mypy_boto3_cloudformation-1.35.93-py3-none-any.whl", hash = "sha256:4111913cb2c9fd9099ecd616212923312fde0c126ee41f5821759ae9df4272b9", size = 66124 },
+]
+
+[[package]]
+name = "mypy-boto3-ecr"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/92/ae/1598bf3dc7069f0e48a60a482dffa71885e1558aa076243375820de2792f/mypy_boto3_ecr-1.35.93.tar.gz", hash = "sha256:57295a72a9473b8542578ab15eb0a4909cad6f2cee1da41ce6a8a40ab7051438", size = 33904 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/3b/4130e22423812da282bd9ebbf08a0f14ed2e314409847bc336b841c8177b/mypy_boto3_ecr-1.35.93-py3-none-any.whl", hash = "sha256:49d98ac7376e919c0061da44aeae9577b63343eee2c1d537fd636d8886db9ad2", size = 39733 },
+]
+
+[[package]]
+name = "mypy-boto3-iam"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/24/7cb0b26c3af8207496880155441cfd7f5d8c5404d4669e39385eb307672d/mypy_boto3_iam-1.35.93.tar.gz", hash = "sha256:2595c8dac406e4e771d3b7d7835faacb936d20449b9cdd17a53f076219cc7712", size = 85815 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/5a/2694c8c692fad6908c3a52f629eb87b04c242dc8bb0091e56ff3780cdb45/mypy_boto3_iam-1.35.93-py3-none-any.whl", hash = "sha256:e2955040062bf9cb587a1874e1b2f2cca33cbf167187fd3a56b6c5412cc13dc9", size = 91125 },
+]
+
+[[package]]
+name = "mypy-boto3-kinesis"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/c3/eb9f1aeaf42ea55c473b0281fe5813aafe3283733ad84fbd27c370416753/mypy_boto3_kinesis-1.35.93.tar.gz", hash = "sha256:f0718f5b54b955761790b4b33bdcab8d0c779bd50cc671c6862a8e0554515bda", size = 22476 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/bd/e44b999f516116dcb034262a1ed04d8ed3b830e84970b1224823ce866031/mypy_boto3_kinesis-1.35.93-py3-none-any.whl", hash = "sha256:fb11df380319e3cf5c26f43536107593836e36c6b9f3b415a7016aeaed2af1de", size = 32164 },
+]
+
+[[package]]
+name = "mypy-boto3-lambda"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/ef/b90e51be87b5c226005c765a7109a26b5ce39cf349f2603336bd5c365863/mypy_boto3_lambda-1.35.93.tar.gz", hash = "sha256:c11b047743c7635ea8385abffaf97788a108b71479612e9b5e7d0bb19029d7a4", size = 41120 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/f0/3c03cc63c157046106f59768e915c21377a372be6bc9f079601dd646cf4d/mypy_boto3_lambda-1.35.93-py3-none-any.whl", hash = "sha256:6bcd623c827724cde0b21b30c328515811b178763b75f0701a641cc7aa3aa414", size = 47708 },
+]
+
+[[package]]
+name = "mypy-boto3-s3"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/53/99667aad21b236612ecb50eee09fdc4de6fbe39c3a75a6bad387d108ed1f/mypy_boto3_s3-1.35.93.tar.gz", hash = "sha256:b4529e57a8d5f21d4c61fe650fa6764fee2ba7ab524a455a34ba2698ef6d27a8", size = 72871 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/52/9d45db5690eb2b3160c43259d70dd6890d9bc24633848bcb8ef835d44d6c/mypy_boto3_s3-1.35.93-py3-none-any.whl", hash = "sha256:4cd3f1718fa0d8a54212c495cdff493bdcc6a8ae419d95428c60fb6bc7db7980", size = 79501 },
+]
+
+[[package]]
+name = "mypy-boto3-schemas"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/f7/63c5b0db122b99265a14f179f41ab01566610c78abe14e63a4df3ebca7fa/mypy_boto3_schemas-1.35.93.tar.gz", hash = "sha256:7f2255ddd6d531101ec67fbd1afca8be02568f4e5787d1631199aa25b58a480f", size = 20680 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b2/37/cf848ce4ec07bbd7d64c91efe8d31f5aa86bf5d6d2a9f7123ca3ce3fed44/mypy_boto3_schemas-1.35.93-py3-none-any.whl", hash = "sha256:9e82b7d6e059a531359cc0304b5d4c979406d06e9d19482c7a22ccb61b40c7ff", size = 28746 },
+]
+
+[[package]]
+name = "mypy-boto3-secretsmanager"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/c6/1c69c3ac9fadeb6cc01da5a90edd5f36cbf09a4fa66e8cef638917eba4d1/mypy_boto3_secretsmanager-1.35.93.tar.gz", hash = "sha256:b6c4bc88a5fe4143124272728d41342e01c778b406db9d647a20dad0de7d6f47", size = 19624 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/ff/758f8869d10b10bf6bec7908bd9d532fdd26b6f04c2af4de3751d2c92b93/mypy_boto3_secretsmanager-1.35.93-py3-none-any.whl", hash = "sha256:521075d42b6d05f0d7302d1837520e9111a84d6613152d32dc8cbb3cd6fceeec", size = 26581 },
+]
+
+[[package]]
+name = "mypy-boto3-signer"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d1/00/954104765b3414b0221cf18efebcee656f7b8be603866682a0dcf9e00ecf/mypy_boto3_signer-1.35.93.tar.gz", hash = "sha256:f12c7c7025cc25804146431f639f3eb9db664a4695bf28d2a87f58111fc7f888", size = 20496 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/a0/142a49f1bd98b9a393896e0912cc8dd7a1ac91c2fff224f2c4efb166e180/mypy_boto3_signer-1.35.93-py3-none-any.whl", hash = "sha256:e1ac026096be6a52b6de45771226efbd3909a1861a638441572d926650d7fd8c", size = 28770 },
+]
+
+[[package]]
+name = "mypy-boto3-sqs"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/29/5b/040ba82c53d5edf578ad0aafcac501b91a259b40f296ef6662db975b6595/mypy_boto3_sqs-1.35.93.tar.gz", hash = "sha256:8ea7f63e0878544705c31996ae4c064095fbb4f780f8323a84f7a75281d643fe", size = 23344 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/eb/d8c10da3f905921f70f008f3bca092711e316ced49287e42f45309860aca/mypy_boto3_sqs-1.35.93-py3-none-any.whl", hash = "sha256:341974f77e66851b9a4190d0014481e6baabae82d32f9ee559faa823b693609b", size = 33491 },
+]
+
+[[package]]
+name = "mypy-boto3-stepfunctions"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/f9/44a59a6c84edfd94477e5427befcbecdb4f92ae34d897536671dc4994e23/mypy_boto3_stepfunctions-1.35.93.tar.gz", hash = "sha256:20230615c42e7aabbd43b62657ca3534e96767245705d12d42672ac87cd1b59c", size = 30894 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/39/0964782eff12ec9c22a5dd78bc19f755df313fb6aa1215293444899dc40e/mypy_boto3_stepfunctions-1.35.93-py3-none-any.whl", hash = "sha256:7994450153298b87382119680d7fae4d8b5a6e6250cef364148ad8d0b84bd237", size = 35602 },
+]
+
+[[package]]
+name = "mypy-boto3-sts"
+version = "1.35.97"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9f/fc/652992367bad0bae7d1c8d8bd5fa455570de77337f8d0c2021263dc4e695/mypy_boto3_sts-1.35.97.tar.gz", hash = "sha256:6df698f6a400a82ebcc2f10adb43557f66278467200e0f75588e7de3e4a1622d", size = 16487 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/7c/092999366962bbe0bab5af8e18e0c8f70943ca34a42c214e3862df2fa80b/mypy_boto3_sts-1.35.97-py3-none-any.whl", hash = "sha256:50c32613aa9e8d33e5df922392e32daed6fcd0e4d4cc8d43f5948c69be1c9e1e", size = 19991 },
+]
+
+[[package]]
+name = "mypy-boto3-xray"
+version = "1.35.93"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/98/1ffe456cf073fe6ee1826f053943793d4082fe02412a109c72c0f414a66c/mypy_boto3_xray-1.35.93.tar.gz", hash = "sha256:7e0af9474f06da1923aa37c8639b051042cc3a56d1a36b0141124d9de7be6709", size = 31639 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8f/b4/826f269d883bd76df41b44fba4a49b2cd9b2a2a34a5561bc251bdb6778f2/mypy_boto3_xray-1.35.93-py3-none-any.whl", hash = "sha256:e80c2be40c5cb4851dc08c145101b4e52a6f471dab0fc5f488975f6e14f7cb93", size = 36455 },
+]
+
+[[package]]
+name = "networkx"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.10.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.27.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
+ { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
+ { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
+ { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
+ { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
+ { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
+ { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
+ { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
+ { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
+ { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
+ { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
+ { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
+ { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
+ { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
+ { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
+ { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
+ { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
+ { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
+ { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
+ { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
+ { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
+ { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
+ { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
+ { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
+ { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
+ { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
+ { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
+ { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
+]
+
+[[package]]
+name = "pyopenssl"
+version = "24.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c1/d4/1067b82c4fc674d6f6e9e8d26b3dff978da46d351ca3bac171544693e085/pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36", size = 178944 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/22/40f9162e943f86f0fc927ebc648078be87def360d9d8db346619fb97df2b/pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a", size = 56111 },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
+]
+
+[[package]]
+name = "python-slugify"
+version = "8.0.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "text-unidecode" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051 },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 },
+]
+
+[[package]]
+name = "pywin32"
+version = "309"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/2c/b0240b14ff3dba7a8a7122dc9bbf7fbd21ed0e8b57c109633675b5d1761f/pywin32-309-cp312-cp312-win32.whl", hash = "sha256:de9acacced5fa82f557298b1fed5fef7bd49beee04190f68e1e4783fbdc19926", size = 8790648 },
+ { url = "https://files.pythonhosted.org/packages/dd/11/c36884c732e2b3397deee808b5dac1abbb170ec37f94c6606fcb04d1e9d7/pywin32-309-cp312-cp312-win_amd64.whl", hash = "sha256:6ff9eebb77ffc3d59812c68db33c0a7817e1337e3537859499bd27586330fc9e", size = 9497399 },
+ { url = "https://files.pythonhosted.org/packages/18/9f/79703972958f8ba3fd38bc9bf1165810bd75124982419b0cc433a2894d46/pywin32-309-cp312-cp312-win_arm64.whl", hash = "sha256:619f3e0a327b5418d833f44dc87859523635cf339f86071cc65a13c07be3110f", size = 8454122 },
+ { url = "https://files.pythonhosted.org/packages/6c/c3/51aca6887cc5e410aa4cdc55662cf8438212440c67335c3f141b02eb8d52/pywin32-309-cp313-cp313-win32.whl", hash = "sha256:008bffd4afd6de8ca46c6486085414cc898263a21a63c7f860d54c9d02b45c8d", size = 8789700 },
+ { url = "https://files.pythonhosted.org/packages/dd/66/330f265140fa814b4ed1bf16aea701f9d005f8f4ab57a54feb17f53afe7e/pywin32-309-cp313-cp313-win_amd64.whl", hash = "sha256:bd0724f58492db4cbfbeb1fcd606495205aa119370c0ddc4f70e5771a3ab768d", size = 9496714 },
+ { url = "https://files.pythonhosted.org/packages/2c/84/9a51e6949a03f25cd329ece54dbf0846d57fadd2e79046c3b8d140aaa132/pywin32-309-cp313-cp313-win_arm64.whl", hash = "sha256:8fd9669cfd41863b688a1bc9b1d4d2d76fd4ba2128be50a70b0ea66b8d37953b", size = 8453052 },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
+]
+
+[[package]]
+name = "referencing"
+version = "0.36.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 },
+]
+
+[[package]]
+name = "regex"
+version = "2024.11.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 },
+ { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 },
+ { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 },
+ { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 },
+ { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 },
+ { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 },
+ { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 },
+ { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 },
+ { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 },
+ { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 },
+ { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 },
+ { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 },
+ { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 },
+ { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 },
+ { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 },
+ { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 },
+ { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 },
+ { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 },
+ { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 },
+ { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 },
+ { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 },
+ { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 },
+ { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 },
+ { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 },
+ { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 },
+ { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 },
+ { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 },
+ { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 },
+ { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 },
+ { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
+]
+
+[[package]]
+name = "rich"
+version = "13.9.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.23.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/79/2ce611b18c4fd83d9e3aecb5cba93e1917c050f556db39842889fa69b79f/rpds_py-0.23.1.tar.gz", hash = "sha256:7f3240dcfa14d198dba24b8b9cb3b108c06b68d45b7babd9eefc1038fdf7e707", size = 26806 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f3/8c/d17efccb9f5b9137ddea706664aebae694384ae1d5997c0202093e37185a/rpds_py-0.23.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3902df19540e9af4cc0c3ae75974c65d2c156b9257e91f5101a51f99136d834c", size = 364369 },
+ { url = "https://files.pythonhosted.org/packages/6e/c0/ab030f696b5c573107115a88d8d73d80f03309e60952b64c584c70c659af/rpds_py-0.23.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66f8d2a17e5838dd6fb9be6baaba8e75ae2f5fa6b6b755d597184bfcd3cb0eba", size = 349965 },
+ { url = "https://files.pythonhosted.org/packages/b3/55/b40170f5a079c4fb0b6a82b299689e66e744edca3c3375a8b160fb797660/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112b8774b0b4ee22368fec42749b94366bd9b536f8f74c3d4175d4395f5cbd31", size = 389064 },
+ { url = "https://files.pythonhosted.org/packages/ab/1c/b03a912c59ec7c1e16b26e587b9dfa8ddff3b07851e781e8c46e908a365a/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0df046f2266e8586cf09d00588302a32923eb6386ced0ca5c9deade6af9a149", size = 397741 },
+ { url = "https://files.pythonhosted.org/packages/52/6f/151b90792b62fb6f87099bcc9044c626881fdd54e31bf98541f830b15cea/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3288930b947cbebe767f84cf618d2cbe0b13be476e749da0e6a009f986248c", size = 448784 },
+ { url = "https://files.pythonhosted.org/packages/71/2a/6de67c0c97ec7857e0e9e5cd7c52405af931b303eb1e5b9eff6c50fd9a2e/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce473a2351c018b06dd8d30d5da8ab5a0831056cc53b2006e2a8028172c37ce5", size = 440203 },
+ { url = "https://files.pythonhosted.org/packages/db/5e/e759cd1c276d98a4b1f464b17a9bf66c65d29f8f85754e27e1467feaa7c3/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d550d7e9e7d8676b183b37d65b5cd8de13676a738973d330b59dc8312df9c5dc", size = 391611 },
+ { url = "https://files.pythonhosted.org/packages/1c/1e/2900358efcc0d9408c7289769cba4c0974d9db314aa884028ed7f7364f61/rpds_py-0.23.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e14f86b871ea74c3fddc9a40e947d6a5d09def5adc2076ee61fb910a9014fb35", size = 423306 },
+ { url = "https://files.pythonhosted.org/packages/23/07/6c177e6d059f5d39689352d6c69a926ee4805ffdb6f06203570234d3d8f7/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf5be5ba34e19be579ae873da515a2836a2166d8d7ee43be6ff909eda42b72b", size = 562323 },
+ { url = "https://files.pythonhosted.org/packages/70/e4/f9097fd1c02b516fff9850792161eb9fc20a2fd54762f3c69eae0bdb67cb/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7031d493c4465dbc8d40bd6cafefef4bd472b17db0ab94c53e7909ee781b9ef", size = 588351 },
+ { url = "https://files.pythonhosted.org/packages/87/39/5db3c6f326bfbe4576ae2af6435bd7555867d20ae690c786ff33659f293b/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55ff4151cfd4bc635e51cfb1c59ac9f7196b256b12e3a57deb9e5742e65941ad", size = 557252 },
+ { url = "https://files.pythonhosted.org/packages/fd/14/2d5ad292f144fa79bafb78d2eb5b8a3a91c358b6065443cb9c49b5d1fedf/rpds_py-0.23.1-cp312-cp312-win32.whl", hash = "sha256:a9d3b728f5a5873d84cba997b9d617c6090ca5721caaa691f3b1a78c60adc057", size = 222181 },
+ { url = "https://files.pythonhosted.org/packages/a3/4f/0fce63e0f5cdd658e71e21abd17ac1bc9312741ebb8b3f74eeed2ebdf771/rpds_py-0.23.1-cp312-cp312-win_amd64.whl", hash = "sha256:b03a8d50b137ee758e4c73638b10747b7c39988eb8e6cd11abb7084266455165", size = 237426 },
+ { url = "https://files.pythonhosted.org/packages/13/9d/b8b2c0edffb0bed15be17b6d5ab06216f2f47f9ee49259c7e96a3ad4ca42/rpds_py-0.23.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4caafd1a22e5eaa3732acb7672a497123354bef79a9d7ceed43387d25025e935", size = 363672 },
+ { url = "https://files.pythonhosted.org/packages/bd/c2/5056fa29e6894144d7ba4c938b9b0445f75836b87d2dd00ed4999dc45a8c/rpds_py-0.23.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:178f8a60fc24511c0eb756af741c476b87b610dba83270fce1e5a430204566a4", size = 349602 },
+ { url = "https://files.pythonhosted.org/packages/b0/bc/33779a1bb0ee32d8d706b173825aab75c628521d23ce72a7c1e6a6852f86/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c632419c3870507ca20a37c8f8f5352317aca097639e524ad129f58c125c61c6", size = 388746 },
+ { url = "https://files.pythonhosted.org/packages/62/0b/71db3e36b7780a619698ec82a9c87ab44ad7ca7f5480913e8a59ff76f050/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:698a79d295626ee292d1730bc2ef6e70a3ab135b1d79ada8fde3ed0047b65a10", size = 397076 },
+ { url = "https://files.pythonhosted.org/packages/bb/2e/494398f613edf77ba10a916b1ddea2acce42ab0e3b62e2c70ffc0757ce00/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:271fa2184cf28bdded86bb6217c8e08d3a169fe0bbe9be5e8d96e8476b707122", size = 448399 },
+ { url = "https://files.pythonhosted.org/packages/dd/53/4bd7f5779b1f463243ee5fdc83da04dd58a08f86e639dbffa7a35f969a84/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b91cceb5add79ee563bd1f70b30896bd63bc5f78a11c1f00a1e931729ca4f1f4", size = 439764 },
+ { url = "https://files.pythonhosted.org/packages/f6/55/b3c18c04a460d951bf8e91f2abf46ce5b6426fb69784166a6a25827cb90a/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a6cb95074777f1ecda2ca4fa7717caa9ee6e534f42b7575a8f0d4cb0c24013", size = 390662 },
+ { url = "https://files.pythonhosted.org/packages/2a/65/cc463044a3cbd616029b2aa87a651cdee8288d2fdd7780b2244845e934c1/rpds_py-0.23.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50fb62f8d8364978478b12d5f03bf028c6bc2af04082479299139dc26edf4c64", size = 422680 },
+ { url = "https://files.pythonhosted.org/packages/fa/8e/1fa52990c7836d72e8d70cd7753f2362c72fbb0a49c1462e8c60e7176d0b/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8f7e90b948dc9dcfff8003f1ea3af08b29c062f681c05fd798e36daa3f7e3e8", size = 561792 },
+ { url = "https://files.pythonhosted.org/packages/57/b8/fe3b612979b1a29d0c77f8585903d8b3a292604b26d4b300e228b8ac6360/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b98b6c953e5c2bda51ab4d5b4f172617d462eebc7f4bfdc7c7e6b423f6da957", size = 588127 },
+ { url = "https://files.pythonhosted.org/packages/44/2d/fde474de516bbc4b9b230f43c98e7f8acc5da7fc50ceed8e7af27553d346/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2893d778d4671ee627bac4037a075168b2673c57186fb1a57e993465dbd79a93", size = 556981 },
+ { url = "https://files.pythonhosted.org/packages/18/57/767deeb27b81370bbab8f74ef6e68d26c4ea99018f3c71a570e506fede85/rpds_py-0.23.1-cp313-cp313-win32.whl", hash = "sha256:2cfa07c346a7ad07019c33fb9a63cf3acb1f5363c33bc73014e20d9fe8b01cdd", size = 221936 },
+ { url = "https://files.pythonhosted.org/packages/7d/6c/3474cfdd3cafe243f97ab8474ea8949236eb2a1a341ca55e75ce00cd03da/rpds_py-0.23.1-cp313-cp313-win_amd64.whl", hash = "sha256:3aaf141d39f45322e44fc2c742e4b8b4098ead5317e5f884770c8df0c332da70", size = 237145 },
+ { url = "https://files.pythonhosted.org/packages/ec/77/e985064c624230f61efa0423759bb066da56ebe40c654f8b5ba225bd5d63/rpds_py-0.23.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:759462b2d0aa5a04be5b3e37fb8183615f47014ae6b116e17036b131985cb731", size = 359623 },
+ { url = "https://files.pythonhosted.org/packages/62/d9/a33dcbf62b29e40559e012d525bae7d516757cf042cc9234bd34ca4b6aeb/rpds_py-0.23.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3e9212f52074fc9d72cf242a84063787ab8e21e0950d4d6709886fb62bcb91d5", size = 345900 },
+ { url = "https://files.pythonhosted.org/packages/92/eb/f81a4be6397861adb2cb868bb6a28a33292c2dcac567d1dc575226055e55/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e9f3a3ac919406bc0414bbbd76c6af99253c507150191ea79fab42fdb35982a", size = 386426 },
+ { url = "https://files.pythonhosted.org/packages/09/47/1f810c9b5e83be005341201b5389f1d240dfa440346ea7189f9b3fd6961d/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c04ca91dda8a61584165825907f5c967ca09e9c65fe8966ee753a3f2b019fe1e", size = 392314 },
+ { url = "https://files.pythonhosted.org/packages/83/bd/bc95831432fd6c46ed8001f01af26de0763a059d6d7e6d69e3c5bf02917a/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab923167cfd945abb9b51a407407cf19f5bee35001221f2911dc85ffd35ff4f", size = 447706 },
+ { url = "https://files.pythonhosted.org/packages/19/3e/567c04c226b1802dc6dc82cad3d53e1fa0a773258571c74ac5d8fbde97ed/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed6f011bedca8585787e5082cce081bac3d30f54520097b2411351b3574e1219", size = 437060 },
+ { url = "https://files.pythonhosted.org/packages/fe/77/a77d2c6afe27ae7d0d55fc32f6841502648070dc8d549fcc1e6d47ff8975/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959bb9928c5c999aba4a3f5a6799d571ddc2c59ff49917ecf55be2bbb4e3722", size = 389347 },
+ { url = "https://files.pythonhosted.org/packages/3f/47/6b256ff20a74cfebeac790ab05586e0ac91f88e331125d4740a6c86fc26f/rpds_py-0.23.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ed7de3c86721b4e83ac440751329ec6a1102229aa18163f84c75b06b525ad7e", size = 415554 },
+ { url = "https://files.pythonhosted.org/packages/fc/29/d4572469a245bc9fc81e35166dca19fc5298d5c43e1a6dd64bf145045193/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb89edee2fa237584e532fbf78f0ddd1e49a47c7c8cfa153ab4849dc72a35e6", size = 557418 },
+ { url = "https://files.pythonhosted.org/packages/9c/0a/68cf7228895b1a3f6f39f51b15830e62456795e61193d2c8b87fd48c60db/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7e5413d2e2d86025e73f05510ad23dad5950ab8417b7fc6beaad99be8077138b", size = 583033 },
+ { url = "https://files.pythonhosted.org/packages/14/18/017ab41dcd6649ad5db7d00155b4c212b31ab05bd857d5ba73a1617984eb/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d31ed4987d72aabdf521eddfb6a72988703c091cfc0064330b9e5f8d6a042ff5", size = 554880 },
+ { url = "https://files.pythonhosted.org/packages/2e/dd/17de89431268da8819d8d51ce67beac28d9b22fccf437bc5d6d2bcd1acdb/rpds_py-0.23.1-cp313-cp313t-win32.whl", hash = "sha256:f3429fb8e15b20961efca8c8b21432623d85db2228cc73fe22756c6637aa39e7", size = 219743 },
+ { url = "https://files.pythonhosted.org/packages/68/15/6d22d07e063ce5e9bfbd96db9ec2fbb4693591b4503e3a76996639474d02/rpds_py-0.23.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6f6512a90bd5cd9030a6237f5346f046c6f0e40af98657568fa45695d4de59d", size = 235415 },
+]
+
+[[package]]
+name = "ruamel-yaml"
+version = "0.18.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729 },
+]
+
+[[package]]
+name = "ruamel-yaml-clib"
+version = "0.2.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433 },
+ { url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362 },
+ { url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118 },
+ { url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497 },
+ { url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042 },
+ { url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831 },
+ { url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692 },
+ { url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777 },
+ { url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523 },
+ { url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011 },
+ { url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488 },
+ { url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066 },
+ { url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785 },
+ { url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017 },
+ { url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270 },
+ { url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059 },
+ { url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583 },
+ { url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190 },
+]
+
+[[package]]
+name = "s3transfer"
+version = "0.11.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "botocore" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/ec/aa1a215e5c126fe5decbee2e107468f51d9ce190b9763cb649f76bb45938/s3transfer-0.11.4.tar.gz", hash = "sha256:559f161658e1cf0a911f45940552c696735f5c74e64362e515f333ebed87d679", size = 148419 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/62/8d3fc3ec6640161a5649b2cddbbf2b9fa39c92541225b33f117c37c5a2eb/s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:ac265fa68318763a03bf2dc4f39d5cbd6a9e178d81cc9483ad27da33637e320d", size = 84412 },
+]
+
+[[package]]
+name = "setuptools"
+version = "76.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/32/d2/7b171caf085ba0d40d8391f54e1c75a1cda9255f542becf84575cfd8a732/setuptools-76.0.0.tar.gz", hash = "sha256:43b4ee60e10b0d0ee98ad11918e114c70701bc6051662a9a675a0496c1a158f4", size = 1349387 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/37/66/d2d7e6ad554f3a7c7297c3f8ef6e22643ad3d35ef5c63bf488bc89f32f31/setuptools-76.0.0-py3-none-any.whl", hash = "sha256:199466a166ff664970d0ee145839f5582cb9bca7a0a3a2e795b6a9cb2308e9c6", size = 1236106 },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
+]
+
+[[package]]
+name = "sympy"
+version = "1.13.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/11/8a/5a7fd6284fa8caac23a26c9ddf9c30485a48169344b4bd3b0f02fef1890f/sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9", size = 7533196 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/ff/c87e0622b1dadea79d2fb0b25ade9ed98954c9033722eb707053d310d4f3/sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73", size = 6189483 },
+]
+
+[[package]]
+name = "test-lambda-locally"
+version = "0"
+source = { virtual = "." }
+dependencies = [
+ { name = "aws-sam-cli" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "aws-sam-cli", specifier = ">=1.135.0" }]
+
+[[package]]
+name = "text-unidecode"
+version = "1.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154 },
+]
+
+[[package]]
+name = "tomlkit"
+version = "0.13.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 },
+]
+
+[[package]]
+name = "types-awscrt"
+version = "0.24.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/6e/32779b967eee6ef627eaf10f3414163482b3980fc45ba21765fdd05359d4/types_awscrt-0.24.1.tar.gz", hash = "sha256:fc6eae56f8dc5a3f8cc93cc2c7c332fa82909f8284fbe25e014c575757af397d", size = 15450 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a6/1a/22e327d29fe231a10ed00e35ed2a100d2462cea253c3d24d41162769711a/types_awscrt-0.24.1-py3-none-any.whl", hash = "sha256:f3f2578ff74a254a79882b95961fb493ba217cebc350b3eb239d1cd948d4d7fa", size = 19414 },
+]
+
+[[package]]
+name = "types-python-dateutil"
+version = "2.9.0.20241206"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/60/47d92293d9bc521cd2301e423a358abfac0ad409b3a1606d8fbae1321961/types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb", size = 13802 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/b3/ca41df24db5eb99b00d97f89d7674a90cb6b3134c52fb8121b6d8d30f15c/types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53", size = 14384 },
+]
+
+[[package]]
+name = "types-s3transfer"
+version = "0.11.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/93/a9/440d8ba72a81bcf2cc5a56ef63f23b58ce93e7b9b62409697553bdcdd181/types_s3transfer-0.11.4.tar.gz", hash = "sha256:05fde593c84270f19fd053f0b1e08f5a057d7c5f036b9884e68fb8cd3041ac30", size = 14074 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/69/0b5ae42c3c33d31a32f7dcb9f35a3e327365360a6e4a2a7b491904bd38aa/types_s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:2a76d92c07d4a3cb469e5343b2e7560e0b8078b2e03696a65407b8c44c861b61", size = 19516 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 },
+]
+
+[[package]]
+name = "tzlocal"
+version = "5.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/04/d3/c19d65ae67636fe63953b20c2e4a8ced4497ea232c43ff8d01db16de8dc0/tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e", size = 30201 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/97/3f/c4c51c55ff8487f2e6d0e618dba917e3c3ee2caae6cf0fbb59c9b1876f2e/tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8", size = 17859 },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
+]
+
+[[package]]
+name = "watchdog"
+version = "4.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4f/38/764baaa25eb5e35c9a043d4c4588f9836edfe52a708950f4b6d5f714fd42/watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270", size = 126587 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/f5/ea22b095340545faea37ad9a42353b265ca751f543da3fb43f5d00cdcd21/watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a", size = 100342 },
+ { url = "https://files.pythonhosted.org/packages/cb/d2/8ce97dff5e465db1222951434e3115189ae54a9863aef99c6987890cc9ef/watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29", size = 92306 },
+ { url = "https://files.pythonhosted.org/packages/49/c4/1aeba2c31b25f79b03b15918155bc8c0b08101054fc727900f1a577d0d54/watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a", size = 92915 },
+ { url = "https://files.pythonhosted.org/packages/79/63/eb8994a182672c042d85a33507475c50c2ee930577524dd97aea05251527/watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b", size = 100343 },
+ { url = "https://files.pythonhosted.org/packages/ce/82/027c0c65c2245769580605bcd20a1dc7dfd6c6683c8c4e2ef43920e38d27/watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d", size = 92313 },
+ { url = "https://files.pythonhosted.org/packages/2a/89/ad4715cbbd3440cb0d336b78970aba243a33a24b1a79d66f8d16b4590d6a/watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7", size = 92919 },
+ { url = "https://files.pythonhosted.org/packages/8a/b1/25acf6767af6f7e44e0086309825bd8c098e301eed5868dc5350642124b9/watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83", size = 82947 },
+ { url = "https://files.pythonhosted.org/packages/e8/90/aebac95d6f954bd4901f5d46dcd83d68e682bfd21798fd125a95ae1c9dbf/watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c", size = 82942 },
+ { url = "https://files.pythonhosted.org/packages/15/3a/a4bd8f3b9381824995787488b9282aff1ed4667e1110f31a87b871ea851c/watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a", size = 82947 },
+ { url = "https://files.pythonhosted.org/packages/09/cc/238998fc08e292a4a18a852ed8274159019ee7a66be14441325bcd811dfd/watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73", size = 82946 },
+ { url = "https://files.pythonhosted.org/packages/80/f1/d4b915160c9d677174aa5fae4537ae1f5acb23b3745ab0873071ef671f0a/watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc", size = 82947 },
+ { url = "https://files.pythonhosted.org/packages/db/02/56ebe2cf33b352fe3309588eb03f020d4d1c061563d9858a9216ba004259/watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757", size = 82944 },
+ { url = "https://files.pythonhosted.org/packages/01/d2/c8931ff840a7e5bd5dcb93f2bb2a1fd18faf8312e9f7f53ff1cf76ecc8ed/watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8", size = 82947 },
+ { url = "https://files.pythonhosted.org/packages/d0/d8/cdb0c21a4a988669d7c210c75c6a2c9a0e16a3b08d9f7e633df0d9a16ad8/watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19", size = 82935 },
+ { url = "https://files.pythonhosted.org/packages/99/2e/b69dfaae7a83ea64ce36538cc103a3065e12c447963797793d5c0a1d5130/watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b", size = 82934 },
+ { url = "https://files.pythonhosted.org/packages/b0/0b/43b96a9ecdd65ff5545b1b13b687ca486da5c6249475b1a45f24d63a1858/watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c", size = 82933 },
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 },
+]
+
+[[package]]
+name = "wheel"
+version = "0.45.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 },
+]
diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py
index 90cb01ec17..c1b2f49b3c 100644
--- a/sentry_sdk/__init__.py
+++ b/sentry_sdk/__init__.py
@@ -42,6 +42,7 @@
"start_transaction",
"trace",
"monitor",
+ "_experimental_logger",
]
# Initialize the debug support after everything is loaded
diff --git a/sentry_sdk/_experimental_logger.py b/sentry_sdk/_experimental_logger.py
new file mode 100644
index 0000000000..1f3cd5e443
--- /dev/null
+++ b/sentry_sdk/_experimental_logger.py
@@ -0,0 +1,20 @@
+# NOTE: this is the logger sentry exposes to users, not some generic logger.
+import functools
+from typing import Any
+
+from sentry_sdk import get_client, get_current_scope
+
+
+def _capture_log(severity_text, severity_number, template, **kwargs):
+ # type: (str, int, str, **Any) -> None
+ client = get_client()
+ scope = get_current_scope()
+ client.capture_log(scope, severity_text, severity_number, template, **kwargs)
+
+
+trace = functools.partial(_capture_log, "trace", 1)
+debug = functools.partial(_capture_log, "debug", 5)
+info = functools.partial(_capture_log, "info", 9)
+warn = functools.partial(_capture_log, "warn", 13)
+error = functools.partial(_capture_log, "error", 17)
+fatal = functools.partial(_capture_log, "fatal", 21)
diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py
index 35bd1ba8b1..3062d2bf0a 100644
--- a/sentry_sdk/_types.py
+++ b/sentry_sdk/_types.py
@@ -205,6 +205,17 @@ class SDKInfo(TypedDict):
]
Hint = Dict[str, Any]
+ Log = TypedDict(
+ "Log",
+ {
+ "severity_text": str,
+ "severity_number": int,
+ "body": str,
+ "attributes": dict[str, str | bool | float | int],
+ "time_unix_nano": int,
+ "trace_id": Optional[str],
+ },
+ )
Breadcrumb = Dict[str, Any]
BreadcrumbHint = Dict[str, Any]
@@ -215,6 +226,7 @@ class SDKInfo(TypedDict):
ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]]
BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]]
TransactionProcessor = Callable[[Event, Hint], Optional[Event]]
+ LogProcessor = Callable[[Log, Hint], Optional[Log]]
TracesSampler = Callable[[SamplingContext], Union[float, int, bool]]
@@ -234,6 +246,7 @@ class SDKInfo(TypedDict):
"profile_chunk",
"monitor",
"span",
+ "log",
]
SessionStatus = Literal["ok", "exited", "crashed", "abnormal"]
diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py
index c905785b8a..8887a136b2 100644
--- a/sentry_sdk/client.py
+++ b/sentry_sdk/client.py
@@ -1,7 +1,10 @@
+import json
import os
+import time
import uuid
import random
import socket
+import logging
from collections.abc import Mapping
from datetime import datetime, timezone
from importlib import import_module
@@ -32,6 +35,7 @@
ClientConstructor,
)
from sentry_sdk.integrations import setup_integrations
+from sentry_sdk.integrations.dedupe import DedupeIntegration
from sentry_sdk.sessions import SessionFlusher
from sentry_sdk.envelope import Envelope
from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler
@@ -53,7 +57,7 @@
from typing import Union
from typing import TypeVar
- from sentry_sdk._types import Event, Hint, SDKInfo
+ from sentry_sdk._types import Event, Hint, SDKInfo, Log
from sentry_sdk.integrations import Integration
from sentry_sdk.scope import Scope
from sentry_sdk.session import Session
@@ -191,6 +195,10 @@ def capture_event(self, *args, **kwargs):
# type: (*Any, **Any) -> Optional[str]
return None
+ def capture_log(self, scope, severity_text, severity_number, template, **kwargs):
+ # type: (Scope, str, int, str, **Any) -> None
+ pass
+
def capture_session(self, *args, **kwargs):
# type: (*Any, **Any) -> None
return None
@@ -550,7 +558,15 @@ def _prepare_event(
self.transport.record_lost_event(
"before_send", data_category="error"
)
- event = new_event # type: Optional[Event] # type: ignore[no-redef]
+
+ # If this is an exception, reset the DedupeIntegration. It still
+ # remembers the dropped exception as the last exception, meaning
+ # that if the same exception happens again and is not dropped
+ # in before_send, it'd get dropped by DedupeIntegration.
+ if event.get("exception"):
+ DedupeIntegration.reset_last_seen()
+
+ event = new_event
before_send_transaction = self.options["before_send_transaction"]
if (
@@ -800,6 +816,110 @@ def capture_event(
return return_value
+ def capture_log(self, scope, severity_text, severity_number, template, **kwargs):
+ # type: (Scope, str, int, str, **Any) -> None
+ logs_enabled = self.options["_experiments"].get("enable_sentry_logs", False)
+ if not logs_enabled:
+ return
+
+ headers = {
+ "sent_at": format_timestamp(datetime.now(timezone.utc)),
+ } # type: dict[str, object]
+
+ attrs = {
+ "sentry.message.template": template,
+ } # type: dict[str, str | bool | float | int]
+
+ kwargs_attributes = kwargs.get("attributes")
+ if kwargs_attributes is not None:
+ attrs.update(kwargs_attributes)
+
+ environment = self.options.get("environment")
+ if environment is not None:
+ attrs["sentry.environment"] = environment
+
+ release = self.options.get("release")
+ if release is not None:
+ attrs["sentry.release"] = release
+
+ span = scope.span
+ if span is not None:
+ attrs["sentry.trace.parent_span_id"] = span.span_id
+
+ for k, v in kwargs.items():
+ attrs[f"sentry.message.parameters.{k}"] = v
+
+ log = {
+ "severity_text": severity_text,
+ "severity_number": severity_number,
+ "body": template.format(**kwargs),
+ "attributes": attrs,
+ "time_unix_nano": time.time_ns(),
+ "trace_id": None,
+ } # type: Log
+
+ # If debug is enabled, log the log to the console
+ debug = self.options.get("debug", False)
+ if debug:
+ severity_text_to_logging_level = {
+ "trace": logging.DEBUG,
+ "debug": logging.DEBUG,
+ "info": logging.INFO,
+ "warn": logging.WARNING,
+ "error": logging.ERROR,
+ "fatal": logging.CRITICAL,
+ }
+ logger.log(
+ severity_text_to_logging_level.get(severity_text, logging.DEBUG),
+ f'[Sentry Logs] {log["body"]}',
+ )
+
+ propagation_context = scope.get_active_propagation_context()
+ if propagation_context is not None:
+ headers["trace_id"] = propagation_context.trace_id
+ log["trace_id"] = propagation_context.trace_id
+
+ envelope = Envelope(headers=headers)
+
+ before_emit_log = self.options["_experiments"].get("before_emit_log")
+ if before_emit_log is not None:
+ log = before_emit_log(log, {})
+ if log is None:
+ return
+
+ def format_attribute(key, val):
+ # type: (str, int | float | str | bool) -> Any
+ if isinstance(val, bool):
+ return {"key": key, "value": {"boolValue": val}}
+ if isinstance(val, int):
+ return {"key": key, "value": {"intValue": str(val)}}
+ if isinstance(val, float):
+ return {"key": key, "value": {"doubleValue": val}}
+ if isinstance(val, str):
+ return {"key": key, "value": {"stringValue": val}}
+ return {"key": key, "value": {"stringValue": json.dumps(val)}}
+
+ otel_log = {
+ "severityText": log["severity_text"],
+ "severityNumber": log["severity_number"],
+ "body": {"stringValue": log["body"]},
+ "timeUnixNano": str(log["time_unix_nano"]),
+ "attributes": [
+ format_attribute(k, v) for (k, v) in log["attributes"].items()
+ ],
+ }
+
+ if "trace_id" in log:
+ otel_log["traceId"] = log["trace_id"]
+
+ envelope.add_log(otel_log) # TODO: batch these
+
+ if self.spotlight:
+ self.spotlight.capture_envelope(envelope)
+
+ if self.transport is not None:
+ self.transport.capture_envelope(envelope)
+
def capture_session(
self, session # type: Session
):
diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py
index b18e888266..eee70006fe 100644
--- a/sentry_sdk/consts.py
+++ b/sentry_sdk/consts.py
@@ -547,6 +547,387 @@ def __init__(
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
):
# type: (...) -> None
+ """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
+
+ :param dsn: The DSN tells the SDK where to send the events.
+
+ If this option is not set, the SDK will just not send any data.
+
+ The `dsn` config option takes precedence over the environment variable.
+
+ Learn more about `DSN utilization `_.
+
+ :param debug: Turns debug mode on or off.
+
+ When `True`, the SDK will attempt to print out debugging information. This can be useful if something goes
+ wrong with event sending.
+
+ The default is always `False`. It's generally not recommended to turn it on in production because of the
+ increase in log output.
+
+ The `debug` config option takes precedence over the environment variable.
+
+ :param release: Sets the release.
+
+ If not set, the SDK will try to automatically configure a release out of the box but it's a better idea to
+ manually set it to guarantee that the release is in sync with your deploy integrations.
+
+ Release names are strings, but some formats are detected by Sentry and might be rendered differently.
+
+ See `the releases documentation `_ to learn how the SDK tries to
+ automatically configure a release.
+
+ The `release` config option takes precedence over the environment variable.
+
+ Learn more about how to send release data so Sentry can tell you about regressions between releases and
+ identify the potential source in `the product documentation `_.
+
+ :param environment: Sets the environment. This string is freeform and set to `production` by default.
+
+ A release can be associated with more than one environment to separate them in the UI (think `staging` vs
+ `production` or similar).
+
+ The `environment` config option takes precedence over the environment variable.
+
+ :param dist: The distribution of the application.
+
+ Distributions are used to disambiguate build or deployment variants of the same release of an application.
+
+ The dist can be for example a build number.
+
+ :param sample_rate: Configures the sample rate for error events, in the range of `0.0` to `1.0`.
+
+ The default is `1.0`, which means that 100% of error events will be sent. If set to `0.1`, only 10% of
+ error events will be sent.
+
+ Events are picked randomly.
+
+ :param error_sampler: Dynamically configures the sample rate for error events on a per-event basis.
+
+ This configuration option accepts a function, which takes two parameters (the `event` and the `hint`), and
+ which returns a boolean (indicating whether the event should be sent to Sentry) or a floating-point number
+ between `0.0` and `1.0`, inclusive.
+
+ The number indicates the probability the event is sent to Sentry; the SDK will randomly decide whether to
+ send the event with the given probability.
+
+ If this configuration option is specified, the `sample_rate` option is ignored.
+
+ :param ignore_errors: A list of exception class names that shouldn't be sent to Sentry.
+
+ Errors that are an instance of these exceptions or a subclass of them, will be filtered out before they're
+ sent to Sentry.
+
+ By default, all errors are sent.
+
+ :param max_breadcrumbs: This variable controls the total amount of breadcrumbs that should be captured.
+
+ This defaults to `100`, but you can set this to any number.
+
+ However, you should be aware that Sentry has a `maximum payload size `_
+ and any events exceeding that payload size will be dropped.
+
+ :param attach_stacktrace: When enabled, stack traces are automatically attached to all messages logged.
+
+ Stack traces are always attached to exceptions; however, when this option is set, stack traces are also
+ sent with messages.
+
+ This option means that stack traces appear next to all log messages.
+
+ Grouping in Sentry is different for events with stack traces and without. As a result, you will get new
+ groups as you enable or disable this flag for certain events.
+
+ :param send_default_pii: If this flag is enabled, `certain personally identifiable information (PII)
+ `_ is added by active integrations.
+
+ If you enable this option, be sure to manually remove what you don't want to send using our features for
+ managing `Sensitive Data `_.
+
+ :param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and
+ passwords from a `denylist`.
+
+ It can additionally be used to scrub from another `pii_denylist` if `send_default_pii` is disabled.
+
+ See how to `configure the scrubber here `_.
+
+ :param include_source_context: When enabled, source context will be included in events sent to Sentry.
+
+ This source context includes the five lines of code above and below the line of code where an error
+ happened.
+
+ :param include_local_variables: When enabled, the SDK will capture a snapshot of local variables to send with
+ the event to help with debugging.
+
+ :param add_full_stack: When capturing errors, Sentry stack traces typically only include frames that start the
+ moment an error occurs.
+
+ But if the `add_full_stack` option is enabled (set to `True`), all frames from the start of execution will
+ be included in the stack trace sent to Sentry.
+
+ :param max_stack_frames: This option limits the number of stack frames that will be captured when
+ `add_full_stack` is enabled.
+
+ :param server_name: This option can be used to supply a server name.
+
+ When provided, the name of the server is sent along and persisted in the event.
+
+ For many integrations, the server name actually corresponds to the device hostname, even in situations
+ where the machine is not actually a server.
+
+ :param project_root: The full path to the root directory of your application.
+
+ The `project_root` is used to mark frames in a stack trace either as being in your application or outside
+ of the application.
+
+ :param in_app_include: A list of string prefixes of module names that belong to the app.
+
+ This option takes precedence over `in_app_exclude`.
+
+ Sentry differentiates stack frames that are directly related to your application ("in application") from
+ stack frames that come from other packages such as the standard library, frameworks, or other dependencies.
+
+ The application package is automatically marked as `inApp`.
+
+ The difference is visible in [sentry.io](https://sentry.io), where only the "in application" frames are
+ displayed by default.
+
+ :param in_app_exclude: A list of string prefixes of module names that do not belong to the app, but rather to
+ third-party packages.
+
+ Modules considered not part of the app will be hidden from stack traces by default.
+
+ This option can be overridden using `in_app_include`.
+
+ :param max_request_body_size: This parameter controls whether integrations should capture HTTP request bodies.
+ It can be set to one of the following values:
+
+ - `never`: Request bodies are never sent.
+ - `small`: Only small request bodies will be captured. The cutoff for small depends on the SDK (typically
+ 4KB).
+ - `medium`: Medium and small requests will be captured (typically 10KB).
+ - `always`: The SDK will always capture the request body as long as Sentry can make sense of it.
+
+ Please note that the Sentry server [limits HTTP request body size](https://develop.sentry.dev/sdk/
+ expected-features/data-handling/#variable-size). The server always enforces its size limit, regardless of
+ how you configure this option.
+
+ :param max_value_length: The number of characters after which the values containing text in the event payload
+ will be truncated.
+
+ WARNING: If the value you set for this is exceptionally large, the event may exceed 1 MiB and will be
+ dropped by Sentry.
+
+ :param ca_certs: A path to an alternative CA bundle file in PEM-format.
+
+ :param send_client_reports: Set this boolean to `False` to disable sending of client reports.
+
+ Client reports allow the client to send status reports about itself to Sentry, such as information about
+ events that were dropped before being sent.
+
+ :param integrations: List of integrations to enable in addition to `auto-enabling integrations (overview)
+ `_.
+
+ This setting can be used to override the default config options for a specific auto-enabling integration
+ or to add an integration that is not auto-enabled.
+
+ :param disabled_integrations: List of integrations that will be disabled.
+
+ This setting can be used to explicitly turn off specific `auto-enabling integrations (list)
+ `_ or
+ `default `_ integrations.
+
+ :param auto_enabling_integrations: Configures whether `auto-enabling integrations (configuration)
+ `_ should be enabled.
+
+ When set to `False`, no auto-enabling integrations will be enabled by default, even if the corresponding
+ framework/library is detected.
+
+ :param default_integrations: Configures whether `default integrations
+ `_ should be enabled.
+
+ Setting `default_integrations` to `False` disables all default integrations **as well as all auto-enabling
+ integrations**, unless they are specifically added in the `integrations` option, described above.
+
+ :param before_send: This function is called with an SDK-specific message or error event object, and can return
+ a modified event object, or `null` to skip reporting the event.
+
+ This can be used, for instance, for manual PII stripping before sending.
+
+ By the time `before_send` is executed, all scope data has already been applied to the event. Further
+ modification of the scope won't have any effect.
+
+ :param before_send_transaction: This function is called with an SDK-specific transaction event object, and can
+ return a modified transaction event object, or `null` to skip reporting the event.
+
+ One way this might be used is for manual PII stripping before sending.
+
+ :param before_breadcrumb: This function is called with an SDK-specific breadcrumb object before the breadcrumb
+ is added to the scope.
+
+ When nothing is returned from the function, the breadcrumb is dropped.
+
+ To pass the breadcrumb through, return the first argument, which contains the breadcrumb object.
+
+ The callback typically gets a second argument (called a "hint") which contains the original object from
+ which the breadcrumb was created to further customize what the breadcrumb should look like.
+
+ :param transport: Switches out the transport used to send events.
+
+ How this works depends on the SDK. It can, for instance, be used to capture events for unit-testing or to
+ send it through some more complex setup that requires proxy authentication.
+
+ :param transport_queue_size: The maximum number of events that will be queued before the transport is forced to
+ flush.
+
+ :param http_proxy: When set, a proxy can be configured that should be used for outbound requests.
+
+ This is also used for HTTPS requests unless a separate `https_proxy` is configured. However, not all SDKs
+ support a separate HTTPS proxy.
+
+ SDKs will attempt to default to the system-wide configured proxy, if possible. For instance, on Unix
+ systems, the `http_proxy` environment variable will be picked up.
+
+ :param https_proxy: Configures a separate proxy for outgoing HTTPS requests.
+
+ This value might not be supported by all SDKs. When not supported the `http-proxy` value is also used for
+ HTTPS requests at all times.
+
+ :param proxy_headers: A dict containing additional proxy headers (usually for authentication) to be forwarded
+ to `urllib3`'s `ProxyManager `_.
+
+ :param shutdown_timeout: Controls how many seconds to wait before shutting down.
+
+ Sentry SDKs send events from a background queue. This queue is given a certain amount to drain pending
+ events. The default is SDK specific but typically around two seconds.
+
+ Setting this value too low may cause problems for sending events from command line applications.
+
+ Setting the value too high will cause the application to block for a long time for users experiencing
+ network connectivity problems.
+
+ :param keep_alive: Determines whether to keep the connection alive between requests.
+
+ This can be useful in environments where you encounter frequent network issues such as connection resets.
+
+ :param cert_file: Path to the client certificate to use.
+
+ If set, supersedes the `CLIENT_CERT_FILE` environment variable.
+
+ :param key_file: Path to the key file to use.
+
+ If set, supersedes the `CLIENT_KEY_FILE` environment variable.
+
+ :param socket_options: An optional list of socket options to use.
+
+ These provide fine-grained, low-level control over the way the SDK connects to Sentry.
+
+ If provided, the options will override the default `urllib3` `socket options
+ `_.
+
+ :param traces_sample_rate: A number between `0` and `1`, controlling the percentage chance a given transaction
+ will be sent to Sentry.
+
+ (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app.
+
+ Either this or `traces_sampler` must be defined to enable tracing.
+
+ If `traces_sample_rate` is `0`, this means that no new traces will be created. However, if you have
+ another service (for example a JS frontend) that makes requests to your service that include trace
+ information, those traces will be continued and thus transactions will be sent to Sentry.
+
+ If you want to disable all tracing you need to set `traces_sample_rate=None`. In this case, no new traces
+ will be started and no incoming traces will be continued.
+
+ :param traces_sampler: A function responsible for determining the percentage chance a given transaction will be
+ sent to Sentry.
+
+ It will automatically be passed information about the transaction and the context in which it's being
+ created, and must return a number between `0` (0% chance of being sent) and `1` (100% chance of being
+ sent).
+
+ Can also be used for filtering transactions, by returning `0` for those that are unwanted.
+
+ Either this or `traces_sample_rate` must be defined to enable tracing.
+
+ :param trace_propagation_targets: An optional property that controls which downstream services receive tracing
+ data, in the form of a `sentry-trace` and a `baggage` header attached to any outgoing HTTP requests.
+
+ The option may contain a list of strings or regex against which the URLs of outgoing requests are matched.
+
+ If one of the entries in the list matches the URL of an outgoing request, trace data will be attached to
+ that request.
+
+ String entries do not have to be full matches, meaning the URL of a request is matched when it _contains_
+ a string provided through the option.
+
+ If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the
+ instrumented client.
+
+ :param functions_to_trace: An optional list of functions that should be set up for tracing.
+
+ For each function in the list, a span will be created when the function is executed.
+
+ Functions in the list are represented as strings containing the fully qualified name of the function.
+
+ This is a convenient option, making it possible to have one central place for configuring what functions
+ to trace, instead of having custom instrumentation scattered all over your code base.
+
+ To learn more, see the `Custom Instrumentation `_ documentation.
+
+ :param enable_backpressure_handling: When enabled, a new monitor thread will be spawned to perform health
+ checks on the SDK.
+
+ If the system is unhealthy, the SDK will keep halving the `traces_sample_rate` set by you in 10 second
+ intervals until recovery.
+
+ This down sampling helps ensure that the system stays stable and reduces SDK overhead under high load.
+
+ This option is enabled by default.
+
+ :param enable_db_query_source: When enabled, the source location will be added to database queries.
+
+ :param db_query_source_threshold_ms: The threshold in milliseconds for adding the source location to database
+ queries.
+
+ The query location will be added to the query for queries slower than the specified threshold.
+
+ :param custom_repr: A custom `repr `_ function to run
+ while serializing an object.
+
+ Use this to control how your custom objects and classes are visible in Sentry.
+
+ Return a string for that repr value to be used or `None` to continue serializing how Sentry would have
+ done it anyway.
+
+ :param profiles_sample_rate: A number between `0` and `1`, controlling the percentage chance a given sampled
+ transaction will be profiled.
+
+ (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app.
+
+ This is relative to the tracing sample rate - e.g. `0.5` means 50% of sampled transactions will be
+ profiled.
+
+ :param profiles_sampler:
+
+ :param profiler_mode:
+
+ :param profile_lifecycle:
+
+ :param profile_session_sample_rate:
+
+
+ :param enable_tracing:
+
+ :param propagate_traces:
+
+ :param auto_session_tracking:
+
+ :param spotlight:
+
+ :param instrumenter:
+
+ :param _experiments:
+ """
pass
@@ -570,4 +951,4 @@ def _get_default_options():
del _get_default_options
-VERSION = "2.22.0"
+VERSION = "2.23.1"
diff --git a/sentry_sdk/debug.py b/sentry_sdk/debug.py
index e4c686a3e8..f740d92dec 100644
--- a/sentry_sdk/debug.py
+++ b/sentry_sdk/debug.py
@@ -19,7 +19,7 @@ def filter(self, record):
def init_debug_support():
# type: () -> None
- if not logger.handlers:
+ if not logger.hasHandlers():
configure_logger()
diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py
index 2bad3db700..3ffeccc263 100644
--- a/sentry_sdk/envelope.py
+++ b/sentry_sdk/envelope.py
@@ -102,6 +102,12 @@ def add_sessions(
# type: (...) -> None
self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))
+ def add_log(
+ self, log # type: Any
+ ):
+ # type: (...) -> None
+ self.add_item(Item(payload=PayloadRef(json=log), type="otel_log"))
+
def add_item(
self, item # type: Item
):
@@ -268,6 +274,8 @@ def data_category(self):
return "transaction"
elif ty == "event":
return "error"
+ elif ty == "otel_log":
+ return "log"
elif ty == "client_report":
return "internal"
elif ty == "profile":
diff --git a/sentry_sdk/integrations/arq.py b/sentry_sdk/integrations/arq.py
index f656b6ece3..ee80d211f5 100644
--- a/sentry_sdk/integrations/arq.py
+++ b/sentry_sdk/integrations/arq.py
@@ -210,12 +210,13 @@ def _sentry_create_worker(*args, **kwargs):
if isinstance(settings_cls, dict):
if "functions" in settings_cls:
settings_cls["functions"] = [
- _get_arq_function(func) for func in settings_cls["functions"]
+ _get_arq_function(func)
+ for func in settings_cls.get("functions", [])
]
if "cron_jobs" in settings_cls:
settings_cls["cron_jobs"] = [
_get_arq_cron_job(cron_job)
- for cron_job in settings_cls["cron_jobs"]
+ for cron_job in settings_cls.get("cron_jobs", [])
]
if hasattr(settings_cls, "functions"):
@@ -229,11 +230,11 @@ def _sentry_create_worker(*args, **kwargs):
if "functions" in kwargs:
kwargs["functions"] = [
- _get_arq_function(func) for func in kwargs["functions"]
+ _get_arq_function(func) for func in kwargs.get("functions", [])
]
if "cron_jobs" in kwargs:
kwargs["cron_jobs"] = [
- _get_arq_cron_job(cron_job) for cron_job in kwargs["cron_jobs"]
+ _get_arq_cron_job(cron_job) for cron_job in kwargs.get("cron_jobs", [])
]
return old_create_worker(*args, **kwargs)
diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py
index d9bdf4a592..e6c2321240 100644
--- a/sentry_sdk/integrations/asyncio.py
+++ b/sentry_sdk/integrations/asyncio.py
@@ -1,9 +1,10 @@
import sys
+import signal
import sentry_sdk
from sentry_sdk.consts import OP
from sentry_sdk.integrations import Integration, DidNotEnable
-from sentry_sdk.utils import event_from_exception, reraise
+from sentry_sdk.utils import event_from_exception, logger, reraise
try:
import asyncio
@@ -11,7 +12,7 @@
except ImportError:
raise DidNotEnable("asyncio not available")
-from typing import TYPE_CHECKING
+from typing import cast, TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any
@@ -36,10 +37,26 @@ def patch_asyncio():
loop = asyncio.get_running_loop()
orig_task_factory = loop.get_task_factory()
+ # Add a shutdown handler to log a helpful message
+ def shutdown_handler():
+ # type: () -> None
+ logger.info(
+ "AsyncIO is shutting down. If you see 'Task was destroyed but it is pending!' "
+ "errors with '_task_with_sentry_span_creation', these are normal during shutdown "
+ "and not a problem with your code or Sentry."
+ )
+
+ try:
+ loop.add_signal_handler(signal.SIGINT, shutdown_handler)
+ loop.add_signal_handler(signal.SIGTERM, shutdown_handler)
+ except (NotImplementedError, AttributeError):
+ # Signal handlers might not be supported on all platforms
+ pass
+
def _sentry_task_factory(loop, coro, **kwargs):
# type: (asyncio.AbstractEventLoop, Coroutine[Any, Any, Any], Any) -> asyncio.Future[Any]
- async def _coro_creating_hub_and_span():
+ async def _task_with_sentry_span_creation():
# type: () -> Any
result = None
@@ -57,27 +74,47 @@ async def _coro_creating_hub_and_span():
return result
+ task = None
+
# Trying to use user set task factory (if there is one)
if orig_task_factory:
- return orig_task_factory(loop, _coro_creating_hub_and_span(), **kwargs)
-
- # The default task factory in `asyncio` does not have its own function
- # but is just a couple of lines in `asyncio.base_events.create_task()`
- # Those lines are copied here.
-
- # WARNING:
- # If the default behavior of the task creation in asyncio changes,
- # this will break!
- task = Task(_coro_creating_hub_and_span(), loop=loop, **kwargs)
- if task._source_traceback: # type: ignore
- del task._source_traceback[-1] # type: ignore
+ task = orig_task_factory(
+ loop, _task_with_sentry_span_creation(), **kwargs
+ )
+
+ if task is None:
+ # The default task factory in `asyncio` does not have its own function
+ # but is just a couple of lines in `asyncio.base_events.create_task()`
+ # Those lines are copied here.
+
+ # WARNING:
+ # If the default behavior of the task creation in asyncio changes,
+ # this will break!
+ task = Task(_task_with_sentry_span_creation(), loop=loop, **kwargs)
+ if task._source_traceback: # type: ignore
+ del task._source_traceback[-1] # type: ignore
+
+ # Set the task name to include the original coroutine's name
+ try:
+ cast("asyncio.Task[Any]", task).set_name(
+ f"{get_name(coro)} (Sentry-wrapped)"
+ )
+ except AttributeError:
+ # set_name might not be available in all Python versions
+ pass
return task
loop.set_task_factory(_sentry_task_factory) # type: ignore
+
except RuntimeError:
# When there is no running loop, we have nothing to patch.
- pass
+ logger.warning(
+ "There is no running asyncio loop so there is nothing Sentry can patch. "
+ "Please make sure you call sentry_sdk.init() within a running "
+ "asyncio loop for the AsyncioIntegration to work. "
+ "See https://docs.sentry.io/platforms/python/integrations/asyncio/"
+ )
def _capture_exception():
diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py
index 148b86852e..8a9fc41208 100644
--- a/sentry_sdk/integrations/bottle.py
+++ b/sentry_sdk/integrations/bottle.py
@@ -177,14 +177,20 @@ def _set_transaction_name_and_source(event, transaction_style, request):
name = ""
if transaction_style == "url":
- name = request.route.rule or ""
+ try:
+ name = request.route.rule or ""
+ except RuntimeError:
+ pass
elif transaction_style == "endpoint":
- name = (
- request.route.name
- or transaction_from_function(request.route.callback)
- or ""
- )
+ try:
+ name = (
+ request.route.name
+ or transaction_from_function(request.route.callback)
+ or ""
+ )
+ except RuntimeError:
+ pass
event["transaction"] = name
event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]}
diff --git a/sentry_sdk/integrations/cloud_resource_context.py b/sentry_sdk/integrations/cloud_resource_context.py
index 8d080899f3..ca5ae47e6b 100644
--- a/sentry_sdk/integrations/cloud_resource_context.py
+++ b/sentry_sdk/integrations/cloud_resource_context.py
@@ -13,6 +13,8 @@
CONTEXT_TYPE = "cloud_resource"
+HTTP_TIMEOUT = 2.0
+
AWS_METADATA_HOST = "169.254.169.254"
AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST)
AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format(
@@ -59,7 +61,7 @@ class CloudResourceContextIntegration(Integration):
cloud_provider = ""
aws_token = ""
- http = urllib3.PoolManager()
+ http = urllib3.PoolManager(timeout=HTTP_TIMEOUT)
gcp_metadata = None
@@ -83,7 +85,13 @@ def _is_aws(cls):
cls.aws_token = r.data.decode()
return True
- except Exception:
+ except urllib3.exceptions.TimeoutError:
+ logger.debug(
+ "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT
+ )
+ return False
+ except Exception as e:
+ logger.debug("Error checking AWS metadata service: %s", str(e))
return False
@classmethod
@@ -131,8 +139,12 @@ def _get_aws_context(cls):
except Exception:
pass
- except Exception:
- pass
+ except urllib3.exceptions.TimeoutError:
+ logger.debug(
+ "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT
+ )
+ except Exception as e:
+ logger.debug("Error fetching AWS metadata: %s", str(e))
return ctx
@@ -152,7 +164,13 @@ def _is_gcp(cls):
cls.gcp_metadata = json.loads(r.data.decode("utf-8"))
return True
- except Exception:
+ except urllib3.exceptions.TimeoutError:
+ logger.debug(
+ "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT
+ )
+ return False
+ except Exception as e:
+ logger.debug("Error checking GCP metadata service: %s", str(e))
return False
@classmethod
@@ -201,8 +219,12 @@ def _get_gcp_context(cls):
except Exception:
pass
- except Exception:
- pass
+ except urllib3.exceptions.TimeoutError:
+ logger.debug(
+ "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT
+ )
+ except Exception as e:
+ logger.debug("Error fetching GCP metadata: %s", str(e))
return ctx
diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py
index be6d9311a3..a115e35292 100644
--- a/sentry_sdk/integrations/dedupe.py
+++ b/sentry_sdk/integrations/dedupe.py
@@ -40,3 +40,12 @@ def processor(event, hint):
return None
integration._last_seen.set(exc)
return event
+
+ @staticmethod
+ def reset_last_seen():
+ # type: () -> None
+ integration = sentry_sdk.get_client().get_integration(DedupeIntegration)
+ if integration is None:
+ return
+
+ integration._last_seen.set(None)
diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py
index 45b4f0b2b1..f45ec6db20 100644
--- a/sentry_sdk/integrations/flask.py
+++ b/sentry_sdk/integrations/flask.py
@@ -72,6 +72,18 @@ def __init__(
@staticmethod
def setup_once():
# type: () -> None
+ try:
+ from quart import Quart # type: ignore
+
+ if Flask == Quart:
+ # This is Quart masquerading as Flask, don't enable the Flask
+ # integration. See https://github.com/getsentry/sentry-python/issues/2709
+ raise DidNotEnable(
+ "This is not a Flask app but rather Quart pretending to be Flask"
+ )
+ except ImportError:
+ pass
+
version = package_version("flask")
_check_minimum_version(FlaskIntegration, version)
diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py
index 52c56a8e60..1d30ec3b9a 100644
--- a/sentry_sdk/integrations/logging.py
+++ b/sentry_sdk/integrations/logging.py
@@ -237,10 +237,10 @@ def _emit(self, record):
event["logger"] = record.name
# Log records from `warnings` module as separate issues
- record_caputured_from_warnings_module = (
+ record_captured_from_warnings_module = (
record.name == "py.warnings" and record.msg == "%s"
)
- if record_caputured_from_warnings_module:
+ if record_captured_from_warnings_module:
# use the actual message and not "%s" as the message
# this prevents grouping all warnings under one "%s" issue
msg = record.args[0] # type: ignore
@@ -253,7 +253,11 @@ def _emit(self, record):
else:
event["logentry"] = {
"message": to_string(record.msg),
- "params": record.args,
+ "params": (
+ tuple(str(arg) if arg is None else arg for arg in record.args)
+ if record.args
+ else ()
+ ),
}
event["extra"] = self._extra_from_record(record)
diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py
index da99dfc4d6..5b76ea812a 100644
--- a/sentry_sdk/integrations/loguru.py
+++ b/sentry_sdk/integrations/loguru.py
@@ -11,7 +11,7 @@
if TYPE_CHECKING:
from logging import LogRecord
- from typing import Optional, Tuple
+ from typing import Optional, Tuple, Any
try:
import loguru
@@ -31,6 +31,16 @@ class LoggingLevels(enum.IntEnum):
CRITICAL = 50
+SENTRY_LEVEL_FROM_LOGURU_LEVEL = {
+ "TRACE": "DEBUG",
+ "DEBUG": "DEBUG",
+ "INFO": "INFO",
+ "SUCCESS": "INFO",
+ "WARNING": "WARNING",
+ "ERROR": "ERROR",
+ "CRITICAL": "CRITICAL",
+}
+
DEFAULT_LEVEL = LoggingLevels.INFO.value
DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value
# We need to save the handlers to be able to remove them later
@@ -87,14 +97,34 @@ class _LoguruBaseHandler(_BaseHandler):
def _logging_to_event_level(self, record):
# type: (LogRecord) -> str
try:
- return LoggingLevels(record.levelno).name.lower()
- except ValueError:
+ return SENTRY_LEVEL_FROM_LOGURU_LEVEL[
+ LoggingLevels(record.levelno).name
+ ].lower()
+ except (ValueError, KeyError):
return record.levelname.lower() if record.levelname else ""
class LoguruEventHandler(_LoguruBaseHandler, EventHandler):
"""Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names."""
+ def __init__(self, *args, **kwargs):
+ # type: (*Any, **Any) -> None
+ if kwargs.get("level"):
+ kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
+ kwargs.get("level", ""), DEFAULT_LEVEL
+ )
+
+ super().__init__(*args, **kwargs)
+
class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler):
"""Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names."""
+
+ def __init__(self, *args, **kwargs):
+ # type: (*Any, **Any) -> None
+ if kwargs.get("level"):
+ kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
+ kwargs.get("level", ""), DEFAULT_LEVEL
+ )
+
+ super().__init__(*args, **kwargs)
diff --git a/sentry_sdk/integrations/spark/spark_driver.py b/sentry_sdk/integrations/spark/spark_driver.py
index a86f16344d..701ba12d89 100644
--- a/sentry_sdk/integrations/spark/spark_driver.py
+++ b/sentry_sdk/integrations/spark/spark_driver.py
@@ -260,7 +260,12 @@ def onStageSubmitted(self, stageSubmitted): # noqa: N802,N803
# type: (Any) -> None
stage_info = stageSubmitted.stageInfo()
message = "Stage {} Submitted".format(stage_info.stageId())
- data = {"attemptId": stage_info.attemptId(), "name": stage_info.name()}
+
+ data = {"name": stage_info.name()}
+ attempt_id = _get_attempt_id(stage_info)
+ if attempt_id is not None:
+ data["attemptId"] = attempt_id
+
self._add_breadcrumb(level="info", message=message, data=data)
_set_app_properties()
@@ -271,7 +276,11 @@ def onStageCompleted(self, stageCompleted): # noqa: N802,N803
stage_info = stageCompleted.stageInfo()
message = ""
level = ""
- data = {"attemptId": stage_info.attemptId(), "name": stage_info.name()}
+
+ data = {"name": stage_info.name()}
+ attempt_id = _get_attempt_id(stage_info)
+ if attempt_id is not None:
+ data["attemptId"] = attempt_id
# Have to Try Except because stageInfo.failureReason() is typed with Scala Option
try:
@@ -283,3 +292,18 @@ def onStageCompleted(self, stageCompleted): # noqa: N802,N803
level = "info"
self._add_breadcrumb(level=level, message=message, data=data)
+
+
+def _get_attempt_id(stage_info):
+ # type: (Any) -> Optional[int]
+ try:
+ return stage_info.attemptId()
+ except Exception:
+ pass
+
+ try:
+ return stage_info.attemptNumber()
+ except Exception:
+ pass
+
+ return None
diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py
index cb2da74a04..c308441dc0 100644
--- a/sentry_sdk/integrations/starlette.py
+++ b/sentry_sdk/integrations/starlette.py
@@ -365,13 +365,13 @@ def patch_middlewares():
if not_yet_patched:
- def _sentry_middleware_init(self, cls, **options):
- # type: (Any, Any, Any) -> None
+ def _sentry_middleware_init(self, cls, *args, **kwargs):
+ # type: (Any, Any, Any, Any) -> None
if cls == SentryAsgiMiddleware:
- return old_middleware_init(self, cls, **options)
+ return old_middleware_init(self, cls, *args, **kwargs)
span_enabled_cls = _enable_span_for_middleware(cls)
- old_middleware_init(self, span_enabled_cls, **options)
+ old_middleware_init(self, span_enabled_cls, *args, **kwargs)
if cls == AuthenticationMiddleware:
patch_authentication_middleware(cls)
@@ -696,7 +696,11 @@ def _transaction_name_from_router(scope):
for route in router.routes:
match = route.matches(scope)
if match[0] == Match.FULL:
- return route.path
+ try:
+ return route.path
+ except AttributeError:
+ # routes added via app.host() won't have a path attribute
+ return scope.get("path")
return None
diff --git a/sentry_sdk/profiler/__init__.py b/sentry_sdk/profiler/__init__.py
index d8d4e076d5..0bc63e3a6d 100644
--- a/sentry_sdk/profiler/__init__.py
+++ b/sentry_sdk/profiler/__init__.py
@@ -25,10 +25,10 @@
)
__all__ = [
- "start_profile_session",
- "start_profiler", # TODO: Deprecate this in favor of `start_profile_session`
- "stop_profile_session",
- "stop_profiler", # TODO: Deprecate this in favor of `stop_profile_session`
+ "start_profile_session", # TODO: Deprecate this in favor of `start_profiler`
+ "start_profiler",
+ "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler`
+ "stop_profiler",
# DEPRECATED: The following was re-exported for backwards compatibility. It
# will be removed from sentry_sdk.profiler in a future release.
"MAX_PROFILE_DURATION_NS",
diff --git a/sentry_sdk/profiler/continuous_profiler.py b/sentry_sdk/profiler/continuous_profiler.py
index 9e2aa35fc1..47f63d8f59 100644
--- a/sentry_sdk/profiler/continuous_profiler.py
+++ b/sentry_sdk/profiler/continuous_profiler.py
@@ -145,32 +145,32 @@ def try_profile_lifecycle_trace_start():
def start_profiler():
# type: () -> None
+ if _scheduler is None:
+ return
- # TODO: deprecate this as it'll be replaced by `start_profile_session`
- start_profile_session()
+ _scheduler.manual_start()
def start_profile_session():
# type: () -> None
- if _scheduler is None:
- return
- _scheduler.manual_start()
+ # TODO: deprecate this as it'll be replaced by `start_profiler`
+ start_profiler()
def stop_profiler():
# type: () -> None
+ if _scheduler is None:
+ return
- # TODO: deprecate this as it'll be replaced by `stop_profile_session`
- stop_profile_session()
+ _scheduler.manual_stop()
def stop_profile_session():
# type: () -> None
- if _scheduler is None:
- return
- _scheduler.manual_stop()
+ # TODO: deprecate this as it'll be replaced by `stop_profiler`
+ stop_profiler()
def teardown_continuous_profiler():
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index e2998cf164..359014d961 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -286,7 +286,7 @@ def set_measurement(self, name, value, unit=""):
pass
def set_context(self, key, value):
- # type: (str, Any) -> None
+ # type: (str, dict[str, Any]) -> None
pass
def init_span_recorder(self, maxlen):
diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py
index 3afc43f28b..11b5361de9 100644
--- a/sentry_sdk/tracing_utils.py
+++ b/sentry_sdk/tracing_utils.py
@@ -450,6 +450,10 @@ def __repr__(self):
class Baggage:
"""
The W3C Baggage header information (see https://www.w3.org/TR/baggage/).
+
+ Before mutating a `Baggage` object, calling code must check that `mutable` is `True`.
+ Mutating a `Baggage` object that has `mutable` set to `False` is not allowed, but
+ it is the caller's responsibility to enforce this restriction.
"""
__slots__ = ("sentry_items", "third_party_items", "mutable")
diff --git a/setup.py b/setup.py
index cb0b774324..6f10d60d81 100644
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@ def get_file_text(file_name):
setup(
name="sentry-sdk",
- version="2.22.0",
+ version="2.23.1",
author="Sentry Team and Contributors",
author_email="hello@sentry.io",
url="https://github.com/getsentry/sentry-python",
diff --git a/tests/integrations/arq/test_arq.py b/tests/integrations/arq/test_arq.py
index 0ebb97cd2b..ce3d624f1e 100644
--- a/tests/integrations/arq/test_arq.py
+++ b/tests/integrations/arq/test_arq.py
@@ -1,4 +1,6 @@
import asyncio
+from datetime import timedelta
+
import pytest
from sentry_sdk import get_client, start_span
@@ -376,3 +378,48 @@ async def job(ctx):
assert event["contexts"]["trace"]["origin"] == "auto.queue.arq"
assert event["spans"][0]["origin"] == "auto.db.redis"
assert event["spans"][1]["origin"] == "auto.db.redis"
+
+
+@pytest.mark.asyncio
+async def test_job_concurrency(capture_events, init_arq):
+ """
+ 10 - division starts
+ 70 - sleepy starts
+ 110 - division raises error
+ 120 - sleepy finishes
+
+ """
+
+ async def sleepy(_):
+ await asyncio.sleep(0.05)
+
+ async def division(_):
+ await asyncio.sleep(0.1)
+ return 1 / 0
+
+ sleepy.__qualname__ = sleepy.__name__
+ division.__qualname__ = division.__name__
+
+ pool, worker = init_arq([sleepy, division])
+
+ events = capture_events()
+
+ await pool.enqueue_job(
+ "division", _job_id="123", _defer_by=timedelta(milliseconds=10)
+ )
+ await pool.enqueue_job(
+ "sleepy", _job_id="456", _defer_by=timedelta(milliseconds=70)
+ )
+
+ loop = asyncio.get_event_loop()
+ task = loop.create_task(worker.async_run())
+ await asyncio.sleep(1)
+
+ task.cancel()
+
+ await worker.close()
+
+ exception_event = events[1]
+ assert exception_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
+ assert exception_event["transaction"] == "division"
+ assert exception_event["extra"]["arq-job"]["task"] == "division"
diff --git a/tests/integrations/aws_lambda/__init__.py b/tests/integrations/aws_lambda/__init__.py
index 71eb245353..449f4dc95d 100644
--- a/tests/integrations/aws_lambda/__init__.py
+++ b/tests/integrations/aws_lambda/__init__.py
@@ -1,3 +1,5 @@
import pytest
pytest.importorskip("boto3")
+pytest.importorskip("fastapi")
+pytest.importorskip("uvicorn")
diff --git a/tests/integrations/aws_lambda/client.py b/tests/integrations/aws_lambda/client.py
deleted file mode 100644
index afacf6fc42..0000000000
--- a/tests/integrations/aws_lambda/client.py
+++ /dev/null
@@ -1,408 +0,0 @@
-import base64
-import boto3
-import glob
-import hashlib
-import os
-import subprocess
-import sys
-import tempfile
-
-from sentry_sdk.consts import VERSION as SDK_VERSION
-from sentry_sdk.utils import get_git_revision
-
-AWS_REGION_NAME = "us-east-1"
-AWS_CREDENTIALS = {
- "aws_access_key_id": os.environ["SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID"],
- "aws_secret_access_key": os.environ["SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY"],
-}
-AWS_LAMBDA_EXECUTION_ROLE_NAME = "lambda-ex"
-AWS_LAMBDA_EXECUTION_ROLE_ARN = None
-
-
-def _install_dependencies(base_dir, subprocess_kwargs):
- """
- Installs dependencies for AWS Lambda function
- """
- setup_cfg = os.path.join(base_dir, "setup.cfg")
- with open(setup_cfg, "w") as f:
- f.write("[install]\nprefix=")
-
- # Install requirements for Lambda Layer (these are more limited than the SDK requirements,
- # because Lambda does not support the newest versions of some packages)
- subprocess.check_call(
- [
- sys.executable,
- "-m",
- "pip",
- "install",
- "-r",
- "requirements-aws-lambda-layer.txt",
- "--target",
- base_dir,
- ],
- **subprocess_kwargs,
- )
- # Install requirements used for testing
- subprocess.check_call(
- [
- sys.executable,
- "-m",
- "pip",
- "install",
- "mock==3.0.0",
- "funcsigs",
- "--target",
- base_dir,
- ],
- **subprocess_kwargs,
- )
- # Create a source distribution of the Sentry SDK (in parent directory of base_dir)
- subprocess.check_call(
- [
- sys.executable,
- "setup.py",
- "sdist",
- "--dist-dir",
- os.path.dirname(base_dir),
- ],
- **subprocess_kwargs,
- )
- # Install the created Sentry SDK source distribution into the target directory
- # Do not install the dependencies of the SDK, because they where installed by requirements-aws-lambda-layer.txt above
- source_distribution_archive = glob.glob(
- "{}/*.tar.gz".format(os.path.dirname(base_dir))
- )[0]
- subprocess.check_call(
- [
- sys.executable,
- "-m",
- "pip",
- "install",
- source_distribution_archive,
- "--no-deps",
- "--target",
- base_dir,
- ],
- **subprocess_kwargs,
- )
-
-
-def _create_lambda_function_zip(base_dir):
- """
- Zips the given base_dir omitting Python cache files
- """
- subprocess.run(
- [
- "zip",
- "-q",
- "-x",
- "**/__pycache__/*",
- "-r",
- "lambda-function-package.zip",
- "./",
- ],
- cwd=base_dir,
- check=True,
- )
-
-
-def _create_lambda_package(
- base_dir, code, initial_handler, layer, syntax_check, subprocess_kwargs
-):
- """
- Creates deployable packages (as zip files) for AWS Lambda function
- and optional the accompanying Sentry Lambda layer
- """
- if initial_handler:
- # If Initial handler value is provided i.e. it is not the default
- # `test_lambda.test_handler`, then create another dir level so that our path is
- # test_dir.test_lambda.test_handler
- test_dir_path = os.path.join(base_dir, "test_dir")
- python_init_file = os.path.join(test_dir_path, "__init__.py")
- os.makedirs(test_dir_path)
- with open(python_init_file, "w"):
- # Create __init__ file to make it a python package
- pass
-
- test_lambda_py = os.path.join(base_dir, "test_dir", "test_lambda.py")
- else:
- test_lambda_py = os.path.join(base_dir, "test_lambda.py")
-
- with open(test_lambda_py, "w") as f:
- f.write(code)
-
- if syntax_check:
- # Check file for valid syntax first, and that the integration does not
- # crash when not running in Lambda (but rather a local deployment tool
- # such as chalice's)
- subprocess.check_call([sys.executable, test_lambda_py])
-
- if layer is None:
- _install_dependencies(base_dir, subprocess_kwargs)
- _create_lambda_function_zip(base_dir)
-
- else:
- _create_lambda_function_zip(base_dir)
-
- # Create Lambda layer zip package
- from scripts.build_aws_lambda_layer import build_packaged_zip
-
- build_packaged_zip(
- base_dir=base_dir,
- make_dist=True,
- out_zip_filename="lambda-layer-package.zip",
- )
-
-
-def _get_or_create_lambda_execution_role():
- global AWS_LAMBDA_EXECUTION_ROLE_ARN
-
- policy = """{
- "Version": "2012-10-17",
- "Statement": [
- {
- "Effect": "Allow",
- "Principal": {
- "Service": "lambda.amazonaws.com"
- },
- "Action": "sts:AssumeRole"
- }
- ]
- }
- """
- iam_client = boto3.client(
- "iam",
- region_name=AWS_REGION_NAME,
- **AWS_CREDENTIALS,
- )
-
- try:
- response = iam_client.get_role(RoleName=AWS_LAMBDA_EXECUTION_ROLE_NAME)
- AWS_LAMBDA_EXECUTION_ROLE_ARN = response["Role"]["Arn"]
- except iam_client.exceptions.NoSuchEntityException:
- # create role for lambda execution
- response = iam_client.create_role(
- RoleName=AWS_LAMBDA_EXECUTION_ROLE_NAME,
- AssumeRolePolicyDocument=policy,
- )
- AWS_LAMBDA_EXECUTION_ROLE_ARN = response["Role"]["Arn"]
-
- # attach policy to role
- iam_client.attach_role_policy(
- RoleName=AWS_LAMBDA_EXECUTION_ROLE_NAME,
- PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
- )
-
-
-def get_boto_client():
- _get_or_create_lambda_execution_role()
-
- return boto3.client(
- "lambda",
- region_name=AWS_REGION_NAME,
- **AWS_CREDENTIALS,
- )
-
-
-def run_lambda_function(
- client,
- runtime,
- code,
- payload,
- add_finalizer,
- syntax_check=True,
- timeout=30,
- layer=None,
- initial_handler=None,
- subprocess_kwargs=(),
-):
- """
- Creates a Lambda function with the given code, and invokes it.
-
- If the same code is run multiple times the function will NOT be
- created anew each time but the existing function will be reused.
- """
- subprocess_kwargs = dict(subprocess_kwargs)
-
- # Making a unique function name depending on all the code that is run in it (function code plus SDK version)
- # The name needs to be short so the generated event/envelope json blobs are small enough to be output
- # in the log result of the Lambda function.
- rev = get_git_revision() or SDK_VERSION
- function_hash = hashlib.shake_256((code + rev).encode("utf-8")).hexdigest(6)
- fn_name = "test_{}".format(function_hash)
- full_fn_name = "{}_{}".format(
- fn_name, runtime.replace(".", "").replace("python", "py")
- )
-
- function_exists_in_aws = True
- try:
- client.get_function(
- FunctionName=full_fn_name,
- )
- print(
- "Lambda function in AWS already existing, taking it (and do not create a local one)"
- )
- except client.exceptions.ResourceNotFoundException:
- function_exists_in_aws = False
-
- if not function_exists_in_aws:
- tmp_base_dir = tempfile.gettempdir()
- base_dir = os.path.join(tmp_base_dir, fn_name)
- dir_already_existing = os.path.isdir(base_dir)
-
- if dir_already_existing:
- print("Local Lambda function directory already exists, skipping creation")
-
- if not dir_already_existing:
- os.mkdir(base_dir)
- _create_lambda_package(
- base_dir, code, initial_handler, layer, syntax_check, subprocess_kwargs
- )
-
- @add_finalizer
- def clean_up():
- # this closes the web socket so we don't get a
- # ResourceWarning: unclosed
- # warning on every test
- # based on https://github.com/boto/botocore/pull/1810
- # (if that's ever merged, this can just become client.close())
- session = client._endpoint.http_session
- managers = [session._manager] + list(session._proxy_managers.values())
- for manager in managers:
- manager.clear()
-
- layers = []
- environment = {}
- handler = initial_handler or "test_lambda.test_handler"
-
- if layer is not None:
- with open(
- os.path.join(base_dir, "lambda-layer-package.zip"), "rb"
- ) as lambda_layer_zip:
- response = client.publish_layer_version(
- LayerName="python-serverless-sdk-test",
- Description="Created as part of testsuite for getsentry/sentry-python",
- Content={"ZipFile": lambda_layer_zip.read()},
- )
-
- layers = [response["LayerVersionArn"]]
- handler = (
- "sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler"
- )
- environment = {
- "Variables": {
- "SENTRY_INITIAL_HANDLER": initial_handler
- or "test_lambda.test_handler",
- "SENTRY_DSN": "https://123abc@example.com/123",
- "SENTRY_TRACES_SAMPLE_RATE": "1.0",
- }
- }
-
- try:
- with open(
- os.path.join(base_dir, "lambda-function-package.zip"), "rb"
- ) as lambda_function_zip:
- client.create_function(
- Description="Created as part of testsuite for getsentry/sentry-python",
- FunctionName=full_fn_name,
- Runtime=runtime,
- Timeout=timeout,
- Role=AWS_LAMBDA_EXECUTION_ROLE_ARN,
- Handler=handler,
- Code={"ZipFile": lambda_function_zip.read()},
- Environment=environment,
- Layers=layers,
- )
-
- waiter = client.get_waiter("function_active_v2")
- waiter.wait(FunctionName=full_fn_name)
- except client.exceptions.ResourceConflictException:
- print(
- "Lambda function already exists, this is fine, we will just invoke it."
- )
-
- response = client.invoke(
- FunctionName=full_fn_name,
- InvocationType="RequestResponse",
- LogType="Tail",
- Payload=payload,
- )
-
- assert 200 <= response["StatusCode"] < 300, response
- return response
-
-
-# This is for inspecting new Python runtime environments in AWS Lambda
-# If you need to debug a new runtime, use this REPL to run arbitrary Python or bash commands
-# in that runtime in a Lambda function:
-#
-# pip3 install click
-# python3 tests/integrations/aws_lambda/client.py --runtime=python4.0
-#
-
-
-_REPL_CODE = """
-import os
-
-def test_handler(event, context):
- line = {line!r}
- if line.startswith(">>> "):
- exec(line[4:])
- elif line.startswith("$ "):
- os.system(line[2:])
- else:
- print("Start a line with $ or >>>")
-
- return b""
-"""
-
-try:
- import click
-except ImportError:
- pass
-else:
-
- @click.command()
- @click.option(
- "--runtime", required=True, help="name of the runtime to use, eg python3.11"
- )
- @click.option("--verbose", is_flag=True, default=False)
- def repl(runtime, verbose):
- """
- Launch a "REPL" against AWS Lambda to inspect their runtime.
- """
-
- cleanup = []
- client = get_boto_client()
-
- print("Start a line with `$ ` to run shell commands, or `>>> ` to run Python")
-
- while True:
- line = input()
-
- response = run_lambda_function(
- client,
- runtime,
- _REPL_CODE.format(line=line),
- b"",
- cleanup.append,
- subprocess_kwargs=(
- {
- "stdout": subprocess.DEVNULL,
- "stderr": subprocess.DEVNULL,
- }
- if not verbose
- else {}
- ),
- )
-
- for line in base64.b64decode(response["LogResult"]).splitlines():
- print(line.decode("utf8"))
-
- for f in cleanup:
- f()
-
- cleanup = []
-
- if __name__ == "__main__":
- repl()
diff --git a/tests/integrations/aws_lambda/lambda_functions/BasicException/index.py b/tests/integrations/aws_lambda/lambda_functions/BasicException/index.py
new file mode 100644
index 0000000000..875b984e2a
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions/BasicException/index.py
@@ -0,0 +1,6 @@
+def handler(event, context):
+ raise RuntimeError("Oh!")
+
+ return {
+ "event": event,
+ }
diff --git a/tests/integrations/aws_lambda/lambda_functions/BasicOk/index.py b/tests/integrations/aws_lambda/lambda_functions/BasicOk/index.py
new file mode 100644
index 0000000000..257fea04f0
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions/BasicOk/index.py
@@ -0,0 +1,4 @@
+def handler(event, context):
+ return {
+ "event": event,
+ }
diff --git a/tests/integrations/aws_lambda/lambda_functions/InitError/index.py b/tests/integrations/aws_lambda/lambda_functions/InitError/index.py
new file mode 100644
index 0000000000..20b4fcc111
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions/InitError/index.py
@@ -0,0 +1,3 @@
+# We have no handler() here and try to call a non-existing function.
+
+func() # noqa: F821
diff --git a/tests/integrations/aws_lambda/lambda_functions/TimeoutError/index.py b/tests/integrations/aws_lambda/lambda_functions/TimeoutError/index.py
new file mode 100644
index 0000000000..01334bbfbc
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions/TimeoutError/index.py
@@ -0,0 +1,8 @@
+import time
+
+
+def handler(event, context):
+ time.sleep(15)
+ return {
+ "event": event,
+ }
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceDisabled/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceDisabled/.gitignore
new file mode 100644
index 0000000000..ee0b7b9305
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceDisabled/.gitignore
@@ -0,0 +1,11 @@
+# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies
+# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry.
+
+# Ignore everything
+*
+
+# But not index.py
+!index.py
+
+# And not .gitignore itself
+!.gitignore
\ No newline at end of file
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceDisabled/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceDisabled/index.py
new file mode 100644
index 0000000000..12f43f0009
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceDisabled/index.py
@@ -0,0 +1,14 @@
+import os
+import sentry_sdk
+from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
+
+
+sentry_sdk.init(
+ dsn=os.environ.get("SENTRY_DSN"),
+ traces_sample_rate=None, # this is the default, just added for clarity
+ integrations=[AwsLambdaIntegration()],
+)
+
+
+def handler(event, context):
+ raise Exception("Oh!")
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceEnabled/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceEnabled/.gitignore
new file mode 100644
index 0000000000..ee0b7b9305
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceEnabled/.gitignore
@@ -0,0 +1,11 @@
+# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies
+# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry.
+
+# Ignore everything
+*
+
+# But not index.py
+!index.py
+
+# And not .gitignore itself
+!.gitignore
\ No newline at end of file
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceEnabled/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceEnabled/index.py
new file mode 100644
index 0000000000..c694299682
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceEnabled/index.py
@@ -0,0 +1,14 @@
+import os
+import sentry_sdk
+from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
+
+
+sentry_sdk.init(
+ dsn=os.environ.get("SENTRY_DSN"),
+ traces_sample_rate=1.0,
+ integrations=[AwsLambdaIntegration()],
+)
+
+
+def handler(event, context):
+ raise Exception("Oh!")
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TracesSampler/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TracesSampler/.gitignore
new file mode 100644
index 0000000000..ee0b7b9305
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TracesSampler/.gitignore
@@ -0,0 +1,11 @@
+# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies
+# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry.
+
+# Ignore everything
+*
+
+# But not index.py
+!index.py
+
+# And not .gitignore itself
+!.gitignore
\ No newline at end of file
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TracesSampler/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TracesSampler/index.py
new file mode 100644
index 0000000000..ce797faf71
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TracesSampler/index.py
@@ -0,0 +1,49 @@
+import json
+import os
+import sentry_sdk
+from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
+
+# Global variables to store sampling context for verification
+sampling_context_data = {
+ "aws_event_present": False,
+ "aws_context_present": False,
+ "event_data": None,
+}
+
+
+def trace_sampler(sampling_context):
+ # Store the sampling context for verification
+ global sampling_context_data
+
+ # Check if aws_event and aws_context are in the sampling_context
+ if "aws_event" in sampling_context:
+ sampling_context_data["aws_event_present"] = True
+ sampling_context_data["event_data"] = sampling_context["aws_event"]
+
+ if "aws_context" in sampling_context:
+ sampling_context_data["aws_context_present"] = True
+
+ print("Sampling context data:", sampling_context_data)
+ return 1.0 # Always sample
+
+
+sentry_sdk.init(
+ dsn=os.environ.get("SENTRY_DSN"),
+ traces_sample_rate=1.0,
+ traces_sampler=trace_sampler,
+ integrations=[AwsLambdaIntegration()],
+)
+
+
+def handler(event, context):
+ # Return the sampling context data for verification
+ return {
+ "statusCode": 200,
+ "body": json.dumps(
+ {
+ "message": "Hello from Lambda with embedded Sentry SDK!",
+ "event": event,
+ "sampling_context_data": sampling_context_data,
+ }
+ ),
+ }
diff --git a/tests/integrations/aws_lambda/test_aws.py b/tests/integrations/aws_lambda/test_aws.py
deleted file mode 100644
index 325d4886ee..0000000000
--- a/tests/integrations/aws_lambda/test_aws.py
+++ /dev/null
@@ -1,908 +0,0 @@
-"""
-# AWS Lambda System Tests
-
-This testsuite uses boto3 to upload actual Lambda functions to AWS Lambda and invoke them.
-
-For running test locally you need to set these env vars:
-(You can find the values in the Sentry password manager by searching for "AWS Lambda for Python SDK Tests").
-
- export SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID="..."
- export SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY="..."
-
-
-You can use `scripts/aws-cleanup.sh` to delete all files generated by this test suite.
-
-
-If you need to debug a new runtime, use this REPL to run arbitrary Python or bash commands
-in that runtime in a Lambda function: (see the bottom of client.py for more information.)
-
- pip3 install click
- python3 tests/integrations/aws_lambda/client.py --runtime=python4.0
-
-IMPORTANT:
-
-During running of this test suite temporary folders will be created for compiling the Lambda functions.
-This temporary folders will not be cleaned up. This is because in CI generated files have to be shared
-between tests and thus the folders can not be deleted right after use.
-
-If you run your tests locally, you need to clean up the temporary folders manually. The location of
-the temporary folders is printed when running a test.
-"""
-
-import base64
-import json
-import re
-from textwrap import dedent
-
-import pytest
-
-RUNTIMES_TO_TEST = [
- "python3.8",
- "python3.10",
- "python3.12",
- "python3.13",
-]
-
-LAMBDA_PRELUDE = """
-from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration, get_lambda_bootstrap
-import sentry_sdk
-import json
-import time
-
-from sentry_sdk.transport import Transport
-
-def truncate_data(data):
- # AWS Lambda truncates the log output to 4kb, which is small enough to miss
- # parts of even a single error-event/transaction-envelope pair if considered
- # in full, so only grab the data we need.
-
- cleaned_data = {}
-
- if data.get("type") is not None:
- cleaned_data["type"] = data["type"]
-
- if data.get("contexts") is not None:
- cleaned_data["contexts"] = {}
-
- if data["contexts"].get("trace") is not None:
- cleaned_data["contexts"]["trace"] = data["contexts"].get("trace")
- if cleaned_data["contexts"]["trace"].get("data", {}) != {}:
- cleaned_data["contexts"]["trace"]["data"] = {"removed": "by truncate_data()"}
-
- if data.get("transaction") is not None:
- cleaned_data["transaction"] = data.get("transaction")
-
- if data.get("request") is not None:
- cleaned_data["request"] = data.get("request")
-
- if data.get("tags") is not None:
- cleaned_data["tags"] = data.get("tags")
-
- if data.get("exception") is not None:
- cleaned_data["exception"] = data.get("exception")
-
- for value in cleaned_data["exception"]["values"]:
- for frame in value.get("stacktrace", {}).get("frames", []):
- del frame["vars"]
- del frame["pre_context"]
- del frame["context_line"]
- del frame["post_context"]
-
- if data.get("extra") is not None:
- cleaned_data["extra"] = {}
-
- for key in data["extra"].keys():
- if key == "lambda":
- for lambda_key in data["extra"]["lambda"].keys():
- if lambda_key in ["function_name"]:
- cleaned_data["extra"].setdefault("lambda", {})[lambda_key] = data["extra"]["lambda"][lambda_key]
- elif key == "cloudwatch logs":
- for cloudwatch_key in data["extra"]["cloudwatch logs"].keys():
- if cloudwatch_key in ["url", "log_group", "log_stream"]:
- cleaned_data["extra"].setdefault("cloudwatch logs", {})[cloudwatch_key] = data["extra"]["cloudwatch logs"][cloudwatch_key].split("=")[0]
-
- if data.get("level") is not None:
- cleaned_data["level"] = data.get("level")
-
- if data.get("message") is not None:
- cleaned_data["message"] = data.get("message")
-
- if "contexts" not in cleaned_data:
- raise Exception(json.dumps(data))
-
- return cleaned_data
-
-def event_processor(event):
- return truncate_data(event)
-
-def envelope_processor(envelope):
- (item,) = envelope.items
- item_json = json.loads(item.get_bytes())
-
- return truncate_data(item_json)
-
-
-class TestTransport(Transport):
- def capture_envelope(self, envelope):
- envelope_items = envelope_processor(envelope)
- print("\\nENVELOPE: {}\\n".format(json.dumps(envelope_items)))
-
-def init_sdk(timeout_warning=False, **extra_init_args):
- sentry_sdk.init(
- dsn="https://123abc@example.com/123",
- transport=TestTransport,
- integrations=[AwsLambdaIntegration(timeout_warning=timeout_warning)],
- shutdown_timeout=10,
- **extra_init_args
- )
-"""
-
-
-@pytest.fixture
-def lambda_client():
- from tests.integrations.aws_lambda.client import get_boto_client
-
- return get_boto_client()
-
-
-@pytest.fixture(params=RUNTIMES_TO_TEST)
-def lambda_runtime(request):
- return request.param
-
-
-@pytest.fixture
-def run_lambda_function(request, lambda_client, lambda_runtime):
- def inner(
- code, payload, timeout=30, syntax_check=True, layer=None, initial_handler=None
- ):
- from tests.integrations.aws_lambda.client import run_lambda_function
-
- response = run_lambda_function(
- client=lambda_client,
- runtime=lambda_runtime,
- code=code,
- payload=payload,
- add_finalizer=request.addfinalizer,
- timeout=timeout,
- syntax_check=syntax_check,
- layer=layer,
- initial_handler=initial_handler,
- )
-
- # Make sure the "ENVELOPE:" and "EVENT:" log entries are always starting a new line. (Sometimes they don't.)
- response["LogResult"] = (
- base64.b64decode(response["LogResult"])
- .replace(b"EVENT:", b"\nEVENT:")
- .replace(b"ENVELOPE:", b"\nENVELOPE:")
- .splitlines()
- )
- response["Payload"] = json.loads(response["Payload"].read().decode("utf-8"))
- del response["ResponseMetadata"]
-
- envelope_items = []
-
- for line in response["LogResult"]:
- print("AWS:", line)
- if line.startswith(b"ENVELOPE: "):
- line = line[len(b"ENVELOPE: ") :]
- envelope_items.append(json.loads(line.decode("utf-8")))
- else:
- continue
-
- return envelope_items, response
-
- return inner
-
-
-def test_basic(run_lambda_function):
- envelope_items, response = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk()
-
- def test_handler(event, context):
- raise Exception("Oh!")
- """
- ),
- b'{"foo": "bar"}',
- )
-
- assert response["FunctionError"] == "Unhandled"
-
- (event,) = envelope_items
- assert event["level"] == "error"
- (exception,) = event["exception"]["values"]
- assert exception["type"] == "Exception"
- assert exception["value"] == "Oh!"
-
- (frame1,) = exception["stacktrace"]["frames"]
- assert frame1["filename"] == "test_lambda.py"
- assert frame1["abs_path"] == "/var/task/test_lambda.py"
- assert frame1["function"] == "test_handler"
-
- assert frame1["in_app"] is True
-
- assert exception["mechanism"]["type"] == "aws_lambda"
- assert not exception["mechanism"]["handled"]
-
- assert event["extra"]["lambda"]["function_name"].startswith("test_")
-
- logs_url = event["extra"]["cloudwatch logs"]["url"]
- assert logs_url.startswith("https://console.aws.amazon.com/cloudwatch/home?region")
- assert not re.search("(=;|=$)", logs_url)
- assert event["extra"]["cloudwatch logs"]["log_group"].startswith(
- "/aws/lambda/test_"
- )
-
- log_stream_re = "^[0-9]{4}/[0-9]{2}/[0-9]{2}/\\[[^\\]]+][a-f0-9]+$"
- log_stream = event["extra"]["cloudwatch logs"]["log_stream"]
-
- assert re.match(log_stream_re, log_stream)
-
-
-def test_initialization_order(run_lambda_function):
- """Zappa lazily imports our code, so by the time we monkeypatch the handler
- as seen by AWS already runs. At this point at least draining the queue
- should work."""
-
- envelope_items, _ = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- def test_handler(event, context):
- init_sdk()
- sentry_sdk.capture_exception(Exception("Oh!"))
- """
- ),
- b'{"foo": "bar"}',
- )
-
- (event,) = envelope_items
-
- assert event["level"] == "error"
- (exception,) = event["exception"]["values"]
- assert exception["type"] == "Exception"
- assert exception["value"] == "Oh!"
-
-
-def test_request_data(run_lambda_function):
- envelope_items, _ = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk()
- def test_handler(event, context):
- sentry_sdk.capture_message("hi")
- return "ok"
- """
- ),
- payload=b"""
- {
- "resource": "/asd",
- "path": "/asd",
- "httpMethod": "GET",
- "headers": {
- "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com",
- "User-Agent": "custom",
- "X-Forwarded-Proto": "https"
- },
- "queryStringParameters": {
- "bonkers": "true",
- "wild": "false"
- },
- "pathParameters": null,
- "stageVariables": null,
- "requestContext": {
- "identity": {
- "sourceIp": "213.47.147.207",
- "userArn": "42"
- }
- },
- "body": null,
- "isBase64Encoded": false
- }
- """,
- )
-
- (event,) = envelope_items
-
- assert event["request"] == {
- "headers": {
- "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com",
- "User-Agent": "custom",
- "X-Forwarded-Proto": "https",
- },
- "method": "GET",
- "query_string": "bonkers=true&wild=false",
- "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd",
- }
-
-
-def test_init_error(run_lambda_function, lambda_runtime):
- envelope_items, _ = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk()
- func()
- """
- ),
- b'{"foo": "bar"}',
- syntax_check=False,
- )
-
- # We just take the last one, because it could be that in the output of the Lambda
- # invocation there is still the envelope of the previous invocation of the function.
- event = envelope_items[-1]
- assert event["exception"]["values"][0]["value"] == "name 'func' is not defined"
-
-
-def test_timeout_error(run_lambda_function):
- envelope_items, _ = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk(timeout_warning=True)
-
- def test_handler(event, context):
- time.sleep(10)
- return 0
- """
- ),
- b'{"foo": "bar"}',
- timeout=2,
- )
-
- (event,) = envelope_items
- assert event["level"] == "error"
- (exception,) = event["exception"]["values"]
- assert exception["type"] == "ServerlessTimeoutWarning"
- assert exception["value"] in (
- "WARNING : Function is expected to get timed out. Configured timeout duration = 3 seconds.",
- "WARNING : Function is expected to get timed out. Configured timeout duration = 2 seconds.",
- )
-
- assert exception["mechanism"]["type"] == "threading"
- assert not exception["mechanism"]["handled"]
-
- assert event["extra"]["lambda"]["function_name"].startswith("test_")
-
- logs_url = event["extra"]["cloudwatch logs"]["url"]
- assert logs_url.startswith("https://console.aws.amazon.com/cloudwatch/home?region")
- assert not re.search("(=;|=$)", logs_url)
- assert event["extra"]["cloudwatch logs"]["log_group"].startswith(
- "/aws/lambda/test_"
- )
-
- log_stream_re = "^[0-9]{4}/[0-9]{2}/[0-9]{2}/\\[[^\\]]+][a-f0-9]+$"
- log_stream = event["extra"]["cloudwatch logs"]["log_stream"]
-
- assert re.match(log_stream_re, log_stream)
-
-
-def test_performance_no_error(run_lambda_function):
- envelope_items, _ = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk(traces_sample_rate=1.0)
-
- def test_handler(event, context):
- return "test_string"
- """
- ),
- b'{"foo": "bar"}',
- )
-
- (envelope,) = envelope_items
-
- assert envelope["type"] == "transaction"
- assert envelope["contexts"]["trace"]["op"] == "function.aws"
- assert envelope["transaction"].startswith("test_")
- assert envelope["transaction"] in envelope["request"]["url"]
-
-
-def test_performance_error(run_lambda_function):
- envelope_items, _ = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk(traces_sample_rate=1.0)
-
- def test_handler(event, context):
- raise Exception("Oh!")
- """
- ),
- b'{"foo": "bar"}',
- )
-
- (
- error_event,
- transaction_event,
- ) = envelope_items
-
- assert error_event["level"] == "error"
- (exception,) = error_event["exception"]["values"]
- assert exception["type"] == "Exception"
- assert exception["value"] == "Oh!"
-
- assert transaction_event["type"] == "transaction"
- assert transaction_event["contexts"]["trace"]["op"] == "function.aws"
- assert transaction_event["transaction"].startswith("test_")
- assert transaction_event["transaction"] in transaction_event["request"]["url"]
-
-
-@pytest.mark.parametrize(
- "aws_event, has_request_data, batch_size",
- [
- (b"1231", False, 1),
- (b"11.21", False, 1),
- (b'"Good dog!"', False, 1),
- (b"true", False, 1),
- (
- b"""
- [
- {"good dog": "Maisey"},
- {"good dog": "Charlie"},
- {"good dog": "Cory"},
- {"good dog": "Bodhi"}
- ]
- """,
- False,
- 4,
- ),
- (
- b"""
- [
- {
- "headers": {
- "Host": "x1.io",
- "X-Forwarded-Proto": "https"
- },
- "httpMethod": "GET",
- "path": "/1",
- "queryStringParameters": {
- "done": "f"
- },
- "d": "D1"
- },
- {
- "headers": {
- "Host": "x2.io",
- "X-Forwarded-Proto": "http"
- },
- "httpMethod": "POST",
- "path": "/2",
- "queryStringParameters": {
- "done": "t"
- },
- "d": "D2"
- }
- ]
- """,
- True,
- 2,
- ),
- (b"[]", False, 1),
- ],
- ids=[
- "int",
- "float",
- "string",
- "bool",
- "list",
- "list_with_request_data",
- "empty_list",
- ],
-)
-def test_non_dict_event(
- run_lambda_function,
- aws_event,
- has_request_data,
- batch_size,
- DictionaryContaining, # noqa:N803
-):
- envelope_items, response = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk(traces_sample_rate=1.0)
-
- def test_handler(event, context):
- raise Exception("Oh?")
- """
- ),
- aws_event,
- )
-
- assert response["FunctionError"] == "Unhandled"
-
- (
- error_event,
- transaction_event,
- ) = envelope_items
- assert error_event["level"] == "error"
- assert error_event["contexts"]["trace"]["op"] == "function.aws"
-
- function_name = error_event["extra"]["lambda"]["function_name"]
- assert function_name.startswith("test_")
- assert error_event["transaction"] == function_name
-
- exception = error_event["exception"]["values"][0]
- assert exception["type"] == "Exception"
- assert exception["value"] == "Oh?"
- assert exception["mechanism"]["type"] == "aws_lambda"
-
- assert transaction_event["type"] == "transaction"
- assert transaction_event["contexts"]["trace"] == DictionaryContaining(
- error_event["contexts"]["trace"]
- )
- assert transaction_event["contexts"]["trace"]["status"] == "internal_error"
- assert transaction_event["transaction"] == error_event["transaction"]
- assert transaction_event["request"]["url"] == error_event["request"]["url"]
-
- if has_request_data:
- request_data = {
- "headers": {"Host": "x1.io", "X-Forwarded-Proto": "https"},
- "method": "GET",
- "url": "https://x1.io/1",
- "query_string": "done=f",
- }
- else:
- request_data = {"url": "awslambda:///{}".format(function_name)}
-
- assert error_event["request"] == request_data
- assert transaction_event["request"] == request_data
-
- if batch_size > 1:
- assert error_event["tags"]["batch_size"] == batch_size
- assert error_event["tags"]["batch_request"] is True
- assert transaction_event["tags"]["batch_size"] == batch_size
- assert transaction_event["tags"]["batch_request"] is True
-
-
-def test_traces_sampler_gets_correct_values_in_sampling_context(
- run_lambda_function,
- DictionaryContaining, # noqa: N803
- ObjectDescribedBy, # noqa: N803
- StringContaining, # noqa: N803
-):
- # TODO: This whole thing is a little hacky, specifically around the need to
- # get `conftest.py` code into the AWS runtime, which is why there's both
- # `inspect.getsource` and a copy of `_safe_is_equal` included directly in
- # the code below. Ideas which have been discussed to fix this:
-
- # - Include the test suite as a module installed in the package which is
- # shot up to AWS
- # - In client.py, copy `conftest.py` (or wherever the necessary code lives)
- # from the test suite into the main SDK directory so it gets included as
- # "part of the SDK"
-
- # It's also worth noting why it's necessary to run the assertions in the AWS
- # runtime rather than asserting on side effects the way we do with events
- # and envelopes. The reasons are two-fold:
-
- # - We're testing against the `LambdaContext` class, which only exists in
- # the AWS runtime
- # - If we were to transmit call args data they way we transmit event and
- # envelope data (through JSON), we'd quickly run into the problem that all
- # sorts of stuff isn't serializable by `json.dumps` out of the box, up to
- # and including `datetime` objects (so anything with a timestamp is
- # automatically out)
-
- # Perhaps these challenges can be solved in a cleaner and more systematic
- # way if we ever decide to refactor the entire AWS testing apparatus.
-
- import inspect
-
- function_code = (
- LAMBDA_PRELUDE
- + dedent(inspect.getsource(StringContaining))
- + dedent(inspect.getsource(DictionaryContaining))
- + dedent(inspect.getsource(ObjectDescribedBy))
- + dedent(
- """
- from unittest import mock
-
- def _safe_is_equal(x, y):
- # copied from conftest.py - see docstring and comments there
- try:
- is_equal = x.__eq__(y)
- except AttributeError:
- is_equal = NotImplemented
-
- if is_equal == NotImplemented:
- # using == smoothes out weird variations exposed by raw __eq__
- return x == y
-
- return is_equal
-
- def test_handler(event, context):
- # this runs after the transaction has started, which means we
- # can make assertions about traces_sampler
- try:
- traces_sampler.assert_any_call(
- DictionaryContaining(
- {
- "http.request.method": "GET",
- "url.path": "/sit/stay/rollover",
- "url.query": "repeat=twice",
- "url.full": "http://x.io/sit/stay/rollover?repeat=twice",
- "network.protocol.name": "http",
- "server.address": "x.io",
- "http.request.header.custom-header": "Custom Value",
- }
- )
- )
- except AssertionError:
- # catch the error and return it because the error itself will
- # get swallowed by the SDK as an "internal exception"
- return {"AssertionError raised": True,}
-
- return {"AssertionError raised": False,}
-
-
- traces_sampler = mock.Mock(return_value=True)
-
- init_sdk(
- traces_sampler=traces_sampler,
- )
- """
- )
- )
-
- payload = b'{"httpMethod": "GET", "path": "/sit/stay/rollover", "queryStringParameters": {"repeat": "twice"}, "headers": {"Host": "x.io", "X-Forwarded-Proto": "http", "Custom-Header": "Custom Value"}}'
-
- _, response = run_lambda_function(
- code=function_code,
- payload=payload,
- )
- assert response["Payload"]["AssertionError raised"] is False
-
-
-@pytest.mark.xfail(
- reason="The limited log output we depend on is being clogged by a new warning"
-)
-def test_serverless_no_code_instrumentation(run_lambda_function):
- """
- Test that ensures that just by adding a lambda layer containing the
- python sdk, with no code changes sentry is able to capture errors
- """
-
- for initial_handler in [
- None,
- "test_dir/test_lambda.test_handler",
- "test_dir.test_lambda.test_handler",
- ]:
- print("Testing Initial Handler ", initial_handler)
- _, response = run_lambda_function(
- dedent(
- """
- import sentry_sdk
-
- def test_handler(event, context):
- current_client = sentry_sdk.get_client()
-
- assert current_client.is_active()
-
- assert len(current_client.options['integrations']) == 1
- assert isinstance(current_client.options['integrations'][0],
- sentry_sdk.integrations.aws_lambda.AwsLambdaIntegration)
-
- raise Exception("Oh!")
- """
- ),
- b'{"foo": "bar"}',
- layer=True,
- initial_handler=initial_handler,
- )
- assert response["FunctionError"] == "Unhandled"
- assert response["StatusCode"] == 200
-
- assert response["Payload"]["errorType"] != "AssertionError"
-
- assert response["Payload"]["errorType"] == "Exception"
- assert response["Payload"]["errorMessage"] == "Oh!"
-
- assert "sentry_handler" in response["LogResult"][3].decode("utf-8")
-
-
-@pytest.mark.xfail(
- reason="The limited log output we depend on is being clogged by a new warning"
-)
-def test_error_has_new_trace_context_performance_enabled(run_lambda_function):
- envelope_items, _ = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk(traces_sample_rate=1.0)
-
- def test_handler(event, context):
- sentry_sdk.capture_message("hi")
- raise Exception("Oh!")
- """
- ),
- payload=b'{"foo": "bar"}',
- )
-
- (msg_event, error_event, transaction_event) = envelope_items
-
- assert "trace" in msg_event["contexts"]
- assert "trace_id" in msg_event["contexts"]["trace"]
-
- assert "trace" in error_event["contexts"]
- assert "trace_id" in error_event["contexts"]["trace"]
-
- assert "trace" in transaction_event["contexts"]
- assert "trace_id" in transaction_event["contexts"]["trace"]
-
- assert (
- msg_event["contexts"]["trace"]["trace_id"]
- == error_event["contexts"]["trace"]["trace_id"]
- == transaction_event["contexts"]["trace"]["trace_id"]
- )
-
-
-def test_error_has_new_trace_context_performance_disabled(run_lambda_function):
- envelope_items, _ = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk(traces_sample_rate=None) # this is the default, just added for clarity
-
- def test_handler(event, context):
- sentry_sdk.capture_message("hi")
- raise Exception("Oh!")
- """
- ),
- payload=b'{"foo": "bar"}',
- )
-
- (msg_event, error_event) = envelope_items
-
- assert "trace" in msg_event["contexts"]
- assert "trace_id" in msg_event["contexts"]["trace"]
-
- assert "trace" in error_event["contexts"]
- assert "trace_id" in error_event["contexts"]["trace"]
-
- assert (
- msg_event["contexts"]["trace"]["trace_id"]
- == error_event["contexts"]["trace"]["trace_id"]
- )
-
-
-@pytest.mark.xfail(
- reason="The limited log output we depend on is being clogged by a new warning"
-)
-def test_error_has_existing_trace_context_performance_enabled(run_lambda_function):
- trace_id = "471a43a4192642f0b136d5159a501701"
- parent_span_id = "6e8f22c393e68f19"
- parent_sampled = 1
- sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled)
-
- # We simulate here AWS Api Gateway's behavior of passing HTTP headers
- # as the `headers` dict in the event passed to the Lambda function.
- payload = {
- "headers": {
- "sentry-trace": sentry_trace_header,
- }
- }
-
- envelope_items, _ = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk(traces_sample_rate=1.0)
-
- def test_handler(event, context):
- sentry_sdk.capture_message("hi")
- raise Exception("Oh!")
- """
- ),
- payload=json.dumps(payload).encode(),
- )
-
- (msg_event, error_event, transaction_event) = envelope_items
-
- assert "trace" in msg_event["contexts"]
- assert "trace_id" in msg_event["contexts"]["trace"]
-
- assert "trace" in error_event["contexts"]
- assert "trace_id" in error_event["contexts"]["trace"]
-
- assert "trace" in transaction_event["contexts"]
- assert "trace_id" in transaction_event["contexts"]["trace"]
-
- assert (
- msg_event["contexts"]["trace"]["trace_id"]
- == error_event["contexts"]["trace"]["trace_id"]
- == transaction_event["contexts"]["trace"]["trace_id"]
- == "471a43a4192642f0b136d5159a501701"
- )
-
-
-def test_error_has_existing_trace_context_performance_disabled(run_lambda_function):
- trace_id = "471a43a4192642f0b136d5159a501701"
- parent_span_id = "6e8f22c393e68f19"
- parent_sampled = 1
- sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled)
-
- # We simulate here AWS Api Gateway's behavior of passing HTTP headers
- # as the `headers` dict in the event passed to the Lambda function.
- payload = {
- "headers": {
- "sentry-trace": sentry_trace_header,
- }
- }
-
- envelope_items, _ = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk(traces_sample_rate=None) # this is the default, just added for clarity
-
- def test_handler(event, context):
- sentry_sdk.capture_message("hi")
- raise Exception("Oh!")
- """
- ),
- payload=json.dumps(payload).encode(),
- )
-
- (msg_event, error_event) = envelope_items
-
- assert "trace" in msg_event["contexts"]
- assert "trace_id" in msg_event["contexts"]["trace"]
-
- assert "trace" in error_event["contexts"]
- assert "trace_id" in error_event["contexts"]["trace"]
-
- assert (
- msg_event["contexts"]["trace"]["trace_id"]
- == error_event["contexts"]["trace"]["trace_id"]
- == "471a43a4192642f0b136d5159a501701"
- )
-
-
-def test_basic_with_eventbridge_source(run_lambda_function):
- envelope_items, response = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk()
-
- def test_handler(event, context):
- raise Exception("Oh!")
- """
- ),
- b'[{"topic":"lps-ranges","partition":1,"offset":0,"timestamp":1701268939207,"timestampType":"CREATE_TIME","key":"REDACTED","value":"REDACTED","headers":[],"eventSourceArn":"REDACTED","bootstrapServers":"REDACTED","eventSource":"aws:kafka","eventSourceKey":"lps-ranges-1"}]',
- )
-
- assert response["FunctionError"] == "Unhandled"
-
- (event,) = envelope_items
- assert event["level"] == "error"
- (exception,) = event["exception"]["values"]
- assert exception["type"] == "Exception"
- assert exception["value"] == "Oh!"
-
-
-def test_span_origin(run_lambda_function):
- envelope_items, response = run_lambda_function(
- LAMBDA_PRELUDE
- + dedent(
- """
- init_sdk(traces_sample_rate=1.0)
-
- def test_handler(event, context):
- pass
- """
- ),
- b'{"foo": "bar"}',
- )
-
- (event,) = envelope_items
-
- assert event["contexts"]["trace"]["origin"] == "auto.function.aws_lambda"
diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py
new file mode 100644
index 0000000000..85da7e0b14
--- /dev/null
+++ b/tests/integrations/aws_lambda/test_aws_lambda.py
@@ -0,0 +1,550 @@
+import boto3
+import docker
+import json
+import pytest
+import subprocess
+import tempfile
+import time
+import yaml
+
+from unittest import mock
+
+from aws_cdk import App
+
+from .utils import LocalLambdaStack, SentryServerForTesting, SAM_PORT
+
+
+DOCKER_NETWORK_NAME = "lambda-test-network"
+SAM_TEMPLATE_FILE = "sam.template.yaml"
+
+
+@pytest.fixture(scope="session", autouse=True)
+def test_environment():
+ print("[test_environment fixture] Setting up AWS Lambda test infrastructure")
+
+ # Create a Docker network
+ docker_client = docker.from_env()
+ docker_client.networks.prune()
+ docker_client.networks.create(DOCKER_NETWORK_NAME, driver="bridge")
+
+ # Start Sentry server
+ server = SentryServerForTesting()
+ server.start()
+ time.sleep(1) # Give it a moment to start up
+
+ # Create local AWS SAM stack
+ app = App()
+ stack = LocalLambdaStack(app, "LocalLambdaStack")
+
+ # Write SAM template to file
+ template = app.synth().get_stack_by_name("LocalLambdaStack").template
+ with open(SAM_TEMPLATE_FILE, "w") as f:
+ yaml.dump(template, f)
+
+ # Write SAM debug log to file
+ debug_log_file = tempfile.gettempdir() + "/sentry_aws_lambda_tests_sam_debug.log"
+ debug_log = open(debug_log_file, "w")
+ print("[test_environment fixture] Writing SAM debug log to: %s" % debug_log_file)
+
+ # Start SAM local
+ process = subprocess.Popen(
+ [
+ "sam",
+ "local",
+ "start-lambda",
+ "--debug",
+ "--template",
+ SAM_TEMPLATE_FILE,
+ "--warm-containers",
+ "EAGER",
+ "--docker-network",
+ DOCKER_NETWORK_NAME,
+ ],
+ stdout=debug_log,
+ stderr=debug_log,
+ text=True, # This makes stdout/stderr return strings instead of bytes
+ )
+
+ try:
+ # Wait for SAM to be ready
+ LocalLambdaStack.wait_for_stack()
+
+ def before_test():
+ server.clear_envelopes()
+
+ yield {
+ "stack": stack,
+ "server": server,
+ "before_test": before_test,
+ }
+
+ finally:
+ print("[test_environment fixture] Tearing down AWS Lambda test infrastructure")
+
+ process.terminate()
+ process.wait(timeout=5) # Give it time to shut down gracefully
+
+ # Force kill if still running
+ if process.poll() is None:
+ process.kill()
+
+
+@pytest.fixture(autouse=True)
+def clear_before_test(test_environment):
+ test_environment["before_test"]()
+
+
+@pytest.fixture
+def lambda_client():
+ """
+ Create a boto3 client configured to use the local AWS SAM instance.
+ """
+ return boto3.client(
+ "lambda",
+ endpoint_url=f"http://127.0.0.1:{SAM_PORT}", # noqa: E231
+ aws_access_key_id="dummy",
+ aws_secret_access_key="dummy",
+ region_name="us-east-1",
+ )
+
+
+def test_basic_no_exception(lambda_client, test_environment):
+ lambda_client.invoke(
+ FunctionName="BasicOk",
+ Payload=json.dumps({}),
+ )
+ envelopes = test_environment["server"].envelopes
+
+ (transaction_event,) = envelopes
+
+ assert transaction_event["type"] == "transaction"
+ assert transaction_event["transaction"] == "BasicOk"
+ assert transaction_event["sdk"]["name"] == "sentry.python.aws_lambda"
+ assert transaction_event["tags"] == {"aws_region": "us-east-1"}
+
+ assert transaction_event["extra"]["cloudwatch logs"] == {
+ "log_group": mock.ANY,
+ "log_stream": mock.ANY,
+ "url": mock.ANY,
+ }
+ assert transaction_event["extra"]["lambda"] == {
+ "aws_request_id": mock.ANY,
+ "execution_duration_in_millis": mock.ANY,
+ "function_name": "BasicOk",
+ "function_version": "$LATEST",
+ "invoked_function_arn": "arn:aws:lambda:us-east-1:012345678912:function:BasicOk",
+ "remaining_time_in_millis": mock.ANY,
+ }
+ assert transaction_event["contexts"]["trace"] == {
+ "op": "function.aws",
+ "description": mock.ANY,
+ "span_id": mock.ANY,
+ "parent_span_id": mock.ANY,
+ "trace_id": mock.ANY,
+ "origin": "auto.function.aws_lambda",
+ "data": mock.ANY,
+ }
+
+
+def test_basic_exception(lambda_client, test_environment):
+ lambda_client.invoke(
+ FunctionName="BasicException",
+ Payload=json.dumps({}),
+ )
+ envelopes = test_environment["server"].envelopes
+
+ # The second envelope we ignore.
+ # It is the transaction that we test in test_basic_no_exception.
+ (error_event, _) = envelopes
+
+ assert error_event["level"] == "error"
+ assert error_event["exception"]["values"][0]["type"] == "RuntimeError"
+ assert error_event["exception"]["values"][0]["value"] == "Oh!"
+ assert error_event["sdk"]["name"] == "sentry.python.aws_lambda"
+
+ assert error_event["tags"] == {"aws_region": "us-east-1"}
+ assert error_event["extra"]["cloudwatch logs"] == {
+ "log_group": mock.ANY,
+ "log_stream": mock.ANY,
+ "url": mock.ANY,
+ }
+ assert error_event["extra"]["lambda"] == {
+ "aws_request_id": mock.ANY,
+ "execution_duration_in_millis": mock.ANY,
+ "function_name": "BasicException",
+ "function_version": "$LATEST",
+ "invoked_function_arn": "arn:aws:lambda:us-east-1:012345678912:function:BasicException",
+ "remaining_time_in_millis": mock.ANY,
+ }
+ assert error_event["contexts"]["trace"] == {
+ "op": "function.aws",
+ "description": mock.ANY,
+ "span_id": mock.ANY,
+ "parent_span_id": mock.ANY,
+ "trace_id": mock.ANY,
+ "origin": "auto.function.aws_lambda",
+ "data": mock.ANY,
+ }
+
+
+def test_init_error(lambda_client, test_environment):
+ lambda_client.invoke(
+ FunctionName="InitError",
+ Payload=json.dumps({}),
+ )
+ envelopes = test_environment["server"].envelopes
+
+ (error_event, transaction_event) = envelopes
+
+ assert (
+ error_event["exception"]["values"][0]["value"] == "name 'func' is not defined"
+ )
+ assert transaction_event["transaction"] == "InitError"
+
+
+def test_timeout_error(lambda_client, test_environment):
+ lambda_client.invoke(
+ FunctionName="TimeoutError",
+ Payload=json.dumps({}),
+ )
+ envelopes = test_environment["server"].envelopes
+
+ (error_event,) = envelopes
+
+ assert error_event["level"] == "error"
+ assert error_event["extra"]["lambda"]["function_name"] == "TimeoutError"
+
+ (exception,) = error_event["exception"]["values"]
+ assert not exception["mechanism"]["handled"]
+ assert exception["type"] == "ServerlessTimeoutWarning"
+ assert exception["value"].startswith(
+ "WARNING : Function is expected to get timed out. Configured timeout duration ="
+ )
+ assert exception["mechanism"]["type"] == "threading"
+
+
+@pytest.mark.parametrize(
+ "aws_event, has_request_data, batch_size",
+ [
+ (b"1231", False, 1),
+ (b"11.21", False, 1),
+ (b'"Good dog!"', False, 1),
+ (b"true", False, 1),
+ (
+ b"""
+ [
+ {"good dog": "Maisey"},
+ {"good dog": "Charlie"},
+ {"good dog": "Cory"},
+ {"good dog": "Bodhi"}
+ ]
+ """,
+ False,
+ 4,
+ ),
+ (
+ b"""
+ [
+ {
+ "headers": {
+ "Host": "x1.io",
+ "X-Forwarded-Proto": "https"
+ },
+ "httpMethod": "GET",
+ "path": "/1",
+ "queryStringParameters": {
+ "done": "f"
+ },
+ "d": "D1"
+ },
+ {
+ "headers": {
+ "Host": "x2.io",
+ "X-Forwarded-Proto": "http"
+ },
+ "httpMethod": "POST",
+ "path": "/2",
+ "queryStringParameters": {
+ "done": "t"
+ },
+ "d": "D2"
+ }
+ ]
+ """,
+ True,
+ 2,
+ ),
+ (b"[]", False, 1),
+ ],
+ ids=[
+ "event as integer",
+ "event as float",
+ "event as string",
+ "event as bool",
+ "event as list of dicts",
+ "event as dict",
+ "event as empty list",
+ ],
+)
+def test_non_dict_event(
+ lambda_client, test_environment, aws_event, has_request_data, batch_size
+):
+ lambda_client.invoke(
+ FunctionName="BasicException",
+ Payload=aws_event,
+ )
+ envelopes = test_environment["server"].envelopes
+
+ (error_event, transaction_event) = envelopes
+
+ assert transaction_event["type"] == "transaction"
+ assert transaction_event["transaction"] == "BasicException"
+ assert transaction_event["sdk"]["name"] == "sentry.python.aws_lambda"
+ assert transaction_event["contexts"]["trace"]["status"] == "internal_error"
+
+ assert error_event["level"] == "error"
+ assert error_event["transaction"] == "BasicException"
+ assert error_event["sdk"]["name"] == "sentry.python.aws_lambda"
+ assert error_event["exception"]["values"][0]["type"] == "RuntimeError"
+ assert error_event["exception"]["values"][0]["value"] == "Oh!"
+ assert error_event["exception"]["values"][0]["mechanism"]["type"] == "aws_lambda"
+
+ if has_request_data:
+ request_data = {
+ "headers": {"Host": "x1.io", "X-Forwarded-Proto": "https"},
+ "method": "GET",
+ "url": "https://x1.io/1",
+ "query_string": {
+ "done": "f",
+ },
+ }
+ else:
+ request_data = {"url": "awslambda:///BasicException"}
+
+ assert error_event["request"] == request_data
+ assert transaction_event["request"] == request_data
+
+ if batch_size > 1:
+ assert error_event["tags"]["batch_size"] == batch_size
+ assert error_event["tags"]["batch_request"] is True
+ assert transaction_event["tags"]["batch_size"] == batch_size
+ assert transaction_event["tags"]["batch_request"] is True
+
+
+def test_request_data(lambda_client, test_environment):
+ payload = b"""
+ {
+ "resource": "/asd",
+ "path": "/asd",
+ "httpMethod": "GET",
+ "headers": {
+ "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com",
+ "User-Agent": "custom",
+ "X-Forwarded-Proto": "https"
+ },
+ "queryStringParameters": {
+ "bonkers": "true"
+ },
+ "pathParameters": null,
+ "stageVariables": null,
+ "requestContext": {
+ "identity": {
+ "sourceIp": "213.47.147.207",
+ "userArn": "42"
+ }
+ },
+ "body": null,
+ "isBase64Encoded": false
+ }
+ """
+
+ lambda_client.invoke(
+ FunctionName="BasicOk",
+ Payload=payload,
+ )
+ envelopes = test_environment["server"].envelopes
+
+ (transaction_event,) = envelopes
+
+ assert transaction_event["request"] == {
+ "headers": {
+ "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com",
+ "User-Agent": "custom",
+ "X-Forwarded-Proto": "https",
+ },
+ "method": "GET",
+ "query_string": {"bonkers": "true"},
+ "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd",
+ }
+
+
+def test_trace_continuation(lambda_client, test_environment):
+ trace_id = "471a43a4192642f0b136d5159a501701"
+ parent_span_id = "6e8f22c393e68f19"
+ parent_sampled = 1
+ sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled)
+
+ # We simulate here AWS Api Gateway's behavior of passing HTTP headers
+ # as the `headers` dict in the event passed to the Lambda function.
+ payload = {
+ "headers": {
+ "sentry-trace": sentry_trace_header,
+ }
+ }
+
+ lambda_client.invoke(
+ FunctionName="BasicException",
+ Payload=json.dumps(payload),
+ )
+ envelopes = test_environment["server"].envelopes
+
+ (error_event, transaction_event) = envelopes
+
+ assert (
+ error_event["contexts"]["trace"]["trace_id"]
+ == transaction_event["contexts"]["trace"]["trace_id"]
+ == "471a43a4192642f0b136d5159a501701"
+ )
+
+
+@pytest.mark.parametrize(
+ "payload",
+ [
+ {},
+ {"headers": None},
+ {"headers": ""},
+ {"headers": {}},
+ {"headers": []}, # EventBridge sends an empty list
+ ],
+ ids=[
+ "no headers",
+ "none headers",
+ "empty string headers",
+ "empty dict headers",
+ "empty list headers",
+ ],
+)
+def test_headers(lambda_client, test_environment, payload):
+ lambda_client.invoke(
+ FunctionName="BasicException",
+ Payload=json.dumps(payload),
+ )
+ envelopes = test_environment["server"].envelopes
+
+ (error_event, _) = envelopes
+
+ assert error_event["level"] == "error"
+ assert error_event["exception"]["values"][0]["type"] == "RuntimeError"
+ assert error_event["exception"]["values"][0]["value"] == "Oh!"
+
+
+def test_span_origin(lambda_client, test_environment):
+ lambda_client.invoke(
+ FunctionName="BasicOk",
+ Payload=json.dumps({}),
+ )
+ envelopes = test_environment["server"].envelopes
+
+ (transaction_event,) = envelopes
+
+ assert (
+ transaction_event["contexts"]["trace"]["origin"] == "auto.function.aws_lambda"
+ )
+
+
+def test_traces_sampler_has_correct_sampling_context(lambda_client, test_environment):
+ """
+ Test that aws_event and aws_context are passed in the custom_sampling_context
+ when using the AWS Lambda integration.
+ """
+ test_payload = {"test_key": "test_value"}
+ response = lambda_client.invoke(
+ FunctionName="TracesSampler",
+ Payload=json.dumps(test_payload),
+ )
+ response_payload = json.loads(response["Payload"].read().decode())
+ sampling_context_data = json.loads(response_payload["body"])[
+ "sampling_context_data"
+ ]
+ assert sampling_context_data.get("aws_event_present") is True
+ assert sampling_context_data.get("aws_context_present") is True
+ assert sampling_context_data.get("event_data", {}).get("test_key") == "test_value"
+
+
+@pytest.mark.parametrize(
+ "lambda_function_name",
+ ["RaiseErrorPerformanceEnabled", "RaiseErrorPerformanceDisabled"],
+)
+def test_error_has_new_trace_context(
+ lambda_client, test_environment, lambda_function_name
+):
+ lambda_client.invoke(
+ FunctionName=lambda_function_name,
+ Payload=json.dumps({}),
+ )
+ envelopes = test_environment["server"].envelopes
+
+ if lambda_function_name == "RaiseErrorPerformanceEnabled":
+ (error_event, transaction_event) = envelopes
+ else:
+ (error_event,) = envelopes
+ transaction_event = None
+
+ assert "trace" in error_event["contexts"]
+ assert "trace_id" in error_event["contexts"]["trace"]
+
+ if transaction_event:
+ assert "trace" in transaction_event["contexts"]
+ assert "trace_id" in transaction_event["contexts"]["trace"]
+ assert (
+ error_event["contexts"]["trace"]["trace_id"]
+ == transaction_event["contexts"]["trace"]["trace_id"]
+ )
+
+
+@pytest.mark.parametrize(
+ "lambda_function_name",
+ ["RaiseErrorPerformanceEnabled", "RaiseErrorPerformanceDisabled"],
+)
+def test_error_has_existing_trace_context(
+ lambda_client, test_environment, lambda_function_name
+):
+ trace_id = "471a43a4192642f0b136d5159a501701"
+ parent_span_id = "6e8f22c393e68f19"
+ parent_sampled = 1
+ sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled)
+
+ # We simulate here AWS Api Gateway's behavior of passing HTTP headers
+ # as the `headers` dict in the event passed to the Lambda function.
+ payload = {
+ "headers": {
+ "sentry-trace": sentry_trace_header,
+ }
+ }
+
+ lambda_client.invoke(
+ FunctionName=lambda_function_name,
+ Payload=json.dumps(payload),
+ )
+ envelopes = test_environment["server"].envelopes
+
+ if lambda_function_name == "RaiseErrorPerformanceEnabled":
+ (error_event, transaction_event) = envelopes
+ else:
+ (error_event,) = envelopes
+ transaction_event = None
+
+ assert "trace" in error_event["contexts"]
+ assert "trace_id" in error_event["contexts"]["trace"]
+ assert (
+ error_event["contexts"]["trace"]["trace_id"]
+ == "471a43a4192642f0b136d5159a501701"
+ )
+
+ if transaction_event:
+ assert "trace" in transaction_event["contexts"]
+ assert "trace_id" in transaction_event["contexts"]["trace"]
+ assert (
+ transaction_event["contexts"]["trace"]["trace_id"]
+ == "471a43a4192642f0b136d5159a501701"
+ )
diff --git a/tests/integrations/aws_lambda/utils.py b/tests/integrations/aws_lambda/utils.py
new file mode 100644
index 0000000000..d20c9352e7
--- /dev/null
+++ b/tests/integrations/aws_lambda/utils.py
@@ -0,0 +1,294 @@
+import gzip
+import json
+import os
+import shutil
+import subprocess
+import requests
+import sys
+import time
+import threading
+import socket
+import platform
+
+from aws_cdk import (
+ CfnResource,
+ Stack,
+)
+from constructs import Construct
+from fastapi import FastAPI, Request
+import uvicorn
+
+from scripts.build_aws_lambda_layer import build_packaged_zip, DIST_PATH
+
+
+LAMBDA_FUNCTION_DIR = "./tests/integrations/aws_lambda/lambda_functions/"
+LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR = (
+ "./tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/"
+)
+LAMBDA_FUNCTION_TIMEOUT = 10
+SAM_PORT = 3001
+
+PYTHON_VERSION = f"python{sys.version_info.major}.{sys.version_info.minor}"
+
+
+def get_host_ip():
+ """
+ Returns the IP address of the host we are running on.
+ """
+ if os.environ.get("GITHUB_ACTIONS"):
+ # Running in GitHub Actions
+ hostname = socket.gethostname()
+ host = socket.gethostbyname(hostname)
+ else:
+ # Running locally
+ if platform.system() in ["Darwin", "Windows"]:
+ # Windows or MacOS
+ host = "host.docker.internal"
+ else:
+ # Linux
+ hostname = socket.gethostname()
+ host = socket.gethostbyname(hostname)
+
+ return host
+
+
+def get_project_root():
+ """
+ Returns the absolute path to the project root directory.
+ """
+ # Start from the current file's directory
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+
+ # Navigate up to the project root (4 levels up from tests/integrations/aws_lambda/)
+ # This is equivalent to the multiple dirname() calls
+ project_root = os.path.abspath(os.path.join(current_dir, "../../../"))
+
+ return project_root
+
+
+class LocalLambdaStack(Stack):
+ """
+ Uses the AWS CDK to create a local SAM stack containing Lambda functions.
+ """
+
+ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
+ print("[LocalLambdaStack] Creating local SAM Lambda Stack")
+ super().__init__(scope, construct_id, **kwargs)
+
+ # Override the template synthesis
+ self.template_options.template_format_version = "2010-09-09"
+ self.template_options.transforms = ["AWS::Serverless-2016-10-31"]
+
+ print("[LocalLambdaStack] Create Sentry Lambda layer package")
+ filename = "sentry-sdk-lambda-layer.zip"
+ build_packaged_zip(
+ make_dist=True,
+ out_zip_filename=filename,
+ )
+
+ print(
+ "[LocalLambdaStack] Add Sentry Lambda layer containing the Sentry SDK to the SAM stack"
+ )
+ self.sentry_layer = CfnResource(
+ self,
+ "SentryPythonServerlessSDK",
+ type="AWS::Serverless::LayerVersion",
+ properties={
+ "ContentUri": os.path.join(DIST_PATH, filename),
+ "CompatibleRuntimes": [
+ PYTHON_VERSION,
+ ],
+ },
+ )
+
+ dsn = f"http://123@{get_host_ip()}:9999/0" # noqa: E231
+ print("[LocalLambdaStack] Using Sentry DSN: %s" % dsn)
+
+ print(
+ "[LocalLambdaStack] Add all Lambda functions defined in "
+ "/tests/integrations/aws_lambda/lambda_functions/ to the SAM stack"
+ )
+ lambda_dirs = [
+ d
+ for d in os.listdir(LAMBDA_FUNCTION_DIR)
+ if os.path.isdir(os.path.join(LAMBDA_FUNCTION_DIR, d))
+ ]
+ for lambda_dir in lambda_dirs:
+ CfnResource(
+ self,
+ lambda_dir,
+ type="AWS::Serverless::Function",
+ properties={
+ "CodeUri": os.path.join(LAMBDA_FUNCTION_DIR, lambda_dir),
+ "Handler": "sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler",
+ "Runtime": PYTHON_VERSION,
+ "Timeout": LAMBDA_FUNCTION_TIMEOUT,
+ "Layers": [
+ {"Ref": self.sentry_layer.logical_id}
+ ], # Add layer containing the Sentry SDK to function.
+ "Environment": {
+ "Variables": {
+ "SENTRY_DSN": dsn,
+ "SENTRY_INITIAL_HANDLER": "index.handler",
+ "SENTRY_TRACES_SAMPLE_RATE": "1.0",
+ }
+ },
+ },
+ )
+ print(
+ "[LocalLambdaStack] - Created Lambda function: %s (%s)"
+ % (
+ lambda_dir,
+ os.path.join(LAMBDA_FUNCTION_DIR, lambda_dir),
+ )
+ )
+
+ print(
+ "[LocalLambdaStack] Add all Lambda functions defined in "
+ "/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/ to the SAM stack"
+ )
+ lambda_dirs = [
+ d
+ for d in os.listdir(LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR)
+ if os.path.isdir(os.path.join(LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR, d))
+ ]
+ for lambda_dir in lambda_dirs:
+ # Copy the Sentry SDK into the function directory
+ sdk_path = os.path.join(
+ LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR, lambda_dir, "sentry_sdk"
+ )
+ if not os.path.exists(sdk_path):
+ # Find the Sentry SDK in the current environment
+ import sentry_sdk as sdk_module
+
+ sdk_source = os.path.dirname(sdk_module.__file__)
+ shutil.copytree(sdk_source, sdk_path)
+
+ # Install the requirements of Sentry SDK into the function directory
+ requirements_file = os.path.join(
+ get_project_root(), "requirements-aws-lambda-layer.txt"
+ )
+
+ # Install the package using pip
+ subprocess.check_call(
+ [
+ sys.executable,
+ "-m",
+ "pip",
+ "install",
+ "--upgrade",
+ "--target",
+ os.path.join(LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR, lambda_dir),
+ "-r",
+ requirements_file,
+ ]
+ )
+
+ CfnResource(
+ self,
+ lambda_dir,
+ type="AWS::Serverless::Function",
+ properties={
+ "CodeUri": os.path.join(
+ LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR, lambda_dir
+ ),
+ "Handler": "index.handler",
+ "Runtime": PYTHON_VERSION,
+ "Timeout": LAMBDA_FUNCTION_TIMEOUT,
+ "Environment": {
+ "Variables": {
+ "SENTRY_DSN": dsn,
+ }
+ },
+ },
+ )
+ print(
+ "[LocalLambdaStack] - Created Lambda function: %s (%s)"
+ % (
+ lambda_dir,
+ os.path.join(LAMBDA_FUNCTION_DIR, lambda_dir),
+ )
+ )
+
+ @classmethod
+ def wait_for_stack(cls, timeout=60, port=SAM_PORT):
+ """
+ Wait for SAM to be ready, with timeout.
+ """
+ start_time = time.time()
+ while True:
+ if time.time() - start_time > timeout:
+ raise TimeoutError(
+ "AWS SAM failed to start within %s seconds. (Maybe Docker is not running?)"
+ % timeout
+ )
+
+ try:
+ # Try to connect to SAM
+ response = requests.get(f"http://127.0.0.1:{port}/") # noqa: E231
+ if response.status_code == 200 or response.status_code == 404:
+ return
+
+ except requests.exceptions.ConnectionError:
+ time.sleep(1)
+ continue
+
+
+class SentryServerForTesting:
+ """
+ A simple Sentry.io style server that accepts envelopes and stores them in a list.
+ """
+
+ def __init__(self, host="0.0.0.0", port=9999, log_level="warning"):
+ self.envelopes = []
+ self.host = host
+ self.port = port
+ self.log_level = log_level
+ self.app = FastAPI()
+
+ @self.app.post("/api/0/envelope/")
+ async def envelope(request: Request):
+ print("[SentryServerForTesting] Received envelope")
+ try:
+ raw_body = await request.body()
+ except Exception:
+ return {"status": "no body received"}
+
+ try:
+ body = gzip.decompress(raw_body).decode("utf-8")
+ except Exception:
+ # If decompression fails, assume it's plain text
+ body = raw_body.decode("utf-8")
+
+ lines = body.split("\n")
+
+ current_line = 1 # line 0 is envelope header
+ while current_line < len(lines):
+ # skip empty lines
+ if not lines[current_line].strip():
+ current_line += 1
+ continue
+
+ # skip envelope item header
+ current_line += 1
+
+ # add envelope item to store
+ envelope_item = lines[current_line]
+ if envelope_item.strip():
+ self.envelopes.append(json.loads(envelope_item))
+
+ return {"status": "ok"}
+
+ def run_server(self):
+ uvicorn.run(self.app, host=self.host, port=self.port, log_level=self.log_level)
+
+ def start(self):
+ print(
+ "[SentryServerForTesting] Starting server on %s:%s" % (self.host, self.port)
+ )
+ server_thread = threading.Thread(target=self.run_server, daemon=True)
+ server_thread.start()
+
+ def clear_envelopes(self):
+ print("[SentryServerForTesting] Clearing envelopes")
+ self.envelopes = []
diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py
index b8631ced7b..95838b1009 100644
--- a/tests/integrations/fastapi/test_fastapi.py
+++ b/tests/integrations/fastapi/test_fastapi.py
@@ -679,3 +679,38 @@ async def _error():
client.get("/error")
assert len(events) == int(expected_error)
+
+
+@pytest.mark.parametrize("transaction_style", ["endpoint", "url"])
+def test_app_host(sentry_init, capture_events, transaction_style):
+ sentry_init(
+ traces_sample_rate=1.0,
+ integrations=[
+ StarletteIntegration(transaction_style=transaction_style),
+ FastApiIntegration(transaction_style=transaction_style),
+ ],
+ )
+
+ app = FastAPI()
+ subapp = FastAPI()
+
+ @subapp.get("/subapp")
+ async def subapp_route():
+ return {"message": "Hello world!"}
+
+ app.host("subapp", subapp)
+
+ events = capture_events()
+
+ client = TestClient(app)
+ client.get("/subapp", headers={"Host": "subapp"})
+
+ assert len(events) == 1
+
+ (event,) = events
+ assert "transaction" in event
+
+ if transaction_style == "url":
+ assert event["transaction"] == "/subapp"
+ else:
+ assert event["transaction"].endswith("subapp_route")
diff --git a/tests/integrations/loguru/test_loguru.py b/tests/integrations/loguru/test_loguru.py
index 6030108de1..64e9f22ba5 100644
--- a/tests/integrations/loguru/test_loguru.py
+++ b/tests/integrations/loguru/test_loguru.py
@@ -8,18 +8,18 @@
@pytest.mark.parametrize(
- "level,created_event",
+ "level,created_event,expected_sentry_level",
[
# None - no breadcrumb
# False - no event
# True - event created
- (LoggingLevels.TRACE, None),
- (LoggingLevels.DEBUG, None),
- (LoggingLevels.INFO, False),
- (LoggingLevels.SUCCESS, False),
- (LoggingLevels.WARNING, False),
- (LoggingLevels.ERROR, True),
- (LoggingLevels.CRITICAL, True),
+ (LoggingLevels.TRACE, None, "debug"),
+ (LoggingLevels.DEBUG, None, "debug"),
+ (LoggingLevels.INFO, False, "info"),
+ (LoggingLevels.SUCCESS, False, "info"),
+ (LoggingLevels.WARNING, False, "warning"),
+ (LoggingLevels.ERROR, True, "error"),
+ (LoggingLevels.CRITICAL, True, "critical"),
],
)
@pytest.mark.parametrize("disable_breadcrumbs", [True, False])
@@ -29,6 +29,7 @@ def test_just_log(
capture_events,
level,
created_event,
+ expected_sentry_level,
disable_breadcrumbs,
disable_events,
):
@@ -48,7 +49,7 @@ def test_just_log(
formatted_message = (
" | "
+ "{:9}".format(level.name.upper())
- + "| tests.integrations.loguru.test_loguru:test_just_log:46 - test"
+ + "| tests.integrations.loguru.test_loguru:test_just_log:47 - test"
)
if not created_event:
@@ -59,7 +60,7 @@ def test_just_log(
not disable_breadcrumbs and created_event is not None
): # not None == not TRACE or DEBUG level
(breadcrumb,) = breadcrumbs
- assert breadcrumb["level"] == level.name.lower()
+ assert breadcrumb["level"] == expected_sentry_level
assert breadcrumb["category"] == "tests.integrations.loguru.test_loguru"
assert breadcrumb["message"][23:] == formatted_message
else:
@@ -72,7 +73,7 @@ def test_just_log(
return
(event,) = events
- assert event["level"] == (level.name.lower())
+ assert event["level"] == expected_sentry_level
assert event["logger"] == "tests.integrations.loguru.test_loguru"
assert event["logentry"]["message"][23:] == formatted_message
diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py
index f15b968ac5..100642d245 100644
--- a/tests/integrations/quart/test_quart.py
+++ b/tests/integrations/quart/test_quart.py
@@ -1,3 +1,4 @@
+import importlib
import json
import threading
from unittest import mock
@@ -13,22 +14,22 @@
from sentry_sdk.integrations.logging import LoggingIntegration
import sentry_sdk.integrations.quart as quart_sentry
-from quart import Quart, Response, abort, stream_with_context
-from quart.views import View
-from quart_auth import AuthUser, login_user
-
-try:
- from quart_auth import QuartAuth
+def quart_app_factory():
+ # These imports are inlined because the `test_quart_flask_patch` testcase
+ # tests behavior that is triggered by importing a package before any Quart
+ # imports happen, so we can't have these on the module level
+ from quart import Quart
- auth_manager = QuartAuth()
-except ImportError:
- from quart_auth import AuthManager
+ try:
+ from quart_auth import QuartAuth
- auth_manager = AuthManager()
+ auth_manager = QuartAuth()
+ except ImportError:
+ from quart_auth import AuthManager
+ auth_manager = AuthManager()
-def quart_app_factory():
app = Quart(__name__)
app.debug = False
app.config["TESTING"] = False
@@ -71,6 +72,42 @@ def integration_enabled_params(request):
raise ValueError(request.param)
+@pytest.mark.asyncio
+@pytest.mark.forked
+@pytest.mark.skipif(
+ not importlib.util.find_spec("quart_flask_patch"),
+ reason="requires quart_flask_patch",
+)
+async def test_quart_flask_patch(sentry_init, capture_events, reset_integrations):
+ # This testcase is forked because `import quart_flask_patch` needs to run
+ # before anything else Quart-related is imported (since it monkeypatches
+ # some things) and we don't want this to affect other testcases.
+ #
+ # It's also important this testcase be run before any other testcase
+ # that uses `quart_app_factory`.
+ import quart_flask_patch # noqa: F401
+
+ app = quart_app_factory()
+ sentry_init(
+ integrations=[quart_sentry.QuartIntegration()],
+ )
+
+ @app.route("/")
+ async def index():
+ 1 / 0
+
+ events = capture_events()
+
+ client = app.test_client()
+ try:
+ await client.get("/")
+ except ZeroDivisionError:
+ pass
+
+ (event,) = events
+ assert event["exception"]["values"][0]["mechanism"]["type"] == "quart"
+
+
@pytest.mark.asyncio
async def test_has_context(sentry_init, capture_events):
sentry_init(integrations=[quart_sentry.QuartIntegration()])
@@ -213,6 +250,8 @@ async def test_quart_auth_configured(
monkeypatch,
integration_enabled_params,
):
+ from quart_auth import AuthUser, login_user
+
sentry_init(send_default_pii=send_default_pii, **integration_enabled_params)
app = quart_app_factory()
@@ -368,6 +407,8 @@ async def error_handler(err):
@pytest.mark.asyncio
async def test_bad_request_not_captured(sentry_init, capture_events):
+ from quart import abort
+
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
events = capture_events()
@@ -385,6 +426,8 @@ async def index():
@pytest.mark.asyncio
async def test_does_not_leak_scope(sentry_init, capture_events):
+ from quart import Response, stream_with_context
+
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
events = capture_events()
@@ -514,6 +557,8 @@ async def error():
@pytest.mark.asyncio
async def test_class_based_views(sentry_init, capture_events):
+ from quart.views import View
+
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
events = capture_events()
diff --git a/tests/integrations/spark/test_spark.py b/tests/integrations/spark/test_spark.py
index 44ba9f8728..7eeab15dc4 100644
--- a/tests/integrations/spark/test_spark.py
+++ b/tests/integrations/spark/test_spark.py
@@ -14,6 +14,7 @@
from py4j.protocol import Py4JJavaError
+
################
# DRIVER TESTS #
################
@@ -166,6 +167,65 @@ def stageInfo(self): # noqa: N802
assert mock_hub.kwargs["data"]["name"] == "run-job"
+def test_sentry_listener_on_stage_submitted_no_attempt_id(sentry_listener):
+ listener = sentry_listener
+ with patch.object(listener, "_add_breadcrumb") as mock_add_breadcrumb:
+
+ class StageInfo:
+ def stageId(self): # noqa: N802
+ return "sample-stage-id-submit"
+
+ def name(self):
+ return "run-job"
+
+ def attemptNumber(self): # noqa: N802
+ return 14
+
+ class MockStageSubmitted:
+ def stageInfo(self): # noqa: N802
+ stageinf = StageInfo()
+ return stageinf
+
+ mock_stage_submitted = MockStageSubmitted()
+ listener.onStageSubmitted(mock_stage_submitted)
+
+ mock_add_breadcrumb.assert_called_once()
+ mock_hub = mock_add_breadcrumb.call_args
+
+ assert mock_hub.kwargs["level"] == "info"
+ assert "sample-stage-id-submit" in mock_hub.kwargs["message"]
+ assert mock_hub.kwargs["data"]["attemptId"] == 14
+ assert mock_hub.kwargs["data"]["name"] == "run-job"
+
+
+def test_sentry_listener_on_stage_submitted_no_attempt_id_or_number(sentry_listener):
+ listener = sentry_listener
+ with patch.object(listener, "_add_breadcrumb") as mock_add_breadcrumb:
+
+ class StageInfo:
+ def stageId(self): # noqa: N802
+ return "sample-stage-id-submit"
+
+ def name(self):
+ return "run-job"
+
+ class MockStageSubmitted:
+ def stageInfo(self): # noqa: N802
+ stageinf = StageInfo()
+ return stageinf
+
+ mock_stage_submitted = MockStageSubmitted()
+ listener.onStageSubmitted(mock_stage_submitted)
+
+ mock_add_breadcrumb.assert_called_once()
+ mock_hub = mock_add_breadcrumb.call_args
+
+ assert mock_hub.kwargs["level"] == "info"
+ assert "sample-stage-id-submit" in mock_hub.kwargs["message"]
+ assert "attemptId" not in mock_hub.kwargs["data"]
+ assert mock_hub.kwargs["data"]["name"] == "run-job"
+
+
@pytest.fixture
def get_mock_stage_completed():
def _inner(failure_reason):
diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py
index 13456bfe75..f3f9027264 100644
--- a/tests/integrations/starlette/test_starlette.py
+++ b/tests/integrations/starlette/test_starlette.py
@@ -32,7 +32,6 @@
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.testclient import TestClient
-
from tests.integrations.conftest import parametrize_test_configurable_status_codes
@@ -239,6 +238,12 @@ async def do_stuff(message):
await self.app(scope, receive, do_stuff)
+class SampleMiddlewareWithArgs(Middleware):
+ def __init__(self, app, bla=None):
+ self.app = app
+ self.bla = bla
+
+
class SampleReceiveSendMiddleware:
def __init__(self, app):
self.app = app
@@ -863,6 +868,22 @@ def test_middleware_partial_receive_send(sentry_init, capture_events):
idx += 1
+@pytest.mark.skipif(
+ STARLETTE_VERSION < (0, 35),
+ reason="Positional args for middleware have been introduced in Starlette >= 0.35",
+)
+def test_middleware_positional_args(sentry_init):
+ sentry_init(
+ traces_sample_rate=1.0,
+ integrations=[StarletteIntegration()],
+ )
+ _ = starlette_app_factory(middleware=[Middleware(SampleMiddlewareWithArgs, "bla")])
+
+ # Only creating the App with an Middleware with args
+ # should not raise an error
+ # So as long as test passes, we are good
+
+
def test_legacy_setup(
sentry_init,
capture_events,
diff --git a/tests/test_basics.py b/tests/test_basics.py
index cbf0177403..75d9fcd0bc 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -571,6 +571,37 @@ def test_dedupe_event_processor_drop_records_client_report(
assert lost_event_call == ("event_processor", "error", None, 1)
+def test_dedupe_doesnt_take_into_account_dropped_exception(sentry_init, capture_events):
+ # Two exceptions happen one after another. The first one is dropped in the
+ # user's before_send. The second one isn't.
+ # Originally, DedupeIntegration would drop the second exception. This test
+ # is making sure that that is no longer the case -- i.e., DedupeIntegration
+ # doesn't consider exceptions dropped in before_send.
+ count = 0
+
+ def before_send(event, hint):
+ nonlocal count
+ count += 1
+ if count == 1:
+ return None
+ return event
+
+ sentry_init(before_send=before_send)
+ events = capture_events()
+
+ exc = ValueError("aha!")
+ for _ in range(2):
+ # The first ValueError will be dropped by before_send. The second
+ # ValueError will be accepted by before_send, and should be sent to
+ # Sentry.
+ try:
+ raise exc
+ except Exception:
+ capture_exception()
+
+ assert len(events) == 1
+
+
def test_event_processor_drop_records_client_report(
sentry_init, capture_events, capture_record_lost_event_calls
):
diff --git a/tests/test_import.py b/tests/test_import.py
new file mode 100644
index 0000000000..e5b07817cb
--- /dev/null
+++ b/tests/test_import.py
@@ -0,0 +1,7 @@
+# As long as this file can be imported, we are good.
+from sentry_sdk import * # noqa: F403, F401
+
+
+def test_import():
+ # As long as this file can be imported, we are good.
+ assert True
diff --git a/tests/test_logs.py b/tests/test_logs.py
new file mode 100644
index 0000000000..173a4028d6
--- /dev/null
+++ b/tests/test_logs.py
@@ -0,0 +1,242 @@
+import sys
+from unittest import mock
+import pytest
+
+import sentry_sdk
+from sentry_sdk import _experimental_logger as sentry_logger
+
+
+minimum_python_37 = pytest.mark.skipif(
+ sys.version_info < (3, 7), reason="Asyncio tests need Python >= 3.7"
+)
+
+
+@minimum_python_37
+def test_logs_disabled_by_default(sentry_init, capture_envelopes):
+ sentry_init()
+ envelopes = capture_envelopes()
+
+ sentry_logger.trace("This is a 'trace' log.")
+ sentry_logger.debug("This is a 'debug' log...")
+ sentry_logger.info("This is a 'info' log...")
+ sentry_logger.warn("This is a 'warn' log...")
+ sentry_logger.error("This is a 'error' log...")
+ sentry_logger.fatal("This is a 'fatal' log...")
+
+ assert len(envelopes) == 0
+
+
+@minimum_python_37
+def test_logs_basics(sentry_init, capture_envelopes):
+ sentry_init(_experiments={"enable_sentry_logs": True})
+ envelopes = capture_envelopes()
+
+ sentry_logger.trace("This is a 'trace' log...")
+ sentry_logger.debug("This is a 'debug' log...")
+ sentry_logger.info("This is a 'info' log...")
+ sentry_logger.warn("This is a 'warn' log...")
+ sentry_logger.error("This is a 'error' log...")
+ sentry_logger.fatal("This is a 'fatal' log...")
+
+ assert (
+ len(envelopes) == 6
+ ) # We will batch those log items into a single envelope at some point
+
+ assert envelopes[0].items[0].payload.json["severityText"] == "trace"
+ assert envelopes[0].items[0].payload.json["severityNumber"] == 1
+
+ assert envelopes[1].items[0].payload.json["severityText"] == "debug"
+ assert envelopes[1].items[0].payload.json["severityNumber"] == 5
+
+ assert envelopes[2].items[0].payload.json["severityText"] == "info"
+ assert envelopes[2].items[0].payload.json["severityNumber"] == 9
+
+ assert envelopes[3].items[0].payload.json["severityText"] == "warn"
+ assert envelopes[3].items[0].payload.json["severityNumber"] == 13
+
+ assert envelopes[4].items[0].payload.json["severityText"] == "error"
+ assert envelopes[4].items[0].payload.json["severityNumber"] == 17
+
+ assert envelopes[5].items[0].payload.json["severityText"] == "fatal"
+ assert envelopes[5].items[0].payload.json["severityNumber"] == 21
+
+
+@minimum_python_37
+def test_logs_before_emit_log(sentry_init, capture_envelopes):
+ def _before_log(record, hint):
+ assert list(record.keys()) == [
+ "severity_text",
+ "severity_number",
+ "body",
+ "attributes",
+ "time_unix_nano",
+ "trace_id",
+ ]
+
+ if record["severity_text"] in ["fatal", "error"]:
+ return None
+
+ return record
+
+ sentry_init(
+ _experiments={
+ "enable_sentry_logs": True,
+ "before_emit_log": _before_log,
+ }
+ )
+ envelopes = capture_envelopes()
+
+ sentry_logger.trace("This is a 'trace' log...")
+ sentry_logger.debug("This is a 'debug' log...")
+ sentry_logger.info("This is a 'info' log...")
+ sentry_logger.warn("This is a 'warn' log...")
+ sentry_logger.error("This is a 'error' log...")
+ sentry_logger.fatal("This is a 'fatal' log...")
+
+ assert len(envelopes) == 4
+
+ assert envelopes[0].items[0].payload.json["severityText"] == "trace"
+ assert envelopes[1].items[0].payload.json["severityText"] == "debug"
+ assert envelopes[2].items[0].payload.json["severityText"] == "info"
+ assert envelopes[3].items[0].payload.json["severityText"] == "warn"
+
+
+@minimum_python_37
+def test_logs_attributes(sentry_init, capture_envelopes):
+ """
+ Passing arbitrary attributes to log messages.
+ """
+ sentry_init(_experiments={"enable_sentry_logs": True})
+ envelopes = capture_envelopes()
+
+ attrs = {
+ "attr_int": 1,
+ "attr_float": 2.0,
+ "attr_bool": True,
+ "attr_string": "string attribute",
+ }
+
+ sentry_logger.warn(
+ "The recorded value was '{my_var}'", my_var="some value", attributes=attrs
+ )
+
+ log_item = envelopes[0].items[0].payload.json
+ assert log_item["body"]["stringValue"] == "The recorded value was 'some value'"
+
+ assert log_item["attributes"][1] == {
+ "key": "attr_int",
+ "value": {"intValue": "1"},
+ } # TODO: this is strange.
+ assert log_item["attributes"][2] == {
+ "key": "attr_float",
+ "value": {"doubleValue": 2.0},
+ }
+ assert log_item["attributes"][3] == {
+ "key": "attr_bool",
+ "value": {"boolValue": True},
+ }
+ assert log_item["attributes"][4] == {
+ "key": "attr_string",
+ "value": {"stringValue": "string attribute"},
+ }
+ assert log_item["attributes"][5] == {
+ "key": "sentry.environment",
+ "value": {"stringValue": "production"},
+ }
+ assert log_item["attributes"][6] == {
+ "key": "sentry.release",
+ "value": {"stringValue": mock.ANY},
+ }
+ assert log_item["attributes"][7] == {
+ "key": "sentry.message.parameters.my_var",
+ "value": {"stringValue": "some value"},
+ }
+
+
+@minimum_python_37
+def test_logs_message_params(sentry_init, capture_envelopes):
+ """
+ This is the official way of how to pass vars to log messages.
+ """
+ sentry_init(_experiments={"enable_sentry_logs": True})
+ envelopes = capture_envelopes()
+
+ sentry_logger.warn("The recorded value was '{int_var}'", int_var=1)
+ sentry_logger.warn("The recorded value was '{float_var}'", float_var=2.0)
+ sentry_logger.warn("The recorded value was '{bool_var}'", bool_var=False)
+ sentry_logger.warn(
+ "The recorded value was '{string_var}'", string_var="some string value"
+ )
+
+ assert (
+ envelopes[0].items[0].payload.json["body"]["stringValue"]
+ == "The recorded value was '1'"
+ )
+ assert envelopes[0].items[0].payload.json["attributes"][-1] == {
+ "key": "sentry.message.parameters.int_var",
+ "value": {"intValue": "1"},
+ } # TODO: this is strange.
+
+ assert (
+ envelopes[1].items[0].payload.json["body"]["stringValue"]
+ == "The recorded value was '2.0'"
+ )
+ assert envelopes[1].items[0].payload.json["attributes"][-1] == {
+ "key": "sentry.message.parameters.float_var",
+ "value": {"doubleValue": 2.0},
+ }
+
+ assert (
+ envelopes[2].items[0].payload.json["body"]["stringValue"]
+ == "The recorded value was 'False'"
+ )
+ assert envelopes[2].items[0].payload.json["attributes"][-1] == {
+ "key": "sentry.message.parameters.bool_var",
+ "value": {"boolValue": False},
+ }
+
+ assert (
+ envelopes[3].items[0].payload.json["body"]["stringValue"]
+ == "The recorded value was 'some string value'"
+ )
+ assert envelopes[3].items[0].payload.json["attributes"][-1] == {
+ "key": "sentry.message.parameters.string_var",
+ "value": {"stringValue": "some string value"},
+ }
+
+
+@minimum_python_37
+def test_logs_tied_to_transactions(sentry_init, capture_envelopes):
+ """
+ Log messages are also tied to transactions.
+ """
+ sentry_init(_experiments={"enable_sentry_logs": True})
+ envelopes = capture_envelopes()
+
+ with sentry_sdk.start_transaction(name="test-transaction") as trx:
+ sentry_logger.warn("This is a log tied to a transaction")
+
+ log_entry = envelopes[0].items[0].payload.json
+ assert log_entry["attributes"][-1] == {
+ "key": "sentry.trace.parent_span_id",
+ "value": {"stringValue": trx.span_id},
+ }
+
+
+@minimum_python_37
+def test_logs_tied_to_spans(sentry_init, capture_envelopes):
+ """
+ Log messages are also tied to spans.
+ """
+ sentry_init(_experiments={"enable_sentry_logs": True})
+ envelopes = capture_envelopes()
+
+ with sentry_sdk.start_transaction(name="test-transaction"):
+ with sentry_sdk.start_span(description="test-span") as span:
+ sentry_logger.warn("This is a log tied to a span")
+
+ log_entry = envelopes[0].items[0].payload.json
+ assert log_entry["attributes"][-1] == {
+ "key": "sentry.trace.parent_span_id",
+ "value": {"stringValue": span.span_id},
+ }
diff --git a/tox.ini b/tox.ini
index 8bb9401a27..ae348e1990 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-02-19T12:41:15.689786+00:00
+# Last generated: 2025-03-20T09:29:11.534740+00:00
[tox]
requires =
@@ -57,10 +57,7 @@ envlist =
{py3.8,py3.11,py3.12}-asyncpg-latest
# AWS Lambda
- # The aws_lambda tests deploy to the real AWS and have their own
- # matrix of Python versions to run the test lambda function in.
- # see `lambda_runtime` fixture in tests/integrations/aws_lambda.py
- {py3.9}-aws_lambda
+ {py3.8,py3.9,py3.11,py3.13}-aws_lambda
# Beam
{py3.7}-beam-v{2.12}
@@ -180,7 +177,7 @@ envlist =
{py3.7}-pymongo-v3.7.2
{py3.7,py3.10,py3.11}-pymongo-v3.13.0
{py3.7,py3.9,py3.10}-pymongo-v4.0.2
- {py3.9,py3.12,py3.13}-pymongo-v4.11.1
+ {py3.9,py3.12,py3.13}-pymongo-v4.11.3
{py3.7}-redis_py_cluster_legacy-v2.0.0
{py3.7,py3.8}-redis_py_cluster_legacy-v2.1.3
@@ -188,54 +185,57 @@ envlist =
{py3.7}-sqlalchemy-v1.3.9
{py3.7,py3.11,py3.12}-sqlalchemy-v1.4.54
{py3.7,py3.10,py3.11}-sqlalchemy-v2.0.9
- {py3.7,py3.12,py3.13}-sqlalchemy-v2.0.38
+ {py3.7,py3.12,py3.13}-sqlalchemy-v2.0.39
# ~~~ 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.8,py3.12,py3.13}-openfeature-v0.7.5
{py3.9,py3.12,py3.13}-openfeature-v0.8.0
{py3.7,py3.12,py3.13}-statsig-v0.55.3
{py3.7,py3.12,py3.13}-statsig-v0.56.0
+ {py3.7,py3.12,py3.13}-statsig-v0.57.1
{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.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.0
+ {py3.9,py3.12,py3.13}-ariadne-v0.26.1
{py3.7,py3.9,py3.10}-gql-v3.4.1
- {py3.7,py3.11,py3.12}-gql-v3.5.0
+ {py3.7,py3.11,py3.12}-gql-v3.5.2
{py3.9,py3.12,py3.13}-gql-v3.6.0b4
{py3.7,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.226.2
- {py3.8,py3.11,py3.12}-strawberry-v0.243.1
- {py3.9,py3.12,py3.13}-strawberry-v0.260.2
+ {py3.8,py3.11,py3.12}-strawberry-v0.227.7
+ {py3.8,py3.11,py3.12}-strawberry-v0.245.0
+ {py3.9,py3.12,py3.13}-strawberry-v0.262.5
# ~~~ Network ~~~
{py3.7,py3.8}-grpc-v1.32.0
{py3.7,py3.9,py3.10}-grpc-v1.44.0
{py3.7,py3.10,py3.11}-grpc-v1.58.3
- {py3.8,py3.12,py3.13}-grpc-v1.70.0
+ {py3.9,py3.12,py3.13}-grpc-v1.71.0
# ~~~ Tasks ~~~
{py3.7,py3.8}-celery-v4.4.7
{py3.7,py3.8}-celery-v5.0.5
{py3.8,py3.11,py3.12}-celery-v5.4.0
- {py3.8,py3.12,py3.13}-celery-v5.5.0rc4
+ {py3.8,py3.12,py3.13}-celery-v5.5.0rc5
{py3.7}-dramatiq-v1.9.0
{py3.7,py3.8,py3.9}-dramatiq-v1.12.3
@@ -245,7 +245,7 @@ envlist =
{py3.8,py3.9}-spark-v3.0.3
{py3.8,py3.9}-spark-v3.2.4
{py3.8,py3.10,py3.11}-spark-v3.4.4
- {py3.8,py3.10,py3.11}-spark-v3.5.4
+ {py3.8,py3.10,py3.11}-spark-v3.5.5
# ~~~ Web 1 ~~~
@@ -257,7 +257,7 @@ envlist =
{py3.7,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.45.3
+ {py3.9,py3.12,py3.13}-starlette-v0.46.1
# ~~~ Web 2 ~~~
@@ -289,9 +289,9 @@ envlist =
{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.9
- {py3.8,py3.11,py3.12}-trytond-v7.4.6
+ {py3.8,py3.11,py3.12}-trytond-v7.4.8
- {py3.7,py3.11,py3.12}-typer-v0.15.1
+ {py3.7,py3.12,py3.13}-typer-v0.15.2
@@ -359,7 +359,12 @@ deps =
asyncpg: pytest-asyncio
# AWS Lambda
+ aws_lambda: aws-cdk-lib
+ aws_lambda: aws-sam-cli
aws_lambda: boto3
+ aws_lambda: fastapi
+ aws_lambda: requests
+ aws_lambda: uvicorn
# Beam
beam-v2.12: apache-beam~=2.12.0
@@ -491,6 +496,7 @@ deps =
# 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
@@ -554,7 +560,7 @@ deps =
pymongo-v3.7.2: pymongo==3.7.2
pymongo-v3.13.0: pymongo==3.13.0
pymongo-v4.0.2: pymongo==4.0.2
- pymongo-v4.11.1: pymongo==4.11.1
+ pymongo-v4.11.3: pymongo==4.11.3
pymongo: mockupdb
redis_py_cluster_legacy-v2.0.0: redis-py-cluster==2.0.0
@@ -563,35 +569,38 @@ deps =
sqlalchemy-v1.3.9: sqlalchemy==1.3.9
sqlalchemy-v1.4.54: sqlalchemy==1.4.54
sqlalchemy-v2.0.9: sqlalchemy==2.0.9
- sqlalchemy-v2.0.38: sqlalchemy==2.0.38
+ sqlalchemy-v2.0.39: sqlalchemy==2.0.39
# ~~~ 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
openfeature-v0.7.5: openfeature-sdk==0.7.5
openfeature-v0.8.0: openfeature-sdk==0.8.0
statsig-v0.55.3: statsig==0.55.3
statsig-v0.56.0: statsig==0.56.0
+ statsig-v0.57.1: statsig==0.57.1
statsig: typing_extensions
unleash-v6.0.1: UnleashClient==6.0.1
unleash-v6.1.0: UnleashClient==6.1.0
+ unleash-v6.2.0: UnleashClient==6.2.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.0: ariadne==0.26.0
+ ariadne-v0.26.1: ariadne==0.26.1
ariadne: fastapi
ariadne: flask
ariadne: httpx
gql-v3.4.1: gql[all]==3.4.1
- gql-v3.5.0: gql[all]==3.5.0
+ gql-v3.5.2: gql[all]==3.5.2
gql-v3.6.0b4: gql[all]==3.6.0b4
graphene-v3.3: graphene==3.3
@@ -603,9 +612,9 @@ deps =
py3.6-graphene: aiocontextvars
strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8
- strawberry-v0.226.2: strawberry-graphql[fastapi,flask]==0.226.2
- strawberry-v0.243.1: strawberry-graphql[fastapi,flask]==0.243.1
- strawberry-v0.260.2: strawberry-graphql[fastapi,flask]==0.260.2
+ strawberry-v0.227.7: strawberry-graphql[fastapi,flask]==0.227.7
+ strawberry-v0.245.0: strawberry-graphql[fastapi,flask]==0.245.0
+ strawberry-v0.262.5: strawberry-graphql[fastapi,flask]==0.262.5
strawberry: httpx
@@ -613,7 +622,7 @@ deps =
grpc-v1.32.0: grpcio==1.32.0
grpc-v1.44.0: grpcio==1.44.0
grpc-v1.58.3: grpcio==1.58.3
- grpc-v1.70.0: grpcio==1.70.0
+ grpc-v1.71.0: grpcio==1.71.0
grpc: protobuf
grpc: mypy-protobuf
grpc: types-protobuf
@@ -624,7 +633,7 @@ deps =
celery-v4.4.7: celery==4.4.7
celery-v5.0.5: celery==5.0.5
celery-v5.4.0: celery==5.4.0
- celery-v5.5.0rc4: celery==5.5.0rc4
+ celery-v5.5.0rc5: celery==5.5.0rc5
celery: newrelic
celery: redis
py3.7-celery: importlib-metadata<5.0
@@ -637,7 +646,7 @@ deps =
spark-v3.0.3: pyspark==3.0.3
spark-v3.2.4: pyspark==3.2.4
spark-v3.4.4: pyspark==3.4.4
- spark-v3.5.4: pyspark==3.5.4
+ spark-v3.5.5: pyspark==3.5.5
# ~~~ Web 1 ~~~
@@ -653,7 +662,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.45.3: starlette==0.45.3
+ starlette-v0.46.1: starlette==0.46.1
starlette: pytest-asyncio
starlette: python-multipart
starlette: requests
@@ -708,10 +717,10 @@ deps =
trytond-v5.8.16: trytond==5.8.16
trytond-v6.8.17: trytond==6.8.17
trytond-v7.0.9: trytond==7.0.9
- trytond-v7.4.6: trytond==7.4.6
+ trytond-v7.4.8: trytond==7.4.8
trytond: werkzeug
- typer-v0.15.1: typer==0.15.1
+ typer-v0.15.2: typer==0.15.2
@@ -783,8 +792,6 @@ setenv =
socket: TESTPATH=tests/integrations/socket
passenv =
- SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID
- SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY
SENTRY_PYTHON_TEST_POSTGRES_HOST
SENTRY_PYTHON_TEST_POSTGRES_USER
SENTRY_PYTHON_TEST_POSTGRES_PASSWORD