diff --git a/.copier-answers.yml b/.copier-answers.yml index 9f14f496..a478b72a 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.0.64 +_commit: v0.0.69 _src_path: gh:LabAutomationAndScreening/copier-base-template.git description: Copier template for creating Python libraries and executables python_ci_versions: diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 930c4f9c..88991bc0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -23,12 +23,12 @@ "eamodio.gitlens@15.5.1", "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905", - "github.copilot@1.320.1564", - "github.copilot-chat@0.28.2025051402", + "github.copilot@1.366.1775", + "github.copilot-chat@0.31.2025090401", // Python - "ms-python.python@2025.7.2025051401", - "ms-python.vscode-pylance@2025.4.104", + "ms-python.python@2025.13.2025090201", + "ms-python.vscode-pylance@2025.7.102", "ms-vscode-remote.remote-containers@0.414.0", "charliermarsh.ruff@2025.24.0", @@ -61,5 +61,5 @@ "initializeCommand": "sh .devcontainer/initialize-command.sh", "onCreateCommand": "sh .devcontainer/on-create-command.sh", "postStartCommand": "sh .devcontainer/post-start-command.sh" - // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): d72f90f7 # spellchecker:disable-line + // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): bec12e35 # spellchecker:disable-line } diff --git a/.devcontainer/install-ci-tooling.py b/.devcontainer/install-ci-tooling.py index a87f08ba..4a7c06a3 100644 --- a/.devcontainer/install-ci-tooling.py +++ b/.devcontainer/install-ci-tooling.py @@ -7,10 +7,10 @@ import tempfile from pathlib import Path -UV_VERSION = "0.8.4" -PNPM_VERSION = "10.14.0" -COPIER_VERSION = "9.9.1" -COPIER_TEMPLATE_EXTENSIONS_VERSION = "0.3.2" +UV_VERSION = "0.8.15" +PNPM_VERSION = "10.15.1" +COPIER_VERSION = "9.10.1" +COPIER_TEMPLATE_EXTENSIONS_VERSION = "0.3.3" PRE_COMMIT_VERSION = "4.3.0" GITHUB_WINDOWS_RUNNER_BIN_PATH = r"C:\Users\runneradmin\.local\bin" INSTALL_SSM_PLUGIN_BY_DEFAULT = False @@ -31,10 +31,10 @@ "--no-node", action="store_true", default=False, help="Do not process any environments using node package managers" ) _ = parser.add_argument( - "--install-ssm-plugin", + "--skip-installing-ssm-plugin", action="store_true", - default=INSTALL_SSM_PLUGIN_BY_DEFAULT, - help="Install the SSM plugin for AWS CLI", + default=False, + help="Skip installing the SSM plugin for AWS CLI", ) @@ -117,26 +117,43 @@ def main(): else [cmd] ) _ = subprocess.run(cmd, shell=True, check=True) - if args.install_ssm_plugin: - if is_windows: - raise NotImplementedError("SSM plugin installation is not implemented for Windows") + if INSTALL_SSM_PLUGIN_BY_DEFAULT and not args.skip_installing_ssm_plugin: with tempfile.TemporaryDirectory() as tmp_dir: - local_package_path = Path(tmp_dir) / "session-manager-plugin.deb" - # Based on https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-debian-and-ubuntu.html - # no specific reason for that version, just pinning it for best practice - _ = subprocess.run( - [ - "curl", - "https://s3.amazonaws.com/session-manager-downloads/plugin/1.2.707.0/ubuntu_64bit/session-manager-plugin.deb", - "-o", - f"{local_package_path}", - ], - check=True, - ) - _ = subprocess.run( - ["sudo", "dpkg", "-i", str(local_package_path)], - check=True, - ) + if is_windows: + local_package_path = Path(tmp_dir) / "SessionManagerPluginSetup.exe" + # Based on https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-windows.html + # no specific reason for that version, just pinning it for best practice + _ = subprocess.run( + [ + "curl", + "https://s3.amazonaws.com/session-manager-downloads/plugin/1.2.707.0/windows/SessionManagerPluginSetup.exe", + "-o", + f"{local_package_path}", + ], + check=True, + ) + _ = subprocess.run( + [str(local_package_path), "/quiet"], + check=True, + ) + else: + local_package_path = Path(tmp_dir) / "session-manager-plugin.deb" + # Based on https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-debian-and-ubuntu.html + # no specific reason for that version, just pinning it for best practice + _ = subprocess.run( + [ + "curl", + "https://s3.amazonaws.com/session-manager-downloads/plugin/1.2.707.0/ubuntu_64bit/session-manager-plugin.deb", + "-o", + f"{local_package_path}", + ], + check=True, + ) + _ = subprocess.run( + ["sudo", "dpkg", "-i", str(local_package_path)], + check=True, + ) + print("SSM Plugin Manager Version: ") _ = subprocess.run( ["session-manager-plugin", "--version"], check=True, diff --git a/.devcontainer/manual-setup-deps.sh b/.devcontainer/manual-setup-deps.sh deleted file mode 100644 index 70e9c2db..00000000 --- a/.devcontainer/manual-setup-deps.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env sh -# can pass in the full major.minor.patch version of python as an optional argument -# can set `--skip-lock` as optional argument to just install dependencies without verifying lock file -# can set `--optionally-lock` to check for a uv.lock file in the project directory and only respect the lock if it already exists (useful for initially instantiating the repository) (mutually exclusive with --skip-lock) - -set -ex - -# Ensure that uv won't use the default system Python -python_version="3.12.7" - -# Parse arguments -skip_lock=false -optionally_lock=false -while [ "$#" -gt 0 ]; do - case $1 in - --skip-lock) skip_lock=true ;; - --optionally-lock) optionally_lock=true ;; - *) python_version="${1:-$python_version}" ;; # Take the first non-flag argument as the input - esac - shift -done - -# Ensure that --skip-lock and --optionally-lock are mutually exclusive -if [ "$skip_lock" = "true" ] && [ "$optionally_lock" = "true" ]; then - echo "Error: --skip-lock and --optionally-lock cannot be used together." >&2 - exit 1 -fi - -export UV_PYTHON="$python_version" -export UV_PYTHON_PREFERENCE=only-system - -SCRIPT_DIR="$(dirname "$0")" -PROJECT_ROOT_DIR="$(realpath "$SCRIPT_DIR/..")" - -# If optionally_lock is set, decide whether to skip locking based on the presence of uv.lock -if [ "$optionally_lock" = "true" ]; then - if [ ! -f "$PROJECT_ROOT_DIR/uv.lock" ]; then - skip_lock=true - else - skip_lock=false - fi -fi - - - -# Ensure that the lock file is in a good state -if [ "$skip_lock" = "false" ]; then - uv lock --check --directory "$PROJECT_ROOT_DIR" -fi - -uv sync $( [ "$skip_lock" = "false" ] && echo "--frozen" ) --directory "$PROJECT_ROOT_DIR" -uv pip list --directory "$PROJECT_ROOT_DIR" diff --git a/.github/actions/install_deps/action.yml b/.github/actions/install_deps/action.yml index 3fafd45a..ddfe8b45 100644 --- a/.github/actions/install_deps/action.yml +++ b/.github/actions/install_deps/action.yml @@ -16,6 +16,11 @@ inputs: default: true type: boolean description: Whether to run the setup-deps script, or just to setup basic CI tooling + skip-installing-ssm-plugin-manager: + required: false + default: false + type: boolean + description: Whether to explicitly skip installing the SSM Plugin manager when setting up basic CI tooling project-dir: type: string description: What's the relative path to the project? @@ -48,24 +53,24 @@ runs: - name: Setup python if: ${{ inputs.python-version != 'notUsing' }} - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Setup node if: ${{ inputs.node-version != 'notUsing' }} - uses: actions/setup-node@v4.4.0 + uses: actions/setup-node@v5.0.0 with: node-version: ${{ inputs.node-version }} - name: Install tooling # the funky syntax is github action ternary - run: python .devcontainer/install-ci-tooling.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} + run: python .devcontainer/install-ci-tooling.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} ${{ inputs.skip-installing-ssm-plugin-manager && '--skip-installing-ssm-plugin' || '' }} shell: pwsh - name: OIDC Auth for CodeArtifact if: ${{ inputs.code-artifact-auth-role-name != 'no-code-artifact' }} - uses: aws-actions/configure-aws-credentials@v4.2.0 + uses: aws-actions/configure-aws-credentials@v5.0.0 with: role-to-assume: arn:aws:iam::${{ inputs.code-artifact-auth-role-account-id }}:role/${{ inputs.code-artifact-auth-role-name }} aws-region: ${{ inputs.code-artifact-auth-region }} diff --git a/.github/actions/update-devcontainer-hash/action.yml b/.github/actions/update-devcontainer-hash/action.yml index d5b7d214..fb64cc81 100644 --- a/.github/actions/update-devcontainer-hash/action.yml +++ b/.github/actions/update-devcontainer-hash/action.yml @@ -27,7 +27,7 @@ runs: shell: bash - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: persist-credentials: true fetch-depth: 1 diff --git a/.github/reusable_workflows/build-docker-image.yaml b/.github/reusable_workflows/build-docker-image.yaml index dc5df38f..ae6d3c8c 100644 --- a/.github/reusable_workflows/build-docker-image.yaml +++ b/.github/reusable_workflows/build-docker-image.yaml @@ -31,6 +31,10 @@ on: description: 'Should the image be saved as an artifact?' required: false default: false + outputs: + artifact-name: + description: 'The name of the uploaded artifact of the image tarball' + value: ${{ jobs.build-image.outputs.artifact-name }} permissions: id-token: write @@ -40,6 +44,8 @@ jobs: build-image: name: Build Docker Image runs-on: ubuntu-24.04 + outputs: + artifact-name: ${{ steps.calculate-build-context-hash.outputs.image_name_no_slashes }} steps: - name: Parse ECR URL if: ${{ inputs.push-role-name != 'no-push' }} @@ -59,11 +65,11 @@ jobs: shell: bash - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: OIDC Auth for ECR if: ${{ inputs.push-role-name != 'no-push' }} - uses: aws-actions/configure-aws-credentials@v4.2.0 + uses: aws-actions/configure-aws-credentials@v5.0.0 with: role-to-assume: arn:aws:iam::${{ steps.parse_ecr_url.outputs.aws_account_id }}:role/${{ inputs.push-role-name }} aws-region: ${{ steps.parse_ecr_url.outputs.aws_region }} @@ -114,9 +120,9 @@ jobs: - name: Set up Docker Buildx if: ${{ (inputs.save-as-artifact && inputs.push-role-name == 'no-push') || steps.check-if-exists.outputs.status == 'notfound' }} - uses: docker/setup-buildx-action@v3.10.0 + uses: docker/setup-buildx-action@v3.11.1 with: - version: v0.22.0 + version: v0.27.0 - name: Build Docker Image if: ${{ (inputs.save-as-artifact && inputs.push-role-name == 'no-push') || steps.check-if-exists.outputs.status == 'notfound' }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 07169ccf..3a1d0f2d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,7 +53,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Move python script that replaces private package registry information to temp folder so it doesn't get deleted run: | @@ -108,7 +108,7 @@ jobs: timeout-minutes: 30 # this is the amount of time this action will wait to attempt to acquire the mutex lock before failing, e.g. if other jobs are queued up in front of it - name: Cache Pre-commit hooks - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 env: cache-name: cache-pre-commit-hooks with: diff --git a/.github/workflows/get-values.yaml b/.github/workflows/get-values.yaml index a9f5c333..47dba7cd 100644 --- a/.github/workflows/get-values.yaml +++ b/.github/workflows/get-values.yaml @@ -25,7 +25,7 @@ jobs: pr-short-num: ${{ steps.find-pr-num.outputs.number }} steps: - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Update Devcontainer Hash if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'push' }} diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 24534af2..68addddd 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -32,19 +32,20 @@ jobs: steps: - name: Checkout code during push if: ${{ github.event_name == 'push' }} - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: ref: ${{ github.ref_name }} # explicitly get the head of the branch, which will include any new commits pushed if this is a dependabot branch - name: Checkout code not during push if: ${{ github.event_name != 'push' }} - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Install latest versions of packages uses: ./.github/actions/install_deps with: python-version: ${{ inputs.python-version }} node-version: ${{ inputs.node-version }} + skip-installing-ssm-plugin-manager: true - name: Set up mutex # Github concurrency management is horrible, things get arbitrarily cancelled if queued up. So using mutex until github fixes itself. When multiple jobs are modifying cache at once, weird things can happen. possible issue is https://github.com/actions/toolkit/issues/658 if: ${{ runner.os != 'Windows' }} # we're just gonna have to YOLO on Windows, because this action doesn't support it yet https://github.com/ben-z/gh-action-mutex/issues/14 @@ -54,7 +55,7 @@ jobs: timeout-minutes: 30 # this is the amount of time this action will wait to attempt to acquire the mutex lock before failing, e.g. if other jobs are queued up in front of it - name: Cache Pre-commit hooks - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 env: cache-name: cache-pre-commit-hooks with: diff --git a/.github/workflows/tag-on-merge.yaml b/.github/workflows/tag-on-merge.yaml index a11a90d5..00bb0cab 100644 --- a/.github/workflows/tag-on-merge.yaml +++ b/.github/workflows/tag-on-merge.yaml @@ -13,7 +13,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 with: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: '0' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23ece589..1103e03a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: # Reformatting (should generally come before any file format or other checks, because reformatting can change things) - repo: https://github.com/crate-ci/typos - rev: 7fb6e0951ad91e4772a2470012fc1ae621016b80 # frozen: v1 + rev: 65a25783d8705c6a72d9fead19c44d87b4ff03c3 # frozen: v1 hooks: - id: typos exclude: @@ -218,7 +218,7 @@ repos: exclude: docs/.*\.rst$ - repo: https://github.com/hadolint/hadolint - rev: c3dc18df7a501f02a560a2cc7ba3c69a85ca01d3 # frozen: v2.13.1-beta + rev: 87de847754330ad47ae16bdfe2d1a757ccb4b4d4 # frozen: v2.13.1 hooks: - id: hadolint-docker name: Lint Dockerfiles @@ -226,7 +226,7 @@ repos: description: Runs hadolint to lint Dockerfiles - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 54a455f7ce629598b7535ff828fd5fb796f4b83f # frozen: v0.12.9 + rev: db90487f48a9dd992d243ef63c156eaffddeaf28 # frozen: v0.12.11 hooks: - id: ruff name: ruff-src diff --git a/extensions/context.py b/extensions/context.py index 81604dab..f5c86edf 100644 --- a/extensions/context.py +++ b/extensions/context.py @@ -10,62 +10,71 @@ class ContextUpdater(ContextHook): @override def hook(self, context: dict[Any, Any]) -> dict[Any, Any]: - context["uv_version"] = "0.8.4" - context["pnpm_version"] = "10.14.0" + context["uv_version"] = "0.8.15" + context["pnpm_version"] = "10.15.1" context["pre_commit_version"] = "4.3.0" - context["pyright_version"] = "1.1.403" - context["pytest_version"] = "8.4.1" + context["pyright_version"] = "1.1.405" + context["pytest_version"] = "8.4.2" context["pytest_randomly_version"] = "3.16.0" - context["pytest_cov_version"] = "6.2.1" - context["copier_version"] = "9.9.1" - context["copier_template_extensions_version"] = "0.3.2" + context["pytest_cov_version"] = "6.3.0" + context["copier_version"] = "9.10.1" + context["copier_template_extensions_version"] = "0.3.3" context["sphinx_version"] = "8.1.3" - context["pulumi_version"] = "3.190.0" - context["pulumi_aws_version"] = "7.4.0" - context["pulumi_aws_native_version"] = "1.32.0" + context["pulumi_version"] = "3.193.0" + context["pulumi_aws_version"] = "7.7.0" + context["pulumi_aws_native_version"] = "1.33.0" context["pulumi_command_version"] = "1.1.0" context["pulumi_github_version"] = "6.7.3" - context["pulumi_okta_version"] = "4.20.0" - context["boto3_version"] = "1.40.9" - context["ephemeral_pulumi_deploy_version"] = "0.0.4" + context["pulumi_okta_version"] = "5.2.0" + context["boto3_version"] = "1.40.25" + context["ephemeral_pulumi_deploy_version"] = "0.0.5" context["pydantic_version"] = "2.11.7" context["pyinstaller_version"] = "6.13.0" context["setuptools_version"] = "80.7.1" - context["strawberry_graphql_version"] = "0.270.4" - context["fastapi_version"] = "0.115.14" + context["strawberry_graphql_version"] = "0.282.0" + context["fastapi_version"] = "0.116.1" + context["fastapi_offline_version"] = "1.7.4" context["uvicorn_version"] = "0.35.0" - context["lab_auto_pulumi_version"] = "0.1.15" + context["lab_auto_pulumi_version"] = "0.1.16" - context["nuxt_ui_version"] = "^3.3.0" - context["nuxt_version"] = "^4.0.3" + context["node_version"] = "24.7.0" + context["nuxt_ui_version"] = "^3.3.2" + context["nuxt_version"] = "^4.1.0" context["nuxt_icon_version"] = "^2.0.0" context["typescript_version"] = "^5.8.2" context["dot_env_cli_version"] = "^9.0.0" - context["playwright_version"] = "^1.52.0" - context["vue_version"] = "^3.5.18" + context["playwright_version"] = "^1.55.0" + context["vue_version"] = "^3.5.21" + context["vue_tsc_version"] = "^3.0.6" + context["vue_devtools_api_version"] = "^8.0.0" context["vue_router_version"] = "^4.5.1" - context["faker_version"] = "^9.9.0" - context["eslint_version"] = "^9.33.0" - context["zod_version"] = "^4.0.17" + context["dotenv_cli_version"] = "^9.0.0" + context["faker_version"] = "^10.0.0" + context["vitest_version"] = "^3.2.4" + context["eslint_version"] = "^9.34.0" + context["nuxt_eslint_version"] = "^1.9.0" + context["zod_version"] = "^4.1.5" + context["zod_from_json_schema_version"] = "^0.5.0" + context["types_node_version"] = "^24.3.1" context["nuxt_apollo_version"] = "5.0.0-alpha.15" context["graphql_codegen_cli_version"] = "^5.0.5" context["graphql_codegen_typescript_version"] = "^4.1.6" - context["gha_checkout"] = "v4.2.2" - context["gha_setup_python"] = "v5.6.0" - context["gha_cache"] = "v4.2.3" + context["gha_checkout"] = "v5.0.0" + context["gha_setup_python"] = "v6.0.0" + context["gha_cache"] = "v4.2.4" context["gha_upload_artifact"] = "v4.6.2" - context["gha_download_artifact"] = "v4.3.0" + context["gha_download_artifact"] = "v5.0.0" context["gha_github_script"] = "v7.0.1" - context["gha_setup_buildx"] = "v3.10.0" - context["buildx_version"] = "v0.22.0" + context["gha_setup_buildx"] = "v3.11.1" + context["buildx_version"] = "v0.27.0" context["gha_docker_build_push"] = "v6.16.0" - context["gha_configure_aws_credentials"] = "v4.2.0" + context["gha_configure_aws_credentials"] = "v5.0.0" context["gha_amazon_ecr_login"] = "v2.0.1" - context["gha_setup_node"] = "v4.4.0" + context["gha_setup_node"] = "v5.0.0" context["gha_action_gh_release"] = "v2.2.1" context["gha_mutex"] = "1ebad517141198e08d47cf72f3c0975316620a65 # v1.0.0-alpha.10" - context["gha_pypi_publish"] = "v1.12.4" + context["gha_pypi_publish"] = "v1.13.0" context["gha_sleep"] = "v2.0.3" context["gha_linux_runner"] = "ubuntu-24.04" context["gha_windows_runner"] = "windows-2025" @@ -75,8 +84,8 @@ def hook(self, context: dict[Any, Any]) -> dict[Any, Any]: context["py313_version"] = "3.13.2" context["debian_release_name"] = "bookworm" - context["alpine_image_version"] = "3.21" - context["nginx_image_version"] = "1.28.0" + context["alpine_image_version"] = "3.22" + context["nginx_image_version"] = "1.29.1" # Kludge to be able to help symlinked jinja files in the child and grandchild templates context["template_uses_vuejs"] = False diff --git a/pyproject.toml b/pyproject.toml index 3912ed1c..9a250ed2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,12 @@ readme = "README.md" requires-python = ">=3.12.7" dependencies = [ # Managed by upstream template - "pytest>=8.4.1", - "pytest-cov>=6.2.1", + "pytest>=8.4.2", + "pytest-cov>=6.3.0", "pytest-randomly>=3.16.0", - "pyright[nodejs]>=1.1.403", - "copier>=9.9.1", - "copier-template-extensions>=0.3.2" + "pyright[nodejs]>=1.1.405", + "copier>=9.10.1", + "copier-template-extensions>=0.3.3" # Specific to this template diff --git a/template/.devcontainer/devcontainer.json.jinja b/template/.devcontainer/devcontainer.json.jinja index 16bd1318..b69eddd9 100644 --- a/template/.devcontainer/devcontainer.json.jinja +++ b/template/.devcontainer/devcontainer.json.jinja @@ -29,21 +29,21 @@ "eamodio.gitlens@15.5.1", "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905", - "github.copilot@1.320.1564", - "github.copilot-chat@0.28.2025051402", + "github.copilot@1.366.1775", + "github.copilot-chat@0.31.2025090401", // Python - "ms-python.python@2025.7.2025051401", - "ms-python.vscode-pylance@2025.4.104", + "ms-python.python@2025.13.2025090201", + "ms-python.vscode-pylance@2025.7.102", "ms-vscode-remote.remote-containers@0.414.0", "charliermarsh.ruff@2025.24.0", {% endraw %}{% if is_child_of_copier_base_template is not defined and template_uses_vuejs is defined and template_uses_vuejs is sameas(true) %}{% raw %} // VueJS - "vue.volar@2.2.8", + "vue.volar@3.0.6", "vitest.explorer@1.16.1", {% endraw %}{% endif %}{% raw %}{% endraw %}{% if is_child_of_copier_base_template is not defined and template_uses_javascript is defined and template_uses_javascript is sameas(true) %}{% raw %} // All javascript - "dbaeumer.vscode-eslint@3.0.13", + "dbaeumer.vscode-eslint@3.0.19", {% endraw %}{% endif %}{% raw %} // Misc file formats "bierner.markdown-mermaid@1.28.0", diff --git a/template/.devcontainer/install-ci-tooling.py.jinja b/template/.devcontainer/install-ci-tooling.py.jinja index 7a5e4f3c..4709e86e 100644 --- a/template/.devcontainer/install-ci-tooling.py.jinja +++ b/template/.devcontainer/install-ci-tooling.py.jinja @@ -31,10 +31,10 @@ _ = parser.add_argument( "--no-node", action="store_true", default=False, help="Do not process any environments using node package managers" ) _ = parser.add_argument( - "--install-ssm-plugin", + "--skip-installing-ssm-plugin", action="store_true", - default=INSTALL_SSM_PLUGIN_BY_DEFAULT, - help="Install the SSM plugin for AWS CLI", + default=False, + help="Skip installing the SSM plugin for AWS CLI", ) @@ -117,26 +117,43 @@ def main(): else [cmd] ) _ = subprocess.run(cmd, shell=True, check=True) - if args.install_ssm_plugin: - if is_windows: - raise NotImplementedError("SSM plugin installation is not implemented for Windows") + if INSTALL_SSM_PLUGIN_BY_DEFAULT and not args.skip_installing_ssm_plugin: with tempfile.TemporaryDirectory() as tmp_dir: - local_package_path = Path(tmp_dir) / "session-manager-plugin.deb" - # Based on https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-debian-and-ubuntu.html - # no specific reason for that version, just pinning it for best practice - _ = subprocess.run( - [ - "curl", - "https://s3.amazonaws.com/session-manager-downloads/plugin/1.2.707.0/ubuntu_64bit/session-manager-plugin.deb", - "-o", - f"{local_package_path}", - ], - check=True, - ) - _ = subprocess.run( - ["sudo", "dpkg", "-i", str(local_package_path)], - check=True, - ) + if is_windows: + local_package_path = Path(tmp_dir) / "SessionManagerPluginSetup.exe" + # Based on https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-windows.html + # no specific reason for that version, just pinning it for best practice + _ = subprocess.run( + [ + "curl", + "https://s3.amazonaws.com/session-manager-downloads/plugin/1.2.707.0/windows/SessionManagerPluginSetup.exe", + "-o", + f"{local_package_path}", + ], + check=True, + ) + _ = subprocess.run( + [str(local_package_path), "/quiet"], + check=True, + ) + else: + local_package_path = Path(tmp_dir) / "session-manager-plugin.deb" + # Based on https://docs.aws.amazon.com/systems-manager/latest/userguide/install-plugin-debian-and-ubuntu.html + # no specific reason for that version, just pinning it for best practice + _ = subprocess.run( + [ + "curl", + "https://s3.amazonaws.com/session-manager-downloads/plugin/1.2.707.0/ubuntu_64bit/session-manager-plugin.deb", + "-o", + f"{local_package_path}", + ], + check=True, + ) + _ = subprocess.run( + ["sudo", "dpkg", "-i", str(local_package_path)], + check=True, + ) + print("SSM Plugin Manager Version: ") _ = subprocess.run( ["session-manager-plugin", "--version"], check=True, diff --git a/template/.github/actions/install_deps/action.yml b/template/.github/actions/install_deps/action.yml index 3fafd45a..ddfe8b45 100644 --- a/template/.github/actions/install_deps/action.yml +++ b/template/.github/actions/install_deps/action.yml @@ -16,6 +16,11 @@ inputs: default: true type: boolean description: Whether to run the setup-deps script, or just to setup basic CI tooling + skip-installing-ssm-plugin-manager: + required: false + default: false + type: boolean + description: Whether to explicitly skip installing the SSM Plugin manager when setting up basic CI tooling project-dir: type: string description: What's the relative path to the project? @@ -48,24 +53,24 @@ runs: - name: Setup python if: ${{ inputs.python-version != 'notUsing' }} - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Setup node if: ${{ inputs.node-version != 'notUsing' }} - uses: actions/setup-node@v4.4.0 + uses: actions/setup-node@v5.0.0 with: node-version: ${{ inputs.node-version }} - name: Install tooling # the funky syntax is github action ternary - run: python .devcontainer/install-ci-tooling.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} + run: python .devcontainer/install-ci-tooling.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} ${{ inputs.skip-installing-ssm-plugin-manager && '--skip-installing-ssm-plugin' || '' }} shell: pwsh - name: OIDC Auth for CodeArtifact if: ${{ inputs.code-artifact-auth-role-name != 'no-code-artifact' }} - uses: aws-actions/configure-aws-credentials@v4.2.0 + uses: aws-actions/configure-aws-credentials@v5.0.0 with: role-to-assume: arn:aws:iam::${{ inputs.code-artifact-auth-role-account-id }}:role/${{ inputs.code-artifact-auth-role-name }} aws-region: ${{ inputs.code-artifact-auth-region }} diff --git a/template/.github/actions/update-devcontainer-hash/action.yml b/template/.github/actions/update-devcontainer-hash/action.yml index d5b7d214..fb64cc81 100644 --- a/template/.github/actions/update-devcontainer-hash/action.yml +++ b/template/.github/actions/update-devcontainer-hash/action.yml @@ -27,7 +27,7 @@ runs: shell: bash - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: persist-credentials: true fetch-depth: 1 diff --git a/template/.github/workflows/get-values.yaml b/template/.github/workflows/get-values.yaml index a9f5c333..47dba7cd 100644 --- a/template/.github/workflows/get-values.yaml +++ b/template/.github/workflows/get-values.yaml @@ -25,7 +25,7 @@ jobs: pr-short-num: ${{ steps.find-pr-num.outputs.number }} steps: - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Update Devcontainer Hash if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'push' }} diff --git a/template/.github/workflows/pre-commit.yaml b/template/.github/workflows/pre-commit.yaml index 24534af2..68addddd 100644 --- a/template/.github/workflows/pre-commit.yaml +++ b/template/.github/workflows/pre-commit.yaml @@ -32,19 +32,20 @@ jobs: steps: - name: Checkout code during push if: ${{ github.event_name == 'push' }} - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: ref: ${{ github.ref_name }} # explicitly get the head of the branch, which will include any new commits pushed if this is a dependabot branch - name: Checkout code not during push if: ${{ github.event_name != 'push' }} - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Install latest versions of packages uses: ./.github/actions/install_deps with: python-version: ${{ inputs.python-version }} node-version: ${{ inputs.node-version }} + skip-installing-ssm-plugin-manager: true - name: Set up mutex # Github concurrency management is horrible, things get arbitrarily cancelled if queued up. So using mutex until github fixes itself. When multiple jobs are modifying cache at once, weird things can happen. possible issue is https://github.com/actions/toolkit/issues/658 if: ${{ runner.os != 'Windows' }} # we're just gonna have to YOLO on Windows, because this action doesn't support it yet https://github.com/ben-z/gh-action-mutex/issues/14 @@ -54,7 +55,7 @@ jobs: timeout-minutes: 30 # this is the amount of time this action will wait to attempt to acquire the mutex lock before failing, e.g. if other jobs are queued up in front of it - name: Cache Pre-commit hooks - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 env: cache-name: cache-pre-commit-hooks with: diff --git a/template/.github/workflows/publish.yaml.jinja b/template/.github/workflows/publish.yaml.jinja index 153b8f04..099c526d 100644 --- a/template/.github/workflows/publish.yaml.jinja +++ b/template/.github/workflows/publish.yaml.jinja @@ -183,20 +183,66 @@ jobs: with: python-version: ${{ matrix.python-version }} {% endraw %}{% if python_package_registry == "PyPI" %}{% raw %} - - name: Sleep to allow PyPI Index to update before proceeding to the next step - uses: juliangruber/sleep-action@{% endraw %}{{ gha_sleep }}{% raw %} - with: - time: 90s{% endraw %}{% endif %}{% raw %} - - name: Install from staging registry - run: pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://www.pypi.org/simple {% endraw %}{{ package_name | replace('_', '-') }}{% raw %}==${{ needs.get-values.outputs.package-version }} + - name: Wait for Test PyPI to serve the new version + shell: bash + run: | + set -euo pipefail + PKG="{% endraw %}{{ package_name | replace('_', '-') }}{% raw %}" + VER="${{ needs.get-values.outputs.package-version }}" + for i in $(seq 1 60); do + code="$(curl -fsS -o /dev/null -w '%{http_code}' "https://test.pypi.org/pypi/${PKG}/${VER}/json" || true)" + if [ "$code" = "200" ]; then + echo "Found ${PKG}==${VER} on Test PyPI." + break + fi + echo "Not yet available; sleeping 5s..." + sleep 5 + done + + if [ "$code" != "200" ]; then + echo "Timeout waiting for ${PKG}==${VER} on Test PyPI API." + exit 1 + fi + + # Then try to install with retries to ensure it's actually available in the index + for i in $(seq 1 12); do + if pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://www.pypi.org/simple "${PKG}==${VER}" --no-cache-dir --quiet; then + echo "Successfully installed ${PKG}==${VER}" + exit 0 + fi + echo "Package not yet installable; sleeping 10s... (attempt $i/12)" + sleep 10 + done + + echo "Timeout waiting for ${PKG}==${VER} to be installable." + exit 1{% endraw %}{% endif %}{% raw %} - name: Display dependencies run: pip list - name: Confirm library can be imported successfully - run: python -c "import sys; print(f'Python version {sys.version}'); import {% endraw %}{{ package_name | replace('-', '_') }}{% raw %}" + env: + PYTHONPATH: "" # avoid picking up local sources + shell: bash + run: | + python - <<'PY' + import sys, importlib, importlib.util, importlib.metadata as md, pathlib + print(f"Python version {sys.version}") + mod_name = "{% endraw %}{{ package_name | replace('-', '_') }}{% raw %}" + dist_name = mod_name + m = importlib.import_module(mod_name) + spec = importlib.util.find_spec(mod_name) + origin = (getattr(spec, "origin", None) or getattr(m, "__file__", "")) or "" + p = pathlib.Path(origin) + print(f"Imported from: {p}") + assert any(s in str(p) for s in ("site-packages", "dist-packages")), f"Expected site/dist-packages, got {p}" + expected = "${{ needs.get-values.outputs.package-version }}" + installed = md.version(dist_name) + print(f"Installed distribution version: {installed} (expected {expected})") + assert installed == expected, f"Version mismatch: expected {expected}, got {installed}" + PY create-tag: name: Create the git tag - if: ${{ github.event.inputs.publish_to_primary }} + if: ${{ fromJSON(github.event.inputs.publish_to_primary) }} needs: [ install-from-staging ] permissions: contents: write # needed to push the tag @@ -215,7 +261,7 @@ jobs: publish-to-primary: name: Publish Python distribution to Primary Package Registry - if: ${{ github.event.inputs.publish_to_primary }} + if: ${{ fromJSON(github.event.inputs.publish_to_primary) }} needs: [ create-tag ] runs-on: {% endraw %}{{ gha_linux_runner }}{% raw %} environment: @@ -250,7 +296,7 @@ jobs: install-from-primary: name: Install package from primary registry - if: ${{ github.event.inputs.publish_to_primary }} + if: ${{ fromJSON(github.event.inputs.publish_to_primary) }} needs: [ publish-to-primary, get-values ] strategy: matrix: @@ -278,13 +324,59 @@ jobs: with: python-version: ${{ matrix.python-version }} {% endraw %}{% if python_package_registry == "PyPI" %}{% raw %} - - name: Sleep to allow PyPI Index to update before proceeding to the next step - uses: juliangruber/sleep-action@{% endraw %}{{ gha_sleep }}{% raw %} - with: - time: 90s{% endraw %}{% endif %}{% raw %} - - name: Install from primary registry - run: pip install {% endraw %}{{ package_name | replace('_', '-') }}{% raw %}==${{ needs.get-values.outputs.package-version }} + - name: Wait for PyPI to serve the new version + shell: bash + run: | + set -euo pipefail + PKG="{% endraw %}{{ package_name | replace('_', '-') }}{% raw %}" + VER="${{ needs.get-values.outputs.package-version }}" + for i in $(seq 1 60); do + code="$(curl -fsS -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/${PKG}/${VER}/json" || true)" + if [ "$code" = "200" ]; then + echo "Found ${PKG}==${VER} on PyPI." + break + fi + echo "Not yet available; sleeping 5s..." + sleep 5 + done + + if [ "$code" != "200" ]; then + echo "Timeout waiting for ${PKG}==${VER} on PyPI API." + exit 1 + fi + + # Then try to install with retries to ensure it's actually available in the index + for i in $(seq 1 12); do + if pip install --index-url https://pypi.org/simple/ "${PKG}==${VER}" --no-cache-dir --quiet; then + echo "Successfully installed ${PKG}==${VER}" + exit 0 + fi + echo "Package not yet installable; sleeping 10s... (attempt $i/12)" + sleep 10 + done + + echo "Timeout waiting for ${PKG}==${VER} to be installable." + exit 1{% endraw %}{% endif %}{% raw %} - name: Display dependencies run: pip list - name: Confirm library can be imported successfully - run: python -c "import sys; print(f'Python version {sys.version}'); import {% endraw %}{{ package_name | replace('-', '_') }}{% raw %}"{% endraw %} + env: + PYTHONPATH: "" # avoid picking up local sources + shell: bash + run: | + python - <<'PY' + import sys, importlib, importlib.util, importlib.metadata as md, pathlib + print(f"Python version {sys.version}") + mod_name = "{% endraw %}{{ package_name | replace('-', '_') }}{% raw %}" + dist_name = mod_name + m = importlib.import_module(mod_name) + spec = importlib.util.find_spec(mod_name) + origin = (getattr(spec, "origin", None) or getattr(m, "__file__", "")) or "" + p = pathlib.Path(origin) + print(f"Imported from: {p}") + assert any(s in str(p) for s in ("site-packages", "dist-packages")), f"Expected site/dist-packages, got {p}" + expected = "${{ needs.get-values.outputs.package-version }}" + installed = md.version(dist_name) + print(f"Installed distribution version: {installed} (expected {expected})") + assert installed == expected, f"Version mismatch: expected {expected}, got {installed}" + PY{% endraw %} diff --git a/template/.pre-commit-config.yaml b/template/.pre-commit-config.yaml index 23ece589..1103e03a 100644 --- a/template/.pre-commit-config.yaml +++ b/template/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: # Reformatting (should generally come before any file format or other checks, because reformatting can change things) - repo: https://github.com/crate-ci/typos - rev: 7fb6e0951ad91e4772a2470012fc1ae621016b80 # frozen: v1 + rev: 65a25783d8705c6a72d9fead19c44d87b4ff03c3 # frozen: v1 hooks: - id: typos exclude: @@ -218,7 +218,7 @@ repos: exclude: docs/.*\.rst$ - repo: https://github.com/hadolint/hadolint - rev: c3dc18df7a501f02a560a2cc7ba3c69a85ca01d3 # frozen: v2.13.1-beta + rev: 87de847754330ad47ae16bdfe2d1a757ccb4b4d4 # frozen: v2.13.1 hooks: - id: hadolint-docker name: Lint Dockerfiles @@ -226,7 +226,7 @@ repos: description: Runs hadolint to lint Dockerfiles - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 54a455f7ce629598b7535ff828fd5fb796f4b83f # frozen: v0.12.9 + rev: db90487f48a9dd992d243ef63c156eaffddeaf28 # frozen: v0.12.11 hooks: - id: ruff name: ruff-src diff --git a/uv.lock b/uv.lock index d4604309..c916ab01 100644 --- a/uv.lock +++ b/uv.lock @@ -22,7 +22,7 @@ wheels = [ [[package]] name = "copier" -version = "9.9.1" +version = "9.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, @@ -34,14 +34,15 @@ dependencies = [ { name = "pathspec" }, { name = "platformdirs" }, { name = "plumbum" }, + { name = "prompt-toolkit" }, { name = "pydantic" }, { name = "pygments" }, { name = "pyyaml" }, { name = "questionary" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/68/e938aec15c204e3c55db570598409fe6bbd81fb81e26616b4b23d96ea92c/copier-9.9.1.tar.gz", hash = "sha256:244bdf3ec5eb460dbe45ef22b825e9897b0bcc9d94ca96dad3ced3d786cfeab7", size = 586353, upload-time = "2025-08-18T12:14:17.316Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/a5/0cf693f3fa51cba1d36765939e0d9956c0487426ad581868a2507c208bad/copier-9.10.1.tar.gz", hash = "sha256:ba2d729465508da04a62bc9b76eed13d952aa7634a74a69519252fcf8a54d94e", size = 586680, upload-time = "2025-08-28T13:04:54.307Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/d9/f7abe1fe19f58c3ed2f1625dcd46ad76612c0f6b3ac320db77c267223f39/copier-9.9.1-py3-none-any.whl", hash = "sha256:9439280baca00194933b04885f1376b17e3287c2324053666fbe9a7a0aceaa44", size = 56118, upload-time = "2025-08-18T12:14:15.009Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4b/4a12d04124b0158b77958fb12349828bb4023f6530aaa1713ceb784c77a3/copier-9.10.1-py3-none-any.whl", hash = "sha256:8b1b406367c67e5ee389778246cea18cddd55e71bfc6503d5fa13fe73304407a", size = 56017, upload-time = "2025-08-28T13:04:52.444Z" }, ] [[package]] @@ -59,11 +60,11 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "copier", specifier = ">=9.9.1" }, - { name = "copier-template-extensions", specifier = ">=0.3.2" }, - { name = "pyright", extras = ["nodejs"], specifier = ">=1.1.403" }, - { name = "pytest", specifier = ">=8.4.1" }, - { name = "pytest-cov", specifier = ">=6.2.1" }, + { name = "copier", specifier = ">=9.10.1" }, + { name = "copier-template-extensions", specifier = ">=0.3.3" }, + { name = "pyright", extras = ["nodejs"], specifier = ">=1.1.405" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-cov", specifier = ">=6.3.0" }, { name = "pytest-randomly", specifier = ">=3.16.0" }, ] @@ -358,15 +359,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.403" +version = "1.1.405" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/6c/ba4bbee22e76af700ea593a1d8701e3225080956753bee9750dcc25e2649/pyright-1.1.405.tar.gz", hash = "sha256:5c2a30e1037af27eb463a1cc0b9f6d65fec48478ccf092c1ac28385a15c55763", size = 4068319, upload-time = "2025-09-04T03:37:06.776Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1a/524f832e1ff1962a22a1accc775ca7b143ba2e9f5924bb6749dce566784a/pyright-1.1.405-py3-none-any.whl", hash = "sha256:a2cb13700b5508ce8e5d4546034cb7ea4aedb60215c6c33f56cec7f53996035a", size = 5905038, upload-time = "2025-09-04T03:37:04.913Z" }, ] [package.optional-dependencies] @@ -376,7 +377,7 @@ nodejs = [ [[package]] name = "pytest" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -385,23 +386,23 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] name = "pytest-cov" -version = "6.2.1" +version = "6.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4c/f883ab8f0daad69f47efdf95f55a66b51a8b939c430dadce0611508d9e99/pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2", size = 70398, upload-time = "2025-09-06T15:40:14.361Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/80/b4/bb7263e12aade3842b938bc5c6958cae79c5ee18992f9b9349019579da0f/pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749", size = 25115, upload-time = "2025-09-06T15:40:12.44Z" }, ] [[package]]