From 8c61faa823a3f6d5460f17b810a736f04125330d Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 6 Apr 2025 17:23:44 +0900 Subject: [PATCH 01/13] =?UTF-8?q?add:django=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EB=B3=B8=20=EC=84=B8=ED=8C=85=20=EB=B0=8F?= =?UTF-8?q?=20python-korea-payment=EC=97=90=EC=84=9C=20=EC=9D=BC=EB=B6=80?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/get_new_version.py | 86 + .github/scripts/update_ssm_parameter_store.py | 102 ++ .github/workflows/lint.yaml | 36 + .github/workflows/release.yaml | 188 ++ .python-version | 1 + Makefile | 125 ++ app/core/__init__.py | 0 app/core/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 159 bytes app/core/__pycache__/settings.cpython-313.pyc | Bin 0 -> 2483 bytes app/core/asgi.py | 16 + app/core/settings.py | 122 ++ app/core/urls.py | 22 + app/core/wsgi.py | 16 + app/manage.py | 22 + envfile/.env.docker | 1 + envfile/.env.local | 10 + infra/Dockerfile | 59 + infra/docker-compose.dev.yaml | 21 + pyproject.toml | 46 + uv.lock | 1522 +++++++++++++++++ 20 files changed, 2395 insertions(+) create mode 100644 .github/scripts/get_new_version.py create mode 100644 .github/scripts/update_ssm_parameter_store.py create mode 100644 .github/workflows/lint.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .python-version create mode 100644 Makefile create mode 100644 app/core/__init__.py create mode 100644 app/core/__pycache__/__init__.cpython-313.pyc create mode 100644 app/core/__pycache__/settings.cpython-313.pyc create mode 100644 app/core/asgi.py create mode 100644 app/core/settings.py create mode 100644 app/core/urls.py create mode 100644 app/core/wsgi.py create mode 100755 app/manage.py create mode 100644 envfile/.env.docker create mode 100644 envfile/.env.local create mode 100644 infra/Dockerfile create mode 100644 infra/docker-compose.dev.yaml create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.github/scripts/get_new_version.py b/.github/scripts/get_new_version.py new file mode 100644 index 0000000..fc53e82 --- /dev/null +++ b/.github/scripts/get_new_version.py @@ -0,0 +1,86 @@ +""" +Description: +- Print the calculated new version. + +Usage: +- python3 update_version.py --current-version (--stage) + +Version Scheme: +- ..(a) + +example: +- case 1: + - given: + - current version: 2025.1.1 + - today : YEAR = 2025, MONTH = 1 + - then: + - if stage is false: + - new version: 2025.1.2 + - if stage is true: + - new version: 2025.1.2a0 +- case 2: + - given: + - current version: 2025.1.2a0 + - today : YEAR = 2025, MONTH = 1 + - then: + - if stage is false: + - new version: 2025.1.2 + - if stage is true: + - new version: 2025.1.2a1 +- case 3: + - given: + - current version: 2025.1.1 + - today : YEAR = 2025, MONTH = 2 + - then: + - if stage is false: + - new version: 2025.2.0 + - if stage is true: + - new version: 2025.2.0a0 +""" + +import argparse +import datetime +import typing + +import packaging.version + +PreType = tuple[typing.Literal["a", "b", "rc"], int] | None + + +class ArgumentNamespace(argparse.Namespace): + current: str + stage: bool = False + + +def increment_version_count(version: packaging.version.Version, is_stage: bool) -> str: + if (current_pre := version.pre) and current_pre[0] != "a": + raise ValueError(f"Unsupported pre-release version: {current_pre[0]}") + + # Get the current date + today: datetime.date = datetime.date.today() + + # Calculate the new version + if version.major == today.year and version.minor == today.month: + if current_pre: + # If the current version is a pre-release, do not increment the count + new_count: int = version.micro + else: + # Same month, increment the count + new_count: int = version.micro + 1 + else: + # Different month, reset the count + new_count: int = 1 + current_pre = None + + new_pre: PreType = ((current_pre[0], current_pre[1] + 1) if current_pre else ("a", 0)) if is_stage else None + new_pre_str = f"{new_pre[0]}{new_pre[1]}" if new_pre else "" + return f"{today.year}.{today.month}.{new_count}{new_pre_str}" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Update version in files.") + parser.add_argument("--current", type=str, required=True) + parser.add_argument("--stage", default=False, action="store_true") + + args = parser.parse_args(namespace=ArgumentNamespace()) + print(increment_version_count(packaging.version.parse(args.current), args.stage)) diff --git a/.github/scripts/update_ssm_parameter_store.py b/.github/scripts/update_ssm_parameter_store.py new file mode 100644 index 0000000..4e78770 --- /dev/null +++ b/.github/scripts/update_ssm_parameter_store.py @@ -0,0 +1,102 @@ +import argparse +import json +import pathlib +import typing + +import boto3 + +if typing.TYPE_CHECKING: + import mypy_boto3_ssm + +ssm_client: "mypy_boto3_ssm.SSMClient" = boto3.client("ssm") + + +class ParameterDiffCollection(typing.NamedTuple): + class ValueDiff(typing.NamedTuple): + old: str | None + new: str | None + + updated: dict[str, ValueDiff] + created: dict[str, ValueDiff] + deleted: dict[str, ValueDiff] + + +def read_json_file(json_file: pathlib.Path, stage: str) -> dict[str, str]: + if not (data_groups := json.loads(json_file.read_text())) or not isinstance(data_groups, dict): + raise ValueError("JSON 파일이 잘못되었습니다.") + + if not (data := typing.cast(dict[str, typing.Any], data_groups.get(stage))): + raise ValueError("JSON 파일에 해당 스테이지의 파라미터가 없습니다.") + + if not all(isinstance(k, str) and isinstance(v, (str, int, float, bool)) for k, v in data.items()): + # object / array / null is not allowed here + raise ValueError("JSON 파일의 파라미터가 잘못되었습니다.") + + return {k: str(v) for k, v in data.items()} + + +def read_parameter_store(project_name: str, stage: str) -> dict[str, str]: + parameters: dict[str, str] = {} + next_token = "" # nosec: B105 + while next_token is not None: + result = ssm_client.get_parameters_by_path( + Path=f"/{project_name}/{stage}", + MaxResults=10, + **({"NextToken": next_token} if next_token else {}), + ) + parameters.update({p["Name"].split("/")[-1]: p["Value"] for p in result["Parameters"]}) + next_token = result.get("NextToken") + return parameters + + +def get_parameter_diff(old_parameters: dict[str, str], new_parameters: dict[str, str]) -> ParameterDiffCollection: + created, updated, deleted = {}, {}, {} + + for fields in old_parameters.keys() | new_parameters.keys(): + value = ParameterDiffCollection.ValueDiff(old=old_parameters.get(fields), new=new_parameters.get(fields)) + if value.old != value.new: + if value.old is None: + created[fields] = value + elif value.new is None: + deleted[fields] = value + else: + updated[fields] = value + + return ParameterDiffCollection(updated=updated, created=created, deleted=deleted) + + +def update_parameter_store(project_name: str, stage: str, diff: ParameterDiffCollection): + for field, values in {**diff.created, **diff.updated}.items(): + ssm_client.put_parameter( + Name=f"/{project_name}/{stage}/{field}", + Value=values.new, + Type="String", + Overwrite=True, + ) + + if diff.deleted: + ssm_client.delete_parameters(Names=[f"/{project_name}/{stage}/{field}" for field in diff.deleted.keys()]) + + +def main(project_name: str, stage: str, json_file: pathlib.Path): + if not all([json_file.is_file(), project_name, stage]): + raise ValueError("인자를 확인해주세요.") + + old_params = read_parameter_store(project_name, stage) + new_params = read_json_file(json_file, stage) + diff = get_parameter_diff(old_params, new_params) + + print(f"Updated: '{', '.join(diff.updated.keys())}'") + print(f"Created: '{', '.join(diff.created.keys())}'") + print(f"Deleted: '{', '.join(diff.deleted.keys())}'") + update_parameter_store(project_name, stage, diff) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--project_name", type=str) + parser.add_argument("--stage", type=str) + parser.add_argument("--json_file", type=pathlib.Path) + + args = parser.parse_args() + main(project_name=args.project_name, stage=args.stage, json_file=args.json_file) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..1d894ed --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,36 @@ +name: Check lint + +on: + pull_request: + push: + branches: + - 'main' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: true + +jobs: + lint: + name: Run lint + runs-on: ubuntu-latest + steps: + - name: Checkout source codes + uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: pip install 'pre-commit' + + - name: cache pre-commit repo + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + + - name: Run pre-commit + id: run-pre-commit + run: pre-commit run --all-files --show-diff-on-failure diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..1edee19 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,188 @@ +name: Release + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: true + +on: + workflow_dispatch: + inputs: + WORKFLOW_PHASE: + description: "zappa env setup" + required: true + default: dev + type: choice + options: + - dev + - prod + push: + branches: + - "main" + +jobs: + BuildAndDeploy: + runs-on: ubuntu-latest + + env: + API_STAGE: ${{ github.event_name == 'workflow_dispatch' && inputs.WORKFLOW_PHASE || 'dev' }} + BUMP_RULE: ${{ github.event_name == 'workflow_dispatch' && '' || '--stage' }} + AWS_ECR_REGISTRY: ${{ github.event_name == 'workflow_dispatch' && secrets.AWS_ECR_PROD_URL || secrets.AWS_ECR_DEV_URL }} + + steps: + # Checkout source codes + - name: Checkout source codes + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Setup AWS Credentials, Python, uv, docker buildx, and login to ECR. + - name: Setup AWS Credentials + uses: aws-actions/configure-aws-credentials@master + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ vars.AWS_REGION }} + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + ignore-nothing-to-cache: true + + - name: Install dependencies + run: uv sync --only-group=deployment + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to ECR + uses: docker/login-action@v3 + with: + registry: ${{ env.AWS_ECR_REGISTRY }} + + - name: Get current date, repo name and release tag version + id: info + run: | + LATEST_TAG=$(git tag -l --sort=-version:refname | head -n 1) + echo "::set-output name=TAG::$(python ./.github/scripts/get_new_version.py --current=$LATEST_TAG ${{ env.BUMP_RULE }})" + echo "::set-output name=date::$(date +'%Y-%m-%d_%H:%M:%S')" + echo "::set-output name=repository_name::$(echo ${{ github.repository }} | sed -e 's/${{ github.repository_owner }}\///')" + + # Build and Push Docker image to ECR + - name: Build and Push Docker image to ECR + uses: docker/build-push-action@v5 + with: + push: true + tags: ${{ env.AWS_ECR_REGISTRY }}:${{ steps.info.outputs.TAG }},${{ env.AWS_ECR_REGISTRY }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max + context: . + file: ./infra/Dockerfile + platforms: linux/amd64 + provenance: false + build-args: | + RELEASE_VERSION=${{ steps.info.outputs.TAG }} + GIT_HASH=${{ github.sha }} + IMAGE_BUILD_DATETIME=${{ steps.info.outputs.date }} + + # Create git tag + - name: Create and push git tag + run: | + git tag ${{ steps.info.outputs.TAG }} + git push origin ${{ steps.info.outputs.TAG }} + + # Checkout and import zappa config & environment variables from pyconkr-secrets repo + - name: Checkout secrets repo + uses: actions/checkout@v4 + with: + repository: ${{ secrets.PYCONKR_SECRET_REPOSITORY }} + ssh-key: ${{ secrets.PYCONKR_SECRET_REPOSITORY_DEPLOY_KEY }} + path: secret_envs + clean: false + sparse-checkout-cone-mode: false + sparse-checkout: | + ${{ steps.info.outputs.repository_name }}/zappa_settings.json + ${{ steps.info.outputs.repository_name }}/environment_variables.json + - run: mv secret_envs/${{ steps.info.outputs.repository_name }}/*.json ./ && rm -rf secret_envs + + # Apply environment variables in environment_variables.json to AWS SSM Parameter Store. + - run: | + python .github/scripts/update_ssm_parameter_store.py \ + --project_name ${{ steps.info.outputs.repository_name }} \ + --stage ${{ env.API_STAGE }} \ + --json_file environment_variables.json + + # Zappa update + - name: Zappa Update + run: uv run zappa update ${{ env.API_STAGE }} --docker-image-uri ${{ env.AWS_ECR_REGISTRY }}:${{ steps.info.outputs.TAG }} + + # Zappa certify + - name: Zappa Certify + run: uv run zappa certify ${{ env.API_STAGE }} --yes + + - name: Collect staticfiles + run: uv run zappa manage ${{ env.API_STAGE }} "collectstatic --no-input" + + # Notify to Slack (Success) + - name: Notify deployment to Slack + if: failure() || cancelled() + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: ${{ vars.SLACK_DEPLOYMENT_ALERT_CHANNEL }} + payload: | + { + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "${{ steps.info.outputs.TAG }} 버전 배포 실패 :rotating_light: (${{ job.status }})", + "emoji": true + } + }, + { + "type": "section", + "text": {"type": "mrkdwn", "text": "GitHub Action 바로가기"}, + "accessory": { + "type": "button", + "text": {"type": "plain_text", "text": "${{ github.run_id }}"}, + "value": "github_action", + "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "action_id": "button-action" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + + # Notify to Slack (Failure) + - name: Notify deployment to Slack + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: ${{ vars.SLACK_DEPLOYMENT_ALERT_CHANNEL }} + payload: | + { + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "${{ steps.info.outputs.TAG }} 버전 배포 성공 :tada:", + "emoji": true + } + }, + { + "type": "section", + "text": {"type": "mrkdwn", "text": "GitHub Action 바로가기"}, + "accessory": { + "type": "button", + "text": {"type": "plain_text", "text": "${{ github.run_id }}"}, + "value": "github_action", + "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "action_id": "button-action" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9779054 --- /dev/null +++ b/Makefile @@ -0,0 +1,125 @@ +MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) +PROJECT_DIR := $(dir $(MKFILE_PATH)) + +# Set additional build args for docker image build using make arguments +IMAGE_NAME := pycon_backend +ifeq (docker-build,$(firstword $(MAKECMDGOALS))) + TAG_NAME := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) + $(eval $(TAG_NAME):;@:) +endif +TAG_NAME := $(if $(TAG_NAME),$(TAG_NAME),local) +CONTAINER_NAME = $(IMAGE_NAME)_$(TAG_NAME)_container + +ifeq ($(DOCKER_DEBUG),true) + DOCKER_MID_BUILD_OPTIONS = --progress=plain --no-cache + DOCKER_END_BUILD_OPTIONS = 2>&1 | tee docker-build.log +else + DOCKER_MID_BUILD_OPTIONS = + DOCKER_END_BUILD_OPTIONS = +endif + +AWS_LAMBDA_READYZ_PAYLOAD = '{\ + "resource": "/readyz/",\ + "path": "/readyz/",\ + "httpMethod": "GET",\ + "requestContext": {\ + "resourcePath": "/readyz/",\ + "httpMethod": "GET",\ + "path": "/readyz/"\ + },\ + "headers": {"accept": "application/json"},\ + "multiValueHeaders": {"accept": ["application/json"]},\ + "queryStringParameters": null,\ + "multiValueQueryStringParameters": null,\ + "pathParameters": null,\ + "stageVariables": null,\ + "body": null,\ + "isBase64Encoded": false\ +}' + +# ============================================================================= +# Local development commands + +# Setup local environments +local-setup: + @uv sync + +# Run local development server +local-api: local-collectstatic + @ENV_PATH=envfile/.env.local uv run python app/manage.py runserver 8000 + +# Run django collectstatic +local-collectstatic: + @ENV_PATH=envfile/.env.local uv run python app/manage.py collectstatic --noinput + +# Run django shell +local-shell: + @ENV_PATH=envfile/.env.local uv run python app/manage.py shell + +# Run django migrations +local-migrate: + @ENV_PATH=envfile/.env.local uv run python app/manage.py migrate + +# Devtools +hooks-install: local-setup + uv run pre-commit install + +hooks-upgrade: + uv run pre-commit autoupdate + +hooks-lint: + uv run pre-commit run --all-files + +lint: hooks-lint # alias + + +# ============================================================================= +# Zappa related commands +zappa-export: + uv run zappa save-python-settings-file + +# ============================================================================= +# Docker related commands + +# Docker image build +# Usage: make docker-build +# if you want to build with debug mode, set DOCKER_DEBUG=true +# ex) make docker-build or make docker-build some_TAG_NAME DOCKER_DEBUG=true +docker-build: + @docker build \ + -f ./infra/Dockerfile -t $(IMAGE_NAME):$(TAG_NAME) \ + --build-arg GIT_HASH=$(shell git rev-parse HEAD) \ + --build-arg IMAGE_BUILD_DATETIME=$(shell date +%Y-%m-%d_%H:%M:%S) \ + $(DOCKER_MID_BUILD_OPTIONS) $(PROJECT_DIR) $(DOCKER_END_BUILD_OPTIONS) + +docker-run: docker-compose-up + @(docker stop $(CONTAINER_NAME) || true && docker rm $(CONTAINER_NAME) || true) > /dev/null 2>&1 + @docker run -d --rm \ + -p 58000:8080 \ + --env-file envfile/.env.local --env-file envfile/.env.docker \ + --name $(CONTAINER_NAME) \ + $(IMAGE_NAME):$(TAG_NAME) + +docker-readyz: + curl -X POST http://localhost:58000/2015-03-31/functions/function/invocations -d $(AWS_LAMBDA_READYZ_PAYLOAD) | jq '.body | fromjson' + +docker-test: docker-run docker-readyz + +docker-build-and-test: docker-build docker-test + +docker-stop: + docker stop $(CONTAINER_NAME) || true + +docker-rm: docker-stop + docker rm $(CONTAINER_NAME) || true + +# Docker compose setup +# Below commands are for local development only +docker-compose-up: + docker compose --env-file envfile/.env.local -f ./infra/docker-compose.dev.yaml up -d + +docker-compose-down: + docker compose --env-file envfile/.env.local -f ./infra/docker-compose.dev.yaml down + +docker-compose-rm: docker-compose-down + docker compose --env-file envfile/.env.local -f ./infra/docker-compose.dev.yaml rm diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/__pycache__/__init__.cpython-313.pyc b/app/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66a1a124d0862fb8f63c6840e6f4c7625aba2ff7 GIT binary patch literal 159 zcmey&%ge<81eZ-drGx0lAOZ#$p^VQgK*m&tbOudEzm*I{OhDdekklY&qU_>=#N^cYg39FlJpH7^KEC8+fC?x;@ literal 0 HcmV?d00001 diff --git a/app/core/__pycache__/settings.cpython-313.pyc b/app/core/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4708f57099f94f9c13256c0677782e2329195c46 GIT binary patch literal 2483 zcmb7G-EZ1f6t{u!4Iz{?X}e8ysT$EPcG4#6HnrIta8gzRgbU49RkIxXLYx?z-fNo0 zPrUDCFO&8!?7wN$hgN=yw5LgXAgT7gV?z=|BCSJkeee1BopXQpoNGQL5>W;|Zg2Ty&zhUwlgBZdf7WtmB-}{I!%e?e`&b;)mumh`FD6w3eO-sV<<%82s9{wTwQdFU1U*=3-BySpg2jQ1WLXP zqRE9A!=M!BTV%;3nnG!?nnKsSG2@NXi!2`?DRiAop&62fc-P>UL9=8U-5}RnH*>e- zdDvF8uqPWWGfS~$X-11?nC|0fo~ z&5=NVxyPAfoj_H#cI~wx2bw{#(j|C7dZjHt*zb}DPt@Fft*I_PF<&bOKP~SbF5RZe z;hZ+NcSpH1_YfP>qvc1rw()g67ql}27)~(_i)cHXN2z^nT-t?*x5k973pOn4u!E^R zGp3}NQlR;=q74eU1s*rYJuEmF#XSz13)u@pZCg`S9UsU9bKX8R>ySh4hGDXKuCtfN z!G__v5q~t0jd*Cjf&`5}ab+Z`R6 zjN1WUY^Y~#z!kiHs}I%GNqcAivn4@;D3Fm!XwGln8;7}U#S#1pn!vK}4{guu&L!yz zdzU=>;h6C`F;_U8>2d)lv?k=AiRFS5C=_eR2@BP#3qQ1@ARNwfJ|`%aH;QG^36zCRk-*K!*iXl@_UfP` zQ+i+$Rf5|p{EJ^M~;mQf7rv4f`!4BLJhUJ9U1SGo0>_m~6 zuZeZ(nfRkKQ4rS}8%{zfl`7j}L3&z2b>t+AWmE?d2%t8Q6WuHp3MFw{sEJOzR;kpb zMy-^ul-Hd|UEHjegt~~F)Hd2Ey7r}FUU19Ei4=so;D$xctk9@Gm8t?l+m%{DdM=cT zpsYakB}zhhqakdFQod3U0kF6!N-ru;&Ce{RL!R2jsjP-ANb3b)iwJ zOMqOF){A1P@CWn3eHA#XyjwYxZ+EHLwA?$Ahtk@o9a+Ir=Mb8MA2u=%)y=!Le*QY) zI)_g9F|gj%@hZ6saPI#L>VvWGXIb{d7i9zg%rfkalOPjM{Tlvd^{yW{>09QzkH%?E8b6=t(3t5$yXD312VL|BT7pf5W5zbt3sb&Am^}{{HwVHGeYU zPl3&ZXLFN@Ouk9I==*)(7xos_(Z$y2qBdAeCqD7LVP^YxnW@=c;y6CliyX%$d*Ne9 WI@pgh$r(sA%3NPLV$$ReaPcqYM=+cK literal 0 HcmV?d00001 diff --git a/app/core/asgi.py b/app/core/asgi.py new file mode 100644 index 0000000..e36e2c8 --- /dev/null +++ b/app/core/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for core project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + +application = get_asgi_application() diff --git a/app/core/settings.py b/app/core/settings.py new file mode 100644 index 0000000..9ee4ad9 --- /dev/null +++ b/app/core/settings.py @@ -0,0 +1,122 @@ +""" +Django settings for core project. + +Generated by 'django-admin startproject' using Django 5.2. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-_qi42kur2=d)+ifd0=ovcw^4hy1!scy#i#j$c$#3en_747)mn9' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'core.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'core.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/app/core/urls.py b/app/core/urls.py new file mode 100644 index 0000000..cd1cf6e --- /dev/null +++ b/app/core/urls.py @@ -0,0 +1,22 @@ +""" +URL configuration for core project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/app/core/wsgi.py b/app/core/wsgi.py new file mode 100644 index 0000000..050d8bc --- /dev/null +++ b/app/core/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for core project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + +application = get_wsgi_application() diff --git a/app/manage.py b/app/manage.py new file mode 100755 index 0000000..f2a662c --- /dev/null +++ b/app/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/envfile/.env.docker b/envfile/.env.docker new file mode 100644 index 0000000..d282037 --- /dev/null +++ b/envfile/.env.docker @@ -0,0 +1 @@ +DATABASE_HOST=host.docker.internal diff --git a/envfile/.env.local b/envfile/.env.local new file mode 100644 index 0000000..a95418b --- /dev/null +++ b/envfile/.env.local @@ -0,0 +1,10 @@ +DJANGO_DEFAULT_STORAGE_BACKEND=django.core.files.storage.FileSystemStorage +DJANGO_STATIC_STORAGE_BACKEND=django.contrib.staticfiles.storage.StaticFilesStorage +DATABASE_ENGINE=django.db.backends.postgresql +DATABASE_NAME=pyconkr-payment-db +DATABASE_HOST=127.0.0.1 +DATABASE_PORT=55432 +DATABASE_USER=user +DATABASE_PASSWORD=password +DEBUG=True +IS_LOCAL=True diff --git a/infra/Dockerfile b/infra/Dockerfile new file mode 100644 index 0000000..43b6b37 --- /dev/null +++ b/infra/Dockerfile @@ -0,0 +1,59 @@ +ARG PYTHON_VERSION=3.12 +FROM public.ecr.aws/lambda/python:${PYTHON_VERSION} +WORKDIR ${LAMBDA_TASK_ROOT} +SHELL [ "/bin/bash", "-euxvc"] + +ENV PATH="${PATH}:/root/.local/bin:" \ + TZ=Asia/Seoul \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + PYTHONIOENCODING=UTF-8 \ + PYTHONUNBUFFERED=1 \ + UV_COMPILE_BYTECODE=1 \ + UV_CONCURRENT_DOWNLOADS=32 \ + UV_LINK_MODE=copy \ + UV_PROJECT_ENVIRONMENT="/var/lang" \ + UV_PYTHON_DOWNLOADS=0 + +# Setup timezone and install gcc +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Install dependencies +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ +COPY --chown=nobody:nobody pyproject.toml uv.lock ${LAMBDA_TASK_ROOT} +RUN --mount=type=cache,target=/root/.cache/uv \ + microdnf install -y gcc \ + && microdnf clean all \ + && uv sync --no-default-groups --frozen + +RUN ZAPPA_HANDLER_PATH=$(python -c 'import zappa.handler; print(zappa.handler.__file__)') \ + && echo $ZAPPA_HANDLER_PATH \ + && cp $ZAPPA_HANDLER_PATH ${LAMBDA_TASK_ROOT} + +ARG GIT_HASH +ARG RELEASE_VERSION=unknown +ENV DEPLOYMENT_GIT_HASH=$GIT_HASH +ENV DEPLOYMENT_RELEASE_VERSION=$RELEASE_VERSION + +# Make docker to always copy app directory so that source code can be refreshed. +ARG IMAGE_BUILD_DATETIME=unknown +ENV DEPLOYMENT_IMAGE_BUILD_DATETIME=$IMAGE_BUILD_DATETIME + +# Copy main app and zappa settings +COPY --chown=nobody:nobody purchase ${LAMBDA_TASK_ROOT}/purchase +COPY --chown=nobody:nobody payment ${LAMBDA_TASK_ROOT}/payment +COPY --chown=nobody:nobody user ${LAMBDA_TASK_ROOT}/user +COPY --chown=nobody:nobody purchase_shared ${LAMBDA_TASK_ROOT}/purchase_shared +COPY --chown=nobody:nobody product ${LAMBDA_TASK_ROOT}/product +COPY --chown=nobody:nobody order ${LAMBDA_TASK_ROOT}/order +COPY --chown=nobody:nobody payment_history ${LAMBDA_TASK_ROOT}/payment_history +COPY --chown=nobody:nobody external_api ${LAMBDA_TASK_ROOT}/external_api +COPY --chown=nobody:nobody zappa_settings.py ${LAMBDA_TASK_ROOT} + +# Pydantic Logfire uses OpenTelemetry, which requires the following environment variables to be set. +# See https://opentelemetry-python.readthedocs.io/en/latest/examples/django/README.html#execution-of-the-django-app +ENV DJANGO_SETTINGS_MODULE="purchase.settings" + +# The reason for using nobody user is to avoid running the app as root, which can be a security risk. +USER nobody +CMD ["handler.lambda_handler"] diff --git a/infra/docker-compose.dev.yaml b/infra/docker-compose.dev.yaml new file mode 100644 index 0000000..cd3adb7 --- /dev/null +++ b/infra/docker-compose.dev.yaml @@ -0,0 +1,21 @@ +name: pyconkr-payment-local + +volumes: + postgres-data: + driver: local + +services: + pyconkr-payment-postgres: + image: postgres:17.2 + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data:rw + + environment: + TZ: Asia/Seoul + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + POSTGRES_DB: ${DATABASE_NAME} + + ports: + - ${DATABASE_PORT}:5432 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2ef474e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[project] +name = "backend" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "argon2-cffi>=23.1.0", + "django>=5.2", + "django-constance>=4.3.2", + "django-cors-headers>=4.7.0", + "django-environ>=0.12.0", + "django-extensions>=3.2.3", + "django-filter>=25.1", + "django-picklefield>=3.3", + "django-simple-history>=3.8.0", + "django-storages[s3]>=1.14.6", + "djangorestframework>=3.16.0", + "drf-spectacular>=0.28.0", + "drf-standardized-errors[openapi]>=0.14.1", + "httpx>=0.28.1", + "packaging>=24.2", + "psycopg[binary]>=3.2.6", + "sentry-sdk[django]>=2.25.1", + "setuptools>=78.1.0", + "zappa>=0.59.0", + "zappa-django-utils>=0.4.1", +] + +[dependency-groups] +dev = [ + "boto3-stubs[essential,s3]>=1.37.28", + "django-stubs[compatible-mypy]>=5.1.3", + "djangorestframework-stubs[compatible-mypy]>=3.15.3", + "ipython>=9.0.2", + "pre-commit>=4.2.0", +] +deployment = [ + "boto3>=1.37.28", + "django>=5.2", + "django-environ>=0.12.0", + "django-storages[s3]>=1.14.6", + "djangorestframework>=3.16.0", + "setuptools>=78.1.0", + "zappa>=0.59.0", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..89a9100 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1522 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + +[[package]] +name = "argcomplete" +version = "3.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/0f/861e168fc813c56a78b35f3c30d91c6757d1fd185af1110f1aec784b35d0/argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf", size = 73403 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708 }, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/fa/57ec2c6d16ecd2ba0cf15f3c7d1c3c2e7b5fcb83555ff56d7ab10888ec8f/argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08", size = 42798 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea", size = 15124 }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658 }, + { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583 }, + { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168 }, + { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709 }, + { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613 }, + { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583 }, + { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475 }, + { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698 }, + { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817 }, + { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104 }, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "backend" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "argon2-cffi" }, + { name = "django" }, + { name = "django-constance" }, + { name = "django-cors-headers" }, + { name = "django-environ" }, + { name = "django-extensions" }, + { name = "django-filter" }, + { name = "django-picklefield" }, + { name = "django-simple-history" }, + { name = "django-storages", extra = ["s3"] }, + { name = "djangorestframework" }, + { name = "drf-spectacular" }, + { name = "drf-standardized-errors", extra = ["openapi"] }, + { name = "httpx" }, + { name = "packaging" }, + { name = "psycopg", extra = ["binary"] }, + { name = "sentry-sdk", extra = ["django"] }, + { name = "setuptools" }, + { name = "zappa" }, + { name = "zappa-django-utils" }, +] + +[package.dev-dependencies] +deployment = [ + { name = "boto3" }, + { name = "django" }, + { name = "django-environ" }, + { name = "django-storages", extra = ["s3"] }, + { name = "djangorestframework" }, + { name = "setuptools" }, + { name = "zappa" }, +] +dev = [ + { name = "boto3-stubs", extra = ["essential", "s3"] }, + { name = "django-stubs", extra = ["compatible-mypy"] }, + { name = "djangorestframework-stubs", extra = ["compatible-mypy"] }, + { name = "ipython" }, + { name = "pre-commit" }, +] + +[package.metadata] +requires-dist = [ + { name = "argon2-cffi", specifier = ">=23.1.0" }, + { name = "django", specifier = ">=5.2" }, + { name = "django-constance", specifier = ">=4.3.2" }, + { name = "django-cors-headers", specifier = ">=4.7.0" }, + { name = "django-environ", specifier = ">=0.12.0" }, + { name = "django-extensions", specifier = ">=3.2.3" }, + { name = "django-filter", specifier = ">=25.1" }, + { name = "django-picklefield", specifier = ">=3.3" }, + { name = "django-simple-history", specifier = ">=3.8.0" }, + { name = "django-storages", extras = ["s3"], specifier = ">=1.14.6" }, + { name = "djangorestframework", specifier = ">=3.16.0" }, + { name = "drf-spectacular", specifier = ">=0.28.0" }, + { name = "drf-standardized-errors", extras = ["openapi"], specifier = ">=0.14.1" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "packaging", specifier = ">=24.2" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.2.6" }, + { name = "sentry-sdk", extras = ["django"], specifier = ">=2.25.1" }, + { name = "setuptools", specifier = ">=78.1.0" }, + { name = "zappa", specifier = ">=0.59.0" }, + { name = "zappa-django-utils", specifier = ">=0.4.1" }, +] + +[package.metadata.requires-dev] +deployment = [ + { name = "boto3", specifier = ">=1.37.28" }, + { name = "django", specifier = ">=5.2" }, + { name = "django-environ", specifier = ">=0.12.0" }, + { name = "django-storages", extras = ["s3"], specifier = ">=1.14.6" }, + { name = "djangorestframework", specifier = ">=3.16.0" }, + { name = "setuptools", specifier = ">=78.1.0" }, + { name = "zappa", specifier = ">=0.59.0" }, +] +dev = [ + { name = "boto3-stubs", extras = ["essential", "s3"], specifier = ">=1.37.28" }, + { name = "django-stubs", extras = ["compatible-mypy"], specifier = ">=5.1.3" }, + { name = "djangorestframework-stubs", extras = ["compatible-mypy"], specifier = ">=3.15.3" }, + { name = "ipython", specifier = ">=9.0.2" }, + { name = "pre-commit", specifier = ">=4.2.0" }, +] + +[[package]] +name = "boto3" +version = "1.37.28" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/f5/dd50ed0a20019fa38c22797718c80d38e8b75b5e97c971a908c638e819aa/boto3-1.37.28.tar.gz", hash = "sha256:09ee85ba70a88286bba0d1bf5f0460a4b3bde52d162216accfe637b8bfac351b", size = 111385 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/76/2723dede8c69d04e37f0897e9c05b597b6906df3d4c80186e39a0bc1b914/boto3-1.37.28-py3-none-any.whl", hash = "sha256:e584d9d33808633e73af3d962e22cf2cea91a38bc5a17577bb25618f8ded504f", size = 139562 }, +] + +[[package]] +name = "boto3-stubs" +version = "1.37.28" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore-stubs" }, + { name = "types-s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/39/2500b02987d17c7ed0a17630ac026021b9629f3de42e2598be648e47dee5/boto3_stubs-1.37.28.tar.gz", hash = "sha256:f859263ce76cb33a4c79dea545cd447588ca23a1fd09083c16f8e58605f89515", size = 99130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/5c/b398522f8f5751a2421bcbcecf2f63724728d405cb783ef03fa663845d62/boto3_stubs-1.37.28-py3-none-any.whl", hash = "sha256:a27cb833f4cfb1795acf04c6106297a552e66cd612c208618542d4abe5ce26bd", size = 68644 }, +] + +[package.optional-dependencies] +essential = [ + { name = "mypy-boto3-cloudformation" }, + { name = "mypy-boto3-dynamodb" }, + { name = "mypy-boto3-ec2" }, + { name = "mypy-boto3-lambda" }, + { name = "mypy-boto3-rds" }, + { name = "mypy-boto3-s3" }, + { name = "mypy-boto3-sqs" }, +] +s3 = [ + { name = "mypy-boto3-s3" }, +] + +[[package]] +name = "botocore" +version = "1.37.28" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/90/557082a8379ece106b37eb00766efc7a32cbfcdaa0d1d78f38f99eefd218/botocore-1.37.28.tar.gz", hash = "sha256:69ea327f70f0607d174c4c2b1dcc87327b9c48e413c9d322179172b614b28e03", size = 13799915 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/c3/29ffcb4c90492bcfcff1b7e5ddb5529846acc0e627569432db9842c47675/botocore-1.37.28-py3-none-any.whl", hash = "sha256:c26b645d7b125bf42ffc1671b862b47500ee658e3a1c95d2438cb689fc85df15", size = 13467675 }, +] + +[[package]] +name = "botocore-stubs" +version = "1.37.28" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-awscrt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/2e/fcc90b0dd4303c65c6125375b89c32afbcadbe7de71902b1a9b0c2479af5/botocore_stubs-1.37.28.tar.gz", hash = "sha256:16c4e5976cb83b863884720350e245ddde73bd39cfe2c948d5ac57c70aaec89e", size = 42236 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/98/8f7d8d9f87af25ce3c7008349f796b56717325c8a7fea005ba16021e3c02/botocore_stubs-1.37.28-py3-none-any.whl", hash = "sha256:adb5376af3f142b01f0f5dd39aa1a155ecc38fd0a0fd070db96ac1c7b8cc16e9", size = 65569 }, +] + +[[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 = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "cfn-flip" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pyyaml" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/75/8eba0bb52a6c58e347bc4c839b249d9f42380de93ed12a14eba4355387b4/cfn_flip-1.3.0.tar.gz", hash = "sha256:003e02a089c35e1230ffd0e1bcfbbc4b12cc7d2deb2fcc6c4228ac9819307362", size = 16113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/a8/a67297cd63ef99c3391c1d143161b187b71afa715a988655758269e3d02f/cfn_flip-1.3.0-py3-none-any.whl", hash = "sha256:faca8e77f0d32fb84cce1db1ef4c18b14a325d31125dae73c13bcc01947d2722", size = 21387 }, +] + +[[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 = "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 = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "django" +version = "5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/1b/c6da718c65228eb3a7ff7ba6a32d8e80fa840ca9057490504e099e4dd1ef/Django-5.2.tar.gz", hash = "sha256:1a47f7a7a3d43ce64570d350e008d2949abe8c7e21737b351b6a1611277c6d89", size = 10824891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/e0/6a5b5ea350c5bd63fe94b05e4c146c18facb51229d9dee42aa39f9fc2214/Django-5.2-py3-none-any.whl", hash = "sha256:91ceed4e3a6db5aedced65e3c8f963118ea9ba753fc620831c77074e620e7d83", size = 8301361 }, +] + +[[package]] +name = "django-constance" +version = "4.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/08/58100c61c15c0d187d257c0f01277e48fdfdbe96bda60e88ffe043412ecc/django_constance-4.3.2.tar.gz", hash = "sha256:d86e6b6a797157a4b49d0c679f1b7b9c70b1b540f36dfcda5346736997ae51bd", size = 174866 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/cd/37312ca0a02905fb3d44649a0bd2ec4ec21b60d967c3ccb9ff9d07db157b/django_constance-4.3.2-py3-none-any.whl", hash = "sha256:cd3e08f4cac457016db550a9244177da39cef8e39f4a56859692306cc8f11dc1", size = 64240 }, +] + +[[package]] +name = "django-cors-headers" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/6c/16f6cb6064c63074fd5b2bd494eb319afd846236d9c1a6c765946df2c289/django_cors_headers-4.7.0.tar.gz", hash = "sha256:6fdf31bf9c6d6448ba09ef57157db2268d515d94fc5c89a0a1028e1fc03ee52b", size = 21037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/a2/7bcfff86314bd9dd698180e31ba00604001606efb518a06cca6833a54285/django_cors_headers-4.7.0-py3-none-any.whl", hash = "sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070", size = 12794 }, +] + +[[package]] +name = "django-environ" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/04/65d2521842c42f4716225f20d8443a50804920606aec018188bbee30a6b0/django_environ-0.12.0.tar.gz", hash = "sha256:227dc891453dd5bde769c3449cf4a74b6f2ee8f7ab2361c93a07068f4179041a", size = 56804 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca", size = 19957 }, +] + +[[package]] +name = "django-extensions" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/f1/318684c9466968bf9a9c221663128206e460c1a67f595055be4b284cde8a/django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a", size = 277216 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/7e/ba12b9660642663f5273141018d2bec0a1cae1711f4f6d1093920e157946/django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401", size = 229868 }, +] + +[[package]] +name = "django-filter" +version = "25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/40/c702a6fe8cccac9bf426b55724ebdf57d10a132bae80a17691d0cf0b9bac/django_filter-25.1.tar.gz", hash = "sha256:1ec9eef48fa8da1c0ac9b411744b16c3f4c31176c867886e4c48da369c407153", size = 143021 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/a6/70dcd68537c434ba7cb9277d403c5c829caf04f35baf5eb9458be251e382/django_filter-25.1-py3-none-any.whl", hash = "sha256:4fa48677cf5857b9b1347fed23e355ea792464e0fe07244d1fdfb8a806215b80", size = 94114 }, +] + +[[package]] +name = "django-picklefield" +version = "3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/70/11411d4f528e4fad57a17dae81c85da039feba90c001b64e677fc0925a97/django-picklefield-3.3.tar.gz", hash = "sha256:4e76dd20f2e95ffdaf18d641226ccecc169ff0473b0d6bec746f3ab97c26b8cb", size = 9559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/e1/988fa6ded7275bb11e373ccd4b708af477f12027d3ee86b7cb5fc5779412/django_picklefield-3.3-py3-none-any.whl", hash = "sha256:d6f6fd94a17177fe0d16b0b452a9860b8a1da97b6e70633ab53ade4975f1ce9a", size = 9565 }, +] + +[[package]] +name = "django-simple-history" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/46/4cbf411f9a8e426ed721785beb2cfd37c47cd5462697d92ff29dc6943d38/django_simple_history-3.8.0.tar.gz", hash = "sha256:e70d70fb4cc2af60a50904f1420d5a6440d24efddceba3daeff8b02d269ebdf0", size = 233906 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/f0/f09f87199a0b4e21a10314b02e04472a8cc601ead4990e813a0b3cc43f09/django_simple_history-3.8.0-py3-none-any.whl", hash = "sha256:7f8bbdaa5b2c4c1c9a48c89a95ff3389eda6c82cf9de9b09ae99b558205d132f", size = 142593 }, +] + +[[package]] +name = "django-storages" +version = "1.14.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/d6/2e50e378fff0408d558f36c4acffc090f9a641fd6e084af9e54d45307efa/django_storages-1.14.6.tar.gz", hash = "sha256:7a25ce8f4214f69ac9c7ce87e2603887f7ae99326c316bc8d2d75375e09341c9", size = 87587 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/21/3cedee63417bc5553eed0c204be478071c9ab208e5e259e97287590194f1/django_storages-1.14.6-py3-none-any.whl", hash = "sha256:11b7b6200e1cb5ffcd9962bd3673a39c7d6a6109e8096f0e03d46fab3d3aabd9", size = 33095 }, +] + +[package.optional-dependencies] +s3 = [ + { name = "boto3" }, +] + +[[package]] +name = "django-stubs" +version = "5.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, + { name = "django-stubs-ext" }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/48/e733ceff94ed3c4ccba4c2f0708739974bbcdbcfb69efefb87b10780937f/django_stubs-5.1.3.tar.gz", hash = "sha256:8c230bc5bebee6da282ba8a27ad1503c84a0c4cd2f46e63d149e76d2a63e639a", size = 267390 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/94/3551a181faf44a63a4ef1ab8e0eb7f27f6af168c2f719ea482e54b39d237/django_stubs-5.1.3-py3-none-any.whl", hash = "sha256:716758ced158b439213062e52de6df3cff7c586f9f9ad7ab59210efbea5dfe78", size = 472753 }, +] + +[package.optional-dependencies] +compatible-mypy = [ + { name = "mypy" }, +] + +[[package]] +name = "django-stubs-ext" +version = "5.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/06/7b210e0073c6cb8824bde82afc25f268e8c410a99d3621297f44fa3f6a6c/django_stubs_ext-5.1.3.tar.gz", hash = "sha256:3e60f82337f0d40a362f349bf15539144b96e4ceb4dbd0239be1cd71f6a74ad0", size = 9613 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/52/50125afcf29382b7f9d88a992e44835108dd2f1694d6d17d6d3d6fe06c81/django_stubs_ext-5.1.3-py3-none-any.whl", hash = "sha256:64561fbc53e963cc1eed2c8eb27e18b8e48dcb90771205180fe29fc8a59e55fd", size = 9034 }, +] + +[[package]] +name = "djangorestframework" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/97/112c5a72e6917949b6d8a18ad6c6e72c46da4290c8f36ee5f1c1dcbc9901/djangorestframework-3.16.0.tar.gz", hash = "sha256:f022ff46613584de994c0c6a4aebbace5fd700555fbe9d33b865ebf173eba6c9", size = 1068408 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/3e/2448e93f4f87fc9a9f35e73e3c05669e0edd0c2526834686e949bb1fd303/djangorestframework-3.16.0-py3-none-any.whl", hash = "sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361", size = 1067305 }, +] + +[[package]] +name = "djangorestframework-stubs" +version = "3.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django-stubs" }, + { name = "requests" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/08/e86db66dbed67bd4e70dc7d059b338ad1a0cadfd15314ad09c554c31fb83/djangorestframework_stubs-3.15.3.tar.gz", hash = "sha256:e7bdec722d98b8a8049bad9f8bb5ead0931f4f64010ffb3c4538c9ae0f35ef2a", size = 34818 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/3f/4d2233e7a6eb5617f377b9cb943c2f6482a2671bdb3b134b955e78b2b946/djangorestframework_stubs-3.15.3-py3-none-any.whl", hash = "sha256:3add29ac343292ffc926a3f3984af42de237cca214c69ca0489d124315a803bc", size = 54609 }, +] + +[package.optional-dependencies] +compatible-mypy = [ + { name = "django-stubs", extra = ["compatible-mypy"] }, + { name = "mypy" }, +] + +[[package]] +name = "drf-spectacular" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "inflection" }, + { name = "jsonschema" }, + { name = "pyyaml" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/b9/741056455aed00fa51a1df41fad5ad27c8e0d433b6bf490d4e60e2808bc6/drf_spectacular-0.28.0.tar.gz", hash = "sha256:2c778a47a40ab2f5078a7c42e82baba07397bb35b074ae4680721b2805943061", size = 237849 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/66/c2929871393b1515c3767a670ff7d980a6882964a31a4ca2680b30d7212a/drf_spectacular-0.28.0-py3-none-any.whl", hash = "sha256:856e7edf1056e49a4245e87a61e8da4baff46c83dbc25be1da2df77f354c7cb4", size = 103928 }, +] + +[[package]] +name = "drf-standardized-errors" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/cc/fd5b8cbc66c361125cba0497573a5ecac94521a715267d7db4d113257a73/drf_standardized_errors-0.14.1.tar.gz", hash = "sha256:0610dcd0096b75365102d276022a22e59a1f8db8825bb0bff05e1b7194ba145d", size = 58730 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/70/589efc32d6f268576e2f3c2a595ef19a305c5d5acbfd26d10ebd45278778/drf_standardized_errors-0.14.1-py3-none-any.whl", hash = "sha256:4941e0f81be94eb0904549999cf221988a5b0f524041c3877530e24f70328ed8", size = 25512 }, +] + +[package.optional-dependencies] +openapi = [ + { name = "drf-spectacular" }, + { name = "inflection" }, +] + +[[package]] +name = "durationpy" +version = "0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/e9/f49c4e7fccb77fa5c43c2480e09a857a78b41e7331a75e128ed5df45c56b/durationpy-0.9.tar.gz", hash = "sha256:fd3feb0a69a0057d582ef643c355c40d2fa1c942191f914d12203b1a01ac722a", size = 3186 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/a3/ac312faeceffd2d8f86bc6dcb5c401188ba5a01bc88e69bed97578a0dfcd/durationpy-0.9-py3-none-any.whl", hash = "sha256:e65359a7af5cedad07fb77a2dd3f390f8eb0b74cb845589fa6c057086834dd38", size = 3461 }, +] + +[[package]] +name = "executing" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "hjson" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/e5/0b56d723a76ca67abadbf7fb71609fb0ea7e6926e94fcca6c65a85b36a0e/hjson-3.1.0.tar.gz", hash = "sha256:55af475a27cf83a7969c808399d7bccdec8fb836a07ddbd574587593b9cdcf75", size = 40541 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/7f/13cd798d180af4bf4c0ceddeefba2b864a63c71645abc0308b768d67bb81/hjson-3.1.0-py3-none-any.whl", hash = "sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89", size = 54018 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "identify" +version = "2.6.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/a71ab060daec766acc30fb47dfca219d03de34a70d616a79a38c6066c5bf/identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf", size = 99249 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/ce/0845144ed1f0e25db5e7a79c2354c1da4b5ce392b8966449d5db8dca18f1/identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150", size = 99101 }, +] + +[[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 = "inflection" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454 }, +] + +[[package]] +name = "ipython" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ce/012a0f40ca58a966f87a6e894d6828e2817657cbdf522b02a5d3a87d92ce/ipython-9.0.2.tar.gz", hash = "sha256:ec7b479e3e5656bf4f58c652c120494df1820f4f28f522fb7ca09e213c2aab52", size = 4366102 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/3a/917cb9e72f4e1a4ea13c862533205ae1319bd664119189ee5cc9e4e95ebf/ipython-9.0.2-py3-none-any.whl", hash = "sha256:143ef3ea6fb1e1bffb4c74b114051de653ffb7737a3f7ab1670e657ca6ae8c44", size = 600524 }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, +] + +[[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 = "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 = "kappa" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, + { name = "click" }, + { name = "placebo" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/fa/1b8328d2199520ef5a257f8a2e9315ed0b0194e353a152ca1959490dfbc8/kappa-0.6.0.tar.gz", hash = "sha256:4b5b372872f25d619e427e04282551048dc975a107385b076b3ffc6406a15833", size = 29680 } + +[[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 = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, +] + +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, +] + +[[package]] +name = "mypy-boto3-cloudformation" +version = "1.37.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/43/69eef04d4da88a002b388b1789b766f9f6140cde44491d62f05f579bbe3e/mypy_boto3_cloudformation-1.37.22.tar.gz", hash = "sha256:bf6bed27d1ad82d1156964adabeb0d627b555ab870d689305a160cb93e0ef255", size = 57670 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/8b/801446e0ec5174601f04a42679d6051d0f5d3986b0cfbca8a8d994769a87/mypy_boto3_cloudformation-1.37.22-py3-none-any.whl", hash = "sha256:c411740ee8d48cd497a2555fd1a525b6e521631e58a1a7f6c863f5ee6005d178", size = 69619 }, +] + +[[package]] +name = "mypy-boto3-dynamodb" +version = "1.37.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/fc/95068ae41a63c19e43e0774dcbeec70523dcf35f948ad66f3375a887c614/mypy_boto3_dynamodb-1.37.12.tar.gz", hash = "sha256:0e4d7a16fb9dba7aab7ac9ba8ff3721f9696a8484b1eed693b7949164b7805ba", size = 47470 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/2f/21716b7a6ffed0a4347b28ec06107b626f92c096c06ea5653f2bf27f8807/mypy_boto3_dynamodb-1.37.12-py3-none-any.whl", hash = "sha256:a4b2770ec1f8d6096b5e6d863800f3ff742c86a17dfa5e6b012ed7f7ccd28921", size = 56374 }, +] + +[[package]] +name = "mypy-boto3-ec2" +version = "1.37.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/97/e137d815d435587d4764403be228c014536c5fdbc4eafe475c9395e57d8a/mypy_boto3_ec2-1.37.28.tar.gz", hash = "sha256:771d2004cfdff9d4dc7cf6e4903fe907c327007a4d704e009a3b1e6b00ae9df4", size = 393729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/1e/678f7ac49ad21722541cc0769c3cdc73ea52df2cce714062091bbfa798b6/mypy_boto3_ec2-1.37.28-py3-none-any.whl", hash = "sha256:67ce870ed108140f6532a51b5964eecf495690d6fd88fe9a593964f674dc5adb", size = 383233 }, +] + +[[package]] +name = "mypy-boto3-lambda" +version = "1.37.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/09/d7dd80cabde53fe7f39a9d7f6342b59af14707bb775606b1d9ffe82efa8b/mypy_boto3_lambda-1.37.16.tar.gz", hash = "sha256:d58f20bb0416aeb04fda6cfaa8a2f2c31532ca5009004c3e2bcbe8ac50a8cf7c", size = 41727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/0b/6d6256885c2de708d21782ba4ab1d2042c98095ca381e983cb3aeeac5810/mypy_boto3_lambda-1.37.16-py3-none-any.whl", hash = "sha256:22dd22f29ef77febad9f4a2ec3d4560385b9b19a4ca1909de863206392145273", size = 48225 }, +] + +[[package]] +name = "mypy-boto3-rds" +version = "1.37.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/e7/d4ed2d847739795bc4e7e336b2a89676db18a5011eeb0d48910275f15059/mypy_boto3_rds-1.37.21.tar.gz", hash = "sha256:8fa78d077226489fef28d1b3449646882425d259f93523f7b08afd9b761dfdf8", size = 84345 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/50/b722981a6a5506513f6cd0222cf014235c642e8d3880561f2809c8262dd8/mypy_boto3_rds-1.37.21-py3-none-any.whl", hash = "sha256:af163f0e97e54b55155bfe57dc1e292edc6a1d5c2e5c9d01e62c28c952c92f2c", size = 90492 }, +] + +[[package]] +name = "mypy-boto3-s3" +version = "1.37.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/1d/8ddb398335991d7f04c3cbc64232aea673c877146f3de5bd7cbea88f1d4f/mypy_boto3_s3-1.37.24.tar.gz", hash = "sha256:4df0975256132ab452896b9d36571866a816157d519cf12c661795b2906e2e9c", size = 73709 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fb/17efbbde4fbbf027078e7577a38dc26c68e608deed8c31627328849c5825/mypy_boto3_s3-1.37.24-py3-none-any.whl", hash = "sha256:8afd8d64be352652bc888ed81750788bebabd2b6e8ee6fe9be00ff25228df39b", size = 80318 }, +] + +[[package]] +name = "mypy-boto3-sqs" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/c9/ebbd0efd7225b3eac16b409ac9a6f14250bc00d123b04c9255e7a482afa3/mypy_boto3_sqs-1.37.0.tar.gz", hash = "sha256:ed56df72494425d4e7d0e048f2321cc17514fa3bbedfc410bbb8b709c4bff564", size = 23508 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8e/45f49fadc0cdea30c5d0286f17849821dd24ee95adffecd901d92dff2588/mypy_boto3_sqs-1.37.0-py3-none-any.whl", hash = "sha256:2622aa681386cdbffdfcfa638dab0e081054ff9d6fe495758f4a817bcc8ec6ed", size = 33611 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, +] + +[[package]] +name = "pip" +version = "25.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/53/b309b4a497b09655cb7e07088966881a57d082f48ac3cb54ea729fd2c6cf/pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea", size = 1950850 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/bc/b7db44f5f39f9d0494071bddae6880eb645970366d0a200022a1a93d57f5/pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f", size = 1841526 }, +] + +[[package]] +name = "placebo" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/35/a60e29abebe274da55a62261a070c173bbb5faf1e6f3b35ac292d944d632/placebo-0.9.0.tar.gz", hash = "sha256:03157f8527bbc2965b71b88f4a139ef8038618b346787f20d63e3c5da541b047", size = 13555 } + +[[package]] +name = "platformdirs" +version = "4.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, +] + +[[package]] +name = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, +] + +[[package]] +name = "psycopg" +version = "3.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/97/eea08f74f1c6dd2a02ee81b4ebfe5b558beb468ebbd11031adbf58d31be0/psycopg-3.2.6.tar.gz", hash = "sha256:16fa094efa2698f260f2af74f3710f781e4a6f226efe9d1fd0c37f384639ed8a", size = 156322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/7d/0ba52deff71f65df8ec8038adad86ba09368c945424a9bd8145d679a2c6a/psycopg-3.2.6-py3-none-any.whl", hash = "sha256:f3ff5488525890abb0566c429146add66b329e20d6d4835662b920cbbf90ac58", size = 199077 }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/c7/220b1273f0befb2cd9fe83d379b3484ae029a88798a90bc0d36f10bea5df/psycopg_binary-3.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f27a46ff0497e882e8c0286e8833c785b4d1a80f23e1bf606f4c90e5f9f3ce75", size = 3857986 }, + { url = "https://files.pythonhosted.org/packages/8a/d8/30176532826cf87c608a6f79dd668bf9aff0cdf8eb80209eddf4c5aa7229/psycopg_binary-3.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b30ee4821ded7de48b8048b14952512588e7c5477b0a5965221e1798afba61a1", size = 3940060 }, + { url = "https://files.pythonhosted.org/packages/54/7c/fa7cd1f057f33f7ae483d6bc5a03ec6eff111f8aa5c678d9aaef92705247/psycopg_binary-3.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e57edf3b1f5427f39660225b01f8e7b97f5cfab132092f014bf1638bc85d81d2", size = 4499082 }, + { url = "https://files.pythonhosted.org/packages/b8/81/1606966f6146187c273993ea6f88f2151b26741df8f4e01349a625983be9/psycopg_binary-3.2.6-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c5172ce3e4ae7a4fd450070210f801e2ce6bc0f11d1208d29268deb0cda34de", size = 4307509 }, + { url = "https://files.pythonhosted.org/packages/69/ad/01c87aab17a4b89128b8036800d11ab296c7c2c623940cc7e6f2668f375a/psycopg_binary-3.2.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcfab3804c43571a6615e559cdc4c4115785d258a4dd71a721be033f5f5f378d", size = 4547813 }, + { url = "https://files.pythonhosted.org/packages/65/30/f93a193846ee738ffe5d2a4837e7ddeb7279707af81d088cee96cae853a0/psycopg_binary-3.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fa1c920cce16f1205f37b20c685c58b9656b170b8b4c93629100d342d0d118e", size = 4259847 }, + { url = "https://files.pythonhosted.org/packages/8e/73/65c4ae71be86675a62154407c92af4b917146f9ff3baaf0e4166c0734aeb/psycopg_binary-3.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e118d818101c1608c6b5ba52a6c977614d8f05aa89467501172ba4d10588e11", size = 3846550 }, + { url = "https://files.pythonhosted.org/packages/53/cc/a24626cac3f208c776bb22e15e9a5e483aa81145221e6427e50381f40811/psycopg_binary-3.2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:763319a8bfeca77d31512da71f5a33459b9568a7621c481c3828c62f9c38f351", size = 3320269 }, + { url = "https://files.pythonhosted.org/packages/55/e6/68c76fb9d6c53d5e4170a0c9216c7aa6c2903808f626d84d002b47a16931/psycopg_binary-3.2.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2fbc05819560389dbece046966bc88e0f2ea77673497e274c4293b8b4c1d0703", size = 3399365 }, + { url = "https://files.pythonhosted.org/packages/b4/2c/55b140f5a2c582dae42ef38502c45ef69c938274242a40bd04c143081029/psycopg_binary-3.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a57f99bb953b4bd6f32d0a9844664e7f6ca5ead9ba40e96635be3cd30794813", size = 3438908 }, + { url = "https://files.pythonhosted.org/packages/ae/f6/589c95cceccee2ab408b6b2e16f1ed6db4536fb24f2f5c9ce568cf43270c/psycopg_binary-3.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:5de6809e19a465dcb9c269675bded46a135f2d600cd99f0735afbb21ddad2af4", size = 2782886 }, + { url = "https://files.pythonhosted.org/packages/bf/32/3d06c478fd3070ac25a49c2e8ca46b6d76b0048fa9fa255b99ee32f32312/psycopg_binary-3.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54af3fbf871baa2eb19df96fd7dc0cbd88e628a692063c3d1ab5cdd00aa04322", size = 3852672 }, + { url = "https://files.pythonhosted.org/packages/34/97/e581030e279500ede3096adb510f0e6071874b97cfc047a9a87b7d71fc77/psycopg_binary-3.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad5da1e4636776c21eaeacdec42f25fa4612631a12f25cd9ab34ddf2c346ffb9", size = 3936562 }, + { url = "https://files.pythonhosted.org/packages/74/b6/6a8df4cb23c3d327403a83406c06c9140f311cb56c4e4d720ee7abf6fddc/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7956b9ea56f79cd86eddcfbfc65ae2af1e4fe7932fa400755005d903c709370", size = 4499167 }, + { url = "https://files.pythonhosted.org/packages/e4/5b/950eafef61e5e0b8ddb5afc5b6b279756411aa4bf70a346a6f091ad679bb/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e2efb763188008cf2914820dcb9fb23c10fe2be0d2c97ef0fac7cec28e281d8", size = 4311651 }, + { url = "https://files.pythonhosted.org/packages/72/b9/b366c49afc854c26b3053d4d35376046eea9aebdc48ded18ea249ea1f80c/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b3aab3451679f1e7932270e950259ed48c3b79390022d3f660491c0e65e4838", size = 4547852 }, + { url = "https://files.pythonhosted.org/packages/ab/d4/0e047360e2ea387dc7171ca017ffcee5214a0762f74b9dd982035f2e52fb/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:849a370ac4e125f55f2ad37f928e588291a67ccf91fa33d0b1e042bb3ee1f986", size = 4261725 }, + { url = "https://files.pythonhosted.org/packages/e3/ea/a1b969804250183900959ebe845d86be7fed2cbd9be58f64cd0fc24b2892/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:566d4ace928419d91f1eb3227fc9ef7b41cf0ad22e93dd2c3368d693cf144408", size = 3850073 }, + { url = "https://files.pythonhosted.org/packages/e5/71/ec2907342f0675092b76aea74365b56f38d960c4c635984dcfe25d8178c8/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f1981f13b10de2f11cfa2f99a8738b35b3f0a0f3075861446894a8d3042430c0", size = 3320323 }, + { url = "https://files.pythonhosted.org/packages/d7/d7/0d2cb4b42f231e2efe8ea1799ce917973d47486212a2c4d33cd331e7ac28/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:36f598300b55b3c983ae8df06473ad27333d2fd9f3e2cfdb913b3a5aaa3a8bcf", size = 3402335 }, + { url = "https://files.pythonhosted.org/packages/66/92/7050c372f78e53eba14695cec6c3a91b2d9ca56feaf0bfe95fe90facf730/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0f4699fa5fe1fffb0d6b2d14b31fd8c29b7ea7375f89d5989f002aaf21728b21", size = 3440442 }, + { url = "https://files.pythonhosted.org/packages/5f/4c/bebcaf754189283b2f3d457822a3d9b233d08ff50973d8f1e8d51f4d35ed/psycopg_binary-3.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:afe697b8b0071f497c5d4c0f41df9e038391534f5614f7fb3a8c1ca32d66e860", size = 2783465 }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, +] + +[[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 = "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 = "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 = "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 = "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 = "rpds-py" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/b3/52b213298a0ba7097c7ea96bee95e1947aa84cc816d48cebb539770cdf41/rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e", size = 26863 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e0/1c55f4a3be5f1ca1a4fd1f3ff1504a1478c1ed48d84de24574c4fa87e921/rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205", size = 366945 }, + { url = "https://files.pythonhosted.org/packages/39/1b/a3501574fbf29118164314dbc800d568b8c1c7b3258b505360e8abb3902c/rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7", size = 351935 }, + { url = "https://files.pythonhosted.org/packages/dc/47/77d3d71c55f6a374edde29f1aca0b2e547325ed00a9da820cabbc9497d2b/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9", size = 390817 }, + { url = "https://files.pythonhosted.org/packages/4e/ec/1e336ee27484379e19c7f9cc170f4217c608aee406d3ae3a2e45336bff36/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e", size = 401983 }, + { url = "https://files.pythonhosted.org/packages/07/f8/39b65cbc272c635eaea6d393c2ad1ccc81c39eca2db6723a0ca4b2108fce/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda", size = 451719 }, + { url = "https://files.pythonhosted.org/packages/32/05/05c2b27dd9c30432f31738afed0300659cb9415db0ff7429b05dfb09bbde/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e", size = 442546 }, + { url = "https://files.pythonhosted.org/packages/7d/e0/19383c8b5d509bd741532a47821c3e96acf4543d0832beba41b4434bcc49/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029", size = 393695 }, + { url = "https://files.pythonhosted.org/packages/9d/15/39f14e96d94981d0275715ae8ea564772237f3fa89bc3c21e24de934f2c7/rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9", size = 427218 }, + { url = "https://files.pythonhosted.org/packages/22/b9/12da7124905a680f690da7a9de6f11de770b5e359f5649972f7181c8bf51/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7", size = 568062 }, + { url = "https://files.pythonhosted.org/packages/88/17/75229017a2143d915f6f803721a6d721eca24f2659c5718a538afa276b4f/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91", size = 596262 }, + { url = "https://files.pythonhosted.org/packages/aa/64/8e8a1d8bd1b6b638d6acb6d41ab2cec7f2067a5b8b4c9175703875159a7c/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56", size = 564306 }, + { url = "https://files.pythonhosted.org/packages/68/1c/a7eac8d8ed8cb234a9b1064647824c387753343c3fab6ed7c83481ed0be7/rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30", size = 224281 }, + { url = "https://files.pythonhosted.org/packages/bb/46/b8b5424d1d21f2f2f3f2d468660085318d4f74a8df8289e3dd6ad224d488/rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034", size = 239719 }, + { url = "https://files.pythonhosted.org/packages/9d/c3/3607abc770395bc6d5a00cb66385a5479fb8cd7416ddef90393b17ef4340/rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c", size = 367072 }, + { url = "https://files.pythonhosted.org/packages/d8/35/8c7ee0fe465793e3af3298dc5a9f3013bd63e7a69df04ccfded8293a4982/rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c", size = 351919 }, + { url = "https://files.pythonhosted.org/packages/91/d3/7e1b972501eb5466b9aca46a9c31bcbbdc3ea5a076e9ab33f4438c1d069d/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240", size = 390360 }, + { url = "https://files.pythonhosted.org/packages/a2/a8/ccabb50d3c91c26ad01f9b09a6a3b03e4502ce51a33867c38446df9f896b/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8", size = 400704 }, + { url = "https://files.pythonhosted.org/packages/53/ae/5fa5bf0f3bc6ce21b5ea88fc0ecd3a439e7cb09dd5f9ffb3dbe1b6894fc5/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8", size = 450839 }, + { url = "https://files.pythonhosted.org/packages/e3/ac/c4e18b36d9938247e2b54f6a03746f3183ca20e1edd7d3654796867f5100/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b", size = 441494 }, + { url = "https://files.pythonhosted.org/packages/bf/08/b543969c12a8f44db6c0f08ced009abf8f519191ca6985509e7c44102e3c/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d", size = 393185 }, + { url = "https://files.pythonhosted.org/packages/da/7e/f6eb6a7042ce708f9dfc781832a86063cea8a125bbe451d663697b51944f/rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7", size = 426168 }, + { url = "https://files.pythonhosted.org/packages/38/b0/6cd2bb0509ac0b51af4bb138e145b7c4c902bb4b724d6fd143689d6e0383/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad", size = 567622 }, + { url = "https://files.pythonhosted.org/packages/64/b0/c401f4f077547d98e8b4c2ec6526a80e7cb04f519d416430ec1421ee9e0b/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120", size = 595435 }, + { url = "https://files.pythonhosted.org/packages/9f/ec/7993b6e803294c87b61c85bd63e11142ccfb2373cf88a61ec602abcbf9d6/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9", size = 563762 }, + { url = "https://files.pythonhosted.org/packages/1f/29/4508003204cb2f461dc2b83dd85f8aa2b915bc98fe6046b9d50d4aa05401/rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143", size = 223510 }, + { url = "https://files.pythonhosted.org/packages/f9/12/09e048d1814195e01f354155fb772fb0854bd3450b5f5a82224b3a319f0e/rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a", size = 239075 }, + { url = "https://files.pythonhosted.org/packages/d2/03/5027cde39bb2408d61e4dd0cf81f815949bb629932a6c8df1701d0257fc4/rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114", size = 362974 }, + { url = "https://files.pythonhosted.org/packages/bf/10/24d374a2131b1ffafb783e436e770e42dfdb74b69a2cd25eba8c8b29d861/rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405", size = 348730 }, + { url = "https://files.pythonhosted.org/packages/7a/d1/1ef88d0516d46cd8df12e5916966dbf716d5ec79b265eda56ba1b173398c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47", size = 387627 }, + { url = "https://files.pythonhosted.org/packages/4e/35/07339051b8b901ecefd449ebf8e5522e92bcb95e1078818cbfd9db8e573c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272", size = 394094 }, + { url = "https://files.pythonhosted.org/packages/dc/62/ee89ece19e0ba322b08734e95441952062391065c157bbd4f8802316b4f1/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd", size = 449639 }, + { url = "https://files.pythonhosted.org/packages/15/24/b30e9f9e71baa0b9dada3a4ab43d567c6b04a36d1cb531045f7a8a0a7439/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a", size = 438584 }, + { url = "https://files.pythonhosted.org/packages/28/d9/49f7b8f3b4147db13961e19d5e30077cd0854ccc08487026d2cb2142aa4a/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d", size = 391047 }, + { url = "https://files.pythonhosted.org/packages/49/b0/e66918d0972c33a259ba3cd7b7ff10ed8bd91dbcfcbec6367b21f026db75/rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7", size = 418085 }, + { url = "https://files.pythonhosted.org/packages/e1/6b/99ed7ea0a94c7ae5520a21be77a82306aac9e4e715d4435076ead07d05c6/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d", size = 564498 }, + { url = "https://files.pythonhosted.org/packages/28/26/1cacfee6b800e6fb5f91acecc2e52f17dbf8b0796a7c984b4568b6d70e38/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797", size = 590202 }, + { url = "https://files.pythonhosted.org/packages/a9/9e/57bd2f9fba04a37cef673f9a66b11ca8c43ccdd50d386c455cd4380fe461/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c", size = 561771 }, + { url = "https://files.pythonhosted.org/packages/9f/cf/b719120f375ab970d1c297dbf8de1e3c9edd26fe92c0ed7178dd94b45992/rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba", size = 221195 }, + { url = "https://files.pythonhosted.org/packages/2d/e5/22865285789f3412ad0c3d7ec4dc0a3e86483b794be8a5d9ed5a19390900/rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350", size = 237354 }, +] + +[[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 = "sentry-sdk" +version = "2.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2f/a0f732270cc7c1834f5ec45539aec87c360d5483a8bd788217a9102ccfbd/sentry_sdk-2.25.1.tar.gz", hash = "sha256:f9041b7054a7cf12d41eadabe6458ce7c6d6eea7a97cfe1b760b6692e9562cf0", size = 322190 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/b6/84049ab0967affbc7cc7590d86ae0170c1b494edb69df8786707100420e5/sentry_sdk-2.25.1-py2.py3-none-any.whl", hash = "sha256:60b016d0772789454dc55a284a6a44212044d4a16d9f8448725effee97aaf7f6", size = 339851 }, +] + +[package.optional-dependencies] +django = [ + { name = "django" }, +] + +[[package]] +name = "setuptools" +version = "78.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/0db4da3bc908df06e5efae42b44e75c81dd52716e10192ff36d0c1c8e379/setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54", size = 1367827 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 }, +] + +[[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 = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sqlparse" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, +] + +[[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 = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, +] + +[[package]] +name = "troposphere" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfn-flip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/ab/99197febf40e643ef52f7826402ac5c841656567e58a5a121855212df15f/troposphere-4.9.0.tar.gz", hash = "sha256:c0eab90c3723f70a04ece651fc648e5c9b8afbb80ce2dadb87cc22404b2fc884", size = 476041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/70/c0d9de1e5c3e2dc42d5443d5e29d065859ece93d390245da8be0daadfb81/troposphere-4.9.0-py3-none-any.whl", hash = "sha256:a5550eccb2b4fb4fd861af54d01bcea1704845b00e45376a7fe87a0aa6f0646d", size = 552342 }, +] + +[[package]] +name = "types-awscrt" +version = "0.25.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/29/29cfb4acd07571e49a1781cd89a65846f6854913dfc8e91ca21b25209ceb/types_awscrt-0.25.7.tar.gz", hash = "sha256:e11298750c99647f7f3b98d6d6d648790096cd32d445fd0d49a6041a63336c9a", size = 15576 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/5b/9358ec92f9cd7ad865be76e346170a40e877af524f3718c02fb8fe182166/types_awscrt-0.25.7-py3-none-any.whl", hash = "sha256:7bcd649aedca3c41007ca5757096d3b3bdb454b73ca66970ddae6c2c2f541c8c", size = 19577 }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250402" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329 }, +] + +[[package]] +name = "types-requests" +version = "2.32.0.20250328" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/7d/eb174f74e3f5634eaacb38031bbe467dfe2e545bc255e5c90096ec46bc46/types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32", size = 22995 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/15/3700282a9d4ea3b37044264d3e4d1b1f0095a4ebf860a99914fd544e3be3/types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2", size = 20663 }, +] + +[[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.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, +] + +[[package]] +name = "uritemplate" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/5a/4742fdba39cd02a56226815abfa72fe0aa81c33bed16ed045647d6000eba/uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", size = 273898 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c0/7461b49cd25aeece13766f02ee576d1db528f1c37ce69aee300e075b485b/uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e", size = 10356 }, +] + +[[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 = "virtualenv" +version = "20.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/e0/633e369b91bbc664df47dcb5454b6c7cf441e8f5b9d0c250ce9f0546401e/virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", size = 4346945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6", size = 4329461 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[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 }, +] + +[[package]] +name = "zappa" +version = "0.59.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "boto3" }, + { name = "durationpy" }, + { name = "hjson" }, + { name = "jmespath" }, + { name = "kappa" }, + { name = "pip" }, + { name = "placebo" }, + { name = "python-dateutil" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "toml" }, + { name = "tqdm" }, + { name = "troposphere" }, + { name = "werkzeug" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/a8/a29c409fe23bbb187c329c409fb7f96b63747f8af6b895192a95d8a9bf42/zappa-0.59.0.tar.gz", hash = "sha256:239c25eb949cadafc6dc2efe94230191d3e99c1bf437529d99bb933ff409d55e", size = 204546 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/d3/dc0e154e572c810b4354a8aab7c2d0471f8718009f81001046fb420c271d/zappa-0.59.0-py3-none-any.whl", hash = "sha256:4f5f2459ce0acb72a9aa2241ce0aa48661d651196055738769017eaad8422017", size = 118479 }, +] + +[[package]] +name = "zappa-django-utils" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/a6/5e8b411f7c5f110853947cb85f60fdde849772139a09780e8fbfccd78bde/zappa-django-utils-0.4.1.tar.gz", hash = "sha256:4803a51e65e12ac5b6cd6398be687cffb04c595032e183f9de54740c4248a4e8", size = 9519 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/82/9fd997e2e3ee6ef52cd86f3100d4385ddee3b4507a784757a2939b1e8bc6/zappa_django_utils-0.4.1-py3-none-any.whl", hash = "sha256:b2783e7b62195565218ef1d856d0f6c4f09e4ae1a51b8fef44f202e2003406c6", size = 12545 }, +] From 868b9e3000877f24612d2c79bb16f3ae6fdc92dd Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 6 Apr 2025 17:42:16 +0900 Subject: [PATCH 02/13] =?UTF-8?q?add:.gitignore=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 255 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00e4d1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,255 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +.idea + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser +/.idea/misc.xml +db.sqlite3 + +# Django collectstatic +admin +admin/* +rest_framework +rest_framework/* +django_extensions +django_extensions/* +static +static/* + +# Jupyter Notebook +.ipynb_checkpoints +.ipynb_checkpoints/* +**/*.ipynb From df3e33854887d7ca39852225c765b6a038d56db2 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 6 Apr 2025 17:43:07 +0900 Subject: [PATCH 03/13] =?UTF-8?q?fix:uv=EC=9D=98=20Python=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=88=98=EC=A0=95,=20mypy=5Fboto3=5Fssm=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20.python-version=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .python-version | 1 - pyproject.toml | 20 +++++++- uv.lock | 120 ++++++++---------------------------------------- 3 files changed, 36 insertions(+), 105 deletions(-) delete mode 100644 .python-version diff --git a/.python-version b/.python-version deleted file mode 100644 index 24ee5b1..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13 diff --git a/pyproject.toml b/pyproject.toml index 2ef474e..e85b5d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "backend" version = "0.1.0" description = "Add your description here" readme = "README.md" -requires-python = ">=3.12" +requires-python = "==3.12.*" dependencies = [ "argon2-cffi>=23.1.0", "django>=5.2", @@ -29,7 +29,7 @@ dependencies = [ [dependency-groups] dev = [ - "boto3-stubs[essential,s3]>=1.37.28", + "boto3-stubs[essential,s3,ssm]>=1.37.28", "django-stubs[compatible-mypy]>=5.1.3", "djangorestframework-stubs[compatible-mypy]>=3.15.3", "ipython>=9.0.2", @@ -44,3 +44,19 @@ deployment = [ "setuptools>=78.1.0", "zappa>=0.59.0", ] + +[tool.uv] +package = false +default-groups = ["dev", "deployment"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.isort] +profile = "black" +line_length = 120 +known_third_party = ["rest_framework"] + +[tool.django-stubs] +django_settings_module = "core.settings" diff --git a/uv.lock b/uv.lock index 89a9100..ab5c418 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -requires-python = ">=3.12" +requires-python = "==3.12.*" [[package]] name = "anyio" @@ -8,7 +8,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } wheels = [ @@ -122,7 +122,7 @@ deployment = [ { name = "zappa" }, ] dev = [ - { name = "boto3-stubs", extra = ["essential", "s3"] }, + { name = "boto3-stubs", extra = ["essential", "s3", "ssm"] }, { name = "django-stubs", extra = ["compatible-mypy"] }, { name = "djangorestframework-stubs", extra = ["compatible-mypy"] }, { name = "ipython" }, @@ -164,7 +164,7 @@ deployment = [ { name = "zappa", specifier = ">=0.59.0" }, ] dev = [ - { name = "boto3-stubs", extras = ["essential", "s3"], specifier = ">=1.37.28" }, + { name = "boto3-stubs", extras = ["essential", "s3", "ssm"], specifier = ">=1.37.28" }, { name = "django-stubs", extras = ["compatible-mypy"], specifier = ">=5.1.3" }, { name = "djangorestframework-stubs", extras = ["compatible-mypy"], specifier = ">=3.15.3" }, { name = "ipython", specifier = ">=9.0.2" }, @@ -211,6 +211,9 @@ essential = [ s3 = [ { name = "mypy-boto3-s3" }, ] +ssm = [ + { name = "mypy-boto3-ssm" }, +] [[package]] name = "botocore" @@ -267,17 +270,6 @@ wheels = [ { 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]] @@ -322,19 +314,6 @@ wheels = [ { 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 }, ] @@ -800,26 +779,6 @@ wheels = [ { 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]] @@ -850,12 +809,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, - { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, - { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, - { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, - { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, - { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, - { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, ] @@ -922,6 +875,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/8e/45f49fadc0cdea30c5d0286f17849821dd24ee95adffecd901d92dff2588/mypy_boto3_sqs-1.37.0-py3-none-any.whl", hash = "sha256:2622aa681386cdbffdfcfa638dab0e081054ff9d6fe495758f4a817bcc8ec6ed", size = 33611 }, ] +[[package]] +name = "mypy-boto3-ssm" +version = "1.37.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/f0/b2fea9e50aa51bee44086082f30044a150a1116d35a99a4330e531a93054/mypy_boto3_ssm-1.37.19.tar.gz", hash = "sha256:6e8a64812cceabb85acfa762dc0fefe065497d2274a09536ebabb1ca02cf85f9", size = 93629 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/91/0061c8d1d2fd9db4ec818f40795aaa60a249a381e4b3737a0891ea466b58/mypy_boto3_ssm-1.37.19-py3-none-any.whl", hash = "sha256:95bf31b705cf17227e34ad59a539cd569f6357d6eb483537df76be608495b602", size = 95011 }, +] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -1027,7 +989,7 @@ name = "psycopg" version = "3.2.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/97/eea08f74f1c6dd2a02ee81b4ebfe5b558beb468ebbd11031adbf58d31be0/psycopg-3.2.6.tar.gz", hash = "sha256:16fa094efa2698f260f2af74f3710f781e4a6f226efe9d1fd0c37f384639ed8a", size = 156322 } @@ -1056,17 +1018,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e6/68c76fb9d6c53d5e4170a0c9216c7aa6c2903808f626d84d002b47a16931/psycopg_binary-3.2.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2fbc05819560389dbece046966bc88e0f2ea77673497e274c4293b8b4c1d0703", size = 3399365 }, { url = "https://files.pythonhosted.org/packages/b4/2c/55b140f5a2c582dae42ef38502c45ef69c938274242a40bd04c143081029/psycopg_binary-3.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a57f99bb953b4bd6f32d0a9844664e7f6ca5ead9ba40e96635be3cd30794813", size = 3438908 }, { url = "https://files.pythonhosted.org/packages/ae/f6/589c95cceccee2ab408b6b2e16f1ed6db4536fb24f2f5c9ce568cf43270c/psycopg_binary-3.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:5de6809e19a465dcb9c269675bded46a135f2d600cd99f0735afbb21ddad2af4", size = 2782886 }, - { url = "https://files.pythonhosted.org/packages/bf/32/3d06c478fd3070ac25a49c2e8ca46b6d76b0048fa9fa255b99ee32f32312/psycopg_binary-3.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54af3fbf871baa2eb19df96fd7dc0cbd88e628a692063c3d1ab5cdd00aa04322", size = 3852672 }, - { url = "https://files.pythonhosted.org/packages/34/97/e581030e279500ede3096adb510f0e6071874b97cfc047a9a87b7d71fc77/psycopg_binary-3.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad5da1e4636776c21eaeacdec42f25fa4612631a12f25cd9ab34ddf2c346ffb9", size = 3936562 }, - { url = "https://files.pythonhosted.org/packages/74/b6/6a8df4cb23c3d327403a83406c06c9140f311cb56c4e4d720ee7abf6fddc/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7956b9ea56f79cd86eddcfbfc65ae2af1e4fe7932fa400755005d903c709370", size = 4499167 }, - { url = "https://files.pythonhosted.org/packages/e4/5b/950eafef61e5e0b8ddb5afc5b6b279756411aa4bf70a346a6f091ad679bb/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e2efb763188008cf2914820dcb9fb23c10fe2be0d2c97ef0fac7cec28e281d8", size = 4311651 }, - { url = "https://files.pythonhosted.org/packages/72/b9/b366c49afc854c26b3053d4d35376046eea9aebdc48ded18ea249ea1f80c/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b3aab3451679f1e7932270e950259ed48c3b79390022d3f660491c0e65e4838", size = 4547852 }, - { url = "https://files.pythonhosted.org/packages/ab/d4/0e047360e2ea387dc7171ca017ffcee5214a0762f74b9dd982035f2e52fb/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:849a370ac4e125f55f2ad37f928e588291a67ccf91fa33d0b1e042bb3ee1f986", size = 4261725 }, - { url = "https://files.pythonhosted.org/packages/e3/ea/a1b969804250183900959ebe845d86be7fed2cbd9be58f64cd0fc24b2892/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:566d4ace928419d91f1eb3227fc9ef7b41cf0ad22e93dd2c3368d693cf144408", size = 3850073 }, - { url = "https://files.pythonhosted.org/packages/e5/71/ec2907342f0675092b76aea74365b56f38d960c4c635984dcfe25d8178c8/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f1981f13b10de2f11cfa2f99a8738b35b3f0a0f3075861446894a8d3042430c0", size = 3320323 }, - { url = "https://files.pythonhosted.org/packages/d7/d7/0d2cb4b42f231e2efe8ea1799ce917973d47486212a2c4d33cd331e7ac28/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:36f598300b55b3c983ae8df06473ad27333d2fd9f3e2cfdb913b3a5aaa3a8bcf", size = 3402335 }, - { url = "https://files.pythonhosted.org/packages/66/92/7050c372f78e53eba14695cec6c3a91b2d9ca56feaf0bfe95fe90facf730/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0f4699fa5fe1fffb0d6b2d14b31fd8c29b7ea7375f89d5989f002aaf21728b21", size = 3440442 }, - { url = "https://files.pythonhosted.org/packages/5f/4c/bebcaf754189283b2f3d457822a3d9b233d08ff50973d8f1e8d51f4d35ed/psycopg_binary-3.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:afe697b8b0071f497c5d4c0f41df9e038391534f5614f7fb3a8c1ca32d66e860", size = 2783465 }, ] [[package]] @@ -1144,15 +1095,6 @@ wheels = [ { 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]] @@ -1162,7 +1104,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } wheels = [ @@ -1203,32 +1145,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/64/8e8a1d8bd1b6b638d6acb6d41ab2cec7f2067a5b8b4c9175703875159a7c/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56", size = 564306 }, { url = "https://files.pythonhosted.org/packages/68/1c/a7eac8d8ed8cb234a9b1064647824c387753343c3fab6ed7c83481ed0be7/rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30", size = 224281 }, { url = "https://files.pythonhosted.org/packages/bb/46/b8b5424d1d21f2f2f3f2d468660085318d4f74a8df8289e3dd6ad224d488/rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034", size = 239719 }, - { url = "https://files.pythonhosted.org/packages/9d/c3/3607abc770395bc6d5a00cb66385a5479fb8cd7416ddef90393b17ef4340/rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c", size = 367072 }, - { url = "https://files.pythonhosted.org/packages/d8/35/8c7ee0fe465793e3af3298dc5a9f3013bd63e7a69df04ccfded8293a4982/rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c", size = 351919 }, - { url = "https://files.pythonhosted.org/packages/91/d3/7e1b972501eb5466b9aca46a9c31bcbbdc3ea5a076e9ab33f4438c1d069d/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240", size = 390360 }, - { url = "https://files.pythonhosted.org/packages/a2/a8/ccabb50d3c91c26ad01f9b09a6a3b03e4502ce51a33867c38446df9f896b/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8", size = 400704 }, - { url = "https://files.pythonhosted.org/packages/53/ae/5fa5bf0f3bc6ce21b5ea88fc0ecd3a439e7cb09dd5f9ffb3dbe1b6894fc5/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8", size = 450839 }, - { url = "https://files.pythonhosted.org/packages/e3/ac/c4e18b36d9938247e2b54f6a03746f3183ca20e1edd7d3654796867f5100/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b", size = 441494 }, - { url = "https://files.pythonhosted.org/packages/bf/08/b543969c12a8f44db6c0f08ced009abf8f519191ca6985509e7c44102e3c/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d", size = 393185 }, - { url = "https://files.pythonhosted.org/packages/da/7e/f6eb6a7042ce708f9dfc781832a86063cea8a125bbe451d663697b51944f/rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7", size = 426168 }, - { url = "https://files.pythonhosted.org/packages/38/b0/6cd2bb0509ac0b51af4bb138e145b7c4c902bb4b724d6fd143689d6e0383/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad", size = 567622 }, - { url = "https://files.pythonhosted.org/packages/64/b0/c401f4f077547d98e8b4c2ec6526a80e7cb04f519d416430ec1421ee9e0b/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120", size = 595435 }, - { url = "https://files.pythonhosted.org/packages/9f/ec/7993b6e803294c87b61c85bd63e11142ccfb2373cf88a61ec602abcbf9d6/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9", size = 563762 }, - { url = "https://files.pythonhosted.org/packages/1f/29/4508003204cb2f461dc2b83dd85f8aa2b915bc98fe6046b9d50d4aa05401/rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143", size = 223510 }, - { url = "https://files.pythonhosted.org/packages/f9/12/09e048d1814195e01f354155fb772fb0854bd3450b5f5a82224b3a319f0e/rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a", size = 239075 }, - { url = "https://files.pythonhosted.org/packages/d2/03/5027cde39bb2408d61e4dd0cf81f815949bb629932a6c8df1701d0257fc4/rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114", size = 362974 }, - { url = "https://files.pythonhosted.org/packages/bf/10/24d374a2131b1ffafb783e436e770e42dfdb74b69a2cd25eba8c8b29d861/rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405", size = 348730 }, - { url = "https://files.pythonhosted.org/packages/7a/d1/1ef88d0516d46cd8df12e5916966dbf716d5ec79b265eda56ba1b173398c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47", size = 387627 }, - { url = "https://files.pythonhosted.org/packages/4e/35/07339051b8b901ecefd449ebf8e5522e92bcb95e1078818cbfd9db8e573c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272", size = 394094 }, - { url = "https://files.pythonhosted.org/packages/dc/62/ee89ece19e0ba322b08734e95441952062391065c157bbd4f8802316b4f1/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd", size = 449639 }, - { url = "https://files.pythonhosted.org/packages/15/24/b30e9f9e71baa0b9dada3a4ab43d567c6b04a36d1cb531045f7a8a0a7439/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a", size = 438584 }, - { url = "https://files.pythonhosted.org/packages/28/d9/49f7b8f3b4147db13961e19d5e30077cd0854ccc08487026d2cb2142aa4a/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d", size = 391047 }, - { url = "https://files.pythonhosted.org/packages/49/b0/e66918d0972c33a259ba3cd7b7ff10ed8bd91dbcfcbec6367b21f026db75/rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7", size = 418085 }, - { url = "https://files.pythonhosted.org/packages/e1/6b/99ed7ea0a94c7ae5520a21be77a82306aac9e4e715d4435076ead07d05c6/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d", size = 564498 }, - { url = "https://files.pythonhosted.org/packages/28/26/1cacfee6b800e6fb5f91acecc2e52f17dbf8b0796a7c984b4568b6d70e38/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797", size = 590202 }, - { url = "https://files.pythonhosted.org/packages/a9/9e/57bd2f9fba04a37cef673f9a66b11ca8c43ccdd50d386c455cd4380fe461/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c", size = 561771 }, - { url = "https://files.pythonhosted.org/packages/9f/cf/b719120f375ab970d1c297dbf8de1e3c9edd26fe92c0ed7178dd94b45992/rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba", size = 221195 }, - { url = "https://files.pythonhosted.org/packages/2d/e5/22865285789f3412ad0c3d7ec4dc0a3e86483b794be8a5d9ed5a19390900/rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350", size = 237354 }, ] [[package]] From 971227f49ad48d5f4e17a378327d5eb3eb802dfb Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 6 Apr 2025 18:17:14 +0900 Subject: [PATCH 04/13] =?UTF-8?q?fix:=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=A1=9C=EC=BB=AC=20=EC=9D=B8?= =?UTF-8?q?=ED=94=84=EB=9D=BC=20=EA=B5=AC=EC=84=B1,=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20=EB=B0=8F=20=ED=8F=AC=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 4 ++-- envfile/.env.local | 4 ++-- infra/Dockerfile | 12 ++---------- infra/docker-compose.dev.yaml | 4 ++-- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 9779054..ddf74c0 100644 --- a/Makefile +++ b/Makefile @@ -95,13 +95,13 @@ docker-build: docker-run: docker-compose-up @(docker stop $(CONTAINER_NAME) || true && docker rm $(CONTAINER_NAME) || true) > /dev/null 2>&1 @docker run -d --rm \ - -p 58000:8080 \ + -p 48000:8080 \ --env-file envfile/.env.local --env-file envfile/.env.docker \ --name $(CONTAINER_NAME) \ $(IMAGE_NAME):$(TAG_NAME) docker-readyz: - curl -X POST http://localhost:58000/2015-03-31/functions/function/invocations -d $(AWS_LAMBDA_READYZ_PAYLOAD) | jq '.body | fromjson' + curl -X POST http://localhost:48000/2015-03-31/functions/function/invocations -d $(AWS_LAMBDA_READYZ_PAYLOAD) | jq '.body | fromjson' docker-test: docker-run docker-readyz diff --git a/envfile/.env.local b/envfile/.env.local index a95418b..47ffda8 100644 --- a/envfile/.env.local +++ b/envfile/.env.local @@ -1,9 +1,9 @@ DJANGO_DEFAULT_STORAGE_BACKEND=django.core.files.storage.FileSystemStorage DJANGO_STATIC_STORAGE_BACKEND=django.contrib.staticfiles.storage.StaticFilesStorage DATABASE_ENGINE=django.db.backends.postgresql -DATABASE_NAME=pyconkr-payment-db +DATABASE_NAME=pyconkr-backend-db DATABASE_HOST=127.0.0.1 -DATABASE_PORT=55432 +DATABASE_PORT=45432 DATABASE_USER=user DATABASE_PASSWORD=password DEBUG=True diff --git a/infra/Dockerfile b/infra/Dockerfile index 43b6b37..7157562 100644 --- a/infra/Dockerfile +++ b/infra/Dockerfile @@ -40,19 +40,11 @@ ARG IMAGE_BUILD_DATETIME=unknown ENV DEPLOYMENT_IMAGE_BUILD_DATETIME=$IMAGE_BUILD_DATETIME # Copy main app and zappa settings -COPY --chown=nobody:nobody purchase ${LAMBDA_TASK_ROOT}/purchase -COPY --chown=nobody:nobody payment ${LAMBDA_TASK_ROOT}/payment -COPY --chown=nobody:nobody user ${LAMBDA_TASK_ROOT}/user -COPY --chown=nobody:nobody purchase_shared ${LAMBDA_TASK_ROOT}/purchase_shared -COPY --chown=nobody:nobody product ${LAMBDA_TASK_ROOT}/product -COPY --chown=nobody:nobody order ${LAMBDA_TASK_ROOT}/order -COPY --chown=nobody:nobody payment_history ${LAMBDA_TASK_ROOT}/payment_history -COPY --chown=nobody:nobody external_api ${LAMBDA_TASK_ROOT}/external_api -COPY --chown=nobody:nobody zappa_settings.py ${LAMBDA_TASK_ROOT} +COPY --chown=nobody:nobody app/ ${LAMBDA_TASK_ROOT}/ # Pydantic Logfire uses OpenTelemetry, which requires the following environment variables to be set. # See https://opentelemetry-python.readthedocs.io/en/latest/examples/django/README.html#execution-of-the-django-app -ENV DJANGO_SETTINGS_MODULE="purchase.settings" +ENV DJANGO_SETTINGS_MODULE="core.settings" # The reason for using nobody user is to avoid running the app as root, which can be a security risk. USER nobody diff --git a/infra/docker-compose.dev.yaml b/infra/docker-compose.dev.yaml index cd3adb7..f7e5e8d 100644 --- a/infra/docker-compose.dev.yaml +++ b/infra/docker-compose.dev.yaml @@ -1,11 +1,11 @@ -name: pyconkr-payment-local +name: pyconkr-backend-local volumes: postgres-data: driver: local services: - pyconkr-payment-postgres: + pyconkr-backend-postgres: image: postgres:17.2 restart: unless-stopped volumes: From 928c9ea26606d6a7057e033d8535c46aeeb584dc Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 6 Apr 2025 18:17:20 +0900 Subject: [PATCH 05/13] =?UTF-8?q?fix:=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + uv.lock | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e85b5d7..bbc6c29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = "==3.12.*" dependencies = [ "argon2-cffi>=23.1.0", + "awslambdaric>=3.0.2", "django>=5.2", "django-constance>=4.3.2", "django-cors-headers>=4.7.0", diff --git a/uv.lock b/uv.lock index ab5c418..e745715 100644 --- a/uv.lock +++ b/uv.lock @@ -84,12 +84,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, ] +[[package]] +name = "awslambdaric" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simplejson" }, + { name = "snapshot-restore-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/36/bb67a89c431b3229b4cd904a841fd498de6b3d2ab41b8356f41dd275d4b5/awslambdaric-3.0.2.tar.gz", hash = "sha256:d1d9f58f4b1f72179a3cde39cef1ce9bc2416978c63871472d2708e2101ffbef", size = 4502918 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/dd/9d84f4e1e8a64d91bc86f18aa124b6bb4448073937ec2716fcdf48a97047/awslambdaric-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670e3156be9e7909fd972872105c1b74adc044827b4a481af896e1f70aba5888", size = 340580 }, + { url = "https://files.pythonhosted.org/packages/99/ee/183f0af47a6bd40542d0b0add92dbbd888f83ccda8b85f65d2fc869fd29d/awslambdaric-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a819855cbd6fdc0496df94c0338c1a77529b217c375d347fb87d6ae9dbf48db", size = 343372 }, +] + [[package]] name = "backend" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "argon2-cffi" }, + { name = "awslambdaric" }, { name = "django" }, { name = "django-constance" }, { name = "django-cors-headers" }, @@ -132,6 +147,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "argon2-cffi", specifier = ">=23.1.0" }, + { name = "awslambdaric", specifier = ">=3.0.2" }, { name = "django", specifier = ">=5.2" }, { name = "django-constance", specifier = ">=4.3.2" }, { name = "django-cors-headers", specifier = ">=4.7.0" }, @@ -1186,6 +1202,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 }, ] +[[package]] +name = "simplejson" +version = "3.20.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/92/51b417685abd96b31308b61b9acce7ec50d8e1de8fbc39a7fd4962c60689/simplejson-3.20.1.tar.gz", hash = "sha256:e64139b4ec4f1f24c142ff7dcafe55a22b811a74d86d66560c8815687143037d", size = 85591 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/eb/34c16a1ac9ba265d024dc977ad84e1659d931c0a700967c3e59a98ed7514/simplejson-3.20.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f31c4a3a7ab18467ee73a27f3e59158255d1520f3aad74315edde7a940f1be23", size = 93100 }, + { url = "https://files.pythonhosted.org/packages/41/fc/2c2c007d135894971e6814e7c0806936e5bade28f8db4dd7e2a58b50debd/simplejson-3.20.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:884e6183d16b725e113b83a6fc0230152ab6627d4d36cb05c89c2c5bccfa7bc6", size = 75464 }, + { url = "https://files.pythonhosted.org/packages/0f/05/2b5ecb33b776c34bb5cace5de5d7669f9b60e3ca13c113037b2ca86edfbd/simplejson-3.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03d7a426e416fe0d3337115f04164cd9427eb4256e843a6b8751cacf70abc832", size = 75112 }, + { url = "https://files.pythonhosted.org/packages/fe/36/1f3609a2792f06cd4b71030485f78e91eb09cfd57bebf3116bf2980a8bac/simplejson-3.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:000602141d0bddfcff60ea6a6e97d5e10c9db6b17fd2d6c66199fa481b6214bb", size = 150182 }, + { url = "https://files.pythonhosted.org/packages/2f/b0/053fbda38b8b602a77a4f7829def1b4f316cd8deb5440a6d3ee90790d2a4/simplejson-3.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af8377a8af78226e82e3a4349efdde59ffa421ae88be67e18cef915e4023a595", size = 158363 }, + { url = "https://files.pythonhosted.org/packages/d1/4b/2eb84ae867539a80822e92f9be4a7200dffba609275faf99b24141839110/simplejson-3.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15c7de4c88ab2fbcb8781a3b982ef883696736134e20b1210bca43fb42ff1acf", size = 148415 }, + { url = "https://files.pythonhosted.org/packages/e0/bd/400b0bd372a5666addf2540c7358bfc3841b9ce5cdbc5cc4ad2f61627ad8/simplejson-3.20.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:455a882ff3f97d810709f7b620007d4e0aca8da71d06fc5c18ba11daf1c4df49", size = 152213 }, + { url = "https://files.pythonhosted.org/packages/50/12/143f447bf6a827ee9472693768dc1a5eb96154f8feb140a88ce6973a3cfa/simplejson-3.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fc0f523ce923e7f38eb67804bc80e0a028c76d7868500aa3f59225574b5d0453", size = 150048 }, + { url = "https://files.pythonhosted.org/packages/5e/ea/dd9b3e8e8ed710a66f24a22c16a907c9b539b6f5f45fd8586bd5c231444e/simplejson-3.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76461ec929282dde4a08061071a47281ad939d0202dc4e63cdd135844e162fbc", size = 151668 }, + { url = "https://files.pythonhosted.org/packages/99/af/ee52a8045426a0c5b89d755a5a70cc821815ef3c333b56fbcad33c4435c0/simplejson-3.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19c2da8c043607bde4d4ef3a6b633e668a7d2e3d56f40a476a74c5ea71949f", size = 158840 }, + { url = "https://files.pythonhosted.org/packages/68/db/ab32869acea6b5de7d75fa0dac07a112ded795d41eaa7e66c7813b17be95/simplejson-3.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2578bedaedf6294415197b267d4ef678fea336dd78ee2a6d2f4b028e9d07be3", size = 154212 }, + { url = "https://files.pythonhosted.org/packages/fa/7a/e3132d454977d75a3bf9a6d541d730f76462ebf42a96fea2621498166f41/simplejson-3.20.1-cp312-cp312-win32.whl", hash = "sha256:339f407373325a36b7fd744b688ba5bae0666b5d340ec6d98aebc3014bf3d8ea", size = 74101 }, + { url = "https://files.pythonhosted.org/packages/bc/5d/4e243e937fa3560107c69f6f7c2eed8589163f5ed14324e864871daa2dd9/simplejson-3.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:627d4486a1ea7edf1f66bb044ace1ce6b4c1698acd1b05353c97ba4864ea2e17", size = 75736 }, + { url = "https://files.pythonhosted.org/packages/4b/30/00f02a0a921556dd5a6db1ef2926a1bc7a8bbbfb1c49cfed68a275b8ab2b/simplejson-3.20.1-py3-none-any.whl", hash = "sha256:8a6c1bbac39fa4a79f83cbf1df6ccd8ff7069582a9fd8db1e52cea073bc2c697", size = 57121 }, +] + [[package]] name = "six" version = "1.17.0" @@ -1195,6 +1233,15 @@ 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 = "snapshot-restore-py" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/19/2d7584749a7f6d1b4b3a129995b1bc31e70ed9c5ecc323f1ee748b767268/snapshot-restore-py-1.0.0.tar.gz", hash = "sha256:4d27f82fb6f09968f422501e9c3c2dea48a46cd19dc798eb7d6cbc57523c8004", size = 3379 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/b8/f9da2457e9dfb5872042202d69b329940527672e20cbfdb26610c09e1d8e/snapshot_restore_py-1.0.0-py3-none-any.whl", hash = "sha256:38f99e696793790f54658e71c68c7a8a40cea877c81232b5052383b1301aceba", size = 3782 }, +] + [[package]] name = "sniffio" version = "1.3.1" From 383a90e4e1409aacea020c8b9b310dea760c304c Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 6 Apr 2025 18:19:37 +0900 Subject: [PATCH 06/13] =?UTF-8?q?add:python-korea-payment=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=B6=94=EA=B0=80=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/external_apis/slack/blocks.py | 77 ++++ app/core/health_check.py | 71 ++++ app/core/logger/formatter/cloudwatch.py | 70 ++++ app/core/logger/formatter/slack.py | 88 +++++ app/core/logger/handler/slack.py | 34 ++ app/core/logger/util/decorator.py | 27 ++ app/core/logger/util/django_helper.py | 67 ++++ app/core/middleware/__init__.py | 0 .../middleware/request_response_logger.py | 73 ++++ app/core/settings.py | 339 +++++++++++++++--- app/core/urls.py | 24 +- app/zappa_settings.py | 2 + 12 files changed, 825 insertions(+), 47 deletions(-) create mode 100644 app/core/external_apis/slack/blocks.py create mode 100644 app/core/health_check.py create mode 100644 app/core/logger/formatter/cloudwatch.py create mode 100644 app/core/logger/formatter/slack.py create mode 100644 app/core/logger/handler/slack.py create mode 100644 app/core/logger/util/decorator.py create mode 100644 app/core/logger/util/django_helper.py create mode 100644 app/core/middleware/__init__.py create mode 100644 app/core/middleware/request_response_logger.py create mode 100644 app/zappa_settings.py diff --git a/app/core/external_apis/slack/blocks.py b/app/core/external_apis/slack/blocks.py new file mode 100644 index 0000000..29183b5 --- /dev/null +++ b/app/core/external_apis/slack/blocks.py @@ -0,0 +1,77 @@ +import dataclasses +import typing + + +@dataclasses.dataclass +class SlackChildBlock: + text: str + block_type: typing.Literal["plain_text", "mrkdwn"] = "" + + def to_dict(self) -> dict[str, str | bool]: + return { + "type": self.block_type, + "text": self.text, + } | ({"emoji": True} if self.block_type == "plain_text" else {}) + + +@dataclasses.dataclass +class SlackPlainTextChildBlock(SlackChildBlock): + block_type: typing.Literal["plain_text"] = "plain_text" + + +@dataclasses.dataclass +class SlackMarkDownChildBlock(SlackChildBlock): + block_type: typing.Literal["mrkdwn"] = "mrkdwn" + + +@dataclasses.dataclass +class SlackCodeChildBlock(SlackMarkDownChildBlock): + title: str = "" + + def __post_init__(self) -> None: + code_title = f"*{self.title}*\n" if self.title else "" + self.text = f"{code_title}```{self.text}```" + + +@dataclasses.dataclass +class SlackParentBlock: + block_type: str + text: SlackChildBlock | None = None + fields: list[SlackChildBlock] | None = None + + def __post_init__(self): + if not (self.text or self.fields): + raise ValueError("At least one of text or fields must be set!") + + def to_dict(self) -> dict[str, str | dict[str, str | bool]]: + result = { + "type": self.block_type, + "text": self.text.to_dict() if self.text else None, + "fields": [f.to_dict() for f in self.fields] if self.fields else None, + } + return {k: v for k, v in result.items() if k and v} + + +@dataclasses.dataclass +class SlackHeaderParentBlock(SlackParentBlock): + block_type: str = "header" + + def __post_init__(self): + super().__post_init__() + if self.fields: + raise ValueError("header block only allows text, not fields!") + if self.text.block_type != "plain_text": + raise ValueError("header block only allows plain_text text block!") + + +@dataclasses.dataclass +class SlackSectionParentBlock(SlackParentBlock): + block_type: str = "section" + + +@dataclasses.dataclass +class SlackBlocks: + blocks: list[SlackParentBlock] + + def to_dict(self): + return {"blocks": [b.to_dict() for b in self.blocks]} diff --git a/app/core/health_check.py b/app/core/health_check.py new file mode 100644 index 0000000..3b3b922 --- /dev/null +++ b/app/core/health_check.py @@ -0,0 +1,71 @@ +import collections +import os +import http +import typing + +from django.conf import settings +from django.db import DEFAULT_DB_ALIAS, DatabaseError, connections +from django.db.migrations.executor import MigrationExecutor +from django.http import HttpRequest +from rest_framework.decorators import api_view, authentication_classes, permission_classes +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + + +def _check_databases() -> tuple[bool, dict[str, typing.Any]]: + results: dict[str, dict[str, typing.Any]] = {} + for alias in settings.DATABASES: + results[alias] = {"success": True, "error": None} + try: + with connections[alias].cursor() as cursor: + cursor.execute("SELECT 1") + except DatabaseError as e: + results[alias].update({"success": False, "error": str(e)}) + return all(results[key]["success"] for key in results), results + + +def _check_django_migrations() -> tuple[bool, collections.defaultdict[str, list[str]]]: + result: collections.defaultdict[str, list[str]] = collections.defaultdict(list) + + executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS]) + migration_plan = executor.migration_plan(executor.loader.graph.leaf_nodes()) + for migration_info, _ in migration_plan: + result[migration_info.app_label].append(migration_info.name) + + return bool(migration_plan), result + + +@api_view(["GET", "POST"]) +@permission_classes([AllowAny]) +@authentication_classes([]) +def readyz(request: HttpRequest) -> Response: + is_dbs_ok, db_status = _check_databases() + requires_migrations, migration_status = _check_django_migrations() + response_data = ( + { + "database": db_status, + "migrations": migration_status, + "version": settings.DEPLOYMENT_RELEASE_VERSION, + "git_sha": os.getenv("DEPLOYMENT_GIT_HASH", ""), + } + if settings.DEBUG + else {} + ) + return Response( + data=response_data, + status=http.HTTPStatus.OK if is_dbs_ok and requires_migrations else http.HTTPStatus.SERVICE_UNAVAILABLE, + ) + + +@api_view(["GET", "POST"]) +@permission_classes([AllowAny]) +@authentication_classes([]) +def livez(request: HttpRequest) -> Response: + return Response({}, status=http.HTTPStatus.OK) + + +@api_view(["GET", "POST", "PUT", "PATCH", "DELETE"]) +@permission_classes([AllowAny]) +@authentication_classes([]) +def raise_exception(request: HttpRequest) -> typing.NoReturn: + raise Exception("This is a test exception") diff --git a/app/core/logger/formatter/cloudwatch.py b/app/core/logger/formatter/cloudwatch.py new file mode 100644 index 0000000..a0525de --- /dev/null +++ b/app/core/logger/formatter/cloudwatch.py @@ -0,0 +1,70 @@ +import logging +import traceback +import types +import typing + +from core.logger.util.django_helper import default_json_dumps + +ExcInfoType: typing.TypeAlias = tuple[type[BaseException] | None, BaseException | None, types.TracebackType | None] + +DEFAULT_CLOUDWATCH_LOG_FORMAT = "[%(levelname)s]\t%(asctime)s.%(msecs)dZ\t%(levelno)s\t%(message)s\n" +DEFAULT_CLOUDWATCH_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" + + +class CloudWatchJsonFormatter(logging.Formatter): + """ + CloudWatchJsonFormatter formats log records as JSON strings. + example: + >> logger.info("This is a log message", extra={"data": {"key": "value"}}) + { + "level_name": "INFO", + "time": "2021-08-31T16:00:00.123456Z", + "aws_request_id": "00000000-0000-0000-0000-000000000000", + "message": "This is a log message", + "module": "module_name", + "func_name": "function_name", + "extra_data": {"key": "value"}, + "exc_info": { + "type": "Exception", + "message": "Exception message", + "traceback_msg": "Traceback(most recent call last)..." + } + } + """ + + def __init__( + self, + fmt: str = DEFAULT_CLOUDWATCH_LOG_FORMAT, + datefmt: str = DEFAULT_CLOUDWATCH_DATE_FORMAT, + **kwargs, + ): + super().__init__(fmt=fmt, datefmt=datefmt, **kwargs) + + def formatException(self, exc_info: ExcInfoType) -> dict: + exc_type, exc_value, _ = exc_info + return { + "type": exc_type.__name__, + "message": str(exc_value), + "traceback_msg": "\n".join(traceback.format_exception(exc_value)), + } + + def format(self, record): + record.message = record.getMessage() + if self.usesTime(): + record.asctime = self.formatTime(record, self.datefmt) + if record.exc_info: + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + + return default_json_dumps( + { + "level_name": record.levelname, + "time": "%(asctime)s.%(msecs)dZ" % dict(asctime=record.asctime, msecs=record.msecs), + "aws_request_id": getattr(record, "aws_request_id", "00000000-0000-0000-0000-000000000000"), + "message": record.message, + "module": record.module, + "func_name": record.funcName, + "extra_data": record.__dict__.get("data", {}), + "exc_info": record.exc_text, + }, + ) diff --git a/app/core/logger/formatter/slack.py b/app/core/logger/formatter/slack.py new file mode 100644 index 0000000..e7052ff --- /dev/null +++ b/app/core/logger/formatter/slack.py @@ -0,0 +1,88 @@ +import logging +import traceback +import types +import typing + +from core.external_apis.slack import blocks +from core.logger.util.django_helper import default_json_dumps + +ExcInfoType: typing.TypeAlias = tuple[type[BaseException] | None, BaseException | None, types.TracebackType | None] + +DEFAULT_SLACK_LOG_FORMAT = "[%(levelname)s]\t%(asctime)s.%(msecs)dZ\t%(levelno)s\t%(message)s\n" +DEFAULT_SLACK_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" + + +class SlackJsonFormatter(logging.Formatter): + """ + SlackJsonFormatter formats log records as JSON strings for Slack BlockKit. + example: + >>> logger.info("This is a log message", extra={"data": {"key": "value"}}) + { + "blocks": [ + {"type": "header", "text": {"type": "plain_text", "text": ":pencil: 로그[INFO]", "emoji": true}}, + {"type": "section", "text": {"type": "plain_text", "text": "This is a log message", "emoji": true}}, + { + "type": "section", + "fields": [ + {"type": "mrkdwn", "text": "*Timestamp*\n```2024-07-20T21:24:20.296Z```"}, + {"type": "mrkdwn", "text": "*AWS Request ID*\n```00000000-0000-0000-0000-000000000000```"} + ] + }, + {"type": "section", "text": {"type": "mrkdwn", "text": "*key1*\n```\"value1\"```"}}, + {"type": "section", "text": {"type": "mrkdwn", "text": "*key2*\n```\"value2\"```"}}, + {"type": "section", "text": {"type": "mrkdwn", "text": "*Traceback*\n```...```"}} + ] + } + """ + + def __init__(self, fmt: str = DEFAULT_SLACK_LOG_FORMAT, datefmt: str = DEFAULT_SLACK_DATE_FORMAT, **kwargs): + super().__init__(fmt=fmt, datefmt=datefmt, **kwargs) + + def formatException(self, exc_info: ExcInfoType) -> blocks.SlackCodeChildBlock: + exc_type, exc_value, _ = exc_info + return blocks.SlackSectionParentBlock( + text=blocks.SlackCodeChildBlock( + title=f"Traceback<{exc_type.__name__}>", + text="\n".join(traceback.format_exception(exc_value)), + ) + ) + + def format(self, record): + record.message = record.getMessage() + if self.usesTime(): + record.asctime = self.formatTime(record, self.datefmt) + + aws_request_id = getattr(record, "aws_request_id", "00000000-0000-0000-0000-000000000000") + header_text = ( + f":pencil: 로그 [{record.levelname}]" + if record.levelname != "ERROR" + else f":rotating_light: 에러 발생! [{record.levelname}]" + ) + time_text = "%(asctime)s.%(msecs)dZ" % dict(asctime=record.asctime, msecs=record.msecs) + + slack_block = blocks.SlackBlocks( + blocks=[ + blocks.SlackHeaderParentBlock(text=blocks.SlackPlainTextChildBlock(text=header_text)), + blocks.SlackSectionParentBlock(text=blocks.SlackPlainTextChildBlock(text=record.message)), + blocks.SlackSectionParentBlock( + fields=[ + blocks.SlackCodeChildBlock(title="Timestamp", text=time_text), + blocks.SlackCodeChildBlock(title="AWS Request ID", text=aws_request_id), + ], + ), + ] + ) + if extra_data := record.__dict__.get("data"): + for key, value in extra_data.items(): + slack_block.blocks.append( + blocks.SlackSectionParentBlock( + text=blocks.SlackCodeChildBlock( + title=key, + text=value if isinstance(value, (str, int, float, bool)) else default_json_dumps(value), + ) + ) + ) + if record.exc_info: + slack_block.blocks.append(self.formatException(record.exc_info)) + + return slack_block.to_dict()["blocks"] diff --git a/app/core/logger/handler/slack.py b/app/core/logger/handler/slack.py new file mode 100644 index 0000000..45b627d --- /dev/null +++ b/app/core/logger/handler/slack.py @@ -0,0 +1,34 @@ +import json +import logging +import logging.handlers + +from django.conf import settings + + +class SlackHandler(logging.handlers.HTTPHandler): + def __init__(self): + super().__init__(host="slack.com", url="/api/chat.postMessage", method="POST", secure=True) + + def mapLogRecord(self, record): + return { + "channel": settings.SLACK.channel, + "text": "서버 알림", + "blocks": self.formatter.format(record), + } + + def emit(self, record): + """From the logging.handlers.HTTPHandler.emit, but with some modifications to send a message to Slack.""" + try: + connection = self.getConnection(self.host, self.secure) + connection.request( + method=self.method, + url=self.url, + body=json.dumps(self.mapLogRecord(record)).encode("utf-8"), + headers={ + "Content-type": "application/json", + "Authorization": f"Bearer {settings.SLACK.token}", + }, + ) + connection.getresponse() + except Exception: + self.handleError(record) diff --git a/app/core/logger/util/decorator.py b/app/core/logger/util/decorator.py new file mode 100644 index 0000000..418602a --- /dev/null +++ b/app/core/logger/util/decorator.py @@ -0,0 +1,27 @@ +import collections.abc +import logging +import typing + +from django.http.request import HttpRequest +from django.http.response import HttpResponseBase +from rest_framework.request import Request + +logger = logging.getLogger(__name__) +slack_logger = logging.getLogger("slack_logger") + +ViewFuncType = collections.abc.Callable[[HttpRequest, typing.Any, typing.Any], HttpResponseBase] + + +def bad_response_slack_logger(tag: str): + def wrapper(view_func: ViewFuncType) -> ViewFuncType: + def inner_wrapper(*args, **kwargs): + try: + request: Request | HttpRequest = args[1] + request.META["bad_response_slack_logger_tag"] = tag + except Exception: + logger.warning("bad_response_slack_logger: logging disabled as args length is less than 2") + return view_func(*args, **kwargs) + + return inner_wrapper + + return wrapper diff --git a/app/core/logger/util/django_helper.py b/app/core/logger/util/django_helper.py new file mode 100644 index 0000000..aee1941 --- /dev/null +++ b/app/core/logger/util/django_helper.py @@ -0,0 +1,67 @@ +import contextlib +import json +import types + +from django.http.request import HttpRequest, RawPostDataException +from django.http.response import HttpResponseBase +from rest_framework.request import Request + +PLACEHOLDER_AWS_REQUEST_ID = "00000000-0000-0000-0000-000000000000" +PLACEHOLDER_AWS_LAMBDA_CONTEXT = types.SimpleNamespace(AWS_REQUEST_ID=PLACEHOLDER_AWS_REQUEST_ID) + + +def default_json_dumps(obj: object, **kwargs) -> str: + return json.dumps( + obj=obj, + skipkeys=True, + ensure_ascii=False, + default=lambda o: o.__dict__ if hasattr(o, "__dict__") else str(o), + **kwargs, + ) + + +def get_request_log_data(request: HttpRequest | Request) -> dict[str, str | dict]: + # Request에 headers와 body의 크기를 합쳐서 100kib가 넘는 경우, 로깅을 하지 않도록 수정해야 합니다. + try: + body = request.data if isinstance(request, Request) else request.body.decode("utf-8", "ignore") + except RawPostDataException: + body = "Request body is not readable." + + request_data = { + "method": request.method, + "path": request.path, + "user": request.user.username if request.user.is_authenticated else None, + "query_params": request.GET, + "headers": dict(request.headers), + "body": body, + } + with contextlib.suppress(json.JSONDecodeError): + if json.dumps(request_data).encode("utf-8").__sizeof__() > 102400: + request_data["query_params"] = "Query params are filtered out due to large size." + request_data["headers"] = "Request headers are filtered out due to large size." + request_data["body"] = "Request body is filtered out due to large size." + + return request_data + + +def get_response_log_data(response: HttpResponseBase) -> dict[str, str | dict]: + response_body = getattr(response, "content", getattr(response, "streaming_content", "Couldn't get response body")) + with contextlib.suppress(json.JSONDecodeError, TypeError): + response_body = response_body and json.loads(response_body) + + with contextlib.suppress(json.JSONDecodeError, TypeError): + if json.dumps(response_body).encode("utf-8").__sizeof__() > 102400: + response_body = "Response body is filtered out due to large size." + + return { + "status_code": response.status_code, + "headers": dict(response.headers), + "body": response_body, + } + + +def get_aws_request_id_from_request(request: HttpRequest) -> str: + lambda_context = request.META.get("lambda.context", PLACEHOLDER_AWS_LAMBDA_CONTEXT) + if aws_request_id := getattr(lambda_context, "AWS_REQUEST_ID", None): + return aws_request_id + return getattr(lambda_context, "aws_request_id", PLACEHOLDER_AWS_REQUEST_ID) diff --git a/app/core/middleware/__init__.py b/app/core/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/middleware/request_response_logger.py b/app/core/middleware/request_response_logger.py new file mode 100644 index 0000000..299db82 --- /dev/null +++ b/app/core/middleware/request_response_logger.py @@ -0,0 +1,73 @@ +import http.client +import logging +import typing + +from constance import config +from django.http.request import HttpRequest +from django.http.response import HttpResponseBase +from django.utils.deprecation import MiddlewareMixin + +from core.logger.util.django_helper import ( + get_aws_request_id_from_request, + get_request_log_data, + get_response_log_data, +) + +cloudwatch_logger = logging.getLogger("cloudwatch_logger") +slack_logger = logging.getLogger("slack_logger") + + +# From django-stubs +class _GetResponseCallable(typing.Protocol): + def __call__(self, request: HttpRequest, /) -> HttpResponseBase: ... + + +# From django-stubs +class _AsyncGetResponseCallable(typing.Protocol): + def __call__(self, request: HttpRequest, /) -> typing.Awaitable[HttpResponseBase]: ... + + +class RequestResponseLogger(MiddlewareMixin): + sync_capable = True + async_capable = False + get_response: _GetResponseCallable | _AsyncGetResponseCallable + + def __init__(self, get_response: _GetResponseCallable | _AsyncGetResponseCallable) -> None: + self.get_response = get_response + + def __call__(self, request: HttpRequest): + before_session_data = dict(request.session.items()) if config.DEBUG_COLLECT_SESSION_DATA else {} + response = self.get_response(request) + after_session_data = dict(request.session.items()) if config.DEBUG_COLLECT_SESSION_DATA else {} + + logger_extra = { + "aws_request_id": get_aws_request_id_from_request(request), + "data": { + "request": get_request_log_data(request), + "response": get_response_log_data(response), + } + | ( + {"session": {"before": before_session_data, "after": after_session_data}} + if config.DEBUG_COLLECT_SESSION_DATA + else {} + ), + } + cloudwatch_logger.info(msg="log_request", extra=logger_extra) + + if (tag := request.META.get("bad_response_slack_logger_tag")) and not (200 <= response.status_code <= 299): + status_info = f"{response.status_code} {http.client.responses[response.status_code]}" + msg = f"Bad Response: [{request.method}] '{request.get_full_path()}' <{status_info}>" + logger_extra["data"]["tag"] = tag + slack_logger.warning(msg=msg, extra=logger_extra) + + return response + + def process_exception(self, request: HttpRequest, exception: Exception) -> None: + slack_logger.exception( + msg="요청 처리 중 예외가 발생했습니다.", + exc_info=(type(exception), exception, exception.__traceback__), + extra={ + "aws_request_id": get_aws_request_id_from_request(request), + "data": {"request": get_request_log_data(request)}, + }, + ) diff --git a/app/core/settings.py b/app/core/settings.py index 9ee4ad9..91af978 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -1,32 +1,148 @@ -""" -Django settings for core project. - -Generated by 'django-admin startproject' using Django 5.2. - -For more information on this file, see -https://docs.djangoproject.com/en/5.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.2/ref/settings/ -""" - -from pathlib import Path +import os +import pathlib +import traceback +import types +import typing + +import boto3 +import environ +import sentry_sdk +import sentry_sdk.integrations.aws_lambda +import sentry_sdk.integrations.django + +if typing.TYPE_CHECKING: + import mypy_boto3_ssm + +is_aws_lambda = os.environ.get("AWS_LAMBDA_FUNCTION_NAME") is not None +if is_aws_lambda and (project_name := os.environ.get("PROJECT_NAME")) and (stage := os.environ.get("API_STAGE")): + print("Running in AWS Lambda environment. Trying to load environment variables from AWS SSM Parameter Store.") + try: + ssm_client: "mypy_boto3_ssm.SSMClient" = boto3.client("ssm") + next_token = "" # nosec: B105 + while next_token is not None: + result = ssm_client.get_parameters_by_path( + Path=f"/{project_name}/{stage}", + MaxResults=10, + **({"NextToken": next_token} if next_token else {}), + ) + os.environ.update({p["Name"].split("/")[-1]: p["Value"] for p in result["Parameters"]}) + next_token = result.get("NextToken") + print("Successfully loaded environment variables from AWS SSM Parameter Store.") + except Exception as e: + print( + "Failed to load environment variables from AWS SSM Parameter Store. Traceback: \n" + + "".join(traceback.format_exception(e)) + ) # Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent +BASE_DIR = pathlib.Path(__file__).resolve().parent.parent +env = environ.Env( + DEBUG=(bool, False), + IS_LOCAL=(bool, False), + LOG_LEVEL=(str, "DEBUG"), +) +env.read_env(env.str("ENV_PATH", default="envfile/.env.local")) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-_qi42kur2=d)+ifd0=ovcw^4hy1!scy#i#j$c$#3en_747)mn9' +SECRET_KEY = env("DJANGO_SECRET_KEY", default="local_secret_key") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = env("DEBUG") +IS_LOCAL = env("IS_LOCAL") + +DEPLOYMENT_RELEASE_VERSION = os.environ.get("DEPLOYMENT_RELEASE_VERSION", "unknown") +# Loggers +SLACK = types.SimpleNamespace(token=env("SLACK_LOG_TOKEN", default=""), channel=env("SLACK_LOG_CHANNEL", default="")) + +LOG_LEVEL = env("LOG_LEVEL") +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "basic": {"format": "%(asctime)s:%(module)s:%(levelname)s:%(message)s", "datefmt": "%Y-%m-%d %H:%M:%S"}, + "slack": {"()": "core.logger.formatter.slack.SlackJsonFormatter"}, + "cloudwatch": {"()": "core.logger.formatter.cloudwatch.CloudWatchJsonFormatter"}, + }, + "handlers": { + "console": { + "level": LOG_LEVEL, + "class": "logging.StreamHandler", + "formatter": "basic", + }, + "cloudwatch": { + "level": LOG_LEVEL, + "class": "logging.StreamHandler", + "formatter": "cloudwatch", + }, + "slack": { + "level": LOG_LEVEL, + "class": "core.logger.handler.slack.SlackHandler", + "formatter": "slack", + }, + }, + "loggers": { + "django.db.backends": ({"level": LOG_LEVEL, "handlers": ["console"]} if IS_LOCAL else {}), + "cloudwatch_logger": {"level": LOG_LEVEL, "handlers": ["cloudwatch"], "propagate": True}, + "slack_logger": ({"level": LOG_LEVEL, "handlers": ["slack"]} if SLACK.token and SLACK.channel else {}), + }, +} -ALLOWED_HOSTS = [] +# Zappa Settings +API_STAGE = env("API_STAGE", default="prod") +ADDITIONAL_TEXT_MIMETYPES: list[str] = [] +ASYNC_RESPONSE_TABLE = "" +AWS_BOT_EVENT_MAPPING = {} +AWS_EVENT_MAPPING = {} +BASE_PATH = None +BINARY_SUPPORT = True +COGNITO_TRIGGER_MAPPING = {} +CONTEXT_HEADER_MAPPINGS = {} +DJANGO_SETTINGS = "core.settings" +DOMAIN = None +ENVIRONMENT_VARIABLES: dict[str, str] = {} +EXCEPTION_HANDLER = None +PROJECT_NAME = "PyConKR-backend" + +ALLOWED_HOSTS = ["*"] + +# CORS Settings +# pycon domain regex pattern +CORS_ALLOWED_ORIGIN_REGEXES = [ + r"^https://\w+\.pycon\.kr$", + r"^http://\w+\.pycon\.kr$", + r"^https://\w+\.dev.pycon\.kr$", + r"^http://\w+\.dev.pycon\.kr$", + r"http://localhost:\d+$", + r"https://localhost:\d+$", + r"http://127.0.0.1:\d+$", + r"https://127.0.0.1:\d+$", +] + +CORS_ALLOWED_ORIGINS = [ + "https://pycon.kr", + "https://2025.pycon.kr", + "http://pycon.kr", + "http://2025.pycon.kr", +] + +if DEBUG: + CORS_ALLOWED_ORIGIN_REGEXES += [] + +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_HEADERS = [ + "authorization", + "content-type", + "x-csrftoken", + "accept", + "accept-encoding", + "origin", + "user-agent", + "x-requested-with", +] # Application definition @@ -37,9 +153,28 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + # CORS + "corsheaders", + # django-rest-framework + "rest_framework", + "rest_framework.authtoken", + "drf_spectacular", + "drf_standardized_errors", + # django-filter + "django_filters", + # simple-history + "simple_history", + # zappa + "zappa_django_utils", + # For Shell Plus + "django_extensions", + # django-app + # django-constance + "constance", ] MIDDLEWARE = [ + # Django default middlewares 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -47,20 +182,28 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # CORS + "corsheaders.middleware.CorsMiddleware", + # simple-history + "simple_history.middleware.HistoryRequestMiddleware", + # Request Response Logger + "core.middleware.request_response_logger.RequestResponseLogger", ] ROOT_URLCONF = 'core.urls' TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ["core/templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.template.context_processors.request", ], }, }, @@ -73,10 +216,21 @@ # https://docs.djangoproject.com/en/5.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } + "default": { + "ENGINE": env("DATABASE_ENGINE", default="django.db.backends.sqlite3"), + "NAME": env("DATABASE_NAME", default=str(BASE_DIR / "db.sqlite3")), + "PORT": env("DATABASE_PORT", default=None), + "HOST": env("DATABASE_HOST", default=None), + "USER": env("DATABASE_USER", default=None), + "PASSWORD": env("DATABASE_PASSWORD", default=None), + }, +} + + +# Constance Settings +CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend" +CONSTANCE_CONFIG: dict[str, tuple[int, str]] = { + "DEBUG_COLLECT_SESSION_DATA": (False, "디버깅용 - 세션 데이터 수집 여부"), } @@ -84,27 +238,19 @@ # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, ] # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = 'ko-kr' -TIME_ZONE = 'UTC' +TIME_ZONE = 'Asia/Seoul' USE_I18N = True @@ -113,10 +259,113 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.2/howto/static-files/ +STATIC_ROOT = BASE_DIR / "static" +STATIC_URL = "static/" -STATIC_URL = 'static/' +DEFAULT_STORAGE_BACKEND = env("DJANGO_DEFAULT_STORAGE_BACKEND", default="storages.backends.s3.S3Storage") +STATIC_STORAGE_BACKEND = env("DJANGO_STATIC_STORAGE_BACKEND", default="storages.backends.s3.S3Storage") + +STORAGE_BUCKET_NAME = f"pyconkr-backend-api-{API_STAGE}" +STORAGE_OPTIONS = ( + { + "bucket_name": STORAGE_BUCKET_NAME, + "file_overwrite": False, + "addressing_style": "path", + } + if DEFAULT_STORAGE_BACKEND == "storages.backends.s3.S3Storage" + else {} +) +STORAGES = { + "default": {"BACKEND": DEFAULT_STORAGE_BACKEND, "OPTIONS": STORAGE_OPTIONS}, + "staticfiles": {"BACKEND": STATIC_STORAGE_BACKEND, "OPTIONS": STORAGE_OPTIONS}, +} # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +# Cookies +# https://docs.djangoproject.com/en/5.2/ref/settings/#cookies +COOKIE_PREFIX = (("LOCAL_" if IS_LOCAL else "DEBUG_") if DEBUG else "") + "PYCONKR_BACKEND_" +COOKIE_SAMESITE = "Lax" if IS_LOCAL else "None" +COOKIE_SECURE = not IS_LOCAL +COOKIE_HTTPONLY = True +COOKIE_DOMAIN = env("COOKIE_DOMAIN", default="pycon.kr") + +SESSION_COOKIE_NAME = f"{COOKIE_PREFIX}sessionid" +SESSION_COOKIE_SAMESITE = COOKIE_SAMESITE +SESSION_COOKIE_SECURE = COOKIE_SECURE +SESSION_COOKIE_HTTPONLY = COOKIE_HTTPONLY +SESSION_COOKIE_DOMAIN = COOKIE_DOMAIN + +CSRF_COOKIE_NAME = f"{COOKIE_PREFIX}csrftoken" +CSRF_COOKIE_SAMESITE = COOKIE_SAMESITE +CSRF_COOKIE_SECURE = COOKIE_SECURE +CSRF_COOKIE_HTTPONLY = COOKIE_HTTPONLY +CSRF_COOKIE_DOMAIN = COOKIE_DOMAIN +CSRF_TRUSTED_ORIGINS = set(env.list("CSRF_TRUSTED_ORIGINS", default=["https://pycon.kr"])) | { + "https://local.dev.pycon.kr:3000", + "https://localhost:3000", + "http://localhost:3000", + "https://127.0.0.1:3000", + "http://127.0.0.1:3000", +} + +# Django Rest Framework Settings +REST_FRAMEWORK = { + "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning", + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), + "EXCEPTION_HANDLER": "drf_standardized_errors.handler.exception_handler", + "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), +} + +# DRF Spectacular Settings +SPECTACULAR_SETTINGS = { + "TITLE": "PyCon KR Backend API", + "SERVE_INCLUDE_SCHEMA": False, +} + +# Sentry Settings +if SENTRY_DSN := env("SENTRY_DSN", default=""): + SENTRY_TRACES_SAMPLE_RATE = env.float("SENTRY_TRACES_SAMPLE_RATE", default=1.0) + SENTRY_PROFILES_SAMPLE_RATE = env.float("SENTRY_PROFILES_SAMPLE_RATE", default=0.0) + SENTRY_IGNORED_TRACE_ROUTES = env.list("SENTRY_IGNORED_TRACE_ROUTES", default=[]) + + def traces_sampler(ctx: dict[str, typing.Any]) -> float: + """ + This function is used to determine if a transaction should be sampled. + from https://stackoverflow.com/a/74412613 + """ + if (parent_sampled := ctx.get("parent_sampled")) is not None: + # If this transaction has a parent, we usually want to sample it + # if and only if its parent was sampled. + return parent_sampled + if "wsgi_environ" in ctx: + # Get the URL for WSGI requests + url = ctx["wsgi_environ"].get("PATH_INFO", "") + elif "asgi_scope" in ctx: + # Get the URL for ASGI requests + url = ctx["asgi_scope"].get("path", "") + else: + # Other kinds of transactions don't have a URL + url = "" + if ctx["transaction_context"]["op"] == "http.server": + # Conditions only relevant to operation "http.server" + if any(url.startswith(ignored_route) for ignored_route in SENTRY_IGNORED_TRACE_ROUTES): + return 0 # Don't trace any of these transactions + return SENTRY_TRACES_SAMPLE_RATE + + sentry_sdk.init( + dsn=SENTRY_DSN, + environment=API_STAGE, + release=DEPLOYMENT_RELEASE_VERSION, + traces_sampler=traces_sampler, + profiles_sample_rate=SENTRY_PROFILES_SAMPLE_RATE, + integrations=[ + sentry_sdk.integrations.aws_lambda.AwsLambdaIntegration(), + sentry_sdk.integrations.django.DjangoIntegration(), + ], + ) diff --git a/app/core/urls.py b/app/core/urls.py index cd1cf6e..2d12b53 100644 --- a/app/core/urls.py +++ b/app/core/urls.py @@ -14,9 +14,29 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin from django.urls import path +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView -urlpatterns = [ - path('admin/', admin.site.urls), +import core.health_check + +v1_apis = [ ] + +urlpatterns = [ + # Health Check + path("readyz/", core.health_check.readyz), + path("livez/", core.health_check.livez), + # Admin + path("admin/", admin.site.urls), +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + +if settings.DEBUG: + urlpatterns += [ + # API Docs + path("api/schema/v1/", SpectacularAPIView.as_view(api_version="v1"), name="v1-schema"), + path("api/schema/v1/swagger/", SpectacularSwaggerView.as_view(url_name="v1-schema"), name="swagger-v1-ui"), + ] diff --git a/app/zappa_settings.py b/app/zappa_settings.py new file mode 100644 index 0000000..69afbda --- /dev/null +++ b/app/zappa_settings.py @@ -0,0 +1,2 @@ +# We'll use django-environ instead of Zappa Settings. +from core.settings import * # noqa: F403, F401 From 02edab782443a6afd16b347486bc6eeb83885844 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 6 Apr 2025 18:31:24 +0900 Subject: [PATCH 07/13] =?UTF-8?q?add:=EB=AF=B8=EB=9E=98=EB=A5=BC=20?= =?UTF-8?q?=EB=8C=80=EB=B9=84=ED=95=98=EC=97=AC=20UserExt=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/fields.py | 19 ++++++++++++ app/core/models.py | 40 +++++++++++++++++++++++++ app/core/settings.py | 4 ++- app/user/__init__.py | 0 app/user/admin.py | 3 ++ app/user/apps.py | 6 ++++ app/user/migrations/0001_initial.py | 46 +++++++++++++++++++++++++++++ app/user/migrations/__init__.py | 0 app/user/models.py | 5 ++++ app/user/tests.py | 3 ++ app/user/views.py | 3 ++ 11 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 app/core/fields.py create mode 100644 app/core/models.py create mode 100644 app/user/__init__.py create mode 100644 app/user/admin.py create mode 100644 app/user/apps.py create mode 100644 app/user/migrations/0001_initial.py create mode 100644 app/user/migrations/__init__.py create mode 100644 app/user/models.py create mode 100644 app/user/tests.py create mode 100644 app/user/views.py diff --git a/app/core/fields.py b/app/core/fields.py new file mode 100644 index 0000000..9f42730 --- /dev/null +++ b/app/core/fields.py @@ -0,0 +1,19 @@ +import uuid + +from django.db.backends.base.operations import BaseDatabaseOperations +from django.db.models import AutoField, UUIDField + +BaseDatabaseOperations.integer_field_ranges["UUIDField"] = (0, 0) + + +class UUIDAutoField(UUIDField, AutoField): + def __init__(self, *args, **kwargs): + kwargs.setdefault("default", uuid.uuid4) + kwargs.setdefault("editable", False) + super().__init__(*args, **kwargs) + + def _check_max_length_warning(self): + return [] + + def get_prep_value(self, value): + return self.to_python(value) diff --git a/app/core/models.py b/app/core/models.py new file mode 100644 index 0000000..df4e99a --- /dev/null +++ b/app/core/models.py @@ -0,0 +1,40 @@ +import uuid + +from django.contrib.auth import get_user_model +from django.db import models +from django.db.models.functions import Now + +User = get_user_model() + + +class BaseAbstractModelQuerySet(models.QuerySet): + def delete(self, *args, **kwargs): + return super().update(*args, **kwargs, deleted_at=Now(), updated_at=Now()) + + def hard_delete(self): + return super().delete() + + def filter_active(self): + return self.filter(deleted_at__isnull=True) + + +class BaseAbstractModel(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(null=True, blank=True) + + created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True, related_name="%(class)s_created_by") + updated_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True, related_name="%(class)s_updated_by") + deleted_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True, related_name="%(class)s_deleted_by") + + objects: BaseAbstractModelQuerySet = BaseAbstractModelQuerySet.as_manager() + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + if update_fields := kwargs.get("update_fields"): + kwargs["update_fields"] = set(update_fields) | {"updated_at"} + super().save(*args, **kwargs) diff --git a/app/core/settings.py b/app/core/settings.py index 91af978..24c3935 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -169,6 +169,7 @@ # For Shell Plus "django_extensions", # django-app + "user", # django-constance "constance", ] @@ -283,8 +284,9 @@ # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = 'core.fields.UUIDAutoField' +AUTH_USER_MODEL = "user.UserExt" # Cookies # https://docs.djangoproject.com/en/5.2/ref/settings/#cookies diff --git a/app/user/__init__.py b/app/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/user/admin.py b/app/user/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/app/user/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/user/apps.py b/app/user/apps.py new file mode 100644 index 0000000..2e4c3ba --- /dev/null +++ b/app/user/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = 'core.fields.UUIDAutoField' + name = 'user' diff --git a/app/user/migrations/0001_initial.py b/app/user/migrations/0001_initial.py new file mode 100644 index 0000000..c814b76 --- /dev/null +++ b/app/user/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# Generated by Django 5.2 on 2025-04-06 09:30 + +import core.fields +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='UserExt', + fields=[ + ('id', core.fields.UUIDAutoField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/app/user/migrations/__init__.py b/app/user/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/user/models.py b/app/user/models.py new file mode 100644 index 0000000..83817dc --- /dev/null +++ b/app/user/models.py @@ -0,0 +1,5 @@ +from django.contrib.auth.models import AbstractUser + + +class UserExt(AbstractUser): + pass diff --git a/app/user/tests.py b/app/user/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/user/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/user/views.py b/app/user/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/app/user/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From e708b7d053760bbf16195b5e7df8b6b0b003313e Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 6 Apr 2025 20:06:33 +0900 Subject: [PATCH 08/13] =?UTF-8?q?add:pre-commit=20=EB=B0=8F=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=B0=ED=8C=85=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/get_new_version.py | 7 +- .github/scripts/update_ssm_parameter_store.py | 15 +-- .pre-commit-config.yaml | 91 ++++++++++++++ README.md | 53 ++++++++ app/core/asgi.py | 2 +- app/core/external_apis/slack/blocks.py | 49 +++++--- app/core/fields.py | 59 ++++++++- app/core/health_check.py | 2 +- app/core/logger/formatter/cloudwatch.py | 13 +- app/core/logger/formatter/slack.py | 27 ++-- app/core/logger/handler/slack.py | 6 +- app/core/logger/util/decorator.py | 4 +- app/core/logger/util/django_helper.py | 24 ++-- .../middleware/request_response_logger.py | 50 ++++---- app/core/models.py | 52 +++++--- app/core/settings.py | 52 ++++---- app/core/urls.py | 10 +- app/core/wsgi.py | 2 +- app/manage.py | 6 +- app/user/admin.py | 3 - app/user/apps.py | 4 +- app/user/migrations/0001_initial.py | 118 ++++++++++++++---- app/user/tests.py | 3 - app/user/views.py | 3 - 24 files changed, 482 insertions(+), 173 deletions(-) create mode 100644 .pre-commit-config.yaml delete mode 100644 app/user/admin.py delete mode 100644 app/user/tests.py delete mode 100644 app/user/views.py diff --git a/.github/scripts/get_new_version.py b/.github/scripts/get_new_version.py index fc53e82..4fa2b2f 100644 --- a/.github/scripts/get_new_version.py +++ b/.github/scripts/get_new_version.py @@ -60,16 +60,17 @@ def increment_version_count(version: packaging.version.Version, is_stage: bool) today: datetime.date = datetime.date.today() # Calculate the new version + new_count: int = 0 if version.major == today.year and version.minor == today.month: if current_pre: # If the current version is a pre-release, do not increment the count - new_count: int = version.micro + new_count = version.micro else: # Same month, increment the count - new_count: int = version.micro + 1 + new_count = version.micro + 1 else: # Different month, reset the count - new_count: int = 1 + new_count = 1 current_pre = None new_pre: PreType = ((current_pre[0], current_pre[1] + 1) if current_pre else ("a", 0)) if is_stage else None diff --git a/.github/scripts/update_ssm_parameter_store.py b/.github/scripts/update_ssm_parameter_store.py index 4e78770..b152d79 100644 --- a/.github/scripts/update_ssm_parameter_store.py +++ b/.github/scripts/update_ssm_parameter_store.py @@ -11,11 +11,12 @@ ssm_client: "mypy_boto3_ssm.SSMClient" = boto3.client("ssm") -class ParameterDiffCollection(typing.NamedTuple): - class ValueDiff(typing.NamedTuple): - old: str | None - new: str | None +class ValueDiff(typing.NamedTuple): + old: str | None + new: str | None + +class ParameterDiffCollection(typing.NamedTuple): updated: dict[str, ValueDiff] created: dict[str, ValueDiff] deleted: dict[str, ValueDiff] @@ -53,7 +54,7 @@ def get_parameter_diff(old_parameters: dict[str, str], new_parameters: dict[str, created, updated, deleted = {}, {}, {} for fields in old_parameters.keys() | new_parameters.keys(): - value = ParameterDiffCollection.ValueDiff(old=old_parameters.get(fields), new=new_parameters.get(fields)) + value = ValueDiff(old=old_parameters.get(fields), new=new_parameters.get(fields)) if value.old != value.new: if value.old is None: created[fields] = value @@ -65,7 +66,7 @@ def get_parameter_diff(old_parameters: dict[str, str], new_parameters: dict[str, return ParameterDiffCollection(updated=updated, created=created, deleted=deleted) -def update_parameter_store(project_name: str, stage: str, diff: ParameterDiffCollection): +def update_parameter_store(project_name: str, stage: str, diff: ParameterDiffCollection) -> None: for field, values in {**diff.created, **diff.updated}.items(): ssm_client.put_parameter( Name=f"/{project_name}/{stage}/{field}", @@ -78,7 +79,7 @@ def update_parameter_store(project_name: str, stage: str, diff: ParameterDiffCol ssm_client.delete_parameters(Names=[f"/{project_name}/{stage}/{field}" for field in diff.deleted.keys()]) -def main(project_name: str, stage: str, json_file: pathlib.Path): +def main(project_name: str, stage: str, json_file: pathlib.Path) -> None: if not all([json_file.is_file(), project_name, stage]): raise ValueError("인자를 확인해주세요.") diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..77eb4f5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,91 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +default_language_version: + python: python3.12 +default_stages: [pre-commit, pre-push] +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-toml + - id: check-xml + - id: check-yaml + - id: check-added-large-files + - id: detect-aws-credentials + args: + - --allow-missing-credentials + - id: end-of-file-fixer + - id: mixed-line-ending + - id: pretty-format-json + args: + - --autofix + - id: trailing-whitespace + exclude_types: + - javascript + - markdown +- repo: https://github.com/PyCQA/flake8 + rev: 7.2.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear + - flake8-noqa + args: + - --max-line-length=120 + - --max-complexity=18 +- repo: https://github.com/psf/black + rev: 25.1.0 + hooks: + - id: black + language_version: python3.12 + args: + - --line-length=120 +- repo: https://github.com/PyCQA/bandit + rev: '1.8.3' + hooks: + - id: bandit +- repo: https://github.com/PyCQA/isort + rev: '6.0.1' + hooks: + - id: isort +- repo: https://github.com/dosisod/refurb + rev: v2.0.0 + hooks: + - id: refurb + additional_dependencies: + - boto3 + - django-constance + - django-cors-headers + - django-environ + - django-extensions + - django-filter + - django-simple-history + - django-stubs[compatible-mypy] + - drf-spectacular + - drf-standardized-errors + - djangorestframework-stubs[compatible-mypy] + - zappa-django-utils +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.15.0' + hooks: + - id: mypy + additional_dependencies: + - boto3 + - django-constance + - django-cors-headers + - django-environ + - django-extensions + - django-filter + - django-simple-history + - django-stubs[compatible-mypy] + - drf-spectacular + - drf-standardized-errors + - djangorestframework-stubs[compatible-mypy] + - zappa-django-utils + args: + - --no-strict-optional + - --ignore-missing-imports + - --check-untyped-defs + - --disallow-untyped-defs + - --disallow-incomplete-defs + - --disallow-untyped-calls diff --git a/README.md b/README.md index e69de29..f2a7a6f 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,53 @@ +# PyCon KR Shop Server +## Setup +### 로컬 인프라 (Docker-Compose) +```bash +make docker-compose-up # PostgreSQL 컨테이너 시작 +make docker-compose-down # PostgreSQL 컨테이너 종료 +make docker-compose-rm # PostgreSQL 컨테이너 삭제 +``` + +### pre-commit hook 설정 +본 프로젝트에서는 협업 시 코딩 컨벤션을 준수하기 위해 [pre-commit](https://pre-commit.com/)을 사용합니다. +pre-commit을 설치하려면 다음을 참고해주세요. + +```bash +# pre-commit hook 설치 +make hooks-install + +# 프로젝트 전체 코드 lint 검사 & format +make lint +``` + +### Django App +본 프로젝트는 [uv](https://github.com/astral-sh/uv)를 사용합니다. uv를 로컬에 설치한 후 아래 명령을 실행해주세요. +```bash +make local-setup +``` + +## Run +아래 명령어로 서버를 실행할 수 있습니다. +`.env.local` 파일은 기본적으로 PostgreSQL 컨테이너를 바라보도록 설정되어 있습니다. +```bash +make local-api +``` + +마지막으로, Docker로도 API 서버를 실행할 수 있습니다. +단, 이때 API 서버는 AWS Lambda에서 요청을 처리하는 것과 동일하게 동작하므로, API Gateway가 Lambda를 호출할때처럼 `/functions/function/invocations` route에 특정한 Payload로 요청해야 합니다. (Makefile의 `docker-readyz`를 참고하세요.) +이 방식은 로컬에서 간단한 smoke test 용도로 유용합니다. +```bash +# Docker 이미지 빌드 +make docker-build + +# Docker 컨테이너 실행 +make docker-run + +# Docker 컨테이너 종료 +make docker-stop + +# Docker 컨테이너 삭제 +make docker-rm + +# Docker로 간단한 smoke test +make docker-test +``` diff --git a/app/core/asgi.py b/app/core/asgi.py index e36e2c8..2d3581e 100644 --- a/app/core/asgi.py +++ b/app/core/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") application = get_asgi_application() diff --git a/app/core/external_apis/slack/blocks.py b/app/core/external_apis/slack/blocks.py index 29183b5..af2576d 100644 --- a/app/core/external_apis/slack/blocks.py +++ b/app/core/external_apis/slack/blocks.py @@ -2,16 +2,33 @@ import typing +class SlackChildBlockType(typing.TypedDict): + block_type: typing.Literal["plain_text", "mrkdwn"] + text: str + emoji: typing.NotRequired[typing.Literal[True]] + title: typing.NotRequired[str] + + +class SlackParentBlockType(typing.TypedDict): + block_type: str + text: typing.NotRequired[SlackChildBlockType] + fields: typing.NotRequired[list[SlackChildBlockType]] + + +class SlackBlocksType(typing.TypedDict): + blocks: list[SlackParentBlockType] + + @dataclasses.dataclass class SlackChildBlock: text: str - block_type: typing.Literal["plain_text", "mrkdwn"] = "" + block_type: typing.Literal["plain_text", "mrkdwn"] = "plain_text" - def to_dict(self) -> dict[str, str | bool]: - return { - "type": self.block_type, - "text": self.text, - } | ({"emoji": True} if self.block_type == "plain_text" else {}) + def to_dict(self) -> SlackChildBlockType: + result = SlackChildBlockType(block_type=self.block_type, text=self.text) + if self.block_type == "plain_text": + result["emoji"] = True + return result @dataclasses.dataclass @@ -39,24 +56,24 @@ class SlackParentBlock: text: SlackChildBlock | None = None fields: list[SlackChildBlock] | None = None - def __post_init__(self): + def __post_init__(self) -> None: if not (self.text or self.fields): raise ValueError("At least one of text or fields must be set!") - def to_dict(self) -> dict[str, str | dict[str, str | bool]]: - result = { - "type": self.block_type, - "text": self.text.to_dict() if self.text else None, - "fields": [f.to_dict() for f in self.fields] if self.fields else None, - } - return {k: v for k, v in result.items() if k and v} + def to_dict(self) -> SlackParentBlockType: + result = SlackParentBlockType(block_type=self.block_type) + if self.text: + result["text"] = self.text.to_dict() + if self.fields: + result["fields"] = [f.to_dict() for f in self.fields] + return result @dataclasses.dataclass class SlackHeaderParentBlock(SlackParentBlock): block_type: str = "header" - def __post_init__(self): + def __post_init__(self) -> None: super().__post_init__() if self.fields: raise ValueError("header block only allows text, not fields!") @@ -73,5 +90,5 @@ class SlackSectionParentBlock(SlackParentBlock): class SlackBlocks: blocks: list[SlackParentBlock] - def to_dict(self): + def to_dict(self) -> SlackBlocksType: return {"blocks": [b.to_dict() for b in self.blocks]} diff --git a/app/core/fields.py b/app/core/fields.py index 9f42730..4e4fe12 100644 --- a/app/core/fields.py +++ b/app/core/fields.py @@ -1,19 +1,68 @@ +import collections.abc +import contextlib +import typing import uuid +from django.core.validators import _ValidatorCallable from django.db.backends.base.operations import BaseDatabaseOperations -from django.db.models import AutoField, UUIDField +from django.db.models import AutoField, UUIDField, expressions, fields +from django.db.models.fields.reverse_related import ForeignObjectRel +from django.utils.choices import _Choices +from django.utils.functional import _StrOrPromise BaseDatabaseOperations.integer_field_ranges["UUIDField"] = (0, 0) +class UUIDFieidInitKwargs(typing.TypedDict): + name: typing.NotRequired[str | None] + primary_key: typing.NotRequired[bool] + max_length: typing.NotRequired[int | None] + unique: typing.NotRequired[bool] + blank: typing.NotRequired[bool] + null: typing.NotRequired[bool] + db_index: typing.NotRequired[bool] + rel: typing.NotRequired[ForeignObjectRel | None] + default: typing.NotRequired[typing.Any] + db_default: typing.NotRequired[type[fields.NOT_PROVIDED] | expressions.Expression | typing.Any] + editable: typing.NotRequired[bool] + serialize: typing.NotRequired[bool] + unique_for_date: typing.NotRequired[str | None] + unique_for_month: typing.NotRequired[str | None] + unique_for_year: typing.NotRequired[str | None] + choices: typing.NotRequired[_Choices | None] + help_text: typing.NotRequired[_StrOrPromise] + db_column: typing.NotRequired[str | None] + db_comment: typing.NotRequired[str | None] + db_tablespace: typing.NotRequired[str | None] + auto_created: typing.NotRequired[bool] + validators: typing.NotRequired[collections.abc.Iterable[_ValidatorCallable]] + error_messages: typing.NotRequired[dict[str, typing.Any] | None] + + class UUIDAutoField(UUIDField, AutoField): - def __init__(self, *args, **kwargs): + _pyi_private_set_type: uuid.UUID # type: ignore[assignment] + _pyi_private_get_type: uuid.UUID # type: ignore[assignment] + _pyi_lookup_exact_type: uuid.UUID # type: ignore[assignment] + + def __init__(self, verbose_name: _StrOrPromise | None = None, **kwargs: typing.Unpack[UUIDFieidInitKwargs]) -> None: kwargs.setdefault("default", uuid.uuid4) kwargs.setdefault("editable", False) - super().__init__(*args, **kwargs) + super().__init__(verbose_name, **kwargs) - def _check_max_length_warning(self): + def _check_max_length_warning(self) -> list[str]: return [] - def get_prep_value(self, value): + def get_prep_value(self, value: typing.Any) -> uuid.UUID | None: + if value in (None, "") or isinstance(value, uuid.UUID): + return None + if isinstance(value, str): + return uuid.UUID(value) + if isinstance(value, bytes): + return uuid.UUID(bytes=value) + if isinstance(value, int): + return uuid.UUID(int=value) + if isinstance(value, collections.abc.Sequence): + return uuid.UUID(bytes=bytes(value)) + with contextlib.suppress(ValueError): + return uuid.UUID(value) return self.to_python(value) diff --git a/app/core/health_check.py b/app/core/health_check.py index 3b3b922..8d95635 100644 --- a/app/core/health_check.py +++ b/app/core/health_check.py @@ -1,6 +1,6 @@ import collections -import os import http +import os import typing from django.conf import settings diff --git a/app/core/logger/formatter/cloudwatch.py b/app/core/logger/formatter/cloudwatch.py index a0525de..9b51312 100644 --- a/app/core/logger/formatter/cloudwatch.py +++ b/app/core/logger/formatter/cloudwatch.py @@ -36,11 +36,14 @@ def __init__( self, fmt: str = DEFAULT_CLOUDWATCH_LOG_FORMAT, datefmt: str = DEFAULT_CLOUDWATCH_DATE_FORMAT, - **kwargs, + style: typing.Literal["%", "{", "$"] = "%", + validate: bool = True, + *, + defaults: typing.Any | None = None, ): - super().__init__(fmt=fmt, datefmt=datefmt, **kwargs) + super().__init__(fmt=fmt, datefmt=datefmt, style=style, validate=validate, defaults=defaults) - def formatException(self, exc_info: ExcInfoType) -> dict: + def formatException(self, exc_info: ExcInfoType) -> dict: # type: ignore[override] exc_type, exc_value, _ = exc_info return { "type": exc_type.__name__, @@ -48,13 +51,13 @@ def formatException(self, exc_info: ExcInfoType) -> dict: "traceback_msg": "\n".join(traceback.format_exception(exc_value)), } - def format(self, record): + def format(self, record: logging.LogRecord) -> str: record.message = record.getMessage() if self.usesTime(): record.asctime = self.formatTime(record, self.datefmt) if record.exc_info: if not record.exc_text: - record.exc_text = self.formatException(record.exc_info) + record.exc_text = self.formatException(record.exc_info) # type: ignore[assignment] return default_json_dumps( { diff --git a/app/core/logger/formatter/slack.py b/app/core/logger/formatter/slack.py index e7052ff..8af0b4c 100644 --- a/app/core/logger/formatter/slack.py +++ b/app/core/logger/formatter/slack.py @@ -35,10 +35,18 @@ class SlackJsonFormatter(logging.Formatter): } """ - def __init__(self, fmt: str = DEFAULT_SLACK_LOG_FORMAT, datefmt: str = DEFAULT_SLACK_DATE_FORMAT, **kwargs): - super().__init__(fmt=fmt, datefmt=datefmt, **kwargs) + def __init__( + self, + fmt: str = DEFAULT_SLACK_LOG_FORMAT, + datefmt: str = DEFAULT_SLACK_DATE_FORMAT, + style: typing.Literal["%", "{", "$"] = "%", + validate: bool = True, + *, + defaults: typing.Any | None = None, + ): + super().__init__(fmt=fmt, datefmt=datefmt, style=style, validate=validate, defaults=defaults) - def formatException(self, exc_info: ExcInfoType) -> blocks.SlackCodeChildBlock: + def formatException(self, exc_info: ExcInfoType) -> blocks.SlackSectionParentBlock: # type: ignore[override] exc_type, exc_value, _ = exc_info return blocks.SlackSectionParentBlock( text=blocks.SlackCodeChildBlock( @@ -47,7 +55,7 @@ def formatException(self, exc_info: ExcInfoType) -> blocks.SlackCodeChildBlock: ) ) - def format(self, record): + def format(self, record: logging.LogRecord) -> list[blocks.SlackParentBlockType]: # type: ignore[override] record.message = record.getMessage() if self.usesTime(): record.asctime = self.formatTime(record, self.datefmt) @@ -74,14 +82,9 @@ def format(self, record): ) if extra_data := record.__dict__.get("data"): for key, value in extra_data.items(): - slack_block.blocks.append( - blocks.SlackSectionParentBlock( - text=blocks.SlackCodeChildBlock( - title=key, - text=value if isinstance(value, (str, int, float, bool)) else default_json_dumps(value), - ) - ) - ) + text = str(value) if isinstance(value, (str, int, float, bool)) else default_json_dumps(value) + block = blocks.SlackSectionParentBlock(text=blocks.SlackCodeChildBlock(title=key, text=text)) + slack_block.blocks.append(block) if record.exc_info: slack_block.blocks.append(self.formatException(record.exc_info)) diff --git a/app/core/logger/handler/slack.py b/app/core/logger/handler/slack.py index 45b627d..280bce7 100644 --- a/app/core/logger/handler/slack.py +++ b/app/core/logger/handler/slack.py @@ -6,17 +6,17 @@ class SlackHandler(logging.handlers.HTTPHandler): - def __init__(self): + def __init__(self) -> None: super().__init__(host="slack.com", url="/api/chat.postMessage", method="POST", secure=True) - def mapLogRecord(self, record): + def mapLogRecord(self, record: logging.LogRecord) -> dict[str, str | dict]: return { "channel": settings.SLACK.channel, "text": "서버 알림", "blocks": self.formatter.format(record), } - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: """From the logging.handlers.HTTPHandler.emit, but with some modifications to send a message to Slack.""" try: connection = self.getConnection(self.host, self.secure) diff --git a/app/core/logger/util/decorator.py b/app/core/logger/util/decorator.py index 418602a..466133c 100644 --- a/app/core/logger/util/decorator.py +++ b/app/core/logger/util/decorator.py @@ -12,9 +12,9 @@ ViewFuncType = collections.abc.Callable[[HttpRequest, typing.Any, typing.Any], HttpResponseBase] -def bad_response_slack_logger(tag: str): +def bad_response_slack_logger(tag: str) -> collections.abc.Callable[[ViewFuncType], ViewFuncType]: def wrapper(view_func: ViewFuncType) -> ViewFuncType: - def inner_wrapper(*args, **kwargs): + def inner_wrapper(*args: typing.Any, **kwargs: typing.Any) -> HttpResponseBase: try: request: Request | HttpRequest = args[1] request.META["bad_response_slack_logger_tag"] = tag diff --git a/app/core/logger/util/django_helper.py b/app/core/logger/util/django_helper.py index aee1941..d2d480e 100644 --- a/app/core/logger/util/django_helper.py +++ b/app/core/logger/util/django_helper.py @@ -1,4 +1,5 @@ import contextlib +import functools import json import types @@ -9,15 +10,12 @@ PLACEHOLDER_AWS_REQUEST_ID = "00000000-0000-0000-0000-000000000000" PLACEHOLDER_AWS_LAMBDA_CONTEXT = types.SimpleNamespace(AWS_REQUEST_ID=PLACEHOLDER_AWS_REQUEST_ID) - -def default_json_dumps(obj: object, **kwargs) -> str: - return json.dumps( - obj=obj, - skipkeys=True, - ensure_ascii=False, - default=lambda o: o.__dict__ if hasattr(o, "__dict__") else str(o), - **kwargs, - ) +default_json_dumps = functools.partial( + json.dumps, + skipkeys=True, + ensure_ascii=False, + default=lambda o: o.__dict__ if hasattr(o, "__dict__") else str(o), +) def get_request_log_data(request: HttpRequest | Request) -> dict[str, str | dict]: @@ -27,10 +25,14 @@ def get_request_log_data(request: HttpRequest | Request) -> dict[str, str | dict except RawPostDataException: body = "Request body is not readable." + user = None + if request.user.is_authenticated: + user = getattr(request.user, "username", None) + request_data = { "method": request.method, "path": request.path, - "user": request.user.username if request.user.is_authenticated else None, + "user": user, "query_params": request.GET, "headers": dict(request.headers), "body": body, @@ -44,7 +46,7 @@ def get_request_log_data(request: HttpRequest | Request) -> dict[str, str | dict return request_data -def get_response_log_data(response: HttpResponseBase) -> dict[str, str | dict]: +def get_response_log_data(response: HttpResponseBase) -> dict[str, int | str | dict]: response_body = getattr(response, "content", getattr(response, "streaming_content", "Couldn't get response body")) with contextlib.suppress(json.JSONDecodeError, TypeError): response_body = response_body and json.loads(response_body) diff --git a/app/core/middleware/request_response_logger.py b/app/core/middleware/request_response_logger.py index 299db82..b2545a7 100644 --- a/app/core/middleware/request_response_logger.py +++ b/app/core/middleware/request_response_logger.py @@ -3,15 +3,14 @@ import typing from constance import config -from django.http.request import HttpRequest -from django.http.response import HttpResponseBase -from django.utils.deprecation import MiddlewareMixin - from core.logger.util.django_helper import ( get_aws_request_id_from_request, get_request_log_data, get_response_log_data, ) +from django.http.request import HttpRequest +from django.http.response import HttpResponseBase +from django.utils.deprecation import MiddlewareMixin cloudwatch_logger = logging.getLogger("cloudwatch_logger") slack_logger = logging.getLogger("slack_logger") @@ -22,36 +21,43 @@ class _GetResponseCallable(typing.Protocol): def __call__(self, request: HttpRequest, /) -> HttpResponseBase: ... -# From django-stubs -class _AsyncGetResponseCallable(typing.Protocol): - def __call__(self, request: HttpRequest, /) -> typing.Awaitable[HttpResponseBase]: ... +class LoggerExtraDataType(typing.TypedDict): + request: dict[str, typing.Any] + response: dict[str, typing.Any] + tag: typing.NotRequired[str] + + +class LoggerExtraSessionType(typing.TypedDict): + before: dict[str, typing.Any] + after: dict[str, typing.Any] + + +class LoggerExtraType(typing.TypedDict): + aws_request_id: str + data: LoggerExtraDataType + session: typing.NotRequired[LoggerExtraSessionType] class RequestResponseLogger(MiddlewareMixin): sync_capable = True async_capable = False - get_response: _GetResponseCallable | _AsyncGetResponseCallable + get_response: _GetResponseCallable - def __init__(self, get_response: _GetResponseCallable | _AsyncGetResponseCallable) -> None: + def __init__(self, get_response: _GetResponseCallable) -> None: self.get_response = get_response - def __call__(self, request: HttpRequest): + def __call__(self, request: HttpRequest) -> HttpResponseBase: before_session_data = dict(request.session.items()) if config.DEBUG_COLLECT_SESSION_DATA else {} response = self.get_response(request) after_session_data = dict(request.session.items()) if config.DEBUG_COLLECT_SESSION_DATA else {} - logger_extra = { - "aws_request_id": get_aws_request_id_from_request(request), - "data": { - "request": get_request_log_data(request), - "response": get_response_log_data(response), - } - | ( - {"session": {"before": before_session_data, "after": after_session_data}} - if config.DEBUG_COLLECT_SESSION_DATA - else {} - ), - } + logger_extra = LoggerExtraType( + aws_request_id=get_aws_request_id_from_request(request), + data=LoggerExtraDataType(request=get_request_log_data(request), response=get_response_log_data(response)), + ) + if config.DEBUG_COLLECT_SESSION_DATA: + logger_extra["session"] = {"before": before_session_data, "after": after_session_data} + cloudwatch_logger.info(msg="log_request", extra=logger_extra) if (tag := request.META.get("bad_response_slack_logger_tag")) and not (200 <= response.status_code <= 299): diff --git a/app/core/models.py b/app/core/models.py index df4e99a..7676c5e 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -1,40 +1,60 @@ +import collections.abc +import datetime +import typing import uuid from django.contrib.auth import get_user_model from django.db import models from django.db.models.functions import Now +if typing.TYPE_CHECKING: + from user.models import UserExt # noqa: F401 + User = get_user_model() class BaseAbstractModelQuerySet(models.QuerySet): - def delete(self, *args, **kwargs): - return super().update(*args, **kwargs, deleted_at=Now(), updated_at=Now()) + def delete(self) -> int: # type: ignore[override] + return super().update(deleted_at=Now(), updated_at=Now()) - def hard_delete(self): + def hard_delete(self) -> tuple[int, dict[str, int]]: return super().delete() - def filter_active(self): + def filter_active(self) -> typing.Self: return self.filter(deleted_at__isnull=True) class BaseAbstractModel(models.Model): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + id = models.UUIDField[uuid.UUID, uuid.UUID](primary_key=True, default=uuid.uuid4, editable=False) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - deleted_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField[datetime.datetime, datetime.datetime](auto_now_add=True) + updated_at = models.DateTimeField[datetime.datetime, datetime.datetime](auto_now=True) + deleted_at = models.DateTimeField[datetime.datetime, datetime.datetime](null=True, blank=True) - created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True, related_name="%(class)s_created_by") - updated_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True, related_name="%(class)s_updated_by") - deleted_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True, related_name="%(class)s_deleted_by") + created_by = models.ForeignKey["UserExt", "UserExt"]( + User, on_delete=models.PROTECT, null=True, related_name="%(class)s_created_by" + ) + updated_by = models.ForeignKey["UserExt", "UserExt"]( + User, on_delete=models.PROTECT, null=True, related_name="%(class)s_updated_by" + ) + deleted_by = models.ForeignKey["UserExt", "UserExt"]( + User, on_delete=models.PROTECT, null=True, related_name="%(class)s_deleted_by" + ) - objects: BaseAbstractModelQuerySet = BaseAbstractModelQuerySet.as_manager() + objects: BaseAbstractModelQuerySet = BaseAbstractModelQuerySet.as_manager() # type: ignore[misc, assignment] class Meta: abstract = True - def save(self, *args, **kwargs): - if update_fields := kwargs.get("update_fields"): - kwargs["update_fields"] = set(update_fields) | {"updated_at"} - super().save(*args, **kwargs) + def save( # type: ignore[override] + self, + *, + force_insert: bool = False, + force_update: bool = False, + using: str | None = None, + update_fields: collections.abc.Iterable[str] | None = None, + ) -> None: + if update_fields: + update_fields = set(update_fields) | {"updated_at"} + + super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) diff --git a/app/core/settings.py b/app/core/settings.py index 24c3935..18ee0e7 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -94,12 +94,12 @@ API_STAGE = env("API_STAGE", default="prod") ADDITIONAL_TEXT_MIMETYPES: list[str] = [] ASYNC_RESPONSE_TABLE = "" -AWS_BOT_EVENT_MAPPING = {} -AWS_EVENT_MAPPING = {} +AWS_BOT_EVENT_MAPPING: dict[str, str] = {} +AWS_EVENT_MAPPING: dict[str, str] = {} BASE_PATH = None BINARY_SUPPORT = True -COGNITO_TRIGGER_MAPPING = {} -CONTEXT_HEADER_MAPPINGS = {} +COGNITO_TRIGGER_MAPPING: dict[str, str] = {} +CONTEXT_HEADER_MAPPINGS: dict[str, str] = {} DJANGO_SETTINGS = "core.settings" DOMAIN = None ENVIRONMENT_VARIABLES: dict[str, str] = {} @@ -147,12 +147,12 @@ # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", # CORS "corsheaders", # django-rest-framework @@ -176,13 +176,13 @@ MIDDLEWARE = [ # Django default middlewares - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", # CORS "corsheaders.middleware.CorsMiddleware", # simple-history @@ -191,7 +191,7 @@ "core.middleware.request_response_logger.RequestResponseLogger", ] -ROOT_URLCONF = 'core.urls' +ROOT_URLCONF = "core.urls" TEMPLATES = [ { @@ -210,7 +210,7 @@ }, ] -WSGI_APPLICATION = 'core.wsgi.application' +WSGI_APPLICATION = "core.wsgi.application" # Database @@ -239,19 +239,19 @@ # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, - {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, - {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, - {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ -LANGUAGE_CODE = 'ko-kr' +LANGUAGE_CODE = "ko-kr" -TIME_ZONE = 'Asia/Seoul' +TIME_ZONE = "Asia/Seoul" USE_I18N = True @@ -284,7 +284,7 @@ # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'core.fields.UUIDAutoField' +DEFAULT_AUTO_FIELD = "core.fields.UUIDAutoField" AUTH_USER_MODEL = "user.UserExt" diff --git a/app/core/urls.py b/app/core/urls.py index 2d12b53..715103c 100644 --- a/app/core/urls.py +++ b/app/core/urls.py @@ -15,16 +15,14 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +import core.health_check from django.conf import settings from django.conf.urls.static import static from django.contrib import admin -from django.urls import path +from django.urls import include, path, re_path, resolvers from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView -import core.health_check - -v1_apis = [ -] +v1_apis: list[resolvers.URLPattern | resolvers.URLResolver] = [] # type: ignore[assignment] urlpatterns = [ # Health Check @@ -32,6 +30,8 @@ path("livez/", core.health_check.livez), # Admin path("admin/", admin.site.urls), + # V1 API + re_path("^v1/", include((v1_apis, "v1"), namespace="v1")), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) if settings.DEBUG: diff --git a/app/core/wsgi.py b/app/core/wsgi.py index 050d8bc..ea0bfdb 100644 --- a/app/core/wsgi.py +++ b/app/core/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") application = get_wsgi_application() diff --git a/app/manage.py b/app/manage.py index f2a662c..8ca2ab6 100755 --- a/app/manage.py +++ b/app/manage.py @@ -4,9 +4,9 @@ import sys -def main(): +def main() -> None: """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/app/user/admin.py b/app/user/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/app/user/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/app/user/apps.py b/app/user/apps.py index 2e4c3ba..df7e550 100644 --- a/app/user/apps.py +++ b/app/user/apps.py @@ -2,5 +2,5 @@ class UserConfig(AppConfig): - default_auto_field = 'core.fields.UUIDAutoField' - name = 'user' + default_auto_field = "core.fields.UUIDAutoField" + name = "user" diff --git a/app/user/migrations/0001_initial.py b/app/user/migrations/0001_initial.py index c814b76..ccbd3ba 100644 --- a/app/user/migrations/0001_initial.py +++ b/app/user/migrations/0001_initial.py @@ -1,46 +1,118 @@ # Generated by Django 5.2 on 2025-04-06 09:30 +import uuid + import core.fields import django.contrib.auth.models import django.contrib.auth.validators +import django.db.migrations +import django.db.models import django.utils.timezone -import uuid -from django.db import migrations, models -class Migration(migrations.Migration): +class Migration(django.db.migrations.Migration): initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ - migrations.CreateModel( - name='UserExt', + django.db.migrations.CreateModel( + name="UserExt", fields=[ - ('id', core.fields.UUIDAutoField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ( + "id", + core.fields.UUIDAutoField( + auto_created=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", django.db.models.CharField(max_length=128, verbose_name="password")), + ("last_login", django.db.models.DateTimeField(blank=True, null=True, verbose_name="last login")), + ( + "is_superuser", + django.db.models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + django.db.models.CharField( + error_messages={"unique": "A user with that username already exists."}, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], + verbose_name="username", + ), + ), + ("first_name", django.db.models.CharField(blank=True, max_length=150, verbose_name="first name")), + ("last_name", django.db.models.CharField(blank=True, max_length=150, verbose_name="last name")), + ("email", django.db.models.EmailField(blank=True, max_length=254, verbose_name="email address")), + ( + "is_staff", + django.db.models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + django.db.models.BooleanField( + default=True, + help_text=( + "Designates whether this user should be treated as active. " + "Unselect this instead of deleting accounts." + ), + verbose_name="active", + ), + ), + ( + "date_joined", + django.db.models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined"), + ), + ( + "groups", + django.db.models.ManyToManyField( + blank=True, + help_text=( + "The groups this user belongs to. " + "A user will get all permissions granted to each of their groups." + ), + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + django.db.models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, }, managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), ] diff --git a/app/user/tests.py b/app/user/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/app/user/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/app/user/views.py b/app/user/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/app/user/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. From fa14451a31a5337e834a1f08ee13ad655fe89fe8 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 6 Apr 2025 20:18:22 +0900 Subject: [PATCH 09/13] =?UTF-8?q?fix:UserExt=20=EB=AA=A8=EB=8D=B8=EC=9D=98?= =?UTF-8?q?=20PK=EA=B0=80=20int=EA=B0=80=20=EC=95=84=EB=8B=88=EB=A9=B4=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=EC=9D=B4?= =?UTF-8?q?=20=EC=8B=A4=ED=8C=A8=ED=95=98=EB=8A=94=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/fields.py | 12 +++++++----- app/user/apps.py | 2 +- app/user/migrations/0001_initial.py | 14 +++----------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/app/core/fields.py b/app/core/fields.py index 4e4fe12..45cff2a 100644 --- a/app/core/fields.py +++ b/app/core/fields.py @@ -3,15 +3,17 @@ import typing import uuid -from django.core.validators import _ValidatorCallable from django.db.backends.base.operations import BaseDatabaseOperations from django.db.models import AutoField, UUIDField, expressions, fields from django.db.models.fields.reverse_related import ForeignObjectRel -from django.utils.choices import _Choices -from django.utils.functional import _StrOrPromise BaseDatabaseOperations.integer_field_ranges["UUIDField"] = (0, 0) +_ValidatorCallable: typing.TypeAlias = collections.abc.Callable[[typing.Any], None] +_Choice: typing.TypeAlias = tuple[typing.Any, typing.Any] +_ChoiceNamedGroup: typing.TypeAlias = tuple[str, collections.abc.Iterable[_Choice]] +_Choices: typing.TypeAlias = collections.abc.Iterable[_Choice | _ChoiceNamedGroup] + class UUIDFieidInitKwargs(typing.TypedDict): name: typing.NotRequired[str | None] @@ -30,7 +32,7 @@ class UUIDFieidInitKwargs(typing.TypedDict): unique_for_month: typing.NotRequired[str | None] unique_for_year: typing.NotRequired[str | None] choices: typing.NotRequired[_Choices | None] - help_text: typing.NotRequired[_StrOrPromise] + help_text: typing.NotRequired[str] db_column: typing.NotRequired[str | None] db_comment: typing.NotRequired[str | None] db_tablespace: typing.NotRequired[str | None] @@ -44,7 +46,7 @@ class UUIDAutoField(UUIDField, AutoField): _pyi_private_get_type: uuid.UUID # type: ignore[assignment] _pyi_lookup_exact_type: uuid.UUID # type: ignore[assignment] - def __init__(self, verbose_name: _StrOrPromise | None = None, **kwargs: typing.Unpack[UUIDFieidInitKwargs]) -> None: + def __init__(self, verbose_name: str | None = None, **kwargs: typing.Unpack[UUIDFieidInitKwargs]) -> None: kwargs.setdefault("default", uuid.uuid4) kwargs.setdefault("editable", False) super().__init__(verbose_name, **kwargs) diff --git a/app/user/apps.py b/app/user/apps.py index df7e550..578292c 100644 --- a/app/user/apps.py +++ b/app/user/apps.py @@ -2,5 +2,5 @@ class UserConfig(AppConfig): - default_auto_field = "core.fields.UUIDAutoField" + default_auto_field = "django.db.models.BigAutoField" name = "user" diff --git a/app/user/migrations/0001_initial.py b/app/user/migrations/0001_initial.py index ccbd3ba..494a95a 100644 --- a/app/user/migrations/0001_initial.py +++ b/app/user/migrations/0001_initial.py @@ -1,8 +1,5 @@ -# Generated by Django 5.2 on 2025-04-06 09:30 +# Generated by Django 5.2 on 2025-04-06 11:15 -import uuid - -import core.fields import django.contrib.auth.models import django.contrib.auth.validators import django.db.migrations @@ -24,13 +21,8 @@ class Migration(django.db.migrations.Migration): fields=[ ( "id", - core.fields.UUIDAutoField( - auto_created=True, - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - verbose_name="ID", + django.db.models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ("password", django.db.models.CharField(max_length=128, verbose_name="password")), From 05d51ebf673d7e609076079b5f4bfd289cebdf3c Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 6 Apr 2025 20:31:21 +0900 Subject: [PATCH 10/13] =?UTF-8?q?fix:=EC=8B=A4=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=EB=90=9C=20pyc=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/__pycache__/__init__.cpython-313.pyc | Bin 159 -> 0 bytes app/core/__pycache__/settings.cpython-313.pyc | Bin 2483 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/core/__pycache__/__init__.cpython-313.pyc delete mode 100644 app/core/__pycache__/settings.cpython-313.pyc diff --git a/app/core/__pycache__/__init__.cpython-313.pyc b/app/core/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 66a1a124d0862fb8f63c6840e6f4c7625aba2ff7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 159 zcmey&%ge<81eZ-drGx0lAOZ#$p^VQgK*m&tbOudEzm*I{OhDdekklY&qU_>=#N^cYg39FlJpH7^KEC8+fC?x;@ diff --git a/app/core/__pycache__/settings.cpython-313.pyc b/app/core/__pycache__/settings.cpython-313.pyc deleted file mode 100644 index 4708f57099f94f9c13256c0677782e2329195c46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2483 zcmb7G-EZ1f6t{u!4Iz{?X}e8ysT$EPcG4#6HnrIta8gzRgbU49RkIxXLYx?z-fNo0 zPrUDCFO&8!?7wN$hgN=yw5LgXAgT7gV?z=|BCSJkeee1BopXQpoNGQL5>W;|Zg2Ty&zhUwlgBZdf7WtmB-}{I!%e?e`&b;)mumh`FD6w3eO-sV<<%82s9{wTwQdFU1U*=3-BySpg2jQ1WLXP zqRE9A!=M!BTV%;3nnG!?nnKsSG2@NXi!2`?DRiAop&62fc-P>UL9=8U-5}RnH*>e- zdDvF8uqPWWGfS~$X-11?nC|0fo~ z&5=NVxyPAfoj_H#cI~wx2bw{#(j|C7dZjHt*zb}DPt@Fft*I_PF<&bOKP~SbF5RZe z;hZ+NcSpH1_YfP>qvc1rw()g67ql}27)~(_i)cHXN2z^nT-t?*x5k973pOn4u!E^R zGp3}NQlR;=q74eU1s*rYJuEmF#XSz13)u@pZCg`S9UsU9bKX8R>ySh4hGDXKuCtfN z!G__v5q~t0jd*Cjf&`5}ab+Z`R6 zjN1WUY^Y~#z!kiHs}I%GNqcAivn4@;D3Fm!XwGln8;7}U#S#1pn!vK}4{guu&L!yz zdzU=>;h6C`F;_U8>2d)lv?k=AiRFS5C=_eR2@BP#3qQ1@ARNwfJ|`%aH;QG^36zCRk-*K!*iXl@_UfP` zQ+i+$Rf5|p{EJ^M~;mQf7rv4f`!4BLJhUJ9U1SGo0>_m~6 zuZeZ(nfRkKQ4rS}8%{zfl`7j}L3&z2b>t+AWmE?d2%t8Q6WuHp3MFw{sEJOzR;kpb zMy-^ul-Hd|UEHjegt~~F)Hd2Ey7r}FUU19Ei4=so;D$xctk9@Gm8t?l+m%{DdM=cT zpsYakB}zhhqakdFQod3U0kF6!N-ru;&Ce{RL!R2jsjP-ANb3b)iwJ zOMqOF){A1P@CWn3eHA#XyjwYxZ+EHLwA?$Ahtk@o9a+Ir=Mb8MA2u=%)y=!Le*QY) zI)_g9F|gj%@hZ6saPI#L>VvWGXIb{d7i9zg%rfkalOPjM{Tlvd^{yW{>09QzkH%?E8b6=t(3t5$yXD312VL|BT7pf5W5zbt3sb&Am^}{{HwVHGeYU zPl3&ZXLFN@Ouk9I==*)(7xos_(Z$y2qBdAeCqD7LVP^YxnW@=c;y6CliyX%$d*Ne9 WI@pgh$r(sA%3NPLV$$ReaPcqYM=+cK From ff5a1666421b5ca2d425961ab2f7d424e4b02ea8 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 7 Apr 2025 07:41:06 +0900 Subject: [PATCH 11/13] =?UTF-8?q?fix:mypy=EB=A5=BC=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 48 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77eb4f5..b702fb0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,27 +65,27 @@ repos: - drf-standardized-errors - djangorestframework-stubs[compatible-mypy] - zappa-django-utils -- repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.15.0' - hooks: - - id: mypy - additional_dependencies: - - boto3 - - django-constance - - django-cors-headers - - django-environ - - django-extensions - - django-filter - - django-simple-history - - django-stubs[compatible-mypy] - - drf-spectacular - - drf-standardized-errors - - djangorestframework-stubs[compatible-mypy] - - zappa-django-utils - args: - - --no-strict-optional - - --ignore-missing-imports - - --check-untyped-defs - - --disallow-untyped-defs - - --disallow-incomplete-defs - - --disallow-untyped-calls +# - repo: https://github.com/pre-commit/mirrors-mypy +# rev: 'v1.15.0' +# hooks: +# - id: mypy +# additional_dependencies: +# - boto3 +# - django-constance +# - django-cors-headers +# - django-environ +# - django-extensions +# - django-filter +# - django-simple-history +# - django-stubs[compatible-mypy] +# - drf-spectacular +# - drf-standardized-errors +# - djangorestframework-stubs[compatible-mypy] +# - zappa-django-utils +# args: +# - --no-strict-optional +# - --ignore-missing-imports +# - --check-untyped-defs +# - --disallow-untyped-defs +# - --disallow-incomplete-defs +# - --disallow-untyped-calls From 4629cd391e5d7ca7b11a9c42d1c18575886a1c84 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 7 Apr 2025 07:47:36 +0900 Subject: [PATCH 12/13] =?UTF-8?q?add:uv-lock=20&=20ruff=EB=A5=BC=20pre-com?= =?UTF-8?q?mit=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 15 +++++++++++++-- app/manage.py | 1 + app/user/migrations/0001_initial.py | 1 - pyproject.toml | 7 +++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b702fb0..97e92ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,8 +31,8 @@ repos: - flake8-bugbear - flake8-noqa args: - - --max-line-length=120 - - --max-complexity=18 + - --max-line-length=120 + - --max-complexity=18 - repo: https://github.com/psf/black rev: 25.1.0 hooks: @@ -65,6 +65,17 @@ repos: - drf-standardized-errors - djangorestframework-stubs[compatible-mypy] - zappa-django-utils +- repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.6.12 + hooks: + - id: uv-lock +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.4 + hooks: + - id: ruff + args: + - --fix + - id: ruff-format # - repo: https://github.com/pre-commit/mirrors-mypy # rev: 'v1.15.0' # hooks: diff --git a/app/manage.py b/app/manage.py index 8ca2ab6..97ea5c0 100755 --- a/app/manage.py +++ b/app/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys diff --git a/app/user/migrations/0001_initial.py b/app/user/migrations/0001_initial.py index 494a95a..a2360bb 100644 --- a/app/user/migrations/0001_initial.py +++ b/app/user/migrations/0001_initial.py @@ -8,7 +8,6 @@ class Migration(django.db.migrations.Migration): - initial = True dependencies = [ diff --git a/pyproject.toml b/pyproject.toml index bbc6c29..134d284 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,3 +61,10 @@ known_third_party = ["rest_framework"] [tool.django-stubs] django_settings_module = "core.settings" + +[tool.ruff] +line-length = 120 +target-version = "py312" + +[tool.ruff.lint] +fixable = ["ALL"] From 713f19fad0752866aad225fdabfcfcb708bf1883 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 7 Apr 2025 07:58:23 +0900 Subject: [PATCH 13/13] =?UTF-8?q?add:sentry=EC=9D=98=20send=5Fdefault=5Fpi?= =?UTF-8?q?i=20=EC=98=B5=EC=85=98=20=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/core/settings.py b/app/core/settings.py index 18ee0e7..c362b7d 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -366,6 +366,7 @@ def traces_sampler(ctx: dict[str, typing.Any]) -> float: release=DEPLOYMENT_RELEASE_VERSION, traces_sampler=traces_sampler, profiles_sample_rate=SENTRY_PROFILES_SAMPLE_RATE, + send_default_pii=True, integrations=[ sentry_sdk.integrations.aws_lambda.AwsLambdaIntegration(), sentry_sdk.integrations.django.DjangoIntegration(),