diff --git a/.tool-versions b/.tool-versions index fd76bf7..94de13f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,4 @@ aws-nuke 3.56.2 # Todo: Bring this up to date once other things are working terraform 1.0.5 +uv 0.9.13 diff --git a/aws-nuke.yaml b/aws-nuke.yaml index 0762e28..b19dcbe 100644 --- a/aws-nuke.yaml +++ b/aws-nuke.yaml @@ -31,31 +31,51 @@ resource-types: presets: nuke: filters: - IAMRole: - - "codebuild-sandbox-nuke" - - "cloudwatch-sandbox-nuke" CloudWatchEventsRule: - "Rule: sandbox-nuke" CloudWatchEventsTarget: - "Rule: sandbox-nuke Target ID: trigger_build" CloudWatchLogsLogGroup: - "/aws/codebuild/sandbox-nuke" - CodeBuildProject: - - "sandbox-nuke" + - "/aws/lambda/notify-slack-sandbox-nuke" CodeBuildBuild: - type: contains value: "sandbox-nuke" + CodeBuildProject: + - "sandbox-nuke" CodeStarConnection: - "sandbox-nuke" + CodeStarNotificationRule: + - property: "Name" + value: "sandbox-nuke" DynamoDBTable: - "sandbox-terraform-locks" DynamoDBTableItem: - "sandbox-terraform-locks -> madetech-sandbox-terraform-state/sandbox.tfstate-md5" - "sandbox-terraform-locks -> madetech-sandbox-terraform-state/bootstrap.tfstate-md5" + IAMRole: + - "cloudwatch-sandbox-nuke" + - "codebuild-sandbox-nuke" + - "notify-slack-sandbox-nuke" + - "sns-logging-sandbox-nuke" IAMRolePolicy: - "cloudwatch-sandbox-nuke -> cloudwatch-sandbox-nuke" + - "notify-slack-sandbox-nuke -> notify-slack-sandbox-nuke" + - "notify-slack-sandbox-nuke -> notify-slack-sandbox-nuke-logs" + - "sns-logging-sandbox-nuke -> sns-logging-sandbox-nuke" + LambdaFunction: + - "notify-slack-sandbox-nuke" S3Bucket: - "s3://madetech-sandbox-terraform-state" + SNSSubscription: + - type: glob + value: "Owner:*:completions-sandbox-nuke:*" + SNSTopic: + - type: glob + value: "TopicARN:*:completions-sandbox-nuke" + SSMParameter: + - "/sandbox-nuke/slack-webhook" + msp-support-interview-non-ephemeral-resources: filters: ECRRepository: diff --git a/terraform/modules/nuke_pipeline/.gitignore b/terraform/modules/nuke_pipeline/.gitignore new file mode 100644 index 0000000..aa8a797 --- /dev/null +++ b/terraform/modules/nuke_pipeline/.gitignore @@ -0,0 +1 @@ +tfbuild diff --git a/terraform/modules/nuke_pipeline/notifications.tf b/terraform/modules/nuke_pipeline/notifications.tf new file mode 100644 index 0000000..2f40429 --- /dev/null +++ b/terraform/modules/nuke_pipeline/notifications.tf @@ -0,0 +1,115 @@ +resource "aws_codestarnotifications_notification_rule" "aws_nuke" { + detail_type = "BASIC" + event_type_ids = ["codebuild-project-build-state-failed", "codebuild-project-build-state-succeeded"] + name = local.name + resource = aws_codebuild_project.aws_nuke.arn + target { + address = aws_sns_topic.completions.arn + } +} + +resource "aws_sns_topic" "completions" { + name = "completions-${local.name}" + lambda_failure_feedback_role_arn = aws_iam_role.sns_logging.arn +} + +resource "aws_sns_topic_policy" "completions" { + arn = aws_sns_topic.completions.arn + policy = data.aws_iam_policy_document.completions_topic.json +} + +data "aws_iam_policy_document" "completions_topic" { + statement { + actions = ["sns:Publish"] + principals { + type = "Service" + identifiers = ["codestar-notifications.amazonaws.com"] + } + resources = [aws_sns_topic.completions.arn] + } +} + +resource "aws_iam_role" "sns_logging" { + name = "sns-logging-${local.name}" + assume_role_policy = data.aws_iam_policy_document.sns_logging_assume_role.json +} + +data "aws_iam_policy_document" "sns_logging_assume_role" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["sns.amazonaws.com"] + } + } +} + +resource "aws_iam_role_policy" "sns_logging" { + name = "sns-logging-${local.name}" + role = aws_iam_role.sns_logging.name + policy = data.aws_iam_policy_document.sns_logging.json +} + +data "aws_iam_policy_document" "sns_logging" { + statement { + actions = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"] + resources = ["*"] + } +} + +resource "aws_sns_topic_subscription" "completions" { + topic_arn = aws_sns_topic.completions.arn + protocol = "lambda" + endpoint = module.notify_slack_lambda.lambda_function_arn +} + +module "notify_slack_lambda" { + source = "terraform-aws-modules/lambda/aws" + + function_name = "notify-slack-${local.name}" + runtime = "python3.13" + architectures = ["arm64"] + source_path = [{ + path = "${path.module}/notify_slack_lambda" + patterns = [ + "src/*.py", + "pyproject.toml", + "uv.lock", + ] + commands = [ + ":zip src .", + "rm -rf ../tfbuild/lambda/vendor", + "mkdir -p ../tfbuild/lambda/vendor", + "uv sync", + "uv export --no-default-groups > ../tfbuild/lambda/requirements.txt", + "pip3 install --target=../tfbuild/lambda/vendor -r ../tfbuild/lambda/requirements.txt", + ":zip ../tfbuild/lambda/vendor .", + ] + }] + handler = "main.lambda_handler" + environment_variables = { + SLACK_WEBHOOK_SSM_PARAMETER_NAME = data.aws_ssm_parameter.slack_webhook.name + } + publish = true + attach_policy_json = true + policy_json = data.aws_iam_policy_document.notify_slack_lambda.json + artifacts_dir = "${path.module}/tfbuild/lambda/artifacts" + allowed_triggers = { + sns = { + service = "sns" + source_arn = aws_sns_topic.completions.arn + } + } +} + +data "aws_iam_policy_document" "notify_slack_lambda" { + statement { + actions = ["ssm:GetParameter"] + resources = [data.aws_ssm_parameter.slack_webhook.arn] + } +} + +data "aws_ssm_parameter" "slack_webhook" { + name = "/${local.name}/slack-webhook" + with_decryption = false +} diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/.gitignore b/terraform/modules/nuke_pipeline/notify_slack_lambda/.gitignore new file mode 100644 index 0000000..ea3b8d7 --- /dev/null +++ b/terraform/modules/nuke_pipeline/notify_slack_lambda/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +*.so +.pytest_cache +.venv diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/.python-version b/terraform/modules/nuke_pipeline/notify_slack_lambda/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/terraform/modules/nuke_pipeline/notify_slack_lambda/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/README.md b/terraform/modules/nuke_pipeline/notify_slack_lambda/README.md new file mode 100644 index 0000000..d571951 --- /dev/null +++ b/terraform/modules/nuke_pipeline/notify_slack_lambda/README.md @@ -0,0 +1,13 @@ +# Slack Webhook Lambda + +This Lambda is triggered whenever the `sandbox-nuke` CodeBuild build run fails. If this happens, the Lambda sends a notification to the `#cop-cloud` Slack channel with a URL to the failed build run. + +## Testing with Pytest + +```sh +# Install all dependencies: +uv sync + +# Run the tests: +uv run pytest +``` diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/pyproject.toml b/terraform/modules/nuke_pipeline/notify_slack_lambda/pyproject.toml new file mode 100644 index 0000000..3bcf8ff --- /dev/null +++ b/terraform/modules/nuke_pipeline/notify_slack_lambda/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "notify-slack-lambda" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "requests>=2.32.5", +] + +[dependency-groups] +# Packages that preinstalled when running in AWS Lambda, but need to be installed when working locally: +lambda-builtin = [ + "boto3>=1.40.66", +] + +# Packages for running the tests and working on the code: +dev = [ + "pytest>=8.4.2", + "pytest-mock>=3.15.1", + "ruff>=0.14.3", +] diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/pytest.ini b/terraform/modules/nuke_pipeline/notify_slack_lambda/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/terraform/modules/nuke_pipeline/notify_slack_lambda/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/src/__init__.py b/terraform/modules/nuke_pipeline/notify_slack_lambda/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/src/functions.py b/terraform/modules/nuke_pipeline/notify_slack_lambda/src/functions.py new file mode 100644 index 0000000..f5fe71e --- /dev/null +++ b/terraform/modules/nuke_pipeline/notify_slack_lambda/src/functions.py @@ -0,0 +1,47 @@ +import json +import requests +from urllib.parse import quote + + +def get_codebuild_event(sns_event: dict) -> dict: + """ + Function for extracting the CodeBuild success/failure event + out of an SNS event. + """ + return json.loads(sns_event["Records"][0]["Sns"]["Message"]) + + +def generate_build_url(data: dict) -> str: + """ + Function for generating a URL for the CodeBuild Project run + Parameters: + data: Dict containing information about the build run + Returns: + Str containing the URL + """ + aws_account = data["account"] + aws_region = data["region"] + project_name = data["detail"]["project-name"] + codebuild_run = data["detail"]["build-id"].split("build/")[1] + codebuild_run_encoded = quote(codebuild_run, safe="") + codebuild_run_url = f"https://{aws_region}.console.aws.amazon.com/codesuite/codebuild/{aws_account}/projects/{project_name}/build/{codebuild_run_encoded}/?region={aws_region}" + return codebuild_run_url + + +def post_to_slack(url: str, payload: dict) -> dict: + """ + Function for posting payload to Slack channel + Parameters: + url: Str containing the Webhook URL + payload: Dict containing the payload + Returns: + Dict containing status message + """ + response = requests.post(url, json=payload) + response.raise_for_status() # raises HTTPError for 4xx/5xx + return { + "success": response.ok, + "status_code": response.status_code, + "message": "Message successfully delivered to Slack", + "response_body": response.text, + } diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/src/main.py b/terraform/modules/nuke_pipeline/notify_slack_lambda/src/main.py new file mode 100644 index 0000000..7961f9b --- /dev/null +++ b/terraform/modules/nuke_pipeline/notify_slack_lambda/src/main.py @@ -0,0 +1,62 @@ +import os +import logging +import boto3 + +try: + # When running under pytest: + from . import functions +except ImportError: + # When running in Lambda: + import functions + +# Initialize the logger +logger = logging.getLogger() +logger.setLevel("INFO") + +# Set SSM client +ssm = boto3.client("ssm") + + +def lambda_handler(event, context) -> dict: + """ + Main Lambda handler function + Parameters: + event: Dict containing the Lambda function event data + context: Lambda runtime context + Returns: + Dict containing status message + """ + try: + # Access environment variables + slack_webhook_ssm_parameter_name = os.environ.get( + "SLACK_WEBHOOK_SSM_PARAMETER_NAME" + ) + if not slack_webhook_ssm_parameter_name: + raise ValueError( + "Missing required environment variable SLACK_WEBHOOK_SSM_PARAMETER_NAME" + ) + + # Get SSM parameter value (raw exception will propagate if missing) + response = ssm.get_parameter( + Name=slack_webhook_ssm_parameter_name, WithDecryption=True + ) + slack_webhook = response["Parameter"]["Value"] + if not slack_webhook: + raise ValueError( + f"SSM parameter '{slack_webhook_ssm_parameter_name}' is empty" + ) + + codebuild_event = functions.get_codebuild_event(event) + + if codebuild_event["detail"]["build-status"] == "SUCCEEDED": + message = "aws-nuke succeeded." + else: + codebuild_run_url = functions.generate_build_url(codebuild_event) + message = f"aws-nuke has failed. See CodeBuild output here: {codebuild_run_url}" + + functions.post_to_slack(slack_webhook, {"text": message}) + + return {"statusCode": 200, "message": "Message successfully delivered to Slack"} + except Exception as e: + logger.error(f"Error sending message: {str(e)}") + raise diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/tests/conftest.py b/terraform/modules/nuke_pipeline/notify_slack_lambda/tests/conftest.py new file mode 100644 index 0000000..798167b --- /dev/null +++ b/terraform/modules/nuke_pipeline/notify_slack_lambda/tests/conftest.py @@ -0,0 +1,5 @@ +import os + +# Tests shouldn't require working AWS credentials, but the boto3 library +# requires a region to be defined even if no requests are going to be made. +os.environ.setdefault("AWS_DEFAULT_REGION", "dummy") diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/tests/test_functions.py b/terraform/modules/nuke_pipeline/notify_slack_lambda/tests/test_functions.py new file mode 100644 index 0000000..ffee720 --- /dev/null +++ b/terraform/modules/nuke_pipeline/notify_slack_lambda/tests/test_functions.py @@ -0,0 +1,89 @@ +import pytest +from src import functions +import requests + + +def test_get_codebuild_event(): + input = { + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "Sns": { + "Type": "Notification", + "Message": """{"account":"261219435789","region":"eu-west-2"}""", + }, + } + ], + } + expected = { + "account": "261219435789", + "region": "eu-west-2", + } + assert functions.get_codebuild_event(input) == expected + + +@pytest.mark.parametrize( + "input,expected", + [ + ( + { + "account": "test-account", + "region": "test-region", + "detail": { + "project-name": "test-project", + "build-id": "arn:aws:codebuild:test-region:test-account:build/test-project:some-id-1", + }, + }, + "https://test-region.console.aws.amazon.com/codesuite/codebuild/test-account/projects/test-project/build/test-project%3Asome-id-1/?region=test-region", + ), + ( + { + "account": "test-account", + "region": "test-region", + "detail": { + "project-name": "test-project", + "build-id": "arn:aws:codebuild:test-region:test-account:build/test-project:some-id-2", + }, + }, + "https://test-region.console.aws.amazon.com/codesuite/codebuild/test-account/projects/test-project/build/test-project%3Asome-id-2/?region=test-region", + ), + ], +) +def test_generate_build_url(input, expected): + result = functions.generate_build_url(input) + + assert result == expected + + +def test_post_message(mocker): + mock_response = mocker.Mock(ok=True) + mocker.patch("src.functions.requests.post", return_value=mock_response) + + result = functions.post_to_slack("https://example.com", {"data": "x"}) + + assert result["success"] is True + + +@pytest.fixture +def mock_requests_post(mocker): + def _mock_requests_post(side_effect): + mocker.patch("src.functions.requests.post", side_effect=side_effect) + + return _mock_requests_post + + +@pytest.mark.parametrize( + "exception, match", + [ + (requests.ConnectionError("Connection failed"), "Connection failed"), + (requests.HTTPError("404 Not Found"), "404 Not Found"), + (requests.Timeout("Request timed out"), "Request timed out"), + (requests.TooManyRedirects("Too many redirects"), "Too many redirects"), + ], +) +def test_post_to_slack_exceptions(mock_requests_post, exception, match): + mock_requests_post(exception) + + with pytest.raises(type(exception), match=match): + functions.post_to_slack("https://example.com", {"data": "x"}) diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/tests/test_main.py b/terraform/modules/nuke_pipeline/notify_slack_lambda/tests/test_main.py new file mode 100644 index 0000000..e46f7ae --- /dev/null +++ b/terraform/modules/nuke_pipeline/notify_slack_lambda/tests/test_main.py @@ -0,0 +1,92 @@ +import pytest +from src import main + +# Variables and helper functions reused across tests in this file +test_event = { + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "Sns": { + "Type": "Notification", + "Message": """{ + "account": "test-account", + "region": "test-region", + "detail": { + "project-name": "test-project", + "build-id": "arn:aws:codebuild:test-region:test-account:build/test-project:some-id-1", + }, + }""", + }, + } + ], +} + + +def setup_common_mocks(mocker, build_status): + mocker.patch.dict("os.environ", {"SLACK_WEBHOOK_SSM_PARAMETER_NAME": "fake"}) + + mocker.patch.object(main.ssm, "get_parameter") + main.ssm.get_parameter.return_value = { + "Parameter": {"Value": "https://example.com/webhook"} + } + + mocker.patch( + "src.main.functions.get_codebuild_event", + return_value={"detail": {"build-status": build_status}}, + ) + + mocker.patch( + "src.main.functions.generate_build_url", return_value="https://fake-build-url" + ) + + mocker.patch("src.main.functions.post_to_slack") + + +# Tests +def test_missing_ssm_env_variable(monkeypatch): + monkeypatch.delenv("SLACK_WEBHOOK_SSM_PARAMETER_NAME", raising=False) + with pytest.raises( + ValueError, + match="Missing required environment variable SLACK_WEBHOOK_SSM_PARAMETER_NAME", + ): + main.lambda_handler(event=None, context=None) + + +def test_ssm_parameter_not_found(mocker): + mocker.patch.object(main.ssm, "get_parameter") + error_response = {"Error": {"Code": "ParameterNotFound", "Message": "Not found"}} + main.ssm.get_parameter.side_effect = main.ssm.exceptions.ParameterNotFound( + error_response, "GetParameter" + ) + mocker.patch.dict("os.environ", {"SLACK_WEBHOOK_SSM_PARAMETER_NAME": "fake"}) + + with pytest.raises(main.ssm.exceptions.ParameterNotFound): + main.lambda_handler(event=None, context=None) + + +def test_missing_slack_webhook_parameter_value(mocker): + mocker.patch.dict("os.environ", {"SLACK_WEBHOOK_SSM_PARAMETER_NAME": "fake"}) + mocker.patch.object(main.ssm, "get_parameter") + main.ssm.get_parameter.return_value = {"Parameter": {"Value": ""}} + + with pytest.raises(ValueError, match="SSM parameter 'fake' is empty"): + main.lambda_handler(event=None, context=None) + + +def test_end_to_end_lambda_handler_for_success(mocker): + setup_common_mocks(mocker, build_status="SUCCEEDED") + main.lambda_handler(test_event, context=None) + main.functions.post_to_slack.assert_called_with( + "https://example.com/webhook", + {"text": f"aws-nuke succeeded."}, + ) + + +def test_end_to_end_lambda_handler_for_failure(mocker): + setup_common_mocks(mocker, build_status="FAILED") + main.lambda_handler(test_event, context=None) + main.functions.post_to_slack.assert_called_with( + "https://example.com/webhook", + {"text": f"aws-nuke has failed. See CodeBuild output here: https://fake-build-url"}, + ) diff --git a/terraform/modules/nuke_pipeline/notify_slack_lambda/uv.lock b/terraform/modules/nuke_pipeline/notify_slack_lambda/uv.lock new file mode 100644 index 0000000..49b41bc --- /dev/null +++ b/terraform/modules/nuke_pipeline/notify_slack_lambda/uv.lock @@ -0,0 +1,284 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "boto3" +version = "1.40.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/95/db1f23bbc5cf1a9b66cb1828a0940305ea300162ae12c55522c738ab6f0e/boto3-1.40.66.tar.gz", hash = "sha256:f2038d9bac5154da7390c29bfd013546ac96609e7ce5a7f3cb6f99412be3f4c0", size = 111564, upload-time = "2025-11-04T20:28:59.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/c2/3097e2492931b8fdcab47217b917c7964dbc8bfce31f89ace49568ed47f8/boto3-1.40.66-py3-none-any.whl", hash = "sha256:ee4fe21c5301cc0e11cc11a53e71e5ddd82d5fae42b10fa8e5403f3aa06434e3", size = 139361, upload-time = "2025-11-04T20:28:57.146Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/f3/5dae6e3b06493f2ac769c6764543b84fa50a8de3fec1e33252271166b394/botocore-1.40.66.tar.gz", hash = "sha256:e49a55ad54426c4ea853a59ff9d8243023a90c935782d4c287e9b3424883c3fa", size = 14411853, upload-time = "2025-11-04T20:28:48.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/48/43f9335e28351f35a939dce366a3943296f381ecd4660bd1c8d2bb8f3006/botocore-1.40.66-py3-none-any.whl", hash = "sha256:98d5766e17e72110b1d08ab510a8475a6597c59d9560235e2d28ae1a4b043b92", size = 14076509, upload-time = "2025-11-04T20:28:44.233Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[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, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[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, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "lambda" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-mock" }, + { name = "ruff" }, +] +lambda-builtin = [ + { name = "boto3" }, +] + +[package.metadata] +requires-dist = [{ name = "requests", specifier = ">=2.32.5" }] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "ruff", specifier = ">=0.14.3" }, +] +lambda-builtin = [{ name = "boto3", specifier = ">=1.40.66" }] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[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, upload-time = "2024-03-01T18:36:20.211Z" } +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, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687, upload-time = "2025-10-31T00:26:26.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613, upload-time = "2025-10-31T00:25:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812, upload-time = "2025-10-31T00:25:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026, upload-time = "2025-10-31T00:25:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818, upload-time = "2025-10-31T00:25:51.949Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745, upload-time = "2025-10-31T00:25:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684, upload-time = "2025-10-31T00:25:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000, upload-time = "2025-10-31T00:25:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450, upload-time = "2025-10-31T00:26:00.889Z" }, + { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414, upload-time = "2025-10-31T00:26:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293, upload-time = "2025-10-31T00:26:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444, upload-time = "2025-10-31T00:26:08.09Z" }, + { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581, upload-time = "2025-10-31T00:26:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503, upload-time = "2025-10-31T00:26:12.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457, upload-time = "2025-10-31T00:26:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980, upload-time = "2025-10-31T00:26:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045, upload-time = "2025-10-31T00:26:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005, upload-time = "2025-10-31T00:26:22.533Z" }, + { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017, upload-time = "2025-10-31T00:26:24.52Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, +] + +[[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, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +]