diff --git a/.copier-answers.yml b/.copier-answers.yml index ba57bf3d..c151c79c 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.0.85 +_commit: v0.0.90 _src_path: gh:LabAutomationAndScreening/copier-base-template.git description: Copier template for creating Python libraries and executables install_claude_cli: false @@ -12,6 +12,8 @@ repo_org_name: LabAutomationAndScreening repo_org_name_for_copyright: Lab Automation & Screening ssh_port_number: 55874 template_might_want_to_install_aws_ssm_port_forwarding_plugin: true +template_might_want_to_use_python_asyncio: true +template_might_want_to_use_vcrpy: true template_uses_javascript: false template_uses_pulumi: false template_uses_python: true diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0438d274..25e01643 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,11 +3,6 @@ "service": "devcontainer", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "features": { - "ghcr.io/devcontainers/features/aws-cli:1.1.2": { - // https://github.com/devcontainers/features/blob/main/src/aws-cli/devcontainer-feature.json - // view latest version https://raw.githubusercontent.com/aws/aws-cli/v2/CHANGELOG.rst - "version": "2.31.11" - }, "ghcr.io/devcontainers/features/python:1.7.1": { // https://github.com/devcontainers/features/blob/main/src/python/devcontainer-feature.json "version": "3.12.7", @@ -21,7 +16,7 @@ "extensions": [ // basic tooling // "eamodio.gitlens@15.5.1", - "coderabbit.coderabbit-vscode@0.16.0", + "coderabbit.coderabbit-vscode@0.16.1", "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905", "github.copilot@1.388.0", @@ -63,5 +58,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): eeccb984 # spellchecker:disable-line + // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): 99b3f7c4 # spellchecker:disable-line } diff --git a/.devcontainer/install-ci-tooling.py b/.devcontainer/install-ci-tooling.py index 006ffe97..0de75239 100644 --- a/.devcontainer/install-ci-tooling.py +++ b/.devcontainer/install-ci-tooling.py @@ -7,8 +7,8 @@ import tempfile from pathlib import Path -UV_VERSION = "0.9.11" -PNPM_VERSION = "10.23.0" +UV_VERSION = "0.9.17" +PNPM_VERSION = "10.25.0" COPIER_VERSION = "9.11.0" COPIER_TEMPLATE_EXTENSIONS_VERSION = "0.3.3" PRE_COMMIT_VERSION = "4.5.0" diff --git a/.devcontainer/windows-host-helper.sh b/.devcontainer/windows-host-helper.sh index b3d1d876..a071d4f7 100644 --- a/.devcontainer/windows-host-helper.sh +++ b/.devcontainer/windows-host-helper.sh @@ -27,38 +27,30 @@ repoName=$(basename "$gitUrl" .git) echo "Repo name extracted as '$repoName'" -# Remove any existing subfolder with the repository name and recreate it -rm -rf "./$repoName" || true # sometimes deleting the .venv folder fails -rm -rf "./$repoName/*.md" # for some reason, sometimes md files are left behind +sudo rm -rf "./$repoName" || true +sudo rm -rf "./$repoName/*.md" mkdir -p "./$repoName" +sudo chown -R "$(whoami):$(whoami)" "./$repoName" # TODO: see if this alone is enough to fix everything # Create a temporary directory for cloning tmpdir=$(mktemp -d) -# Clone the repository into a subfolder inside the temporary directory. -# This creates "$tmpdir/$repoName" with the repository's contents. +# Clone the repository into a subfolder inside the temporary directory git clone "$gitUrl" "$tmpdir/$repoName" - -SRC="$(realpath "$tmpdir/$repoName")" -DST="$(realpath "./$repoName")" - -# 1) Recreate directory tree under $DST -while IFS= read -r -d '' dir; do - rel="${dir#$SRC/}" # strip leading $SRC/ → e.g. "sub/dir" - mkdir -p "$DST/$rel" -done < <(find "$SRC" -type d -print0) - -# 2) Move all files into that mirror -while IFS= read -r -d '' file; do - rel="${file#$SRC/}" # e.g. "sub/dir/file.txt" - # ensure parent exists (though step 1 already did) - mkdir -p "$(dirname "$DST/$rel")" - mv "$file" "$DST/$rel" -done < <(find "$SRC" -type f -print0) - -# 3) Clean up now‑empty dirs and the tmp clone -find "$SRC" -depth -type d -empty -delete +# Use rsync to merge all contents (including hidden files) from cloned repo to target +# -a: archive mode (preserves permissions, timestamps, etc.) +# -v: verbose +# --exclude: skip volume mount directories that should not be overwritten +echo "Syncing repository contents..." +rsync -av \ + --exclude='node_modules' \ + --exclude='.pnpm-store' \ + --exclude='.venv' \ + "$tmpdir/$repoName/" "./$repoName/" + +# Clean up: remove the temporary directory rm -rf "$tmpdir" -echo "Repository '$repoName' has been synced into '$DST'." +echo "Repository '$repoName' has been updated." +echo "Note: Volume mounts (node_modules, .pnpm-store, .venv) were preserved." diff --git a/.github/actions/install_deps/action.yml b/.github/actions/install_deps/action.yml index 0e9f0206..48796359 100644 --- a/.github/actions/install_deps/action.yml +++ b/.github/actions/install_deps/action.yml @@ -58,13 +58,13 @@ runs: - name: Setup python if: ${{ inputs.python-version != 'notUsing' }} - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@v6.1.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Setup node if: ${{ inputs.node-version != 'notUsing' }} - uses: actions/setup-node@v6.0.0 + uses: actions/setup-node@v6.1.0 with: node-version: ${{ inputs.node-version }} @@ -75,7 +75,7 @@ runs: - name: OIDC Auth for CodeArtifact if: ${{ inputs.code-artifact-auth-role-name != 'no-code-artifact' }} - uses: aws-actions/configure-aws-credentials@v5.1.0 + uses: aws-actions/configure-aws-credentials@v5.1.1 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 fb64cc81..4e6d434c 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@v5.0.0 + uses: actions/checkout@v6.0.1 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 50fafeb5..f4f4a7a0 100644 --- a/.github/reusable_workflows/build-docker-image.yaml +++ b/.github/reusable_workflows/build-docker-image.yaml @@ -66,13 +66,13 @@ jobs: shell: bash - name: Checkout code - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v6.0.1 with: persist-credentials: false - name: OIDC Auth for ECR if: ${{ inputs.push-role-name != 'no-push' }} - uses: aws-actions/configure-aws-credentials@v5.1.0 + uses: aws-actions/configure-aws-credentials@v5.1.1 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 }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 59811b57..d49e7666 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,7 +20,8 @@ jobs: contents: write # needed for updating dependabot branches pre-commit: - needs: [ get-values ] + needs: + - get-values uses: ./.github/workflows/pre-commit.yaml permissions: contents: write # needed for mutex @@ -29,7 +30,8 @@ jobs: python-version: 3.12.7 lint-matrix: - needs: [ pre-commit ] + needs: + - pre-commit strategy: matrix: os: @@ -54,7 +56,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v6.0.1 with: persist-credentials: false @@ -111,7 +113,7 @@ jobs: timeout-minutes: 8 # 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.4 + uses: actions/cache@v4.3.0 env: cache-name: cache-pre-commit-hooks with: @@ -123,7 +125,13 @@ jobs: - name: Run pre-commit run: | # skip devcontainer context hash because the template instantiation may make it different every time - SKIP=git-dirty,compute-devcontainer-context-hash pre-commit run -a + SKIP=git-dirty,compute-devcontainer-context-hash pre-commit run -a || PRE_COMMIT_EXIT_CODE=$? + if [ -n "$PRE_COMMIT_EXIT_CODE" ]; then + echo "Pre-commit failed with exit code $PRE_COMMIT_EXIT_CODE" + echo "Showing git diff:" + git --no-pager diff + exit $PRE_COMMIT_EXIT_CODE + fi - name: Upload pre-commit log if failure if: ${{ failure() }} @@ -135,7 +143,9 @@ jobs: required-check: runs-on: ubuntu-24.04 timeout-minutes: 2 - needs: [ lint-matrix, get-values ] + needs: + - lint-matrix + - get-values permissions: statuses: write # needed for updating status on Dependabot PRs if: always() diff --git a/.github/workflows/get-values.yaml b/.github/workflows/get-values.yaml index c7126d34..0be45c49 100644 --- a/.github/workflows/get-values.yaml +++ b/.github/workflows/get-values.yaml @@ -9,6 +9,9 @@ on: dependabot-commit-created: description: whether or not a commit was created on a dependabot branch value: ${{ jobs.get-values.outputs.dependabot-commit-created }} + pr-short-num: + description: the last two digits of the PR number (to be used for fixed width naming, like Pulumi stacks) + value: ${{ jobs.get-values.outputs.pr-short-num }} env: PYTHONUNBUFFERED: True @@ -32,7 +35,7 @@ jobs: JSON - name: Checkout code - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v6.0.1 with: persist-credentials: false diff --git a/.github/workflows/hash_git_files.py b/.github/workflows/hash_git_files.py index 1da0a3f7..40576d44 100644 --- a/.github/workflows/hash_git_files.py +++ b/.github/workflows/hash_git_files.py @@ -65,19 +65,16 @@ def compute_adler32(repo_path: Path, files: list[str]) -> int: if not chunk: break checksum = zlib.adler32(chunk, checksum) - except Exception as e: - if "[Errno 21] Is a directory" in str(e): - # Ignore symlinks that on windows sometimes get confused as being directories - continue - print(f"Error reading file {file}: {e}", file=sys.stderr) # noqa: T201 # this just runs as a simple script, so using print instead of log - raise + except IsADirectoryError: + # Ignore symlinks that on windows sometimes get confused as being directories + continue return checksum def find_devcontainer_hash_line(lines: list[str]) -> tuple[int, str | None]: """Find the line index and current hash in the devcontainer.json file.""" - for i in range(len(lines) - 1, -1, -1): + for i in reversed(range(len(lines))): if lines[i].strip() == "}": # Check the line above it if i > 0: diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 6d6cf4ca..40c0a513 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -33,14 +33,14 @@ jobs: steps: - name: Checkout code during push if: ${{ github.event_name == 'push' }} - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v6.0.1 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 persist-credentials: false - name: Checkout code not during push if: ${{ github.event_name != 'push' }} - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v6.0.1 with: persist-credentials: false @@ -59,7 +59,7 @@ jobs: timeout-minutes: 8 # 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.4 + uses: actions/cache@v4.3.0 env: cache-name: cache-pre-commit-hooks with: @@ -69,4 +69,11 @@ jobs: ubuntu-24.04-py${{ inputs.python-version }}-node-${{ inputs.node-version}}-${{ env.cache-name }}- - name: Run pre-commit - run: pre-commit run -a + run: | + pre-commit run -a || PRE_COMMIT_EXIT_CODE=$? + if [ -n "$PRE_COMMIT_EXIT_CODE" ]; then + echo "Pre-commit failed with exit code $PRE_COMMIT_EXIT_CODE" + echo "Showing git diff:" + git --no-pager diff + exit $PRE_COMMIT_EXIT_CODE + fi diff --git a/.github/workflows/tag-on-merge.yaml b/.github/workflows/tag-on-merge.yaml index 3c208ca7..2e9b2277 100644 --- a/.github/workflows/tag-on-merge.yaml +++ b/.github/workflows/tag-on-merge.yaml @@ -14,7 +14,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v5.0.0 + - uses: actions/checkout@v6.0.1 with: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: '0' diff --git a/.gitignore b/.gitignore index fa36212d..72004381 100644 --- a/.gitignore +++ b/.gitignore @@ -77,7 +77,6 @@ dist **/logs/*.log.* # macOS dev cleanliness -*.DS_Store -.DS_Store +**/.DS_Store # Ignores specific to this repository diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86348a0b..a9e1bbd5 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: 6573587991823ef75e4d6ca97fe895f45e9f14e4 # frozen: v1 + rev: 802d5794ff9cf7b15610c47eca99cd1ab757d8d4 # frozen: v1 hooks: - id: typos exclude: | @@ -108,7 +108,7 @@ repos: )$ - repo: https://github.com/rbubley/mirrors-prettier - rev: 5ba47274f9b181bce26a5150a725577f3c336011 # frozen: v3.6.2 + rev: 14abee445aea04b39069c19b4bd54efff6775819 # frozen: v3.7.4 hooks: - id: prettier # TODO: get template YAML and MD files more in line with prettier expectations so we can start using prettier on those too @@ -195,7 +195,7 @@ repos: - id: check-case-conflict - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 83b816d020105076daac266dbf6bfed199a2da93 # frozen: 0.34.1 + rev: 16a6ad2fead09286ee6eb6b0a3fab55655a6c22a # frozen: 0.35.0 hooks: - id: check-github-workflows @@ -249,7 +249,7 @@ repos: description: Runs hadolint to lint Dockerfiles - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 3db93a2be6f214ed722bf7bce095ec1b1715422a # frozen: v0.14.2 + rev: 1a1f58ba4c35362efe8fed2279715a905baee93d # frozen: v0.14.8 hooks: - id: ruff name: ruff-src @@ -257,7 +257,7 @@ repos: files: src/.+\.py$ exclude: | (?x)^( - .*/graphql_codegen/.*| + .*/generated/graphql/.*| .*/generated/open[-_]api/.*| template/.*| )$ @@ -267,26 +267,26 @@ repos: files: tests?/.+\.py$ exclude: | (?x)^( - .*/graphql_codegen/.*| + .*/generated/graphql/.*| .*/generated/open[-_]api/.*| template/.*| )$ - id: ruff-format exclude: | (?x)^( - .*/graphql_codegen/.*| + .*/generated/graphql/.*| .*/generated/open[-_]api/.*| )$ - repo: https://github.com/pylint-dev/pylint - rev: 0eb92d25fd38ba5bad2f8d2ea7df63ad23e18ae3 # frozen: v4.0.2 + rev: e16f942166511d6fb4427e503a734152fae0c4fe # frozen: v4.0.4 hooks: - id: pylint name: pylint # exclude the template files---duplication within them will be discovered during CI of that template instantiation exclude: | (?x)^( - .*/graphql_codegen/.*| + .*/generated/graphql/.*| .*/generated/open[-_]api/.*| template/.*| )$ @@ -302,7 +302,7 @@ repos: files: '.+\.py$' exclude: | (?x)^( - .*/graphql_codegen/.*| + .*/generated/graphql/.*| .*/generated/open[-_]api/.*| )$ # don't pass filenames else the command line sees them twice diff --git a/copier.yml b/copier.yml index a7ae287e..7750ac88 100644 --- a/copier.yml +++ b/copier.yml @@ -37,6 +37,15 @@ install_aws_ssm_port_forwarding_plugin: help: Should the AWS SSM Port Forwarding Plugin be installed? default: no +configure_vcrpy: + type: bool + help: Should VCRpy be configured for use during unit testing in Python? + default: no +configure_python_asyncio: + type: bool + help: Will python code be using asyncio? + default: no + python_version: type: str help: What version of Python is used for development? diff --git a/copier_template_resources/python_asyncio/asyncio_fixtures.py b/copier_template_resources/python_asyncio/asyncio_fixtures.py new file mode 100644 index 00000000..43bb4405 --- /dev/null +++ b/copier_template_resources/python_asyncio/asyncio_fixtures.py @@ -0,0 +1,36 @@ +import asyncio + +import pytest +from backend_api.background_tasks import background_task_exceptions +from backend_api.background_tasks import background_tasks_set + + +async def _wait_for_tasks(tasks_list: list[asyncio.Task[None]]): + _, pending = await asyncio.wait(tasks_list, timeout=5.0) + if pending: + raise RuntimeError(f"There are still pending tasks: {pending}") + + +@pytest.fixture(autouse=True) +def fail_on_background_task_errors(): + """Automatically fail tests if ANY background task raises an exception.""" + background_task_exceptions.clear() + + yield + + # Wait for background tasks to complete (using asyncio.run for sync fixture) + if background_tasks_set: + tasks_list = list(background_tasks_set) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + asyncio.run(_wait_for_tasks(tasks_list)) + else: + loop.run_until_complete(_wait_for_tasks(tasks_list)) + + # Fail if any exceptions occurred + if background_task_exceptions: + pytest.fail( + f"Background tasks raised {len(background_task_exceptions)} exception(s):\n" + + "\n\n".join(f"{type(e).__name__}: {e}" for e in background_task_exceptions) + ) diff --git a/copier_template_resources/python_asyncio/background_tasks.py b/copier_template_resources/python_asyncio/background_tasks.py new file mode 100644 index 00000000..815f1721 --- /dev/null +++ b/copier_template_resources/python_asyncio/background_tasks.py @@ -0,0 +1,41 @@ +import asyncio +import logging +import traceback +from collections import deque +from weakref import WeakSet + +logger = logging.getLogger(__name__) +background_tasks_set: WeakSet[asyncio.Task[None]] = WeakSet() +background_task_exceptions: deque[Exception] = deque( + maxlen=100 # don't grow infinitely in production +) +# Store creation tracebacks for debugging +_task_creation_tracebacks: dict[int, str] = {} + + +def _task_done_callback(task: asyncio.Task[None]): + task_id = id(task) + background_tasks_set.discard(task) + try: + task.result() + except ( # pragma: no cover # hard to unit test this, but it'd be good to think of a way to do so + asyncio.CancelledError + ): + _ = _task_creation_tracebacks.pop(task_id, None) + return + except Exception as e: # pragma: no cover # hard to unit test this, but it'd be good to think of a way to do so + creation_tb = _task_creation_tracebacks.pop(task_id, "No traceback available") + logger.exception(f"Unhandled exception in background task\nTask was created from:\n{creation_tb}") + background_task_exceptions.append(e) + else: + # Clean up on successful completion + _ = _task_creation_tracebacks.pop(task_id, None) + + +def register_task(task: asyncio.Task[None]) -> None: + # Capture the stack trace at task creation time (excluding this function) + creation_stack = "".join(traceback.format_stack()[:-1]) + _task_creation_tracebacks[id(task)] = creation_stack + + background_tasks_set.add(task) + task.add_done_callback(_task_done_callback) diff --git a/copier_template_resources/vcrpy_fixtures.py b/copier_template_resources/vcrpy_fixtures.py new file mode 100644 index 00000000..4283fe1c --- /dev/null +++ b/copier_template_resources/vcrpy_fixtures.py @@ -0,0 +1,64 @@ +import os +from typing import Any +from typing import cast + +import pytest +from vcr import VCR + +UNREACHABLE_IP_ADDRESS = "192.0.2.1" # RFC 5737 TEST-NET-1 +IGNORED_HOSTS = [ + "testserver", # Skip recording any requests to our own server - let them run live + UNREACHABLE_IP_ADDRESS, # allow this through VCR in order to be able to test network failure handling +] +ALLOWED_HOSTS: list[str] = [] + +CUSTOM_IGNORED_HOSTS: tuple[str, ...] = () + +IGNORED_HOSTS.extend(CUSTOM_IGNORED_HOSTS) +if ( + os.name == "nt" +): # on Windows (in CI), the network calls happen at a lower level socket connection even to our FastAPI test client, and can get automatically blocked. This disables that automatic network guard, which isn't great...but since it's still in place on Linux, any actual problems would hopefully get caught before pushing to CI. + ALLOWED_HOSTS.extend(["127.0.0.1", "localhost", "::1"]) + + +@pytest.fixture(autouse=True) +def vcr_config() -> dict[str, list[str]]: + cfg: dict[str, list[str]] = { + "ignore_hosts": IGNORED_HOSTS, + "filter_headers": ["User-Agent"], + } + if ALLOWED_HOSTS: + cfg["allowed_hosts"] = ALLOWED_HOSTS + return cfg + + +def pytest_recording_configure( + config: pytest.Config, # noqa: ARG001 # the config argument MUST be present (even when unused) or pytest-recording throws an error + vcr: VCR, +): + vcr.match_on = cast(tuple[str, ...], vcr.match_on) # pyright: ignore[reportUnknownMemberType] # I know vcr.match_on is unknown, that's why I'm casting and isinstance-ing it...not sure if there's a different approach pyright prefers + assert isinstance(vcr.match_on, tuple), ( + f"vcr.match_on is not a tuple, it is a {type(vcr.match_on)} with value {vcr.match_on}" + ) + vcr.match_on += ("body",) # body is not included by default, but it seems relevant + + def before_record_response(response: dict[str, str | dict[str, Any]]) -> dict[str, str | dict[str, Any]]: + headers_to_filter = ( + "Transfer-Encoding", + "Date", + "Server", + ) # none of these headers in the response matter for unit testing, so might as well make the cassette files smaller + headers = response["headers"] + assert isinstance(headers, dict), ( + f"Expected response['headers'] to be a dict, got {type(headers)} with value {headers}" + ) + for header in headers_to_filter: + if header in headers: + del headers[header] + if ( + header.lower() in headers + ): # some headers are lowercased by the server in the response (e.g. Date, Server) + del headers[header.lower()] + return response + + vcr.before_record_response = before_record_response diff --git a/extensions/context.py b/extensions/context.py index 8a38b56b..9c9bcffb 100644 --- a/extensions/context.py +++ b/extensions/context.py @@ -10,16 +10,16 @@ class ContextUpdater(ContextHook): @override def hook(self, context: dict[Any, Any]) -> dict[Any, Any]: - context["uv_version"] = "0.9.11" - context["pnpm_version"] = "10.23.0" + context["uv_version"] = "0.9.17" + context["pnpm_version"] = "10.25.0" context["pre_commit_version"] = "4.5.0" context["pyright_version"] = "1.1.407" - context["pytest_version"] = "9.0.1" + context["pytest_version"] = "9.0.2" context["pytest_randomly_version"] = "4.0.1" context["pytest_cov_version"] = "7.0.0" context["copier_version"] = "9.11.0" context["copier_template_extensions_version"] = "0.3.3" - context["sphinx_version"] = "8.1.3" + context["sphinx_version"] = "9.0.4" context["pulumi_version"] = "3.208.0" context["pulumi_aws_version"] = "7.12.0" context["pulumi_aws_native_version"] = "1.38.0" @@ -28,29 +28,32 @@ def hook(self, context: dict[Any, Any]) -> dict[Any, Any]: context["pulumi_okta_version"] = "6.1.0" context["boto3_version"] = "1.41.2" context["ephemeral_pulumi_deploy_version"] = "0.0.5" - context["pydantic_version"] = "2.12.4" - context["pyinstaller_version"] = "6.16.0" + context["pydantic_version"] = "2.12.5" + context["pyinstaller_version"] = "6.17.0" context["setuptools_version"] = "80.7.1" - context["strawberry_graphql_version"] = "0.287.0" - context["fastapi_version"] = "0.121.0" - context["fastapi_offline_version"] = "1.7.4" - context["uvicorn_version"] = "0.38.0" + context["strawberry_graphql_version"] = ">=0.287.0" + context["fastapi_version"] = ">=0.124.2" + context["fastapi_offline_version"] = ">=1.7.4" + context["uvicorn_version"] = ">=0.38.0" context["lab_auto_pulumi_version"] = "0.1.17" - context["ariadne_codegen_version"] = "0.15.2" + context["ariadne_codegen_version"] = ">=0.17.0" context["pytest_mock_version"] = "3.15.1" - context["uuid_utils_version"] = "0.11.0" - context["syrupy_version"] = "5.0.0" - context["structlog_version"] = "25.5.0" + context["uuid_utils_version"] = ">=0.12.0" + context["syrupy_version"] = ">=5.0.0" + context["structlog_version"] = ">=25.5.0" context["httpx_version"] = "0.28.1" - context["python_kiota_bundle_version"] = "1.9.7" + context["python_kiota_bundle_version"] = ">=1.9.7" + context["vcrpy_version"] = ">=8.1.0" + context["pytest_recording_version"] = ">=0.13.4" + context["pytest_asyncio_version"] = ">=1.3.0" - context["node_version"] = "24.7.0" + context["node_version"] = "24.11.1" context["nuxt_ui_version"] = "^4.2.1" - context["nuxt_version"] = "^4.2.0" + context["nuxt_version"] = "^4.2.2" context["nuxt_icon_version"] = "^2.1.0" context["typescript_version"] = "^5.9.3" - context["playwright_version"] = "^1.56.0" - context["vue_version"] = "^3.5.22" + context["playwright_version"] = "^1.57.0" + context["vue_version"] = "^3.5.25" context["vue_tsc_version"] = "^3.1.2" context["vue_devtools_api_version"] = "^8.0.0" context["vue_router_version"] = "^4.6.0" @@ -61,11 +64,12 @@ def hook(self, context: dict[Any, Any]) -> dict[Any, Any]: context["nuxt_eslint_version"] = "^1.10.0" context["zod_version"] = "^4.1.12" context["zod_from_json_schema_version"] = "^0.5.1" - context["types_node_version"] = "^24.10.1" + context["types_node_version"] = "^25.0.0" context["nuxt_apollo_version"] = "5.0.0-alpha.15" context["graphql_codegen_cli_version"] = "^6.0.0" context["graphql_codegen_typescript_version"] = "^5.0.0" context["graphql_codegen_typescript_operations_version"] = "^5.0.0" + context["graphql_tools_mock_version"] = "^9.1.0" context["tailwindcss_version"] = "^4.1.11" context["iconify_vue_version"] = "^5.0.0" context["iconify_json_lucide_version"] = "^1.2.71" @@ -77,18 +81,18 @@ def hook(self, context: dict[Any, Any]) -> dict[Any, Any]: context["happy_dom_version"] = "^20.0.2" context["node_kiota_bundle_version"] = "1.0.0-preview.99" - context["gha_checkout"] = "v5.0.0" - context["gha_setup_python"] = "v6.0.0" - context["gha_cache"] = "v4.2.4" + context["gha_checkout"] = "v6.0.1" + context["gha_setup_python"] = "v6.1.0" + context["gha_cache"] = "v4.3.0" context["gha_upload_artifact"] = "v5.0.0" context["gha_download_artifact"] = "v6.0.0" context["gha_github_script"] = "v7.0.1" context["gha_setup_buildx"] = "v3.11.1" context["buildx_version"] = "v0.27.0" context["gha_docker_build_push"] = "v6.18.0" - context["gha_configure_aws_credentials"] = "v5.1.0" + context["gha_configure_aws_credentials"] = "v5.1.1" context["gha_amazon_ecr_login"] = "v2.0.1" - context["gha_setup_node"] = "v6.0.0" + context["gha_setup_node"] = "v6.1.0" context["gha_action_gh_release"] = "v2.2.1" context["gha_mutex"] = "1ebad517141198e08d47cf72f3c0975316620a65 # v1.0.0-alpha.10" context["gha_pypi_publish"] = "v1.13.0" @@ -101,8 +105,8 @@ def hook(self, context: dict[Any, Any]) -> dict[Any, Any]: context["gha_xlong_timeout_minutes"] = "45" context["debian_release_name"] = "bookworm" - context["alpine_image_version"] = "3.22" - context["nginx_image_version"] = "1.29.1" + context["alpine_image_version"] = "3.23" + context["nginx_image_version"] = "1.29.4" context["kiota_cli_version"] = "1.29.0" diff --git a/pyproject.toml b/pyproject.toml index c538181d..1866b074 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.12.7" dependencies = [ # Managed by upstream template - "pytest>=9.0.1", + "pytest>=9.0.2", "pytest-cov>=7.0.0", "pytest-randomly>=4.0.1", "pyright[nodejs]>=1.1.407", diff --git a/pyrightconfig.json b/pyrightconfig.json index 1e26d9d5..10ed1516 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -10,10 +10,11 @@ "**/.pipx_cache", "**/__pycache__", "**/vendor_files", - "**/graphql_codegen", + "**/generated/graphql", "**/generated/open_api", "**/.venv", - "**/venv" + "**/venv", + "**/copier_template_resources" ], "strictListInference": true, "strictDictionaryInference": true, diff --git a/template/.devcontainer/devcontainer.json.jinja b/template/.devcontainer/devcontainer.json.jinja index 33839d91..12fc0580 100644 --- a/template/.devcontainer/devcontainer.json.jinja +++ b/template/.devcontainer/devcontainer.json.jinja @@ -2,12 +2,12 @@ "dockerComposeFile": "docker-compose.yml", "service": "devcontainer", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - "features": { + "features": {{% endraw %}{% if is_child_of_copier_base_template is not defined %}{% raw %} "ghcr.io/devcontainers/features/aws-cli:1.1.2": { // https://github.com/devcontainers/features/blob/main/src/aws-cli/devcontainer-feature.json // view latest version https://raw.githubusercontent.com/aws/aws-cli/v2/CHANGELOG.rst - "version": "2.31.11" - }, + "version": "2.32.6", + },{% endraw %}{% endif %}{% raw %} "ghcr.io/devcontainers/features/python:1.7.1": { // https://github.com/devcontainers/features/blob/main/src/python/devcontainer-feature.json "version": "{% endraw %}{{ python_version }}{% raw %}",{% endraw %}{% if is_frozen_executable %}{% raw %} @@ -25,10 +25,11 @@ "customizations": { "vscode": { // Add the IDs of extensions you want installed when the container is created. - "extensions": [ + "extensions": [{% endraw %}{% if is_child_of_copier_base_template is not defined %}{% raw %} + "-AmazonWebServices.aws-toolkit-vscode", // the AWS CLI feature installs this automatically, but it's causing problems in VS Code{% endraw %}{% endif %}{% raw %} // basic tooling // "eamodio.gitlens@15.5.1", - "coderabbit.coderabbit-vscode@0.16.0", + "coderabbit.coderabbit-vscode@0.16.1", "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905", "github.copilot@1.388.0", diff --git a/template/.devcontainer/windows-host-helper.sh b/template/.devcontainer/windows-host-helper.sh index b3d1d876..a071d4f7 100644 --- a/template/.devcontainer/windows-host-helper.sh +++ b/template/.devcontainer/windows-host-helper.sh @@ -27,38 +27,30 @@ repoName=$(basename "$gitUrl" .git) echo "Repo name extracted as '$repoName'" -# Remove any existing subfolder with the repository name and recreate it -rm -rf "./$repoName" || true # sometimes deleting the .venv folder fails -rm -rf "./$repoName/*.md" # for some reason, sometimes md files are left behind +sudo rm -rf "./$repoName" || true +sudo rm -rf "./$repoName/*.md" mkdir -p "./$repoName" +sudo chown -R "$(whoami):$(whoami)" "./$repoName" # TODO: see if this alone is enough to fix everything # Create a temporary directory for cloning tmpdir=$(mktemp -d) -# Clone the repository into a subfolder inside the temporary directory. -# This creates "$tmpdir/$repoName" with the repository's contents. +# Clone the repository into a subfolder inside the temporary directory git clone "$gitUrl" "$tmpdir/$repoName" - -SRC="$(realpath "$tmpdir/$repoName")" -DST="$(realpath "./$repoName")" - -# 1) Recreate directory tree under $DST -while IFS= read -r -d '' dir; do - rel="${dir#$SRC/}" # strip leading $SRC/ → e.g. "sub/dir" - mkdir -p "$DST/$rel" -done < <(find "$SRC" -type d -print0) - -# 2) Move all files into that mirror -while IFS= read -r -d '' file; do - rel="${file#$SRC/}" # e.g. "sub/dir/file.txt" - # ensure parent exists (though step 1 already did) - mkdir -p "$(dirname "$DST/$rel")" - mv "$file" "$DST/$rel" -done < <(find "$SRC" -type f -print0) - -# 3) Clean up now‑empty dirs and the tmp clone -find "$SRC" -depth -type d -empty -delete +# Use rsync to merge all contents (including hidden files) from cloned repo to target +# -a: archive mode (preserves permissions, timestamps, etc.) +# -v: verbose +# --exclude: skip volume mount directories that should not be overwritten +echo "Syncing repository contents..." +rsync -av \ + --exclude='node_modules' \ + --exclude='.pnpm-store' \ + --exclude='.venv' \ + "$tmpdir/$repoName/" "./$repoName/" + +# Clean up: remove the temporary directory rm -rf "$tmpdir" -echo "Repository '$repoName' has been synced into '$DST'." +echo "Repository '$repoName' has been updated." +echo "Note: Volume mounts (node_modules, .pnpm-store, .venv) were preserved." diff --git a/template/.github/actions/install_deps/action.yml b/template/.github/actions/install_deps/action.yml index 0e9f0206..48796359 100644 --- a/template/.github/actions/install_deps/action.yml +++ b/template/.github/actions/install_deps/action.yml @@ -58,13 +58,13 @@ runs: - name: Setup python if: ${{ inputs.python-version != 'notUsing' }} - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@v6.1.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Setup node if: ${{ inputs.node-version != 'notUsing' }} - uses: actions/setup-node@v6.0.0 + uses: actions/setup-node@v6.1.0 with: node-version: ${{ inputs.node-version }} @@ -75,7 +75,7 @@ runs: - name: OIDC Auth for CodeArtifact if: ${{ inputs.code-artifact-auth-role-name != 'no-code-artifact' }} - uses: aws-actions/configure-aws-credentials@v5.1.0 + uses: aws-actions/configure-aws-credentials@v5.1.1 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 fb64cc81..4e6d434c 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@v5.0.0 + uses: actions/checkout@v6.0.1 with: persist-credentials: true fetch-depth: 1 diff --git a/template/.github/workflows/get-values.yaml b/template/.github/workflows/get-values.yaml index c7126d34..0be45c49 100644 --- a/template/.github/workflows/get-values.yaml +++ b/template/.github/workflows/get-values.yaml @@ -9,6 +9,9 @@ on: dependabot-commit-created: description: whether or not a commit was created on a dependabot branch value: ${{ jobs.get-values.outputs.dependabot-commit-created }} + pr-short-num: + description: the last two digits of the PR number (to be used for fixed width naming, like Pulumi stacks) + value: ${{ jobs.get-values.outputs.pr-short-num }} env: PYTHONUNBUFFERED: True @@ -32,7 +35,7 @@ jobs: JSON - name: Checkout code - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v6.0.1 with: persist-credentials: false diff --git a/template/.github/workflows/hash_git_files.py b/template/.github/workflows/hash_git_files.py index 1da0a3f7..40576d44 100644 --- a/template/.github/workflows/hash_git_files.py +++ b/template/.github/workflows/hash_git_files.py @@ -65,19 +65,16 @@ def compute_adler32(repo_path: Path, files: list[str]) -> int: if not chunk: break checksum = zlib.adler32(chunk, checksum) - except Exception as e: - if "[Errno 21] Is a directory" in str(e): - # Ignore symlinks that on windows sometimes get confused as being directories - continue - print(f"Error reading file {file}: {e}", file=sys.stderr) # noqa: T201 # this just runs as a simple script, so using print instead of log - raise + except IsADirectoryError: + # Ignore symlinks that on windows sometimes get confused as being directories + continue return checksum def find_devcontainer_hash_line(lines: list[str]) -> tuple[int, str | None]: """Find the line index and current hash in the devcontainer.json file.""" - for i in range(len(lines) - 1, -1, -1): + for i in reversed(range(len(lines))): if lines[i].strip() == "}": # Check the line above it if i > 0: diff --git a/template/.github/workflows/pre-commit.yaml b/template/.github/workflows/pre-commit.yaml index 6d6cf4ca..40c0a513 100644 --- a/template/.github/workflows/pre-commit.yaml +++ b/template/.github/workflows/pre-commit.yaml @@ -33,14 +33,14 @@ jobs: steps: - name: Checkout code during push if: ${{ github.event_name == 'push' }} - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v6.0.1 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 persist-credentials: false - name: Checkout code not during push if: ${{ github.event_name != 'push' }} - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v6.0.1 with: persist-credentials: false @@ -59,7 +59,7 @@ jobs: timeout-minutes: 8 # 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.4 + uses: actions/cache@v4.3.0 env: cache-name: cache-pre-commit-hooks with: @@ -69,4 +69,11 @@ jobs: ubuntu-24.04-py${{ inputs.python-version }}-node-${{ inputs.node-version}}-${{ env.cache-name }}- - name: Run pre-commit - run: pre-commit run -a + run: | + pre-commit run -a || PRE_COMMIT_EXIT_CODE=$? + if [ -n "$PRE_COMMIT_EXIT_CODE" ]; then + echo "Pre-commit failed with exit code $PRE_COMMIT_EXIT_CODE" + echo "Showing git diff:" + git --no-pager diff + exit $PRE_COMMIT_EXIT_CODE + fi diff --git a/template/.gitignore b/template/.gitignore index fa36212d..72004381 100644 --- a/template/.gitignore +++ b/template/.gitignore @@ -77,7 +77,6 @@ dist **/logs/*.log.* # macOS dev cleanliness -*.DS_Store -.DS_Store +**/.DS_Store # Ignores specific to this repository diff --git a/template/.pre-commit-config.yaml b/template/.pre-commit-config.yaml index 86348a0b..a9e1bbd5 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: 6573587991823ef75e4d6ca97fe895f45e9f14e4 # frozen: v1 + rev: 802d5794ff9cf7b15610c47eca99cd1ab757d8d4 # frozen: v1 hooks: - id: typos exclude: | @@ -108,7 +108,7 @@ repos: )$ - repo: https://github.com/rbubley/mirrors-prettier - rev: 5ba47274f9b181bce26a5150a725577f3c336011 # frozen: v3.6.2 + rev: 14abee445aea04b39069c19b4bd54efff6775819 # frozen: v3.7.4 hooks: - id: prettier # TODO: get template YAML and MD files more in line with prettier expectations so we can start using prettier on those too @@ -195,7 +195,7 @@ repos: - id: check-case-conflict - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 83b816d020105076daac266dbf6bfed199a2da93 # frozen: 0.34.1 + rev: 16a6ad2fead09286ee6eb6b0a3fab55655a6c22a # frozen: 0.35.0 hooks: - id: check-github-workflows @@ -249,7 +249,7 @@ repos: description: Runs hadolint to lint Dockerfiles - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 3db93a2be6f214ed722bf7bce095ec1b1715422a # frozen: v0.14.2 + rev: 1a1f58ba4c35362efe8fed2279715a905baee93d # frozen: v0.14.8 hooks: - id: ruff name: ruff-src @@ -257,7 +257,7 @@ repos: files: src/.+\.py$ exclude: | (?x)^( - .*/graphql_codegen/.*| + .*/generated/graphql/.*| .*/generated/open[-_]api/.*| template/.*| )$ @@ -267,26 +267,26 @@ repos: files: tests?/.+\.py$ exclude: | (?x)^( - .*/graphql_codegen/.*| + .*/generated/graphql/.*| .*/generated/open[-_]api/.*| template/.*| )$ - id: ruff-format exclude: | (?x)^( - .*/graphql_codegen/.*| + .*/generated/graphql/.*| .*/generated/open[-_]api/.*| )$ - repo: https://github.com/pylint-dev/pylint - rev: 0eb92d25fd38ba5bad2f8d2ea7df63ad23e18ae3 # frozen: v4.0.2 + rev: e16f942166511d6fb4427e503a734152fae0c4fe # frozen: v4.0.4 hooks: - id: pylint name: pylint # exclude the template files---duplication within them will be discovered during CI of that template instantiation exclude: | (?x)^( - .*/graphql_codegen/.*| + .*/generated/graphql/.*| .*/generated/open[-_]api/.*| template/.*| )$ @@ -302,7 +302,7 @@ repos: files: '.+\.py$' exclude: | (?x)^( - .*/graphql_codegen/.*| + .*/generated/graphql/.*| .*/generated/open[-_]api/.*| )$ # don't pass filenames else the command line sees them twice diff --git a/template/README.md.jinja b/template/README.md.jinja index 55fa0641..62c23983 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -6,8 +6,8 @@ [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url={% endraw %}{{ full_repo_url }}{% raw %}){% endraw %}{% if not is_frozen_executable %}{% raw %} [![PyPI Version](https://img.shields.io/pypi/v/{% endraw %}{{ package_name }}{% raw %}.svg)](https://pypi.org/project/{% endraw %}{{ package_name }}{% raw %}/) [![Downloads](https://pepy.tech/badge/{% endraw %}{{ package_name }}{% raw %})](https://pepy.tech/project/{% endraw %}{{ package_name }}{% raw %}) -[![Python Versions](https://img.shields.io/pypi/pyversions/{% endraw %}{{ package_name }}{% raw %}.svg)](https://pypi.org/project/{% endraw %}{{ package_name }}{% raw %}/) -{% endraw %}{% endif %}{% raw %}[![Codecov](https://codecov.io/gh/{% endraw %}{{ full_repo_url | replace("https://github.com/", "") }}{% raw %}/branch/main/graph/badge.svg)](https://codecov.io/gh/{% endraw %}{{ full_repo_url | replace("https://github.com/", "") }}{% raw %}){% endraw %}{% if create_docs %}{% raw %} +[![Python Versions](https://img.shields.io/pypi/pyversions/{% endraw %}{{ package_name }}{% raw %}.svg)](https://pypi.org/project/{% endraw %}{{ package_name }}{% raw %}/){% endraw %}{% endif %}{% if use_codecov %}{% raw %} +[![Codecov](https://codecov.io/gh/{% endraw %}{{ full_repo_url | replace("https://github.com/", "") }}{% raw %}/branch/main/graph/badge.svg)](https://codecov.io/gh/{% endraw %}{{ full_repo_url | replace("https://github.com/", "") }}{% raw %}){% endraw %}{% endif %}{% if create_docs %}{% raw %} [![Documentation Status](https://readthedocs.org/projects/{% endraw %}{{ package_name }}{% raw %}/badge/?version=latest)](https://{% endraw %}{{ package_name }}{% raw %}.readthedocs.io/en/latest/?badge=latest){% endraw %}{% endif %}{% raw %}{% endraw %}{% if is_open_source%}{% raw %} [![OpenIssues](https://isitmaintained.com/badge/open/{% endraw %}{{ repo_org_name }}/{{ repo_name }}{% raw %}.svg)](https://isitmaintained.com/project/{% endraw %}{{ repo_org_name }}/{{ repo_name }}{% raw %}){% endraw %}{% endif %}{% raw %} diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 2cdf7477..a6d8bffc 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -28,9 +28,11 @@ dev = [ "pyright>={% endraw %}{{ pyright_version }}{% raw %}", "pytest>={% endraw %}{{ pytest_version }}{% raw %}", "pytest-cov>={% endraw %}{{ pytest_cov_version }}{% raw %}", - "pytest-randomly>={% endraw %}{{ pytest_randomly_version }}{% raw %}", -{% endraw %}{% if create_docs %}{% raw %} "sphinx=={% endraw %}{{ sphinx_version }}{% raw %}",{% endraw %}{% endif %}{% raw %} -{% endraw %}{% if is_frozen_executable %}{% raw %} "pyinstaller>={% endraw %}{{ pyinstaller_version }}{% raw %}",{% endraw %}{% endif %}{% raw %} + "pytest-randomly>={% endraw %}{{ pytest_randomly_version }}{% raw %}",{% endraw %}{% if configure_vcrpy %}{% raw %} + "pytest-recording{% endraw %}{{ pytest_recording_version }}{% raw %}", + "vcrpy{% endraw %}{{ vcrpy_version }}{% raw %}",{% endraw %}{% endif %}{% raw %}{% endraw %}{% if create_docs %}{% raw %} + "sphinx=={% endraw %}{{ sphinx_version }}{% raw %}",{% endraw %}{% endif %}{% raw %}{% endraw %}{% if is_frozen_executable %}{% raw %} + "pyinstaller>={% endraw %}{{ pyinstaller_version }}{% raw %}",{% endraw %}{% endif %}{% raw %} ] [tool.setuptools] diff --git a/template/pyrightconfig.json b/template/pyrightconfig.json index 1e26d9d5..10ed1516 100644 --- a/template/pyrightconfig.json +++ b/template/pyrightconfig.json @@ -10,10 +10,11 @@ "**/.pipx_cache", "**/__pycache__", "**/vendor_files", - "**/graphql_codegen", + "**/generated/graphql", "**/generated/open_api", "**/.venv", - "**/venv" + "**/venv", + "**/copier_template_resources" ], "strictListInference": true, "strictDictionaryInference": true, diff --git a/template/tests/unit/conftest.py.jinja b/template/tests/unit/conftest.py.jinja new file mode 100644 index 00000000..6bc122ab --- /dev/null +++ b/template/tests/unit/conftest.py.jinja @@ -0,0 +1,17 @@ +{% raw %}import logging + +import pytest{% endraw %}{% if configure_vcrpy %} + +{% endif %}{% if configure_vcrpy %}{% raw %}from .vcrpy_fixtures import pytest_recording_configure # noqa: F401 # this is configuration we need in conftest scope +from .vcrpy_fixtures import vcr_config # noqa: F401 # this is an autouse fixture{% endraw %}{% endif %}{% raw %} + +logger = logging.getLogger(__name__) + + +def pytest_configure( + config: pytest.Config,{% endraw %}{% if configure_vcrpy %}{% raw %} # noqa: ARG001 # the config argument MUST be present (even when unused) or pytest throws an error{% endraw %}{% endif %}{% raw %} +): + """Configure pytest itself, such as logging levels."""{% endraw %}{% if configure_vcrpy %}{% raw %} + # force the vcr.cassette logger to WARNING+ because otherwise the logs get super noisy with the playback of all the cassettes + vcr_logger = logging.getLogger("vcr.cassette") + vcr_logger.setLevel(logging.WARNING){% endraw %}{% endif %}{% raw %}{% endraw %} diff --git a/template/tests/unit/{% if configure_vcrpy %}vcrpy_fixtures.py{% endif %} b/template/tests/unit/{% if configure_vcrpy %}vcrpy_fixtures.py{% endif %} new file mode 120000 index 00000000..006332d2 --- /dev/null +++ b/template/tests/unit/{% if configure_vcrpy %}vcrpy_fixtures.py{% endif %} @@ -0,0 +1 @@ +../../../copier_template_resources/vcrpy_fixtures.py \ No newline at end of file diff --git a/tests/copier_data/data1.yaml b/tests/copier_data/data1.yaml index 20fb4737..cc5b46e2 100644 --- a/tests/copier_data/data1.yaml +++ b/tests/copier_data/data1.yaml @@ -7,6 +7,8 @@ install_claude_cli: false ssh_port_number: 12345 use_windows_in_ci: false install_aws_ssm_port_forwarding_plugin: true +configure_vcrpy: true +configure_python_asyncio: true python_package_registry: PyPI diff --git a/tests/copier_data/data2.yaml b/tests/copier_data/data2.yaml index 05e6e4a4..19e929c0 100644 --- a/tests/copier_data/data2.yaml +++ b/tests/copier_data/data2.yaml @@ -7,6 +7,8 @@ install_claude_cli: true ssh_port_number: 54321 use_windows_in_ci: true install_aws_ssm_port_forwarding_plugin: false +configure_vcrpy: false +configure_python_asyncio: false python_package_registry: AWS CodeArtifact diff --git a/tests/copier_data/data3.yaml b/tests/copier_data/data3.yaml index 0bf6f907..095dfbf7 100644 --- a/tests/copier_data/data3.yaml +++ b/tests/copier_data/data3.yaml @@ -7,6 +7,8 @@ install_claude_cli: false ssh_port_number: 12345 use_windows_in_ci: false install_aws_ssm_port_forwarding_plugin: false +configure_vcrpy: false +configure_python_asyncio: true python_package_registry: PyPI diff --git a/uv.lock b/uv.lock index c2e6c865..9f07ae0a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12.7" [[package]] @@ -62,7 +62,7 @@ requires-dist = [ { name = "copier", specifier = ">=9.11.0" }, { name = "copier-template-extensions", specifier = ">=0.3.3" }, { name = "pyright", extras = ["nodejs"], specifier = ">=1.1.407" }, - { name = "pytest", specifier = ">=9.0.1" }, + { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-randomly", specifier = ">=4.0.1" }, ] @@ -402,7 +402,7 @@ nodejs = [ [[package]] name = "pytest" -version = "9.0.1" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -411,9 +411,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]]