Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a4ba610
copie
ejfine Mar 13, 2025
c1f1116
codeartifact question
ejfine Mar 13, 2025
d18d0db
copier update
ejfine Mar 13, 2025
fbe705d
dockercompose
ejfine Mar 13, 2025
3e3031a
coiper for profile
ejfine Mar 13, 2025
80d509c
copier
ejfine Mar 13, 2025
abb6da7
on create
ejfine Mar 13, 2025
39af82f
coiper
ejfine Mar 13, 2025
1736bfc
update
ejfine Mar 13, 2025
92507d2
not equal
ejfine Mar 13, 2025
ff64a8b
copier
ejfine Mar 13, 2025
b1120ca
add to uv
ejfine Mar 13, 2025
23cbc31
copier scripts
ejfine Mar 13, 2025
3709b2d
add default
ejfine Mar 13, 2025
9b2f833
whitespace
ejfine Mar 13, 2025
adf6544
update test data
ejfine Mar 13, 2025
73450b6
test data
ejfine Mar 13, 2025
551a160
optionally lock
ejfine Mar 13, 2025
f25b5af
fix CI
ejfine Mar 13, 2025
17aefca
CI syntax
ejfine Mar 13, 2025
5c4ee91
update auth script
ejfine Mar 13, 2025
de73003
update uv action
ejfine Mar 13, 2025
d18606e
auth in CI
ejfine Mar 13, 2025
ebb94f3
more CI
ejfine Mar 13, 2025
5d9f2b1
copier
ejfine Mar 13, 2025
61a1013
region
ejfine Mar 13, 2025
0f70061
quotes
ejfine Mar 13, 2025
36c317b
mas kscert
ejfine Mar 13, 2025
54327d4
coiper
ejfine Mar 13, 2025
ecdad6b
rearrange default
ejfine Mar 13, 2025
673bb16
copier
ejfine Mar 13, 2025
6244893
debug
ejfine Mar 13, 2025
82d35d0
ci
ejfine Mar 14, 2025
c6e1351
More debug
ejfine Mar 14, 2025
cf80c83
more ci
ejfine Mar 14, 2025
c9af6eb
no ls
ejfine Mar 14, 2025
2f57b39
no failfast
ejfine Mar 14, 2025
e0eeebd
python
ejfine Mar 14, 2025
f26a314
move file
ejfine Mar 14, 2025
43ae0b9
username
ejfine Mar 14, 2025
6e729bd
update script
ejfine Mar 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .copier-answers.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier
_commit: v0.0.6-6-g2b24a38
_commit: v0.0.7-44-gea357db
_src_path: gh:LabAutomationAndScreening/copier-base-template.git
description: Copier template for creating Python libraries and executables
python_ci_versions:
Expand Down
8 changes: 7 additions & 1 deletion .devcontainer/create-aws-profile.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh
#!/usr/bin/env sh
set -ex

mkdir -p ~/.aws
Expand All @@ -10,10 +10,16 @@ else
fi

cat >> ~/.aws/config <<EOF




[profile localstack]
region=us-east-1
output=json
endpoint_url = $LOCALSTACK_ENDPOINT_URL


EOF
cat >> ~/.aws/credentials <<EOF
[localstack]
Expand Down
21 changes: 21 additions & 0 deletions .devcontainer/manual-setup-deps.sh
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
#!/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"
Expand Down
4 changes: 2 additions & 2 deletions .devcontainer/on-create-command.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ git config --global --add safe.directory /workspaces/copier-python-package-templ

sh .devcontainer/on-create-command-boilerplate.sh

sh .devcontainer/manual-setup-deps.sh

pre-commit install --install-hooks

sh .devcontainer/manual-setup-deps.sh --optionally-lock
23 changes: 23 additions & 0 deletions .github/actions/install_deps_uv/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ inputs:
description: What's the relative path to the project?
required: false
default: ./
code-artifact-auth-role-name:
type: string
description: What's the role name to use for CodeArtifact authentication?
required: false
default: no-code-artifact
code-artifact-auth-role-account-id:
type: string
description: What's the AWS Account ID that the role is in?
required: false
code-artifact-auth-region:
type: string
description: What region should the role use?
required: false


runs:
Expand Down Expand Up @@ -41,12 +54,22 @@ runs:
run: .github/actions/install_deps_uv/install-ci-tooling.ps1 ${{ env.PYTHON_VERSION }}
shell: pwsh

- name: OIDC Auth for CodeArtifact
if: ${{ inputs.code-artifact-auth-role-name != 'no-code-artifact' }}
uses: aws-actions/configure-aws-credentials@v4.0.2
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 }}

- name: Install Dependencies (Linux)
if: ${{ inputs.uv-sync && runner.os == 'Linux' }}
run: |
sh .devcontainer/manual-setup-deps.sh ${{ env.PYTHON_VERSION }}
shell: bash




- name: Install Dependencies (Windows)
if: ${{ inputs.uv-sync && runner.os == 'Windows' }}
run: .github/actions/install_deps_uv/manual-setup-deps.ps1 ${{ env.PYTHON_VERSION }}
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4.2.2

- name: Move python script that replaces private package registry information to temp folder so it doesn't get deleted
run: |
mv .github/workflows/replace_private_package_registries.py $RUNNER_TEMP

- name: Install python tooling
uses: ./.github/actions/install_deps_uv
with:
Expand Down Expand Up @@ -107,7 +111,12 @@ jobs:


- name: install new dependencies
env:
CODEARTIFACT_AUTH_TOKEN: 'faketoken'
UV_NO_CACHE: 'true'
run: |
# Remove any specification of a Python repository having a default other than PyPI...because in this CI pipeline we can only install from PyPI
python $RUNNER_TEMP/replace_private_package_registries.py
sh .devcontainer/manual-setup-deps.sh ${{ matrix.python-version }} --skip-lock
# Add everything to git so that pre-commit recognizes the files and runs on them
git add .
Expand Down
57 changes: 57 additions & 0 deletions .github/workflows/replace_private_package_registries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Update any project files that point to a private package registry to use public ones.

Since the CI pipelines for testing these copier templates don't have access to private registries, we can't test installing from them as part of CI.

Seems minimal risk, since the only problem we'd be missing is if the pyproject.toml (or similar config files) had syntax errors that would have been
caught by pre-commit.
"""

import re
from pathlib import Path


def process_file(file_path: Path):
# Read the entire file content
content = file_path.read_text()

# Regex to match a block starting with [[tool.uv.index]]
# until the next block header (a line starting with [[) or the end of the file.
pattern = re.compile(r"(\[\[tool\.uv\.index\]\].*?)(?=\n\[\[|$)", re.DOTALL)

# Find all uv.index blocks.
blocks = pattern.findall(content)

# Check if any block contains "default = true"
if not any("default = true" in block for block in blocks):
print(f"No changes in: {file_path}")
return

# If at least one block contains "default = true", remove all uv.index blocks.
new_content = pattern.sub("", content)

# Ensure file ends with a newline before appending the new block.
if not new_content.endswith("\n"):
new_content += "\n"

# Append the new block.
new_block = '[[tool.uv.index]]\nname = "pypi"\nurl = "https://pypi.org/simple/"\n'
new_content += new_block

# Write the updated content back to the file.
_ = file_path.write_text(new_content)
print(f"Updated file: {file_path}")


def main():
base_dir = Path(".")
# Use rglob to find all pyproject.toml files recursively.
for file_path in base_dir.rglob("pyproject.toml"):
# Check if the file is at most two levels deep.
# The relative path's parts count should be <= 3 (e.g. "pyproject.toml" is 1 part,
# "subdir/pyproject.toml" is 2 parts, and "subdir/subsubdir/pyproject.toml" is 3 parts).
if len(file_path.relative_to(base_dir).parts) <= 3:
process_file(file_path)


if __name__ == "__main__":
main()
33 changes: 33 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ python_version:
help: What version of Python is used for development?
default: "3.12.7"

python_package_registry:
type: str
help: What registry should Python Packgaes be installed from?
choices:
- PyPI
- AWS CodeArtifact
default: PyPI

python_ci_versions:
type: str
help: What versions should Python run CI on the instantiated template?
Expand All @@ -35,6 +43,31 @@ python_ci_versions:
- "3.13.2"


aws_identity_center_id:
type: str
help: What's the ID of your Organization's AWS Identity center, e.g. d-9145c20053?
when: "{{ python_package_registry == 'AWS CodeArtifact' }}"

aws_org_home_region:
type: str
help: What is the home region of the AWS Organization (where all of the central infrastructure is deployed)?
default: us-east-1
when: "{{ python_package_registry == 'AWS CodeArtifact' }}"

aws_central_infrastructure_account_id:
type: str
help: What's the ID of your Organization's AWS Account containing Central Infrastructure (e.g. CodeArtifact)?
when: "{{ python_package_registry == 'AWS CodeArtifact' }}"

core_infra_base_access_profile_name:
type: str
help: What's the AWS Identity Center Profile name for base access to the Central Infrastructure account (i.e. to read from CodeArtifact)?
when: "{{ python_package_registry == 'AWS CodeArtifact' }}"
default: CoreInfraBaseAccess





# Questions specific to this template

Expand Down
47 changes: 47 additions & 0 deletions template/.devcontainer/code-artifact-auth.sh.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{% if python_package_registry is defined and python_package_registry == "AWS CodeArtifact" %}{% raw %}#!/usr/bin/env bash
set -ex

# If none of these are set we can't possibly continue and should fail so you can fix it
if [ -z "$AWS_PROFILE" ] && [ -z "$AWS_ACCESS_KEY_ID" ] && [ -z "$CODEARTIFACT_AUTH_TOKEN" ]; then
echo "No AWS profile, access key, or auth token found, cannot proceed."
exit 1
else
# Only regenerate the token if it doesn't exist or wasn't already set as an environmental variable (e.g. during CI or passed into a docker image build)
if [ -z "$CODEARTIFACT_AUTH_TOKEN" ]; then
echo "Fetching CodeArtifact token"
if [ -z "$CI" ]; then
PROFILE_ARGS="--profile={% endraw %}{{ core_infra_base_access_profile_name }}{% raw %}"
else
PROFILE_ARGS=""
fi

# Check if AWS credentials are valid by trying to retrieve the caller identity.
# If the ARN is not returned, assume credentials are expired or not set correctly.
caller_identity=$(aws sts get-caller-identity --region={% endraw %}{{ aws_org_home_region }}{% raw %} $PROFILE_ARGS --query Arn --output text 2>/dev/null || echo "")
if [ -z "$caller_identity" ]; then
if [ -n "$CI" ]; then
echo "Error: In CI environment, aws sso login should never be called...something is wrong with this script or your workflow...perhaps you did not OIDC Auth yet in CI?"
exit 1
fi
echo "SSO credentials not found or expired, logging in..."
aws sso login $PROFILE_ARGS
else
echo "Using existing AWS credentials: $caller_identity"
fi

set +x
export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token \
--domain {% endraw %}{{ repo_org_name }}{% raw %} \
--domain-owner {% endraw %}{{ aws_central_infrastructure_account_id }}{% raw %} \
--region {% endraw %}{{ aws_org_home_region }}{% raw %} \
--query authorizationToken \
--output text $PROFILE_ARGS)
set -x
fi

set +x
export UV_INDEX_CODE_ARTIFACT_PRIMARY_PASSWORD="$CODEARTIFACT_AUTH_TOKEN"
export UV_INDEX_CODE_ARTIFACT_STAGING_PASSWORD="$CODEARTIFACT_AUTH_TOKEN"
set -x

fi{% endraw %}{% else %}{% raw %}# Placeholder file not being used by these copier template answers{% endraw %}{% endif %}
22 changes: 0 additions & 22 deletions template/.devcontainer/create-aws-profile.sh

This file was deleted.

42 changes: 42 additions & 0 deletions template/.devcontainer/create-aws-profile.sh.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% raw %}#!/usr/bin/env sh
set -ex

mkdir -p ~/.aws

if [ "$GITHUB_ACTIONS" = "true" ]; then
LOCALSTACK_ENDPOINT_URL="http://localhost:4566"
else
LOCALSTACK_ENDPOINT_URL="http://localstack:4566"
fi

cat >> ~/.aws/config <<EOF
{% endraw %}{% if aws_production_account_id is defined and aws_production_account_id != "" %}{% raw %}[profile development]
sso_session = org
sso_account_id = {% endraw %}{{ aws_development_account_id if use_staging_environment else aws_production_account_id }}{% raw %}
sso_role_name = LowRiskAccountAdminAccess
region = {% endraw %}{{ aws_org_home_region }}{% raw %}{% endraw %}{% endif %}{% raw %}

{% endraw %}{% if aws_identity_center_id is defined and aws_identity_center_id != "" %}{% raw %}[sso-session org]
sso_start_url = https://{% endraw %}{{ aws_identity_center_id }}{% raw %}.awsapps.com/start
sso_region = {% endraw %}{{ aws_org_home_region }}{% raw %}
sso_registration_scopes = sso:account:access{% endraw %}{% endif %}{% raw %}

[profile localstack]
region={% endraw %}{{ aws_org_home_region if (aws_org_home_region is defined and aws_org_home_region != "") else "us-east-1" }}{% raw %}
output=json
endpoint_url = $LOCALSTACK_ENDPOINT_URL

{% endraw %}{% if aws_central_infrastructure_account_id is defined and aws_central_infrastructure_account_id != "" %}{% raw %}
[profile {% endraw %}{{ core_infra_base_access_profile_name }}{% raw %}]
sso_session = org
sso_role_name = {% endraw %}{{ core_infra_base_access_profile_name }}{% raw %}
sso_account_id = {% endraw %}{{ aws_central_infrastructure_account_id }}{% raw %}
region = {% endraw %}{{ aws_org_home_region }}{% raw %}

{% endraw %}{% endif %}{% raw %}
EOF
cat >> ~/.aws/credentials <<EOF
[localstack]
aws_access_key_id=test
aws_secret_access_key=test
EOF{% endraw %}
2 changes: 1 addition & 1 deletion template/.devcontainer/docker-compose.yml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ services:
- "{% endraw %}{{ ssh_port_number }}{% raw %}:2222"
environment:
- AWS_PROFILE=localstack
- AWS_DEFAULT_REGION={% endraw %}{{ aws_region_for_stack }}{% raw %}
- AWS_DEFAULT_REGION={% endraw %}{{ aws_region_for_stack if (aws_region_for_stack is defined and aws_region_for_stack != "") else "us-east-1" }}{% raw %}


volumes:
Expand Down
Loading