diff --git a/.envrc b/.envrc index 4f4b92b..d12bf3a 100644 --- a/.envrc +++ b/.envrc @@ -11,6 +11,13 @@ # so entry is instant instead of re-evaluating the flake on every single cd use flake +# python tools come from uv.lock now. sync .venv on cd and put it on PATH. +# plain sync (not --locked like ci) so editing a pyproject updates the lock for you +uv sync --quiet +watch_file uv.lock pyproject.toml lambda/*/pyproject.toml codebuild/*/pyproject.toml +export VIRTUAL_ENV="$PWD/.venv" +PATH_add .venv/bin + # keep the AWS_PROFILE / AWS_REGION you'd rather not commit in .envrc.local, # which is gitignored. missing file is fine -- that's what the _if_exists is for dotenv_if_exists .envrc.local diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 1b930bc..8673bb7 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -2,27 +2,15 @@ #║ CraftForm ║ #╠══════════════════════════════════════════════════════════════════════════════╣ #║ DEPENDABOT :: dependabot.yaml ║ -#║ Preforms automated dependency and security checks ║ +#║ Performs automated dependency and security checks ║ #║ Checks to make sure there aren't newer version for actions and dependenceies║ #╚══════════════════════════════════════════════════════════════════════════════╝ version: 2 updates: - # scan the requirements.txt and dependencies for vulnerabilities or updates - - package-ecosystem: 'pip' - directory: "/lambda/startup" - schedule: - interval: 'weekly' - - # scan the requirements.txt and dependencies for vulnerabilities or updates - - package-ecosystem: 'pip' - directory: '/lambda/operations' - schedule: - interval: 'weekly' - - # scan the requirements.txt and dependencies for vulnerabilities or updates - - package-ecosystem: 'pip' - directory: '/codebuild/server' + # one entry covers everything -- the whole repo shares one uv.lock + - package-ecosystem: 'uv' + directory: '/' schedule: interval: 'weekly' diff --git a/.github/workflows/ci-all.yaml b/.github/workflows/ci-all.yaml index c3a9125..92fd1c0 100644 --- a/.github/workflows/ci-all.yaml +++ b/.github/workflows/ci-all.yaml @@ -2,7 +2,7 @@ #║ CraftForm ║ #╠══════════════════════════════════════════════════════════════════════════════╣ #║ WORKFLOW :: ci-all.yaml ║ -#║ Preforms automated code checks on the code pushed into main branch ║ +#║ Performs automated code checks on the code pushed into main branch ║ #║ Helps Lint, Security Check, and basic validation for all the code ║ #╚══════════════════════════════════════════════════════════════════════════════╝ @@ -44,13 +44,16 @@ jobs: - name: Checkout Repo uses: actions/checkout@v7 - - name: Setup Python - uses: actions/setup-python@v7 + - name: Setup uv + Python + uses: astral-sh/setup-uv@v10.3.3 with: - python-version: "3.12" + python-version: "3.12" # uv version comes from the root pyproject -- see lambda-ci.yaml + # cfn-lint is in uv.lock. --only-dev skips the packages' own deps - name: Install cfn-lint - run: pip install cfn-lint==1.43.3 # pinned to the flake + run: | + uv sync --locked --only-dev + echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" - name: Lint CloudFormation Templates run: cfn-lint ./cloudformation/*.yaml @@ -131,15 +134,12 @@ jobs: - name: Checkout Repo uses: actions/checkout@v7 - - name: Setup Python - uses: actions/setup-python@v7 + - name: Setup uv + Python + uses: astral-sh/setup-uv@v10.3.3 with: - python-version: "3.12" + python-version: "3.12" # uv version comes from the root pyproject -- see lambda-ci.yaml - - name: Install Checkov - run: | - pip install checkov==3.3.9 # pinned to the flake - + # uvx runs checkov in its own throwaway env -- kept OUT of uv.lock, its deps are huge - name: Run Checkov IAC Check run: | - checkov --config-file .checkov.yaml \ No newline at end of file + uvx checkov==3.3.9 --config-file .checkov.yaml # pinned to the flake \ No newline at end of file diff --git a/.github/workflows/lambda-ci.yaml b/.github/workflows/lambda-ci.yaml index be259a1..c2b06c8 100644 --- a/.github/workflows/lambda-ci.yaml +++ b/.github/workflows/lambda-ci.yaml @@ -2,7 +2,7 @@ #║ CraftForm ║ #╠══════════════════════════════════════════════════════════════════════════════╣ #║ WORKFLOW :: lambda-ci.yaml ║ -#║ Preforms automated code checks on the lambda python code ║ +#║ Performs automated code checks on the lambda python code ║ #║ Acts as a child workflow to be called in other workflows ║ #╚══════════════════════════════════════════════════════════════════════════════╝ @@ -44,15 +44,20 @@ jobs: - name: Checkout Repo uses: actions/checkout@v7 - - name: Setup Python - uses: actions/setup-python@v7 + # no `version:` on purpose -- setup-uv reads required-version from the root pyproject.toml, + # the ONE place the uv version lives (matches the flake's uv) + - name: Setup uv + Python + uses: astral-sh/setup-uv@v10.3.3 with: - python-version: "3.12" - - # install everything needed for the ci from the requirements-dev.txt + python-version: "3.12" # uv finds or downloads it + + # install from uv.lock. --locked FAILS if the lock is stale instead of quietly fixing it. + # then put .venv/bin on PATH - name: Install Dependencies + working-directory: ${{ github.workspace }} # repo root, where .venv lives run: | - pip install -r requirements-dev.txt + uv sync --locked + echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" # checks formatting using ruff in read-only mode. fails if ruff would change anything - name: Check Formatting @@ -70,3 +75,8 @@ jobs: - name: Security Scan run: bandit -r . -ll + # runs the tests. exit code 5 = "found no tests" -- let that pass while packages are still + # getting their first tests. TODO: drop the `|| [ $? -eq 5 ]` once every package has one + - name: Tests + run: pytest || [ $? -eq 5 ] + diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7c33085..a448ff3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,16 +1,24 @@ - #╔══════════════════════════════════════════════════════════════════════════════╗ #║ CraftForm ║ #╠══════════════════════════════════════════════════════════════════════════════╣ #║ GITHUB ACTIONS :: release.yaml ║ #║ Builds every artifact a CraftForm install pulls down -- the startup and ║ -#║ operations lambda zips, plus the terraform source with its buildspec ║ -#║ riding inside it -- then publishes them as GitHub release assets for the ║ -#║ bootstrap stager to pull. The workflow is not heavy enough to qualify ║ -#║ for a matrix approach ║ +#║ operations lambda zips, the server bake zip, plus the terraform source ║ +#║ with its buildspec riding inside it -- then publishes them as GitHub ║ +#║ release assets for the bootstrap stager to pull. The workflow is not ║ +#║ heavy enough to qualify for a matrix approach ║ #╚══════════════════════════════════════════════════════════════════════════════╝ +#============================================================ +# PYTHON PACKAGES +#============================================================ +# every python folder that ships -- each one becomes .zip, +# built from the uv package craftform- +env: + PACKAGES: lambda/startup lambda/operations codebuild/server + + name: Release Workflow @@ -34,10 +42,15 @@ on: push: tags: ['v*.*.*'] # trigger on version tags like v1.2.3 -# GIT PERMISSIONS: allow the git token to write to repo +# GIT PERMISSIONS: read-only by default. only the release job below gets to write permissions: - contents: write # needed to create releases and upload assets + contents: read +# ONE RELEASE AT A TIME: a tag push + a manual run for the same version can't race each +# other uploading zips. queue instead of cancel, a half-cancelled release is worse +concurrency: + group: release-${{ inputs.version || github.ref_name }} + cancel-in-progress: false #============================================================ @@ -46,51 +59,78 @@ permissions: jobs: + # CHECKS JOB: the exact same ruff/mypy/bandit/pytest run as every PR. if the code is broken, + # nothing gets published -- "latest" is what every install downloads + checks: + uses: ./.github/workflows/lambda-ci.yaml + # RELEASE JOB: job for building every artifact an install pulls down, creating the new release, and uploading them release: - if: github.repository == 'Liamade/CraftForm' # only run this workflow in the main repo, not in forks + needs: checks + # only the main repo (not forks), and only from main or a version tag -- a random branch + # can never turn into a release + if: >- + github.repository == 'Liamade/CraftForm' && + (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) runs-on: ubuntu-latest + permissions: + contents: write # needed to create releases and upload assets + # actions in here are pinned to an exact commit, not a tag -- this job can write to the repo, + # so a moved tag upstream can't sneak new code into it. dependabot bumps the sha + comment steps: # CHECKOUT AND SETUP: check out the code - - name: Checkout Repository - uses: actions/checkout@v7 + - name: Checkout Repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - # PYTHON SETUP: set up python 3.12 for building the lambda code - - name: Set up Python - uses: actions/setup-python@v7 + # UV + PYTHON SETUP: python 3.12 for building the python code, uv to pull its deps out of uv.lock. + # the uv version comes from required-version in the root pyproject.toml + - name: Set up uv + Python + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: python-version: '3.12' - # BUILD LAMBDA CODE: install dependencies and build the lambda code into a zip file for distribution - - name: Build Lambda Code + # BUILD PYTHON CODE: install dependencies and build each python package into a zip file for distribution + - name: Build Python Code run: | - # BUILD OUT THE LAMBDA CODE - for function in startup operations; do - - - # DEPENDENCIES -- install required dependencies from the requirements.txt - pip install -r lambda/$function/requirements.txt -t lambda-build/$function - - # COPY DIRECTORY -- copy everthing except certain files + # BUILD OUT EVERY PYTHON PACKAGE -- the two lambdas and the server bake, all the same way + for dir in $PACKAGES; do + name=$(basename "$dir") # lambda/startup -> startup, codebuild/server -> server + + + # DEPENDENCIES -- pull this package's runtime deps out of uv.lock and install them INTO the zip. + # --locked breaks the release if the lock is stale. lands in RUNNER_TEMP so it stays out of the zip + uv export --package "craftform-$name" --no-dev --locked --no-emit-workspace \ + -o "$RUNNER_TEMP/$name-requirements.txt" + # --python-platform: grab the builds that run on LAMBDA's linux (glibc 2.34), not whatever + # suits this runner -- a newer runner could otherwise pull a pynacl that lambda can't load + uv pip install -r "$RUNNER_TEMP/$name-requirements.txt" --target "lambda-build/$name" \ + --python-platform x86_64-manylinux_2_34 --python-version 3.12 + + # COPY DIRECTORY -- copy everthing except certain files (tests + dev config stay out of the zip) rsync -a \ --exclude='__pycache__' \ --exclude='*.pyc' \ --exclude='.mypy_cache' \ - --exclude='requirements*.txt' \ - --exclude='requirements-dev.txt' \ - lambda/$function/ lambda-build/$function/ + --exclude='.pytest_cache' \ + --exclude='pyproject.toml' \ + --exclude='tests' \ + "$dir/" "lambda-build/$name/" # ZIP FILES -- zip files to make an actual release ( - cd lambda-build/$function - zip -r ../../$function.zip . + cd "lambda-build/$name" + zip -r "../../$name.zip" . ) done + # SANITY CHECK -- codebuild reads buildspec.yaml from the ROOT of server.zip, so same deal + # as terraform.zip below: break the release here instead of every /server create :( + unzip -l server.zip buildspec.yaml >/dev/null || { echo "buildspec.yaml missing from server.zip :("; exit 1; } + # BUILD TERRAFORM SOURCE: the codebuild project pulls this down and runs it on every /region create or delete - name: Build Terraform Source run: | @@ -111,21 +151,33 @@ jobs: # SANITY CHECK -- if the buildspec didn't make it in, every region deploy is dead, so # break the release here instead of letting a user find out weeks later :( - unzip -l terraform.zip | grep -q 'buildspec.yaml' || { echo "buildspec.yaml missing from terraform.zip :("; exit 1; } + unzip -l terraform.zip buildspec.yaml >/dev/null || { echo "buildspec.yaml missing from terraform.zip :("; exit 1; } - # CREATE GITHUB RELEASE: create a new release in GitHub using the version number from the workflow input - - name: Publish Release + # CREATE GITHUB RELEASE: create the release as a DRAFT and upload every zip into it. drafts + # are invisible to installs, so nobody can download a half-uploaded release + - name: Create Draft Release id: release # capture the output of this step to get the release ID - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: + draft: true # flipped to published in the next step, once every zip is up name: CraftForm ${{ inputs.version || github.ref_name }} # set the release name tag_name: ${{ inputs.version || github.ref_name }} # set the tag name to the version tag target_commitish: ${{ github.sha }} # set the target commit for the release to the current commit that triggered the workflow generate_release_notes: true # let the action generate release notes based on merged PRs and commits fail_on_unmatched_files: true # fail if the files to upload are not found + # prereleases never count as "latest" -- and the stager downloads latest, so a test build + # can't leak into real installs. tag pushes have no inputs, so they're always false + prerelease: ${{ inputs.prerelease || false }} files: | startup.zip operations.zip + server.zip terraform.zip - + + # PUBLISH RELEASE: every zip made it up (the step above fails otherwise), so make it public + - name: Publish Release + env: + GH_TOKEN: ${{ github.token }} + run: gh api -X PATCH "repos/${{ github.repository }}/releases/${{ steps.release.outputs.id }}" -F draft=false + diff --git a/.gitignore b/.gitignore index c1566c9..c5af4c1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,20 +7,14 @@ __pycache__/ *.pyd *.env +# uv's venv -- `uv sync` rebuilds it from uv.lock .venv/ -venv/ -env/ .pytest_cache/ .mypy_cache/ .coverage htmlcov/ -dist/ -build/ -*.egg-info/ -*.eggs - # ============================================================================== # Ruff # ============================================================================== @@ -64,8 +58,9 @@ terraform.rc # Lambda build artifacts # ============================================================================== *.zip -package/ -lambda-build/ # the release workflow pip-installs deps into here before zipping +# the release workflow installs deps into here before zipping. (comment on its own +# line on purpose -- git only treats # as a comment at the START of a line) +lambda-build/ # ============================================================================== # Secrets & credentials @@ -85,13 +80,7 @@ secrets.json # nix build output symlinks result result-* -# the interpreter symlink `nix build .#python -o .python-env` drops. vscode -# doesn't need it (direnv puts the shell on PATH and pylance follows), but -# pycharm has no direnv and its sdk has to be an absolute path. doubles as a -# gc root, so nix won't collect the interpreter the ide is pointed at :) -.python-env -# PLACEHOLDER COMMENT -- same idea for the tools pycharm pins by absolute path -# (`nix build .#dev-tools -o .dev-env`); also a gc root +# pycharm's tool symlink from `nix build .#dev-tools` -- also a gc root so nix won't delete it :) .dev-env # personal env overrides (AWS_PROFILE etc) that .envrc sources if present .envrc.local @@ -106,8 +95,8 @@ result-* # same deal for pycharm -- share the module, the interpreter NAME and the # inspection profile; keep the personal half out (window layout, local history, # caches, the aws toolkit's active region). misc.xml and the .iml reference the -# sdk by name, so each machine makes its own "CraftForm flake (Python 3.12)" -# pointing at its own .python-env +# sdk by name, so each machine makes its own "CraftForm (Python 3.12)" +# pointing at its own .venv/bin/python # # runConfigurations/ is shared because the `ci` one is portable -- it's # $PROJECT_DIR$ plus `nix develop -c ci`, and nix is on PATH everywhere the diff --git a/.idea/CraftForm.iml b/.idea/CraftForm.iml index 284323a..4641e24 100644 --- a/.idea/CraftForm.iml +++ b/.idea/CraftForm.iml @@ -5,17 +5,14 @@ - - - - + - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 7cc4602..98e6387 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + diff --git a/.vscode/extensions.json b/.vscode/extensions.json index b9b9546..d980e89 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -15,7 +15,7 @@ // ------------------------------- DIRENV ------------------------------ // the one that actually matters. loads .envrc INTO vscode, so the integrated - // terminal and -- more importantly -- pylance both see the flake's python + // terminal and -- more importantly -- pylance both see .venv's python // instead of the system one. without it the editor can't find pynacl or the // boto3 stubs and underlines half the lambda code in red "mkhl.direnv", diff --git a/.vscode/settings.json b/.vscode/settings.json index 0d0b182..d65b2c4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,13 +15,16 @@ // ==================== PYTHON ==================== // ruff is what ci gates on, so let it own linting in-editor too. - // format-on-save is deliberately OFF -- see the note in the chat, ruff would - // flatten the aligned comment blocks this codebase is written around + // format-on-save is deliberately OFF -- ruff would flatten the aligned + // comment blocks this codebase is written around "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": false }, + // the uv venv -- so pylance finds it even without the direnv extension + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "azureTerraform.checkTerraformCmd": false, "yaml.customTags": [ "!And sequence", diff --git a/cloudformation/home-region.yaml b/cloudformation/home-region.yaml index ed7fdc6..d4021d7 100644 --- a/cloudformation/home-region.yaml +++ b/cloudformation/home-region.yaml @@ -1006,8 +1006,8 @@ Resources: # ================AWS CLIENT================ s3 = boto3.client('s3') # used by stage_assets, which BOTH the /update and cloudformation paths call - so it stays global - # the release assets we stage -- the two lambda zips, plus the terraform source - # that the codebuild project pulls down at the start of every region run + # the release assets we stage -- the two lambda zips, the terraform source the region + # codebuild pulls down every run, and the server bake the server codebuild runs assets = ['startup.zip', 'operations.zip', 'terraform.zip', 'server.zip'] base = "https://github.com/Liamade/CraftForm/releases" diff --git a/codebuild/server/bake.py b/codebuild/server/bake.py index 490ad32..6fde6f9 100644 --- a/codebuild/server/bake.py +++ b/codebuild/server/bake.py @@ -27,7 +27,7 @@ # ------------------------------------------------------------------------------------------ from recipes.vanilla import Vanilla -# stub-only packages -- pip'd for ci, NOT in codebuild, so they can only be imported for mypy +# stub-only packages -- in the uv dev group for ci, NOT in codebuild, so they can only be imported for mypy if TYPE_CHECKING: from mypy_boto3_ec2 import EC2Client from mypy_boto3_ec2.type_defs import TagTypeDef diff --git a/codebuild/server/buildspec.yaml b/codebuild/server/buildspec.yaml index e74ff4b..c233b68 100644 --- a/codebuild/server/buildspec.yaml +++ b/codebuild/server/buildspec.yaml @@ -45,11 +45,11 @@ phases: # ---------------------------------------------------------- # SETUP # ---------------------------------------------------------- + # nothing to pip install -- the release already put boto3 + urllib3 inside server.zip, + # right next to bake.py, so python just finds them install: runtime-versions: python: 3.12 - commands: - - pip install -r requirements.txt # ---------------------------------------------------------- # THE BAKE @@ -57,15 +57,3 @@ phases: build: commands: - python bake.py - - - - - - - - - - - - diff --git a/codebuild/server/pyproject.toml b/codebuild/server/pyproject.toml new file mode 100644 index 0000000..ab8c51f --- /dev/null +++ b/codebuild/server/pyproject.toml @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ CraftForm ║ +# ╠══════════════════════════════════════════════════════════════════════════════╣ +# ║ CODEBUILD :: pyproject.toml ║ +# ║ What the AMI bake needs at RUNTIME inside codebuild. ║ +# ║ Dev tools live in the root one. ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ + +# how this connects to the root -- see the note in lambda/operations/pyproject.toml + +[project] +name = "craftform-server" # matches the codebuild project + server.zip +version = "0.0.0" # unused -- git tags version the releases +requires-python = "==3.12.*" # buildspec.yaml's `python: 3.12`, exactly + +# floors, uv.lock pins them. boto3 IS here -- codebuild doesn't promise one +# (the stubs are dev-only, which is why bake.py hides them behind TYPE_CHECKING) +dependencies = [ + "boto3>=1.43.114", # ec2 + ssm + "urllib3>=2", # the discord followup webhook +] + +# just a zipped folder, not a package -- same as the lambdas, see the note in operations +[tool.uv] +package = false + + +# ========================================================================================== +# PYTEST +# ========================================================================================== +# own folder = own pytest world -- see the note in lambda/operations/pyproject.toml +[tool.pytest] +pythonpath = ["."] +testpaths = ["tests"] diff --git a/codebuild/server/recipes/vanilla.py b/codebuild/server/recipes/vanilla.py index 020c9a0..3cf9725 100644 --- a/codebuild/server/recipes/vanilla.py +++ b/codebuild/server/recipes/vanilla.py @@ -6,7 +6,7 @@ import urllib3 from errors import BakeError -# stub-only package -- pip'd for ci, NOT in codebuild, so it can only be imported for mypy +# stub-only package -- in the uv dev group for ci, NOT in codebuild, so it can only be imported for mypy if TYPE_CHECKING: from mypy_boto3_ec2 import EC2Client diff --git a/codebuild/server/requirements-dev.txt b/codebuild/server/requirements-dev.txt deleted file mode 100644 index a5da5d7..0000000 --- a/codebuild/server/requirements-dev.txt +++ /dev/null @@ -1,24 +0,0 @@ -#===================================================================== -# CI - DEPENDENCIES -#===================================================================== -# same deal as lambda/*/requirements-dev.txt -- every version PINNED to whatever -# the nix flake resolves to, so `ci` on a laptop and lambda-ci.yaml on github run -# the exact same tools. keep these in lockstep with the lambdas' copies. -# -# NOT installed by buildspec.yaml -- codebuild only needs requirements.txt to run -# the bake. the mypy_boto3_* stubs this pulls in are why bake.py and the recipes -# only import them under `if TYPE_CHECKING:` -# -# dependabot watches this directory (see .github/dependabot.yaml), so the pins get -# bump PRs on their own. when one lands, move the flake's nixpkgs input in the -# same breath or the drift just points the other way :) - - --r requirements.txt # install the dependencies for the bake code too - - -ruff==0.16.9 # pkgs.ruff -mypy==2.4.1 # ps.mypy -bandit==1.9.4 # ps.bandit -pytest==9.2.0 # ps.pytest -boto3-stubs[essential,ssm]==1.43.114 # ps.boto3-stubs -- [essential] has ec2, ssm is extra diff --git a/codebuild/server/requirements.txt b/codebuild/server/requirements.txt deleted file mode 100644 index b540c3e..0000000 --- a/codebuild/server/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ - - - -boto3 -urllib3>=2.8.0 diff --git a/flake.nix b/flake.nix index 2b5496b..4f2eef0 100644 --- a/flake.nix +++ b/flake.nix @@ -13,8 +13,7 @@ description = "CraftForm -- discord-driven minecraft server infrastructure"; inputs = { - # unstable on purpose -- ruff/cfn-lint/checkov move fast, and the stable channel - # lags far enough behind to start disagreeing with what CI pip-installs + # unstable on purpose -- checkov/terraform move fast and stable lags behind nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; @@ -46,59 +45,30 @@ permittedInsecurePackages = [ "python3.14-ecdsa-0.19.2" ]; }; })); - - # ==================================================================== - # PYTHON - # ==================================================================== - # mirrors lambda/*/requirements-dev.txt with cfn-lint bolted on. ONE - # interpreter for the whole repo, so mypy sees exactly the stubs the - # rest of the tooling does. - # - # lives up HERE rather than inside the devShell's let so `packages.python` - # can hand an editor the exact same derivation the shell uses -- an ide - # pointed at a different interpreter is an ide that disagrees with ci - pythonFor = pkgs: pkgs.python312.withPackages (ps: [ - ps.cfn-lint # the ci gate on cloudformation/*.yaml - ps.mypy # type check - ps.bandit # python security scan - ps.pynacl # operations lambda -- verifies discord's ed25519 sigs - ps.urllib3 # both lambdas -- the discord + cloudformation http calls - ps.boto3-stubs # what lets mypy understand the boto3 client calls - ps.pytest # PLACEHOLDER COMMENT -- the test runner - ] ++ (with ps.boto3-stubs.optional-dependencies; - # PLACEHOLDER COMMENT -- [essential] covers s3/ec2/lambda but not these three - essential ++ ssm ++ secretsmanager ++ codebuild)); in { # ====================================================================== - # THE EDITOR INTERPRETER + # THE EDITOR TOOLS # ====================================================================== - # `nix build .#python -o .python-env` drops a stable symlink at the repo - # root pointing at the interpreter below. - # - # vscode doesn't need this -- the direnv extension puts the dev shell on - # PATH and pylance follows. pycharm has no direnv, so its sdk has to be an - # absolute path. aim it at .python-env/bin/python3.12 ONCE and a flake.lock - # bump just moves the symlink underneath it, instead of leaving the ide - # quietly pinned to a stale /nix/store path that nothing else uses :) - # - # .python-env is gitignored -- it's a nix gc root, same as .direnv + # pycharm has no direnv -- point its python at .venv/bin/python, uv keeps that path fresh packages = forAllSystems (pkgs: { - python = pythonFor pkgs; - # PLACEHOLDER COMMENT -- same trick as .python-env, for the tools pycharm - # pins by absolute path. `nix build .#dev-tools -o .dev-env` once, then the - # ide points at .dev-env/bin/ and a flake.lock bump just moves it + # `nix build .#dev-tools -o .dev-env` once, then the ide points at + # .dev-env/bin/ and a flake.lock bump just moves the symlink + # underneath it dev-tools = pkgs.symlinkJoin { name = "craftform-dev-tools"; - paths = [ pkgs.ruff pkgs.terraform ]; + paths = [ pkgs.terraform ]; }; }); devShells = forAllSystems (pkgs: let - # the one from up top -- see the note next to pythonFor - python = pythonFor pkgs; + # ==================================================================== + # PYTHON + # ==================================================================== + # BARE 3.12 -- no packages, uv.lock picks those. matches the lambda + codebuild runtimes + python = pkgs.python312; # ==================================================================== # THE CI RUNNER @@ -113,6 +83,11 @@ ROOT="$(git rev-parse --show-toplevel)" || exit 1 FAILED=() + # sync the venv like ci does (--locked = fail if uv.lock is stale) and add it + # to PATH ourselves -- `nix develop -c ci` skips direnv + ( cd "$ROOT" && uv sync --locked --quiet ) || exit 1 + PATH="$ROOT/.venv/bin:$PATH" + step() { # grab the label BEFORE the shift -- after it, $1 is the command local label="$1"; shift @@ -138,12 +113,16 @@ && tflint --recursive --format compact ); } # ---- the python ---- (matches the lambda-ci.yaml matrix) - # takes a path from the repo root, so non-lambda code like the bake fits too + # takes a path from the repo root, so non-lambda code like the bake fits too. + # within ONE package it stops at the first failure, same as the workflow does + # (fix the format diff, run again, then you'll see mypy etc). + # pytest exit 5 = "no tests yet" -- passes for now, same TODO as lambda-ci.yaml py() { ( cd "$ROOT/$1" \ && ruff format --diff . \ && ruff check . \ && mypy . \ - && bandit -r . -ll ); } + && bandit -r . -ll \ + && { pytest || [ $? -eq 5 ]; } ); } # ---- security ---- # the action scans history, so we do too. `git` replaced `detect` in @@ -190,7 +169,9 @@ pkgs.checkov # configured by .checkov.yaml # ================= PYTHON TOOLING ============== - pkgs.ruff # format + lint. the standalone binary, same as pip's + # uv is the ONLY python tool from nix -- everything else comes from uv.lock. + # required-version in the root pyproject.toml must match this -- bump them together + pkgs.uv # 0.12.5 as of this flake.lock # ================ EDITOR SUPPORT =============== # the vscode Nix IDE extension shells out to these -- it does syntax @@ -209,9 +190,9 @@ pkgs.unzip ]; - # mypy writes .mypy_cache wherever it runs -- already gitignored, but this - # keeps the two lambda dirs from fighting over one cache - MYPY_CACHE_DIR = ".mypy_cache"; + # use the flake's 3.12, never let uv download its own + UV_PYTHON = "${python}/bin/python3.12"; + UV_PYTHON_DOWNLOADS = "never"; shellHook = '' # checkov and awscli2 are built against python 3.14, and mkShell helpfully @@ -220,15 +201,23 @@ # importing a C extension built for the wrong ABI. # # nothing here actually wants PYTHONPATH: checkov/aws are wrapped binaries - # that set their own, and python312.withPackages puts our packages on the - # interpreter's real sys.path. so drop it and let each tool use its own :) + # that set their own, and our packages live in .venv's own site-packages. + # so drop it and let each tool use its own :) unset PYTHONPATH + # point uv at the repo root no matter where you run it. without this, + # `uv sync` from inside lambda/operations strips .venv down to JUST that + # package -- no ruff, no mypy. (adding a dep to one package is now + # `uv add --package craftform-operations `) + export UV_PROJECT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + echo "" echo " CraftForm :: dev shell" echo " ---------------------------------------------" echo " ci run every check ci-all.yaml runs" echo "" + echo " no direnv? run: uv sync && source .venv/bin/activate" + echo "" ''; }; }); diff --git a/lambda/operations/pyproject.toml b/lambda/operations/pyproject.toml new file mode 100644 index 0000000..5ebf756 --- /dev/null +++ b/lambda/operations/pyproject.toml @@ -0,0 +1,37 @@ +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ CraftForm ║ +# ╠══════════════════════════════════════════════════════════════════════════════╣ +# ║ OPERATIONS LAMBDA :: pyproject.toml ║ +# ║ What this lambda needs at RUNTIME -- the stuff that ships in the zip. ║ +# ║ Dev tools live in the root one. ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ + +# the root pyproject lists this folder as a workspace member -- this file doesn't point +# back. there's no inheriting in pyproject, so name/version/python have to live here + +[project] +name = "craftform-operations" +version = "0.0.0" # unused -- git tags version the releases +requires-python = "==3.12.*" # the lambda runtime, exactly + +# floors, uv.lock pins them. NO boto3 on purpose -- lambda ships its own +dependencies = [ + "pynacl>=1.6.2", # verifies the ed25519 signature discord signs every interaction with + "urllib3>=2", # the discord http calls +] + +# this is just a folder that gets zipped, not an installable package -- uv installs +# its deps and never tries to build it +[tool.uv] +package = false + + +# ========================================================================================== +# PYTEST +# ========================================================================================== +# lives HERE, not in the root, so pytest treats this folder as its own little world: +# pythonpath -- lets tests do `import responses` the same way the lambda code does +# and it keeps this lambda's index.py from clashing with startup's index.py +[tool.pytest] +pythonpath = ["."] +testpaths = ["tests"] diff --git a/lambda/operations/requirements-dev.txt b/lambda/operations/requirements-dev.txt deleted file mode 100644 index d6210f7..0000000 --- a/lambda/operations/requirements-dev.txt +++ /dev/null @@ -1,22 +0,0 @@ -#===================================================================== -# CI - DEPENDENCIES -#===================================================================== -# every version here is PINNED to whatever the nix flake resolves to, so `ci` on -# a laptop and lambda-ci.yaml on github run the exact same tools. left unpinned, -# the workflow pip-installs whatever is newest that morning and quietly drifts -# away from the dev shell -- ruff especially, its DEFAULT rule set keeps growing, -# so an unpinned bump can turn a green branch red without a line of code changing -# -# dependabot already watches this directory (see .github/dependabot.yaml), so the -# pins get bump PRs on their own. when one lands, move the flake's nixpkgs input -# in the same breath or the drift just points the other way :) - - --r requirements.txt # install the dependencies for the lambda code too - - -ruff==0.17.3 # pkgs.ruff -mypy==2.4.0 # ps.mypy -bandit==1.9.4 # ps.bandit -pytest==9.1.1 # ps.pytest -boto3-stubs[essential,ssm,secretsmanager,codebuild]==1.43.123 # ps.boto3-stubs -- gives mypy the stubs for boto3 diff --git a/lambda/operations/requirements.txt b/lambda/operations/requirements.txt deleted file mode 100644 index ff5f9a9..0000000 --- a/lambda/operations/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -#===================================================================== -# DEPENDENCIES -#===================================================================== - -PyNaCl>=1.6.2 # for verifying the ed25519 signature discord signs every interaction with -urllib3 \ No newline at end of file diff --git a/lambda/operations/tests/conftest.py b/lambda/operations/tests/conftest.py new file mode 100644 index 0000000..a5f8d47 --- /dev/null +++ b/lambda/operations/tests/conftest.py @@ -0,0 +1,22 @@ +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ CraftForm ║ +# ╠══════════════════════════════════════════════════════════════════════════════╣ +# ║ OPERATIONS LAMBDA :: tests/conftest.py ║ +# ║ Shared test setup. pytest loads this file on its own, before any test. ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ +import os + +# ========================================================================================== +# FAKE AWS +# ========================================================================================== +# aws_clients.py builds its boto3 clients the MOMENT it's imported, and boto3 refuses +# to do that without a region. this file runs before any test imports the lambda code, +# so setting these here (not inside a test) is what gets them in early enough. +# +# the fake keys also mean a test can never actually do anything to your real aws +# account -- any call that slips through just fails with bad credentials +os.environ["AWS_DEFAULT_REGION"] = "us-east-1" +os.environ["AWS_ACCESS_KEY_ID"] = "testing" +os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" +os.environ.pop("AWS_SESSION_TOKEN", None) +os.environ.pop("AWS_PROFILE", None) # .envrc.local might set one -- don't let it win diff --git a/lambda/startup/pyproject.toml b/lambda/startup/pyproject.toml new file mode 100644 index 0000000..ec24af9 --- /dev/null +++ b/lambda/startup/pyproject.toml @@ -0,0 +1,32 @@ +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ CraftForm ║ +# ╠══════════════════════════════════════════════════════════════════════════════╣ +# ║ STARTUP LAMBDA :: pyproject.toml ║ +# ║ What this lambda needs at RUNTIME -- the stuff that ships in the zip. ║ +# ║ Dev tools live in the root one. ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ + +# how this connects to the root -- see the note in lambda/operations/pyproject.toml + +[project] +name = "craftform-startup" +version = "0.0.0" # unused -- git tags version the releases +requires-python = "==3.12.*" # the lambda runtime, exactly + +# floors, uv.lock pins them. NO boto3 on purpose -- lambda ships its own +dependencies = [ + "urllib3>=2", # the discord + cloudformation http calls +] + +# just a zipped folder, not a package -- same as operations, see the note there +[tool.uv] +package = false + + +# ========================================================================================== +# PYTEST +# ========================================================================================== +# own folder = own pytest world -- see the note in lambda/operations/pyproject.toml +[tool.pytest] +pythonpath = ["."] +testpaths = ["tests"] diff --git a/lambda/startup/requirements-dev.txt b/lambda/startup/requirements-dev.txt deleted file mode 100644 index 721888b..0000000 --- a/lambda/startup/requirements-dev.txt +++ /dev/null @@ -1,22 +0,0 @@ -#===================================================================== -# CI - DEPENDENCIES -#===================================================================== -# every version here is PINNED to whatever the nix flake resolves to, so `ci` on -# a laptop and lambda-ci.yaml on github run the exact same tools. left unpinned, -# the workflow pip-installs whatever is newest that morning and quietly drifts -# away from the dev shell -- ruff especially, its DEFAULT rule set keeps growing, -# so an unpinned bump can turn a green branch red without a line of code changing -# -# dependabot already watches this directory (see .github/dependabot.yaml), so the -# pins get bump PRs on their own. when one lands, move the flake's nixpkgs input -# in the same breath or the drift just points the other way :) - - --r requirements.txt # install the dependencies for the lambda code too - - -ruff==0.17.3 # pkgs.ruff -mypy==2.4.0 # ps.mypy -bandit==1.9.4 # ps.bandit -pytest==9.1.1 # ps.pytest -boto3-stubs[essential,ssm,secretsmanager,codebuild]==1.43.114 # ps.boto3-stubs -- gives mypy the stubs for boto3 diff --git a/lambda/startup/requirements.txt b/lambda/startup/requirements.txt deleted file mode 100644 index cfa06bf..0000000 --- a/lambda/startup/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -#===================================================================== -# DEPENDENCIES -#===================================================================== - -urllib3 # for the discord + cloudformation http calls \ No newline at end of file diff --git a/lambda/startup/tests/conftest.py b/lambda/startup/tests/conftest.py new file mode 100644 index 0000000..56de777 --- /dev/null +++ b/lambda/startup/tests/conftest.py @@ -0,0 +1,18 @@ +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ CraftForm ║ +# ╠══════════════════════════════════════════════════════════════════════════════╣ +# ║ STARTUP LAMBDA :: tests/conftest.py ║ +# ║ Shared test setup. pytest loads this file on its own, before any test. ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ +import os + +# ========================================================================================== +# FAKE AWS +# ========================================================================================== +# index.py builds its secrets manager client the MOMENT it's imported -- same deal as +# the operations lambda, see the note in lambda/operations/tests/conftest.py +os.environ["AWS_DEFAULT_REGION"] = "us-east-1" +os.environ["AWS_ACCESS_KEY_ID"] = "testing" +os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" +os.environ.pop("AWS_SESSION_TOKEN", None) +os.environ.pop("AWS_PROFILE", None) # .envrc.local might set one -- don't let it win diff --git a/pyproject.toml b/pyproject.toml index cc1fcfb..2a0de5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,53 @@ +# ========================================================================================== +# UV WORKSPACE +# ========================================================================================== +# ties the three python packages into ONE uv.lock. each package lists its own runtime +# deps, the dev tools live once down below. uv.lock has the exact version of everything +[tool.uv.workspace] +members = [ + "lambda/operations", # craftform-operations -- the discord interactions lambda + "lambda/startup", # craftform-startup -- the cloudformation custom resource + "codebuild/server", # craftform-server -- the AMI bake codebuild runs +] + +# the ONE place the uv version lives. your local uv refuses to run if it doesn't match, +# and setup-uv in every workflow installs exactly this. bump it with the flake's pkgs.uv +[tool.uv] +required-version = "==0.12.5" + +# ------------------------------------------------------------------------------------------ +# DEV TOOLS +# ------------------------------------------------------------------------------------------ +# floors -- uv.lock has the exact versions, dependabot bumps it. +# boto3 lives HERE, not in the lambdas -- lambda already ships it, so zipping it is just bloat +[dependency-groups] +dev = [ + "ruff>=0.16.9", # format + lint + "mypy>=2.4.0", # type check + "bandit>=1.9.4", # python security scan + "pytest>=9.1.1", # the test runner + "cfn-lint>=1.43.3", # the ci gate on cloudformation/*.yaml + "boto3>=1.43.114", # the lambdas' runtime-provided boto3, for local tests + + # exactly the aws services the code talks to -- add one here when the code does + "boto3-stubs[ec2,lambda,s3,ssm,secretsmanager,codebuild]>=1.43.114", +] + + +# ========================================================================================== +# MYPY CONFIGURATION +# ========================================================================================== +# mypy walks up to this file from each package, same as ruff. +# check_untyped_defs: check the INSIDE of functions even if they have no type hints -- +# mypy's default skips them completely, so bugs in there would never show up +[tool.mypy] +check_untyped_defs = true + + # ========================================================================================== # RUFF CONFIGURATION # ========================================================================================== -# the ONE ruff config for the repo. both lambdas inherit it -- ruff walks up from +# the ONE ruff config for the repo. every package inherits it -- ruff walks up from # whatever directory it's invoked in, so `cd lambda/operations && ruff check .` # (what ci and the lambda-ci workflow both do) lands here. @@ -13,11 +59,11 @@ line-length = 160 # change the threshold of characters per line # ------------------------------------------------------------------------------------------ # `select` is deliberately LEFT AT THE DEFAULT. ruff's default set has grown over # time (0.16 lints a good deal more than the old E4/E7/E9/F), so what pins our -# behaviour is the ruff VERSION pinned in lambda/*/requirements-dev.txt to match -# the flake -- not a hand-maintained list here that'd rot on its own. +# behaviour is the ruff VERSION pinned in uv.lock -- not a hand-maintained list +# here that'd rot on its own. [tool.ruff.lint] -# placeholder: pep8-naming on top of the default set +# pep8-naming on top of the default set extend-select = ["N"] # rules turned off on purpose, with the reason. same spirit as the skip-check diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..26cef85 --- /dev/null +++ b/uv.lock @@ -0,0 +1,671 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" + +[manifest] +members = [ + "craftform-operations", + "craftform-server", + "craftform-startup", +] + +[manifest.dependency-groups] +dev = [ + { name = "bandit", specifier = ">=1.9.4" }, + { name = "boto3", specifier = ">=1.43.114" }, + { name = "boto3-stubs", extras = ["ec2", "lambda", "s3", "ssm", "secretsmanager", "codebuild"], specifier = ">=1.43.114" }, + { name = "cfn-lint", specifier = ">=1.43.3" }, + { name = "mypy", specifier = ">=2.4.0" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "ruff", specifier = ">=0.16.9" }, +] + +[[package]] +name = "ast-serialize" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/d5/5dc1ee8a80c3b20a0b63c61b43ba6bc53517faecd1def47bcd752d22268e/ast_serialize-0.13.1.tar.gz", hash = "sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025", size = 955137, upload-time = "2026-10-17T00:04:17.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/e2/b0c140fe50fa39dd249e382f7733af6f816c90021fe7959a0b026f86237f/ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:23d768369b89d551077dc173da6892fad00443338417523b6732f4e8ba4a7ed5", size = 1236160, upload-time = "2026-10-17T00:03:21.945Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/a8a2ca21e3943555b48221f8fba546877a7ad39efaf6505506524967d7c4/ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:68f8829d114dab6c9db4a8bbb8d0e342958faee1b5e15a29022678df639960f8", size = 1222214, upload-time = "2026-10-17T00:03:23.462Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/2f58f2ba64e4799b39de85c140aef09a03aa6ea53be75dd292a9d9b9d8ae/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e385b17d5bde892a6184003a9aff3518387908b73f8f5ea539313c07c8c0ede", size = 1282979, upload-time = "2026-10-17T00:03:24.695Z" }, + { url = "https://files.pythonhosted.org/packages/b1/af/ca6087fe84210c1ccd43e3182917bb70649fa1e69745a90c1d59ba08fea8/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02686b281ec9b8e9614f6f0db27e12e4cac515f336c5d05b678bc9eb73ff2af5", size = 1288596, upload-time = "2026-10-17T00:03:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2c/4ef964c763329a0347125e745ec0451ba4fbaceec9efe2ac729b7cd55f17/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8ab2c2585b2e22e48cd4aff2136fce4eab4ced569b85f150ef9a5a5d6f34d49", size = 1545540, upload-time = "2026-10-17T00:03:27.52Z" }, + { url = "https://files.pythonhosted.org/packages/bb/85/aa961dbb75813cfa94de0ce9a16a9510c226b95f604c405421e727f44bb0/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c652d9f5d52d8b81eff3ff2c95c9dd0f281de45f6ad5d3fa95527550c396454a", size = 1309374, upload-time = "2026-10-17T00:03:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/f2b67c327d9f9533e41791804403ccc2dc457821195a2e51f0b587a90b90/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121e1d08d58b75c15491cc3003609cbad71ac2b0016043c36d30a6b2032b6605", size = 1303466, upload-time = "2026-10-17T00:03:30.451Z" }, + { url = "https://files.pythonhosted.org/packages/73/1d/33f18b81d1bb1f5d682943ea92fda8130a6593627f12f6201668d40824fd/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:30cf89b6a6844542d116c758725d13de5b97b9410a51ee577715f761270d0c1c", size = 1300576, upload-time = "2026-10-17T00:03:32.102Z" }, + { url = "https://files.pythonhosted.org/packages/25/cb/64914144925f1a4abdd9217e717b122255c38fc5ebe4342f92052f3bacbc/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:732aaae9e498c973fc63fd7f308009b57c4afa6ff59d0b674d894e57d0bca01a", size = 1355960, upload-time = "2026-10-17T00:03:33.676Z" }, + { url = "https://files.pythonhosted.org/packages/ee/80/bd29379fab2bce19adc0bf9177002b11b23dfe8d8fecf409998b458b4624/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d9c1a920b2aa0c1e016bed7721689717034ca0dba64ac3a3ff243b003d4e8dd8", size = 1459779, upload-time = "2026-10-17T00:03:35.09Z" }, + { url = "https://files.pythonhosted.org/packages/83/89/012dbe733de05ffdf8e40ca0f9fa5d417cb16ec93a74463c57c001914198/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:847cad4c22953823f5e76d0b126c6f9da670cfa45b1df41b9324e22189d77611", size = 1563399, upload-time = "2026-10-17T00:03:37.285Z" }, + { url = "https://files.pythonhosted.org/packages/69/37/ee4bcb0358bee38744eb0f18560446572d095fe3bf6b63d6f739d08a839c/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:aca07de3ca8e3d9f2b716c3c31f4f384fe4bd2d03e09912417cff7a3e08036f7", size = 1558808, upload-time = "2026-10-17T00:03:38.711Z" }, + { url = "https://files.pythonhosted.org/packages/54/1c/ff44c5355251d61ef22934c4430bb31a61de4781d25a992154aac9b8c82a/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:5db12d843d931b9d1f1519ce66b9cbfe72d8563b75c1204d255322254d1964dd", size = 1666324, upload-time = "2026-10-17T00:03:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bb/6da4bfc8aa3403e6447607131bf3463acaf954d990c6c8b6787c1902ddfe/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:fb8021f4dc3296de18737e3b8b34994411da725374d74a0b9eb32a738f2a23d0", size = 1474089, upload-time = "2026-10-17T00:03:41.715Z" }, + { url = "https://files.pythonhosted.org/packages/21/97/897e33bef2eefd880e6350e375cc66bc5bcd00a139be480fd655f5b3f095/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:0d764913374ba21df8cd618045b371ed104ce55c6276de7c59e554a4f5ba26fd", size = 1501542, upload-time = "2026-10-17T00:03:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/821211ac83870c27e4412381b7c7838336456b40575bf840e4680b2018d9/ast_serialize-0.13.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:2bad302ae2427454ef9dab71bd5c3ad67188860c39d36daee38f72031edb39c4", size = 1126118, upload-time = "2026-10-17T00:03:44.616Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/2b2d5e6c4fa352874a80618e742ffd32d249dee6c119f4233b51afc6b8f4/ast_serialize-0.13.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:032b09d86c785c3984e975378b7861f2a68ac44e22ba7b07b93a536b2524eeb8", size = 1163766, upload-time = "2026-10-17T00:03:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/d610cfcdabaa44fd62a54ca1fc57a7dba120643798b089a14914973562de/ast_serialize-0.13.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:929eda0c419e622ab49df3a4c45f81c57dcc10874bbad55d0470a4a03324dac3", size = 1134043, upload-time = "2026-10-17T00:03:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/81/92/d705be88ff452b9ba0088b554a0cbd308fd01879da092b33fd2ad7d1aa60/ast_serialize-0.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4bcf1d5f4eefecaa325a34ecf079b2741189e7d0821523f92cb343a389a67489", size = 1242416, upload-time = "2026-10-17T00:03:51.521Z" }, + { url = "https://files.pythonhosted.org/packages/60/c2/98370845a7f35cb8a25782e78284b48e01bf3128339881d48d613f304f8f/ast_serialize-0.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1b158f1c9e3a0d386913924c57995d6df78224868a4d0f215bdd41587c97b34a", size = 1229729, upload-time = "2026-10-17T00:03:52.825Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8b/2d7ffed76addd9d3fcf37fb171400942735c8912264c763561a85407f6a8/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a6201b69ce42e919f2fd9a559138d5410b5d8afff7c3a6b1f500b0481ff7d8", size = 1292255, upload-time = "2026-10-17T00:03:54.148Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2a/bb9e0e719d89cc950fadfcfcc9cb4bc7640419c4228c852f031ab8abf440/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eb0be64c9b8f1ed26637eef345abe75ea90351c92c85a8aec7838b3af971011", size = 1298501, upload-time = "2026-10-17T00:03:55.559Z" }, + { url = "https://files.pythonhosted.org/packages/7a/32/13ce07dce48226f5bd9cfd8f708ec101a8be05aa430cda4918ce31ce8c82/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2427bddfca208e84de700044fadf9aef2769a70bae740a1bdabb89ee9e1117", size = 1553758, upload-time = "2026-10-17T00:03:56.952Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fb/9b51d186db0c20ca28cbaaefd50f03b573c89327b9a2627820cbce22196b/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88df960e67184a71874e5b2ee014f87d1254a0dbc1f903c7bd7b1ab7733ecaef", size = 1318258, upload-time = "2026-10-17T00:03:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fa/da311559dd3dae9af5825cf21660775a929b970f72e3786af3f2cd667944/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f99786e0d62082a5b121b3437f3167562133711df22128de1e9886f57f0b226", size = 1313260, upload-time = "2026-10-17T00:04:00.194Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/97a4b9e5b4776d72f47fcd9a09a497bbf68ec8c1ae074307a2c51e410d85/ast_serialize-0.13.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:a01b515b25a03184eff4acef25b981ae007916379682e908fc8785b342a0bf3e", size = 1309992, upload-time = "2026-10-17T00:04:01.575Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c4/62f85121a80cd451ef04f550cf0f7ea0ad9ff46cc4e9a5b9ae439b6527b1/ast_serialize-0.13.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29422d7bcd9e7f39411571c36cec45ce8a7a9b3ad4b2e7aa64497c15a9e397f6", size = 1362715, upload-time = "2026-10-17T00:04:02.881Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/c5f4a545f72a7681e3295bab6e3600ee0862ec4391bed961fe9f735b67e0/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ffb0f548a7dc126067fdfb7b2325ab5dfcdeecc29faf5e0f0461c96d6ebfa3d", size = 1469931, upload-time = "2026-10-17T00:04:04.181Z" }, + { url = "https://files.pythonhosted.org/packages/38/a9/65aed4f8fa7a001a7cfdc03e68e844517ab041ba6fb614b7d78a1d6ac056/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2876444a5f87a7c76f371af3c07c2fda563a08bc8af52568df812e8aba4bf568", size = 1570945, upload-time = "2026-10-17T00:04:05.778Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8c/44b2da991cede79bfbf52089f33a5f0168945466b3d9cd38b5dcf08e7130/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:885a62ec6481ce58ab24c6357360d7a82023b62dd7e5e7ceccf59e9662a3272b", size = 1569978, upload-time = "2026-10-17T00:04:07.347Z" }, + { url = "https://files.pythonhosted.org/packages/46/e4/109b2a5beca700237767d5add920c2f8e1ba9bd5799b0faf143d0e9cf4ef/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2015fd0165e463aac3b1aadc6a1fa1e5820c14ca950821f4fe02f8a3f6a388d6", size = 1676082, upload-time = "2026-10-17T00:04:08.7Z" }, + { url = "https://files.pythonhosted.org/packages/73/e0/7df70f6dbb11b5762da5416318f08316a1c51348e0d69ec79ca5f98f836a/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:eb41e1906502ac80d24ff343f45008552a3f5b5158607959fe6a07dbfc6eb037", size = 1483207, upload-time = "2026-10-17T00:04:10.162Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bc/523aa1c08bd69b5654621beb3dc1a9035715a4a63762fb8d219d6fb3eb81/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ca9acc67f796aa2554fa3037dccc022a4b08b38b2ad153edfac9c9bb7cb1f3c6", size = 1512140, upload-time = "2026-10-17T00:04:11.589Z" }, + { url = "https://files.pythonhosted.org/packages/66/83/6607060659998f00afdea0d8375c1e6e2b7d515ab905090d9a5a02f48851/ast_serialize-0.13.1-cp39-abi3-win32.whl", hash = "sha256:4f98da562e5901d69c7dd30885594c5438f859edf354e9ac7517d188fb026c3a", size = 1131889, upload-time = "2026-10-17T00:04:12.953Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6f/bafd6490fe7b355627fabe95f4417e3d02da93102c8552550eb9a0774e1e/ast_serialize-0.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:ddf7cbc9c8abf70102c4a088efe154bb86bfb825b0a10e556d2b15972cf99bd7", size = 1171153, upload-time = "2026-10-17T00:04:14.362Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/4235f4b375a67a37ac595bf478a5407a744eaf7b64113c1c3a5432b8d954/ast_serialize-0.13.1-cp39-abi3-win_arm64.whl", hash = "sha256:48f11d96c9ec71119612f8e79a8c55c12f2b136716d68347433c561d27a25f43", size = 1139035, upload-time = "2026-10-17T00:04:15.767Z" }, +] + +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + +[[package]] +name = "boto3" +version = "1.43.123" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/8a/64b46f075149f9160da44aa7e1052dd18431b9ad4435beb0cfb02cc48624/boto3-1.43.123.tar.gz", hash = "sha256:055fabb990c5d2f31fce4ca569255bd640cdd11a3ca68c9ecad35f4f8a5b83b5", size = 112638, upload-time = "2026-10-30T19:18:50.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/b2/a7986c327f80aa6f1544f4cb8f2fa7da4e1fccd472379a21cad0508b8a0e/boto3-1.43.123-py3-none-any.whl", hash = "sha256:d18d66e1e13faa0c25d0b1b8c5ce946e29fb5fc0b6456b655a93a678ff4cc313", size = 140044, upload-time = "2026-10-30T19:18:47.734Z" }, +] + +[[package]] +name = "boto3-stubs" +version = "1.43.123" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore-stubs" }, + { name = "types-s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/78/ef94fbeac94b8f543038719ba6bf808d0209107fed4217fbc0e31d55bf7b/boto3_stubs-1.43.123.tar.gz", hash = "sha256:7e0565ef2c529c4ca569f51baad2af306c8eba3ddb5814a8ffca8c9fb07c763a", size = 105554, upload-time = "2026-10-30T19:25:33.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/55/adb3eb5a361fae6eaa4bf8e65c13539a4b057de24eff5a201caf71871864/boto3_stubs-1.43.123-py3-none-any.whl", hash = "sha256:29bf383f524b10a59478f87994de423cb58e58498b48cd1065a0f23802c0745e", size = 72011, upload-time = "2026-10-30T19:25:25.782Z" }, +] + +[package.optional-dependencies] +codebuild = [ + { name = "mypy-boto3-codebuild" }, +] +ec2 = [ + { name = "mypy-boto3-ec2" }, +] +lambda = [ + { name = "mypy-boto3-lambda" }, +] +s3 = [ + { name = "mypy-boto3-s3" }, +] +secretsmanager = [ + { name = "mypy-boto3-secretsmanager" }, +] +ssm = [ + { name = "mypy-boto3-ssm" }, +] + +[[package]] +name = "botocore" +version = "1.43.123" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/de/011fc2a47fb066c47064b122498d2fd21f55bc90ea514acbddc37ca0f8c1/botocore-1.43.123.tar.gz", hash = "sha256:80948cb9b1cdf1a1a25809089a42e3a1bc4af0258d198222db6e97dcc660dd0c", size = 16486269, upload-time = "2026-10-30T19:18:44.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/96/4f2dffd304425c07160ab2c12cb13322d852fd956fe9d4386228c93f02ea/botocore-1.43.123-py3-none-any.whl", hash = "sha256:e30580f9c1282c321529238ff8ff75066096a2adbd0a293c8ae592c21d1733a3", size = 16185987, upload-time = "2026-10-30T19:18:39.794Z" }, +] + +[[package]] +name = "botocore-stubs" +version = "1.43.123" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/c3/7a8549739312e033b828a3b481fde2ac61919176eea963533809cdf3757c/botocore_stubs-1.43.123.tar.gz", hash = "sha256:8c0d17ec8464e32097cb1716f45536a92a197ad36011c795fe4407245630c952", size = 42877, upload-time = "2026-10-30T23:34:15.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/98/32e9d3087facc5836472ccf5b37ae0af086c7802a2bafbc931294834ac25/botocore_stubs-1.43.123-py3-none-any.whl", hash = "sha256:bb0dc9d13d7f066385bfbe0ed483ca097a7572ae35fe4e1abe5fa9303355d1df", size = 67248, upload-time = "2026-10-30T23:34:13.603Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, +] + +[[package]] +name = "cfn-lint" +version = "1.59.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "networkx" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "sympy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/1a/9bd7e804cb01d42e0b3c78e45c14c4d42311a7e8970aeeae494c58a35d17/cfn_lint-1.59.0.tar.gz", hash = "sha256:544e0bf49bcfe49da71aab8b770a50edaffeb4c317f35317a391353565e7187c", size = 5104650, upload-time = "2026-10-28T19:35:04.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/35/70d8dd577245afa04351421694b780cd357d8416c82db3d9f75558985737/cfn_lint-1.59.0-py3-none-any.whl", hash = "sha256:6994da513581dc851747a699007a9c5517244c655377e27511e1eb0a11f499c5", size = 5710706, upload-time = "2026-10-28T19:35:02.157Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "craftform-operations" +version = "0.0.0" +source = { virtual = "lambda/operations" } +dependencies = [ + { name = "pynacl" }, + { name = "urllib3" }, +] + +[package.metadata] +requires-dist = [ + { name = "pynacl", specifier = ">=1.6.2" }, + { name = "urllib3", specifier = ">=2" }, +] + +[[package]] +name = "craftform-server" +version = "0.0.0" +source = { virtual = "codebuild/server" } +dependencies = [ + { name = "boto3" }, + { name = "urllib3" }, +] + +[package.metadata] +requires-dist = [ + { name = "boto3", specifier = ">=1.43.114" }, + { name = "urllib3", specifier = ">=2" }, +] + +[[package]] +name = "craftform-startup" +version = "0.0.0" +source = { virtual = "lambda/startup" } +dependencies = [ + { name = "urllib3" }, +] + +[package.metadata] +requires-dist = [{ name = "urllib3", specifier = ">=2" }] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.42" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/f6/4b1c0352b99b71e80d3eac306b9431a46ff109a314ba0a48b4b4bd3b9761/jsonpatch-1.42.tar.gz", hash = "sha256:8d553ab33cc1e5d3da60b8be5224355fb28cde863cc8935c40f011a8c0d72ad4", size = 65555, upload-time = "2026-11-04T12:32:39.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/0e/f304972d58bdae95d130e3b2fbbf46da4ba40a5e6983b73e82e3471a0de3/jsonpatch-1.42-py3-none-any.whl", hash = "sha256:0a94e8725b8b53cafdf3eba3112a7ccd31ea420bb04e6374096d9d17bf4563e9", size = 23038, upload-time = "2026-11-04T12:32:37.816Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/cf/5d665216bd9dea19dd26ea93ca525ef02936a9cccd0cd856f8408eb77f0b/jsonpointer-3.8.tar.gz", hash = "sha256:a627a255641390bfb4d51d4cfbb90648078ae855b45804ef7e19ea13a4cca355", size = 19212, upload-time = "2026-11-02T21:52:18.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/61/20f3a497beac4c8f869b8161b468b31c0f05d9d73600da0313eb759bad8a/jsonpointer-3.8-py3-none-any.whl", hash = "sha256:d69af85adaea202111bbc931067ead68dc16bca4592c74e1a1953152e2a330ab", size = 10233, upload-time = "2026-11-02T21:52:17.617Z" }, +] + +[[package]] +name = "librt" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/76/bbdaeb87b7c47b5c7343e90222b9bfa4e4a8a83f647e02933ad0129225b1/librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", size = 151028, upload-time = "2026-09-29T00:52:41.508Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0c/ab8ed3dab0085931aec4a792c7eaac8dc6c5ff4691fda3a5360d9d8a9cd2/librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", size = 155283, upload-time = "2026-09-29T00:52:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/eb/36/494e79d460c80c1f030661e8287c9eca5e1ad652dc2b2180b6cd42abce0a/librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f", size = 503185, upload-time = "2026-09-29T00:52:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/9b/34/a8464038dd9db6e4381fa2b6eb73dc9a50888d77102c4c139304ac35cddc/librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", size = 496738, upload-time = "2026-09-29T00:52:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/6a/53/e0e5e334ef0c6ed27039d323819368b9ef6712be87d55ee2bf9799398afd/librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", size = 513818, upload-time = "2026-09-29T00:52:48.068Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f7/7ce72cbf19d0addd05090b339152fd0548a02562c2866a603e6e3b3da2df/librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", size = 531952, upload-time = "2026-09-29T00:52:49.816Z" }, + { url = "https://files.pythonhosted.org/packages/83/22/0b1bcb6a8e723c8b4fd60dfc8ae8ec6461c54073fbc8685efeb8d900d407/librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", size = 524465, upload-time = "2026-09-29T00:52:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/64/2e/e9c23b8b9df1813da1be205deca9606beb7ddd033972246cd426d05374a0/librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", size = 543191, upload-time = "2026-09-29T00:52:53.284Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e5/6a8b21b342c03ed7e230fa3afbfd2edc58e6e87ef1f0d11fa2b9a748c252/librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", size = 546979, upload-time = "2026-09-29T00:52:54.932Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/b5129023eced1be01e01c22757f53be551d463b1bb7264f787927404c1d6/librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", size = 555317, upload-time = "2026-09-29T00:52:56.869Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/28bba5938c725fe91f06bf93f7fa6c6b150229df53a87b454b0d5c2a796e/librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", size = 535846, upload-time = "2026-09-29T00:52:58.475Z" }, + { url = "https://files.pythonhosted.org/packages/22/92/63773026614f888c5d4e370e395ce42ca604b89f70b3acdfedbf94851b80/librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", size = 573700, upload-time = "2026-09-29T00:53:00.127Z" }, + { url = "https://files.pythonhosted.org/packages/66/8f/347d4677eefb57cd9f8e01d95a1dcee9a91b4e44e664173c32ff6f3752e5/librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", size = 106158, upload-time = "2026-09-29T00:53:02.031Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2c/5193dc81127cd5ddfad031391b046bf32dda219b38463ab872407ca30646/librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", size = 126176, upload-time = "2026-09-29T00:53:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3d/9668a400c8dd81d162eba38b33fa49fa6205f1a64493570a13fbb815c3ee/librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", size = 116572, upload-time = "2026-09-29T00:53:04.815Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "mypy" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/3c/78ecece71ee4b5445389a4ccd11627cfb9c69e4de554d11060171f376305/mypy-2.4.1.tar.gz", hash = "sha256:63b590965691e540a335fa74dadf31a7069142f6b72e234689bbd244edf55623", size = 4066728, upload-time = "2026-10-28T19:23:13.286Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/1a/0be9dc6af5e0e04128f18a5e36b4ca44bfb0085857d37aa6a7de1ac2b0cb/mypy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d63737426a048a27705a4eedb828669f4bc0873612a1b46b0b9fe892b2ebaaaf", size = 14381321, upload-time = "2026-10-28T19:21:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a6/7907e36711a8a8c703f961f076fab61793383694140bd1fd87589f3f68d4/mypy-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7344869cad05f073b8cae2826774fa0161e3eb2f5c685b76813574008d77d750", size = 14466373, upload-time = "2026-10-28T19:22:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b8/7313212b29de570c25c9859d7c8e4ef2251a6d34338a1e20031d94297721/mypy-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a20ff6cd2af7a670c64f089bfa5fbcebe8ff3bf896c5e6ac7c9862c7960e4494", size = 15553666, upload-time = "2026-10-28T19:22:36.511Z" }, + { url = "https://files.pythonhosted.org/packages/23/8f/92966b6082ff49da9142e1e527eb9af4359a9a61632a1cfc313da96b37fe/mypy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ed1195502cca32be5ef756b1693477f4991db2f2e34eaa90cc4ad6bea4f9531", size = 15802641, upload-time = "2026-10-28T19:21:13.421Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b4/874f6fd54b005020ad63e2cf7a59b178d16a771d478849b29b842cb0cf14/mypy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c194c5d878130fca1ae662dfb1feb6274afbab0b40032c80147377e0ea803d5", size = 11383067, upload-time = "2026-10-28T19:22:03.516Z" }, + { url = "https://files.pythonhosted.org/packages/b0/41/5ae8e6528c2faa96e4a921a999d2c7ac7c565bee184b4282bf62ffa76b5d/mypy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:71eda06e5bae51976ce869fda429159c9cec678bd02f25bb197f0265c8f991be", size = 10946882, upload-time = "2026-10-28T19:21:39.51Z" }, + { url = "https://files.pythonhosted.org/packages/21/5c/6e8010f8f2c4ad817364592d4b3883633417269b913fc9d5174fdeb7e565/mypy-2.4.1-py3-none-any.whl", hash = "sha256:1784648d6ec8c2b5465e3f26241b845a0b3cb4e56c993c64abe16989e7698999", size = 2801286, upload-time = "2026-10-28T19:21:19.516Z" }, +] + +[[package]] +name = "mypy-boto3-codebuild" +version = "1.43.38" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/c7/0f2c2133dd825c9790f23611092a2dcd5187e5f3bb2c80f6f36cfaffe369/mypy_boto3_codebuild-1.43.38.tar.gz", hash = "sha256:19cfeed3a36403a8eecf98e2f633ecdd9f31a7f195aca130845a8bd2319e75a5", size = 41699, upload-time = "2026-06-30T21:05:54.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/cb/bf50ff854b184976eb53837acd95b2dd4ae11b696a243afe46f19693068a/mypy_boto3_codebuild-1.43.38-py3-none-any.whl", hash = "sha256:20f9402f9c28a395f25dcb9bff3750e438cdadd8f91aae0366aec8200cef06b6", size = 48164, upload-time = "2026-06-30T21:05:52.059Z" }, +] + +[[package]] +name = "mypy-boto3-ec2" +version = "1.43.121" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/eb/8bf85f902d0524bdc818b7c254a0325ed1ceaadcd92bbc82cd3993cbdb3a/mypy_boto3_ec2-1.43.121.tar.gz", hash = "sha256:12ed37abe5f28e0dd0cf693752153b543d9c013ced2d35da2711541bd2336fed", size = 472929, upload-time = "2026-10-28T19:35:42.754Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/b1/369c7b5e47ea40b490d2c463c9d4ebb593fb1de0cee9e1f8c69439288a8e/mypy_boto3_ec2-1.43.121-py3-none-any.whl", hash = "sha256:3cea4c44cab342c69fb0554d6a351da41cf2941659b68086218d9adf6b28dafb", size = 461224, upload-time = "2026-10-28T19:35:39.086Z" }, +] + +[[package]] +name = "mypy-boto3-lambda" +version = "1.43.123" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/4f/d8a62e5e0915df3331ef54a2441423704754491998d1ea28d42f0e2b2686/mypy_boto3_lambda-1.43.123.tar.gz", hash = "sha256:201c5731a8b5df52b406d3c40e2efc63d3f9b90f87dbbc478c2cf5ef2cdaf977", size = 53549, upload-time = "2026-10-30T19:25:16.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/4d/96679f36874155748d57e7689aa006dca81c0889f5ab825881425d0a8591/mypy_boto3_lambda-1.43.123-py3-none-any.whl", hash = "sha256:3823ae1873ebe24b7fb8cee7f7593c007f6bff3e8325e5f4efa353893b338ffe", size = 62037, upload-time = "2026-10-30T19:25:12.588Z" }, +] + +[[package]] +name = "mypy-boto3-s3" +version = "1.43.106" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/db/e3922d6c62365c1098da97d9a775e78c66827df5cba11619a7d2a11cdae4/mypy_boto3_s3-1.43.106.tar.gz", hash = "sha256:731195f15830699a36e29d3c8abb2918bccd3587eb2edcb233f8046967893279", size = 79447, upload-time = "2026-09-30T22:44:48.472Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/4a/52419b5496215ab52c6c257134c1ac0da25c1ca5d802f133424d02a43686/mypy_boto3_s3-1.43.106-py3-none-any.whl", hash = "sha256:e233d04dbcf3925522dff4346e75c37344ec3d1cbb82337e5eed2af263847668", size = 86791, upload-time = "2026-09-30T22:44:46.139Z" }, +] + +[[package]] +name = "mypy-boto3-secretsmanager" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/e0/3b9f954dcce063407224e6601ed5af2d684185387b0572901c5f984fc8f6/mypy_boto3_secretsmanager-1.43.0.tar.gz", hash = "sha256:265ee2fddf9d3e42ae39685625fb7861a539110d8e324372847c0e1cbd666b20", size = 19967, upload-time = "2026-04-29T23:05:45.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/93/ae939d61eed240972553395b5e31f780825685b99f0ab4ebe5767430f103/mypy_boto3_secretsmanager-1.43.0-py3-none-any.whl", hash = "sha256:38415cdecb73dd20e485707a7cf456f6dde54ff4b155e7fb255eb001eb47d5bc", size = 27335, upload-time = "2026-04-29T23:05:43.152Z" }, +] + +[[package]] +name = "mypy-boto3-ssm" +version = "1.43.104" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/b4/007e9acec1ccc7b0f9148f3ef2f6fe27ef183c5c7ab08b6b5a7d465d13d5/mypy_boto3_ssm-1.43.104.tar.gz", hash = "sha256:1b1e18762e6b9ceaf735e8165a6dbd789b10d94f56f704ef010d0c8bbef059cc", size = 98088, upload-time = "2026-09-28T20:24:22.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/9a/c7d22d1b1450c6b790cad4fcb2e98defd383527d381b4084cd7ee6b2deaa/mypy_boto3_ssm-1.43.104-py3-none-any.whl", hash = "sha256:4434c02de5101a0bc2fbd9bdedbcc98f5836dc432fce38ebfc8c88adb63dbb90", size = 99719, upload-time = "2026-09-28T20:24:19.363Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/6f/43edf3be2748d0e40974a5ee046a8bb4c9961a4bdd3e7d5c6dcbf32d1af6/mypy_extensions-1.2.0.tar.gz", hash = "sha256:c6e275a07f1c2fc096b62940452d0eaa3f3af7de45d704787866240a7c6d5a51", size = 6566, upload-time = "2026-11-03T16:26:33.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/99/7b1adac7e97ce7bbda34792dc6e9b2f5cc5cffd1875883f78a818b2a2687/mypy_extensions-1.2.0-py3-none-any.whl", hash = "sha256:993e7a20aa5671eb41a7132cf555af30062da99729601601b9a8382b11cae768", size = 5114, upload-time = "2026-11-03T16:26:32.731Z" }, +] + +[[package]] +name = "networkx" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/76/3af777226b63a5e64a6b36b1ec5855c14e2b94a37096d4760e595fc43511/networkx-3.7.tar.gz", hash = "sha256:fd77a511bd90f39f3d016351345b52cf5319b813bdca01de3f755d3cca62e96a", size = 1866482, upload-time = "2026-09-21T16:45:16.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/cd/fe58041e9011f307c490e3e17dd48cc516448f7c698a3f2d9d9d65d7e6a8/networkx-3.7-py3-none-any.whl", hash = "sha256:e3fd2c13a7814cee3746340d8d7f8598a67f16a58bf47fb7f8793fab6efca1b0", size = 2142205, upload-time = "2026-09-21T16:45:14.609Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, +] + +[[package]] +name = "pycparser" +version = "3.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/27/eeb9f307bf07060f3d9f9aa0fb8c15b01bdfded0a941e4b2719c7f94cacd/pycparser-3.12.tar.gz", hash = "sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6", size = 158785, upload-time = "2026-10-22T13:40:03.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/ae/315bbeee126a769cbde43cf4908acb4b40408d8bf8bbf334dd7368d8dadf/pycparser-3.12-py3-none-any.whl", hash = "sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e", size = 62600, upload-time = "2026-10-22T13:40:02.021Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + +[[package]] +name = "pytest" +version = "9.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + +[[package]] +name = "regex" +version = "2026.9.29" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f2/af1da9d3ceed77bfcdce40427d49ba0be94e4fe84245e3bfef68c10e75b6/regex-2026.9.29.tar.gz", hash = "sha256:8b5fcc4771732191b2b7d1dd68d8f0353f47f8d90b6150f6dce58bf1112442cb", size = 419199, upload-time = "2026-09-29T00:49:58.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/48/3fdcde9a0baa84d7d25571223265d6e434e114763b438601d54a8028bf3e/regex-2026.9.29-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc79d36d0618752265f0d575915bdc5c5130ecb9c9f6b3bcefeae32e4bdfafcf", size = 497903, upload-time = "2026-09-29T00:46:38.938Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1c/4ee3e97c76f53940488dfe7a7e18705e78daac8cd7fb161d246b9e328449/regex-2026.9.29-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3a21a9509d0ee88e7a70e1ad228cd2f0e0fd1e187458db132e8a8d18c97daf9d", size = 296416, upload-time = "2026-09-29T00:46:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/37/14/f3f0ba083d2094392d5eabf56db5ea6ba469fd6e927afd187042054ea68a/regex-2026.9.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f57dc6b8fef170f105d2cf5cdce254f47b137d7755086cf7050f47e16582abba", size = 293633, upload-time = "2026-09-29T00:46:41.959Z" }, + { url = "https://files.pythonhosted.org/packages/c9/72/67e7a8ce17f1aea49df215564048efb49cc8c2b31a0e0fc30f36838f8516/regex-2026.9.29-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f93bc1c3486ef3747e07c9d7c1d0a147b8fbaab975f80e348aed6f71309dfaca", size = 805885, upload-time = "2026-09-29T00:46:43.373Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/25436bcfd4d2260b4b4090094d55d7ab53ec8a1ab4865a0b8bcb33c7d5c0/regex-2026.9.29-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e1d3a4cb7993b708f0ada8d0c84590efd853f169e7147d2202c9da503180242", size = 878344, upload-time = "2026-09-29T00:46:45.328Z" }, + { url = "https://files.pythonhosted.org/packages/97/e6/a09ec3a23ae41d6179880e67f0aace9284b2d95f2d7b326eff203f8eec5e/regex-2026.9.29-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dabee8f4935e731fb46b2a3091bdda0d3d94b3bbfb907d2b4f12eefce4009619", size = 919181, upload-time = "2026-09-29T00:46:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/26/83/d2fbd2e4e3afb1167daa825187d196f313cbaa1a4768f311fb041bb0e3d2/regex-2026.9.29-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39ab5894d971f9ac68baa6eca5c50387db579cfcacf36ae8df3feceb1815e6d0", size = 807783, upload-time = "2026-09-29T00:46:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/46/0b/eb429a7016610d44fc89a597163f8c9127505f0d7dc724dc9effbb6a3ac0/regex-2026.9.29-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c1a9a6651197fbed6f0212591418b9def774fc3f8324f78d1bf0e6a63e5f8aa1", size = 783465, upload-time = "2026-09-29T00:46:50.64Z" }, + { url = "https://files.pythonhosted.org/packages/1b/07/58a3c0153c7476898430f6a7cf3d9062a1d17fbea4f43399ecaf411c7b4c/regex-2026.9.29-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87fb80cbe3557e27e7b28b995c2b2eedf689b8886f941ab93e0e288f0976518a", size = 793519, upload-time = "2026-09-29T00:46:52.396Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/161b94d39164520e21a7befe0245569bf7fda4c7cf1fc4e2df2b5def49da/regex-2026.9.29-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3c5c2ef13797466aa64170cbb66ad98a32351dd4127694cea7199f80f213750d", size = 869293, upload-time = "2026-09-29T00:46:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/8f/07/3b02ed829aa2decdc1955d222bd1e2f99d1c8bb4873bbb9a66b2f0a36bff/regex-2026.9.29-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:59b49507f47479e299a9e1bc41b5cb83a7afda0540625f1dbae886615978acbf", size = 770239, upload-time = "2026-09-29T00:46:56.106Z" }, + { url = "https://files.pythonhosted.org/packages/42/5b/ba61f6fe062eb8562e742367d177bb75370434138ef6c9d2a27114f8d613/regex-2026.9.29-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0dd8af32e9f7b56b7f95cc1fd79b23054c3bdc172392ae560acc24d57b7ffe71", size = 861973, upload-time = "2026-09-29T00:46:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/cc/27/767259b20e8a842948990f5e99138d6c077248fd42f8b5468b1d9ca4b814/regex-2026.9.29-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db5e82ba15c142425b8406690032df89e39cca4a2e8afbbb9a3d84edc2373ac3", size = 796470, upload-time = "2026-09-29T00:46:59.236Z" }, + { url = "https://files.pythonhosted.org/packages/a0/05/2566c4ba849b68a8ab81a6bf428fa79d20aae7ddee83979103c0381df254/regex-2026.9.29-cp312-cp312-win32.whl", hash = "sha256:d0c3082bf79bcd6a614d55916590ad4b8f93200e10b97f463ea5d9d07c9b5f23", size = 269327, upload-time = "2026-09-29T00:47:01.135Z" }, + { url = "https://files.pythonhosted.org/packages/93/19/489bc8db91196381c935752df01ba3f607140daece33b78d88573f028e64/regex-2026.9.29-cp312-cp312-win_amd64.whl", hash = "sha256:fdd88ed5e20b1bcdd234421e454962c971aa44b653bdb7f1ea9ef683e90fb649", size = 280334, upload-time = "2026-09-29T00:47:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/fb88ba779d0e5e7d4b0ec1aceeb13845948a2cb876bd572a2d1dfdba090b/regex-2026.9.29-cp312-cp312-win_arm64.whl", hash = "sha256:4fe97894d1b306c919b4e50def1e6f6c522f4d03a7283811f4d108f1ce5d3ac2", size = 279614, upload-time = "2026-09-29T00:47:06.541Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/73/81f140c4edd9cc323de5478e6bbd3bbcfae9a82692df1a258de8cec6a6d2/ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9", size = 5339444, upload-time = "2026-10-27T00:34:05.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/8c/083365310c4e79c842f25bf5a842facbd9b0c0ec336f96dffa5074fe723d/ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4", size = 10216522, upload-time = "2026-10-27T00:33:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/4b2edf31cfebe9605d8f6ed8f1d6f376d3a53d4c9a123e62661c64c440f0/ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11", size = 10217786, upload-time = "2026-10-27T00:33:33.949Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7e/537aba4b7f54397a374d2f6a6fc93e9c8015e18553f31a4a7b7dbced48b5/ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29", size = 9440362, upload-time = "2026-10-27T00:33:35.784Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/283e39734f66c8a5ebaa93c22eb7b7515779a0b1d5a5c0ad20e05c0edb41/ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad", size = 10094281, upload-time = "2026-10-27T00:33:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/21/4c/1ffc768497bd025a32c72152771e40c0b99bddc5cc6e4f50c7949a395346/ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1", size = 10022543, upload-time = "2026-10-27T00:33:40.181Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/f90db547301080fcb82c92abf914c8bd57b89911d9b65a6c4cc6b52d0635/ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f", size = 10683921, upload-time = "2026-10-27T00:33:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/e5/98/92d4a8a26c6c96cf1a5b34dd59036d8a71e52d650aab065c58867e964e63/ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532", size = 11373408, upload-time = "2026-10-27T00:33:44.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/02/1f24d2e3064c9753926deeb901afa57777b37ee46799b09ddd7aee6e5145/ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef", size = 10977469, upload-time = "2026-10-27T00:33:46.179Z" }, + { url = "https://files.pythonhosted.org/packages/f0/20/fecedea2209f574ab92033b344e01a7f0c2dc76c6d6d19a14c9f8141173f/ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e", size = 10577491, upload-time = "2026-10-27T00:33:48.233Z" }, + { url = "https://files.pythonhosted.org/packages/33/36/b93fdf0bc6f84f0cd487aadfb0354deb40013669ea35ac3d26855de1e959/ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5", size = 10608748, upload-time = "2026-10-27T00:33:50.284Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b7/7d3f5f14145daead1efb0b9a8289ed1daa100f419d60774375951723ce00/ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a", size = 10075057, upload-time = "2026-10-27T00:33:52.074Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ed/93ff7a5909ee56e2bd4b982deae9e0efb2724417874a90c9e41ad66c6dd7/ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb", size = 10022431, upload-time = "2026-10-27T00:33:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/07/a83b9eeec8c1c3c0e4eda15f06c3a91001bfd2687ea2f07b750719bf9712/ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2", size = 10394903, upload-time = "2026-10-27T00:33:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/af/b2/05c39857c2526fcbebe46ad93a8dc97424656dfef14d3ef933abe1ffd331/ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe", size = 10799780, upload-time = "2026-10-27T00:33:58.204Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d9/47171a16739eb7a353936b31c20cf54340531e68c9af7d72fd7a784e235d/ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca", size = 9908481, upload-time = "2026-10-27T00:34:00.087Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/f08c1631bfd81faa6a90eaf70c37c786c11bed2046302b56bb506c25e1a0/ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24", size = 10341333, upload-time = "2026-10-27T00:34:02.099Z" }, + { url = "https://files.pythonhosted.org/packages/99/d5/4ad4ed31413c301430918e70d3d38ae6f097af0766cb9ab8c9d502b2a4d6/ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9", size = 9854332, upload-time = "2026-10-27T00:34:03.847Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.19.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/c5/0a5ce655109cb96b8dfad36908a4e108d15f5dc2eff8fbc3146c4312e704/s3transfer-0.19.3.tar.gz", hash = "sha256:8326acc3d056d7f7b8a37110c7d2aea41f67e83f6ae79cd3b5fbcca34c726406", size = 166087, upload-time = "2026-10-19T19:19:49.751Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/a4/88f9e10498759121060b8050a1554753dfc04df39bbbcc7aad9ecc8d9440/s3transfer-0.19.3-py3-none-any.whl", hash = "sha256:24eb7535804306355e31c952a90022bf5b2f9d760f9a11062c7493a217c08553", size = 90252, upload-time = "2026-10-19T19:19:48.309Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "stevedore" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/a1/3b8ed9c1fc3aa6eebb57732d924ddaa0500ecc3b638d0454816320994383/stevedore-5.9.1.tar.gz", hash = "sha256:e97a2667923efda926e8713fde6a73616df68210a3cbc6f02b48967b676fd8bf", size = 518111, upload-time = "2026-08-20T15:25:14.754Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/97/bba6e7ec2f5498b9dcb7b1b6400086b80ae5a8ebaff4b25e8c8add75f439/stevedore-5.9.1-py3-none-any.whl", hash = "sha256:5c8ff3a9f336cc1a06ac0f597bc79d11a2f950bfd32e290ca56b5a301fafafbf", size = 54931, upload-time = "2026-08-20T15:25:13.602Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "types-s3transfer" +version = "0.19.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/b2/4f2d72b5d234c81caa07dee688cacee2c8dc5bd793e9ae70a573f00d6425/types_s3transfer-0.19.3.tar.gz", hash = "sha256:99422ef028a5f854cd92d1e72cf4208bd0569f68a87936ca02fb9f91a68cdcd3", size = 13780, upload-time = "2026-10-20T02:24:19.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/98/972fa03c9a6e1cc9184a28ea0354af50a6764bb172fe2d0751798a295f07/types_s3transfer-0.19.3-py3-none-any.whl", hash = "sha256:6843143f3b79e3cb73e58b6f45738e28e88d9f681fd32cc5ef7fa16f0c193356", size = 19476, upload-time = "2026-10-20T02:24:17.682Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, +]