diff --git a/pulumi/AWSMegatests/seqerakit/configs/nextflow-arm.config b/pulumi/AWSMegatests/seqerakit/configs/nextflow-arm.config new file mode 100644 index 00000000..6a944ba1 --- /dev/null +++ b/pulumi/AWSMegatests/seqerakit/configs/nextflow-arm.config @@ -0,0 +1,17 @@ +// Nextflow configuration for ARM CPU compute environments +// Includes base configuration and ARM-specific settings + +includeConfig 'nextflow-base.config' + +process { + publishDir = [ + path: { params.outdir }, + mode: 'copy', + tags: [ + 'compute_env': 'aws_ireland_fusionv2_nvme_cpu_ARM_snapshots', + 'architecture': 'arm64', + 'fusion': 'enabled', + 'fusionSnapshots': 'enabled' + ] + ] +} \ No newline at end of file diff --git a/pulumi/AWSMegatests/seqerakit/configs/nextflow-cpu.config b/pulumi/AWSMegatests/seqerakit/configs/nextflow-cpu.config new file mode 100644 index 00000000..de3a74b5 --- /dev/null +++ b/pulumi/AWSMegatests/seqerakit/configs/nextflow-cpu.config @@ -0,0 +1,17 @@ +// Nextflow configuration for CPU compute environments +// Includes base configuration and CPU-specific settings + +includeConfig 'nextflow-base.config' + +process { + publishDir = [ + path: { params.outdir }, + mode: 'copy', + tags: [ + 'compute_env': 'aws_ireland_fusionv2_nvme_cpu_snapshots', + 'architecture': 'x86_64', + 'fusion': 'enabled', + 'fusionSnapshots': 'enabled' + ] + ] +} \ No newline at end of file diff --git a/pulumi/AWSMegatests/seqerakit/configs/nextflow-gpu.config b/pulumi/AWSMegatests/seqerakit/configs/nextflow-gpu.config new file mode 100644 index 00000000..b9ca2330 --- /dev/null +++ b/pulumi/AWSMegatests/seqerakit/configs/nextflow-gpu.config @@ -0,0 +1,18 @@ +// Nextflow configuration for GPU compute environments +// Includes base configuration and GPU-specific settings + +includeConfig 'nextflow-base.config' + +process { + publishDir = [ + path: { params.outdir }, + mode: 'copy', + tags: [ + 'compute_env': 'aws_ireland_fusionv2_nvme_gpu_snapshots', + 'architecture': 'x86_64', + 'gpu': 'enabled', + 'fusion': 'enabled', + 'fusionSnapshots': 'enabled' + ] + ] +} \ No newline at end of file diff --git a/pulumi/AWSMegatests/src/infrastructure/credentials.py b/pulumi/AWSMegatests/src/infrastructure/credentials.py new file mode 100644 index 00000000..d133dec3 --- /dev/null +++ b/pulumi/AWSMegatests/src/infrastructure/credentials.py @@ -0,0 +1,508 @@ +"""TowerForge IAM credentials management module. + +This module creates and manages IAM resources for TowerForge AWS operations, +including policies for Forge operations, Launch operations, and S3 bucket access. +It also uploads the credentials to Seqera Platform for use by compute environments. +""" + +import json +import hashlib +from typing import Optional, Tuple, Dict, Any + +import pulumi +import pulumi_aws as aws +import pulumi_seqera as seqera + +from ..utils.constants import ( + TOWERFORGE_USER_NAME, + TOWERFORGE_POLICY_NAMES, + TOWERFORGE_CREDENTIAL_NAME, + TOWERFORGE_CREDENTIAL_DESCRIPTION, + TIMEOUTS, +) + + +class CredentialError(Exception): + """Exception raised when credential operations fail.""" + + pass + + +def _create_forge_policy_document() -> Dict[str, Any]: + """Create TowerForge Forge Policy document with comprehensive permissions.""" + return { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "TowerForge0", + "Effect": "Allow", + "Action": [ + "ssm:GetParameters", + "iam:CreateInstanceProfile", + "iam:DeleteInstanceProfile", + "iam:AddRoleToInstanceProfile", + "iam:RemoveRoleFromInstanceProfile", + "iam:CreateRole", + "iam:DeleteRole", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:DeleteRolePolicy", + "iam:PassRole", + "iam:TagRole", + "iam:TagInstanceProfile", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + "iam:GetRole", + "batch:CreateComputeEnvironment", + "batch:UpdateComputeEnvironment", + "batch:DeleteComputeEnvironment", + "batch:CreateJobQueue", + "batch:UpdateJobQueue", + "batch:DeleteJobQueue", + "batch:DescribeComputeEnvironments", + "batch:DescribeJobQueues", + "fsx:CreateFileSystem", + "fsx:DeleteFileSystem", + "fsx:DescribeFileSystems", + "fsx:TagResource", + "ec2:DescribeSecurityGroups", + "ec2:DescribeAccountAttributes", + "ec2:DescribeSubnets", + "ec2:DescribeLaunchTemplates", + "ec2:DescribeLaunchTemplateVersions", + "ec2:CreateLaunchTemplate", + "ec2:DeleteLaunchTemplate", + "ec2:DescribeKeyPairs", + "ec2:DescribeVpcs", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceTypeOfferings", + "ec2:GetEbsEncryptionByDefault", + "efs:CreateFileSystem", + "efs:DeleteFileSystem", + "efs:DescribeFileSystems", + "efs:CreateMountTarget", + "efs:DeleteMountTarget", + "efs:DescribeMountTargets", + "efs:ModifyFileSystem", + "efs:PutLifecycleConfiguration", + "efs:TagResource", + "elasticfilesystem:CreateFileSystem", + "elasticfilesystem:DeleteFileSystem", + "elasticfilesystem:DescribeFileSystems", + "elasticfilesystem:CreateMountTarget", + "elasticfilesystem:DeleteMountTarget", + "elasticfilesystem:DescribeMountTargets", + "elasticfilesystem:UpdateFileSystem", + "elasticfilesystem:PutLifecycleConfiguration", + "elasticfilesystem:TagResource", + ], + "Resource": "*", + }, + { + "Sid": "TowerLaunch0", + "Effect": "Allow", + "Action": [ + "s3:Get*", + "s3:List*", + "batch:DescribeJobQueues", + "batch:CancelJob", + "batch:SubmitJob", + "batch:ListJobs", + "batch:TagResource", + "batch:DescribeComputeEnvironments", + "batch:TerminateJob", + "batch:DescribeJobs", + "batch:RegisterJobDefinition", + "batch:DescribeJobDefinitions", + "ecs:DescribeTasks", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceAttribute", + "ecs:DescribeContainerInstances", + "ec2:DescribeInstanceStatus", + "ec2:DescribeImages", + "logs:Describe*", + "logs:Get*", + "logs:List*", + "logs:StartQuery", + "logs:StopQuery", + "logs:TestMetricFilter", + "logs:FilterLogEvents", + "ses:SendRawEmail", + "secretsmanager:ListSecrets", + ], + "Resource": "*", + }, + ], + } + + +def _create_launch_policy_document() -> Dict[str, Any]: + """Create TowerForge Launch Policy document with limited permissions.""" + return { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "TowerLaunch0", + "Effect": "Allow", + "Action": [ + "batch:DescribeJobQueues", + "batch:CancelJob", + "batch:SubmitJob", + "batch:ListJobs", + "batch:TagResource", + "batch:DescribeComputeEnvironments", + "batch:TerminateJob", + "batch:DescribeJobs", + "batch:RegisterJobDefinition", + "batch:DescribeJobDefinitions", + "ecs:DescribeTasks", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceAttribute", + "ecs:DescribeContainerInstances", + "ec2:DescribeInstanceStatus", + "ec2:DescribeImages", + "logs:Describe*", + "logs:Get*", + "logs:List*", + "logs:StartQuery", + "logs:StopQuery", + "logs:TestMetricFilter", + "logs:FilterLogEvents", + "ses:SendRawEmail", + "secretsmanager:ListSecrets", + ], + "Resource": "*", + } + ], + } + + +def _create_s3_policy_document(bucket_arn: str) -> Dict[str, Any]: + """Create S3 bucket access policy document with multipart upload support. + + Includes permissions for: + - Basic bucket operations (list, get location) + - Object operations (get, put, tag, delete) + - Multipart upload operations (for large files >5GB) + + Args: + bucket_arn: ARN of the S3 bucket to grant access to + + Returns: + Dict[str, Any]: S3 policy document with enhanced permissions + """ + return { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:ListBucket", + "s3:GetBucketLocation", + "s3:ListBucketMultipartUploads" + ], + "Resource": [bucket_arn], + }, + { + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectTagging", + "s3:DeleteObject", + "s3:AbortMultipartUpload", + "s3:ListMultipartUploadParts" + ], + "Resource": [f"{bucket_arn}/*"], + "Effect": "Allow", + }, + ], + } + + +def create_seqera_credentials( + seqera_provider: seqera.Provider, + workspace_id: float, + access_key_id: pulumi.Output[str], + access_key_secret: pulumi.Output[str], +) -> seqera.Credential: + """Upload TowerForge AWS credentials to Seqera Platform. + + Args: + seqera_provider: Configured Seqera provider instance + workspace_id: Seqera Platform workspace ID + access_key_id: AWS access key ID from TowerForge IAM user + access_key_secret: AWS secret access key from TowerForge IAM user + + Returns: + seqera.Credential: Seqera credential resource with credentials_id + + Raises: + CredentialError: If credential upload fails + """ + pulumi.log.info("Uploading TowerForge credentials to Seqera Platform") + + # Create AWS credentials configuration for Seqera Platform + aws_keys = seqera.CredentialKeysArgs( + aws=seqera.CredentialKeysAwsArgs( + access_key=access_key_id, + secret_key=access_key_secret, + # Note: assume_role_arn is optional and not needed for direct IAM user credentials + ) + ) + + # Upload credentials to Seqera Platform + seqera_credential = seqera.Credential( + "towerforge-aws-credential", + name=TOWERFORGE_CREDENTIAL_NAME, + description=TOWERFORGE_CREDENTIAL_DESCRIPTION, + provider_type="aws", + workspace_id=workspace_id, + keys=aws_keys, + opts=pulumi.ResourceOptions( + provider=seqera_provider, + # Ensure credentials are uploaded after IAM access key is created + custom_timeouts=pulumi.CustomTimeouts( + create=TIMEOUTS["seqera_credential_create"], + update=TIMEOUTS["seqera_credential_update"], + delete=TIMEOUTS["seqera_credential_delete"], + ), + ), + ) + + return seqera_credential + + +def _create_iam_policies( + aws_provider: aws.Provider, s3_bucket +) -> Tuple[aws.iam.Policy, aws.iam.Policy, aws.iam.Policy]: + """Create IAM policies for TowerForge operations. + + Args: + aws_provider: Configured AWS provider instance + s3_bucket: S3 bucket resource for policy attachment + + Returns: + Tuple of (forge_policy, launch_policy, s3_policy) + """ + # TowerForge Forge Policy - Comprehensive permissions for resource creation + forge_policy = aws.iam.Policy( + "towerforge-forge-policy", + name=TOWERFORGE_POLICY_NAMES["forge"], + description="IAM policy for TowerForge to create and manage AWS Batch resources", + policy=json.dumps(_create_forge_policy_document()), + opts=pulumi.ResourceOptions(provider=aws_provider), + ) + + # TowerForge Launch Policy - Limited permissions for pipeline execution + launch_policy = aws.iam.Policy( + "towerforge-launch-policy", + name=TOWERFORGE_POLICY_NAMES["launch"], + description="IAM policy for TowerForge to launch and monitor pipeline executions", + policy=json.dumps(_create_launch_policy_document()), + opts=pulumi.ResourceOptions(provider=aws_provider), + ) + + # TowerForge S3 Bucket Access Policy - Access to specified S3 bucket + s3_policy = aws.iam.Policy( + "towerforge-s3-policy", + name=TOWERFORGE_POLICY_NAMES["s3"], + description=s3_bucket.bucket.apply( + lambda bucket_name: f"IAM policy for TowerForge to access {bucket_name} S3 bucket" + ), + policy=s3_bucket.arn.apply( + lambda arn: json.dumps(_create_s3_policy_document(arn)) + ), + opts=pulumi.ResourceOptions(provider=aws_provider, depends_on=[s3_bucket]), + ) + + return forge_policy, launch_policy, s3_policy + + +def _generate_policy_hash( + forge_policy: aws.iam.Policy, + launch_policy: aws.iam.Policy, + s3_policy: aws.iam.Policy, +) -> str: + """Generate a hash of IAM policies to detect changes. + + Args: + forge_policy: TowerForge Forge policy + launch_policy: TowerForge Launch policy + s3_policy: TowerForge S3 policy + + Returns: + str: SHA256 hash of the combined policy documents + """ + # Create a deterministic hash of all policy documents + forge_doc = _create_forge_policy_document() + launch_doc = _create_launch_policy_document() + + # Combine all policy documents for hashing + combined_policies = json.dumps( + { + "forge": forge_doc, + "launch": launch_doc, + # Note: S3 policy is bucket-specific, so we'll use a placeholder for consistent hashing + # Version updated to include multipart upload permissions + "s3": {"bucket_dependent": True, "version": "v2-multipart"}, + }, + sort_keys=True, + ) + + return hashlib.sha256(combined_policies.encode()).hexdigest() + + +def create_towerforge_credentials( + aws_provider: aws.Provider, + s3_bucket, + seqera_provider: seqera.Provider, + workspace_id: float, +) -> Tuple[ + pulumi.Output[str], pulumi.Output[str], pulumi.Output[str], seqera.Credential, str +]: + """Create TowerForge IAM resources and upload to Seqera Platform. + + Creates IAM policies, user, and access keys for TowerForge operations, + then uploads the credentials to Seqera Platform for use by compute environments. + Based on https://github.com/seqeralabs/nf-tower-aws + + Args: + aws_provider: Configured AWS provider instance + s3_bucket: S3 bucket resource for policy attachment + seqera_provider: Configured Seqera provider instance + workspace_id: Seqera Platform workspace ID + + Returns: + Tuple: (access_key_id, access_key_secret, seqera_credentials_id, seqera_credential_resource, iam_policy_hash) + """ + # Create IAM policies + forge_policy, launch_policy, s3_policy = _create_iam_policies( + aws_provider, s3_bucket + ) + + # Generate policy version hash for compute environment recreation on policy changes + iam_policy_hash = _generate_policy_hash(forge_policy, launch_policy, s3_policy) + + # Create TowerForge IAM User + towerforge_user = aws.iam.User( + "towerforge-user", + name=TOWERFORGE_USER_NAME, + opts=pulumi.ResourceOptions(provider=aws_provider), + ) + + # Attach policies to the TowerForge user + forge_attachment = aws.iam.UserPolicyAttachment( + "towerforge-forge-policy-attachment", + user=towerforge_user.name, + policy_arn=forge_policy.arn, + opts=pulumi.ResourceOptions( + provider=aws_provider, depends_on=[towerforge_user, forge_policy] + ), + ) + + launch_attachment = aws.iam.UserPolicyAttachment( + "towerforge-launch-policy-attachment", + user=towerforge_user.name, + policy_arn=launch_policy.arn, + opts=pulumi.ResourceOptions( + provider=aws_provider, + depends_on=[towerforge_user, launch_policy], + ), + ) + + s3_attachment = aws.iam.UserPolicyAttachment( + "towerforge-s3-policy-attachment", + user=towerforge_user.name, + policy_arn=s3_policy.arn, + opts=pulumi.ResourceOptions( + provider=aws_provider, depends_on=[towerforge_user, s3_policy] + ), + ) + + # Create access keys for the TowerForge user + towerforge_access_key = aws.iam.AccessKey( + "towerforge-access-key", + user=towerforge_user.name, + opts=pulumi.ResourceOptions( + provider=aws_provider, + depends_on=[forge_attachment, launch_attachment, s3_attachment], + additional_secret_outputs=["secret"], + ), + ) + + # Upload the credentials to Seqera Platform + seqera_credential = create_seqera_credentials( + seqera_provider=seqera_provider, + workspace_id=workspace_id, + access_key_id=towerforge_access_key.id, + access_key_secret=towerforge_access_key.secret, + ) + + # Return the access key credentials, Seqera credentials ID, credential resource, and policy hash + return ( + towerforge_access_key.id, + towerforge_access_key.secret, + seqera_credential.credentials_id, + seqera_credential, + iam_policy_hash, + ) + + +def get_towerforge_resources( + aws_provider: aws.Provider, + s3_bucket, + seqera_provider: Optional[seqera.Provider] = None, + workspace_id: Optional[float] = None, +) -> Dict[str, Any]: + """Create TowerForge resources and return resource information for exports. + + This function creates the TowerForge IAM resources and returns a dictionary + containing resource information for Pulumi exports. + + Args: + aws_provider: Configured AWS provider instance + s3_bucket: S3 bucket resource for policy attachment + seqera_provider: Optional Seqera provider for credential upload + workspace_id: Optional workspace ID for Seqera Platform + + Returns: + Dict[str, Any]: Resource information for Pulumi exports + + Raises: + ValueError: If required parameters are missing + """ + # Create the credentials (this will create all the resources) + if seqera_provider and workspace_id: + ( + access_key_id, + access_key_secret, + seqera_credentials_id, + seqera_credential, + iam_policy_hash, + ) = create_towerforge_credentials( + aws_provider, s3_bucket, seqera_provider, workspace_id + ) + else: + # Fallback for backward compatibility - this will raise an error since signature changed + raise ValueError( + "get_towerforge_resources now requires seqera_provider and workspace_id parameters. " + "Please update your code to use the new signature or call create_towerforge_credentials directly." + ) + + return { + "user": { + "name": TOWERFORGE_USER_NAME, + "arn": f"arn:aws:iam::{{aws_account_id}}:user/{TOWERFORGE_USER_NAME}", # Will be populated by Pulumi + }, + "access_key_id": access_key_id, + "access_key_secret": access_key_secret, + "seqera_credentials_id": seqera_credentials_id, + "policies": { + "forge_policy_name": TOWERFORGE_POLICY_NAMES["forge"], + "launch_policy_name": TOWERFORGE_POLICY_NAMES["launch"], + "s3_policy_name": TOWERFORGE_POLICY_NAMES["s3"], + }, + } diff --git a/pulumi/co2_reports/.envrc b/pulumi/co2_reports/.envrc new file mode 100644 index 00000000..2744f12b --- /dev/null +++ b/pulumi/co2_reports/.envrc @@ -0,0 +1,22 @@ +# Environment configuration for CO2 Reports Pulumi project +# This file loads AWS credentials from 1Password + +export OP_ACCOUNT=nf-core + +# Load 1Password integration for direnv +source_url "https://github.com/tmatilai/direnv-1password/raw/v1.0.1/1password.sh" \ + "sha256-4dmKkmlPBNXimznxeehplDfiV+CvJiIzg7H1Pik4oqY=" + +# Load AWS credentials from 1Password +from_op AWS_ACCESS_KEY_ID="op://Dev/Pulumi-AWS-key/access key id" +from_op AWS_SECRET_ACCESS_KEY="op://Dev/Pulumi-AWS-key/secret access key" + +# AWS Configuration +export AWS_REGION="eu-north-1" +export AWS_DEFAULT_REGION="eu-north-1" + +# Load 1Password service account token for Pulumi +# from_op OP_SERVICE_ACCOUNT_TOKEN="op://Employee/doroenisttgrfcmzihhunyizg4/credential" + +# Load Pulumi passphrase from 1Password +from_op PULUMI_CONFIG_PASSPHRASE="op://Employee/Pulumi Passphrase/password" diff --git a/pulumi/co2_reports/.python-version b/pulumi/co2_reports/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/pulumi/co2_reports/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/pulumi/co2_reports/Pulumi.AWSMegatests.yaml b/pulumi/co2_reports/Pulumi.AWSMegatests.yaml new file mode 100644 index 00000000..d980b594 --- /dev/null +++ b/pulumi/co2_reports/Pulumi.AWSMegatests.yaml @@ -0,0 +1,6 @@ +encryptionsalt: v1:Dd5GnLRGGJQ=:v1:HWX/n0HL3VxDJrWR:ouykGXCccBLXFd6kAKWW0uHByWK/qw== +config: + aws:region: eu-north-1 + github:owner: nf-core + pulumi-onepassword:service_account_token: + secure: v1:/9UqNK7J/BSgkoof:PLSCC8wOdrIkvfK0pdodnyOid/HSwFb5Ra6+x0zVOiN+1ZbulPub0T1tRTdyCG4yqJa8dvcnS62kPRu9CCm10tPu+/g0HCVhUaR8DLB5lO5HtWn/OA28xd7OluDhp5e2JRYFV7HWUpnfTV9rlwpxntWAaCnnsSIw53yyiQLl0cMnf/V51VOo+e2YkqZJg/fyPtpeVS28FRZvjxFUIV5nxtaCw7K/5kXhtJOf6ZR160sH9bkkhNzzqQQ53JLY02rgFqBLAQ5dPRZMH6cB6HJiznq/zgEo2331/M6+TCqzyCi1spIvfhC7i3GV1KtVNaKJSD8tVEcscHJHR4hFElsJ5yHVVT/BWYL/A/4QjktY/hRKheDeLbQfkL5zQzbpm8DkNEwBv/x+691uhAJy387nK8+MdACLjT4w5Z7kmn4S+LOi8bPIXvTdpsWdijwT9Rbv4rr7p25nhoB4T+/ikDXWGSW/Lj5eVgcKVZ8KcY2MhhYGQ/LItWvrPO4K3kjZnglOjzU3CX5wuCrki4uHhKGgpWT+/XTgSe4nerp9iFZPs42yneEMz1icYfPkXOybDuV+4TPB2rn7g1Yp1k2+1ILe8aw82Pzw2DEDcFjyVTTHsCytBGKFlOT/MxWmgiY8BCJvEBLzOrDPHqfWFTeyIVdVw62nnFMZwVoDxl68aCGbAWCsBr2sS3uTZAak7RP5cXYa9zEIb6d4dQ6PiUl/V6q4yi+yawde7F7St8qd+MTBvBgfieEXwzwwejb2IGmkI6uKD2mSOUk+Dyb4DymuuYatFf6QUlCQEBlyaLZmA1SIGSMrASKmY54VffU6C4LLZvQc8Sxa20wzuYIQ+RO/o2EGQ2Kq9b2teOVMah6kWHVFrNA46+3cNmUSMXxuoiyjsQo0KMzWY8ZpJ2qXDhCJLYdlsVKY4W3i2rS4smjGbHxpnhC6sUgH3LaJBZFHWk+OGAI35y1AVocquUzipCsJGrMD9SiZla6mOKD9Cwv3kdRQQFbSdmH1hF829lBk6o+uXrhpfxHiyD+GgsuVLTzpOTiZigehafUx22ALfrGmF28vK+uxU4rc0DYsJMzyE+/bmmIzyE6H8NRsCbaYp5XyAUCMm4jq+MoZrR0bPde96yGNNwPP/xEasIOu5S/+QoVCQN8BkeArIGY8T3M= diff --git a/pulumi/co2_reports/Pulumi.yaml b/pulumi/co2_reports/Pulumi.yaml new file mode 100644 index 00000000..beab2319 --- /dev/null +++ b/pulumi/co2_reports/Pulumi.yaml @@ -0,0 +1,11 @@ +name: co2-reports +runtime: + name: python + options: + toolchain: uv + virtualenv: .venv +description: For hosting nf-core CO2 footprint reports +config: + pulumi:tags: + value: + pulumi:template: aws-python diff --git a/pulumi/co2_reports/README.md b/pulumi/co2_reports/README.md new file mode 100644 index 00000000..ae772bfb --- /dev/null +++ b/pulumi/co2_reports/README.md @@ -0,0 +1,192 @@ +# CO2 Reports - Pulumi Infrastructure + +Infrastructure-as-Code for the nf-core CO2 footprint reports S3 bucket using Pulumi. + +## Overview + +This Pulumi project creates and manages AWS infrastructure for storing CO2 footprint reports generated by nf-test runs in the nf-core/modules repository. + +**Created Resources:** + +- 📦 S3 bucket `nf-core-co2-reports` (eu-north-1) +- 👤 IAM user `nf-core-co2-reports-ci` with write access +- 🔑 GitHub Actions secrets for nf-core/modules repository + +## Quick Start + +### Prerequisites + +1. AWS credentials from 1Password (`Dev` vault → `Pulumi-AWS-key`) +2. 1Password service account token +3. uv installed (`brew install uv` or similar) + +### Deployment + +```bash +# Set AWS credentials +export AWS_ACCESS_KEY_ID="" +export AWS_SECRET_ACCESS_KEY="" +export AWS_REGION="eu-north-1" + +# Optional: Set 1Password token (or script will prompt) +export OP_SERVICE_ACCOUNT_TOKEN="" + +# Run deployment script +cd ~/src/nf-core/ops/pulumi/co2_reports +./DEPLOY.sh +``` + +### Manual Deployment + +If you prefer manual control: + +```bash +cd ~/src/nf-core/ops/pulumi/co2_reports + +# Initialize stack +uv run pulumi stack init AWSMegatests + +# Configure +uv run pulumi config set aws:region eu-north-1 +uv run pulumi config set github:owner nf-core +uv run pulumi config set pulumi-onepassword:service_account_token --secret + +# Deploy +uv run pulumi preview # Preview changes +uv run pulumi up # Deploy +``` + +## Infrastructure Details + +### S3 Bucket + +- **Name**: `nf-core-co2-reports` +- **Region**: `eu-north-1` +- **Encryption**: AES256 server-side encryption +- **Versioning**: Enabled +- **Public Access**: Blocked +- **Purpose**: Store CO2 footprint trace files from nf-test runs + +### Report Organization + +Reports are organized by: +``` +s3://nf-core-co2-reports/ + └── modules/ + └── YYYY-MM-DD/ + └── branch-name/ + └── profile/ + └── shard/ + ├── co2footprint_trace.txt + └── co2footprint_trace_*.txt +``` + +### IAM Permissions + +The CI user has permissions for: +- `s3:PutObject` - Upload reports +- `s3:GetObject` - Download reports (for verification) +- `s3:ListBucket` - List bucket contents +- `s3:GetBucketLocation` - Get bucket region + +### GitHub Secrets + +The following secrets are automatically created in the `nf-core/modules` repository: + +- `CO2_REPORTS_AWS_ACCESS_KEY_ID` +- `CO2_REPORTS_AWS_SECRET_ACCESS_KEY` +- `CO2_REPORTS_AWS_REGION` + +## Usage in GitHub Actions + +After deployment, update your GitHub workflow: + +```yaml +- name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.CO2_REPORTS_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.CO2_REPORTS_AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.CO2_REPORTS_AWS_REGION }} + +- name: Upload CO2 footprint reports to S3 + run: | + aws s3 cp co2footprint_trace.txt s3://nf-core-co2-reports/... +``` + +## Management + +### View Stack Outputs + +```bash +cd ~/src/nf-core/ops/pulumi/co2_reports +uv run pulumi stack output +``` + +### Update Infrastructure + +```bash +# Make changes to __main__.py +# Preview changes +uv run pulumi preview + +# Apply changes +uv run pulumi up +``` + +### Destroy Infrastructure + +```bash +# ⚠️ WARNING: This will delete the bucket and all reports! +uv run pulumi destroy +``` + +## Project Structure + +``` +co2_reports/ +├── Pulumi.yaml # Project configuration +├── __main__.py # Infrastructure definition +├── pyproject.toml # Python dependencies +├── DEPLOY.sh # Deployment script +├── README.md # This file +└── .venv/ # Virtual environment +``` + +## Troubleshooting + +### Error: "operation error S3: GetObject" + +This means Pulumi can't access the S3 backend. Ensure AWS credentials are set: + +```bash +export AWS_ACCESS_KEY_ID="" +export AWS_SECRET_ACCESS_KEY="" +``` + +### Error: "No such bookmark: AWSMegatests" + +The stack hasn't been initialized yet. Run: + +```bash +uv run pulumi stack init AWSMegatests +``` + +### GitHub Secrets Not Created + +Check that: +1. The 1Password service account token is correct +2. The GitHub token in 1Password has proper permissions +3. The repository name is correct (`modules`, not `nf-core/modules`) + +## Related Documentation + +- [nf-co2footprint Plugin](https://github.com/nextflow-io/nf-co2footprint) +- [GitHub Issue #9291](https://github.com/nf-core/modules/issues/9291) +- [Pulumi AWS Documentation](https://www.pulumi.com/registry/packages/aws/) + +## Support + +For issues or questions: +- Open an issue in [nf-core/modules](https://github.com/nf-core/modules/issues) +- Contact the nf-core infrastructure team diff --git a/pulumi/co2_reports/SETUP_INSTRUCTIONS.md b/pulumi/co2_reports/SETUP_INSTRUCTIONS.md new file mode 100644 index 00000000..00020941 --- /dev/null +++ b/pulumi/co2_reports/SETUP_INSTRUCTIONS.md @@ -0,0 +1,39 @@ +# CO2 Reports Pulumi Setup Instructions + +## Commands to run: + +```bash +# 1. Initialize the stack +cd ~/src/nf-core/ops/pulumi/co2_reports +uv run pulumi stack init AWSMegatests + +# 2. Configure the stack +uv run pulumi config set aws:region us-east-1 +uv run pulumi config set github:owner nf-core + +# 3. Set the 1Password service account token (get this from 1Password) +uv run pulumi config set pulumi-onepassword:service_account_token --secret + +# 4. Preview the changes +uv run pulumi preview + +# 5. Deploy the infrastructure +uv run pulumi up + +# 6. Verify the GitHub secrets were created +# Check in GitHub: https://github.com/nf-core/modules/settings/secrets/actions +``` + +## What this will create: + +1. S3 bucket: `nf-core-co2-reports` +2. IAM user: `nf-core-co2-reports-ci` +3. IAM policy for write access to the bucket +4. GitHub Actions secrets in nf-core/modules: + - CO2_REPORTS_AWS_ACCESS_KEY_ID + - CO2_REPORTS_AWS_SECRET_ACCESS_KEY + - CO2_REPORTS_AWS_REGION + +## After deployment: + +Update the GitHub workflow in nf-core/modules to use the new bucket and credentials. diff --git a/pulumi/co2_reports/__main__.py b/pulumi/co2_reports/__main__.py new file mode 100644 index 00000000..3b600a01 --- /dev/null +++ b/pulumi/co2_reports/__main__.py @@ -0,0 +1,187 @@ +"""CO2 Reports S3 Infrastructure Management + +A Pulumi program for managing the nf-core CO2 footprint reports S3 bucket. + +This project creates and manages AWS infrastructure for storing CO2 footprint +tracking data from nf-test runs in the nf-core/modules repository. +""" + +import pulumi +import pulumi_aws as aws +import pulumi_github as github +import pulumi_onepassword as onepassword + +# Configure the 1Password provider +onepassword_config = pulumi.Config("pulumi-onepassword") +onepassword_provider = onepassword.Provider( + "onepassword-provider", + service_account_token=onepassword_config.require_secret("service_account_token"), + account="", # Explicitly disable CLI account detection to avoid conflicts +) + +# Get GitHub token from 1Password +github_token_item = onepassword.get_item_output( + vault="Dev", + title="GitHub nf-core PA Token Pulumi", + opts=pulumi.InvokeOptions(provider=onepassword_provider), +) + +# Configure AWS provider +aws_provider = aws.Provider("aws-provider") + +# Configure GitHub provider +github_provider = github.Provider( + "github-provider", + token=github_token_item.credential, + owner=pulumi.Config("github").get("owner"), +) + +# Create S3 bucket for CO2 reports +co2_reports_bucket = aws.s3.Bucket( + "co2-reports-bucket", + bucket="nf-core-co2-reports", + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Configure server-side encryption for the bucket +co2_reports_bucket_encryption = aws.s3.BucketServerSideEncryptionConfigurationV2( + "co2-reports-bucket-encryption", + bucket=co2_reports_bucket.id, + rules=[ + aws.s3.BucketServerSideEncryptionConfigurationV2RuleArgs( + apply_server_side_encryption_by_default=aws.s3.BucketServerSideEncryptionConfigurationV2RuleApplyServerSideEncryptionByDefaultArgs( + sse_algorithm="AES256", + ), + ), + ], + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Enable versioning for the bucket +co2_reports_bucket_versioning = aws.s3.BucketVersioningV2( + "co2-reports-bucket-versioning", + bucket=co2_reports_bucket.id, + versioning_configuration=aws.s3.BucketVersioningV2VersioningConfigurationArgs( + status="Enabled", + ), + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Block public access to the bucket +co2_reports_bucket_public_access_block = aws.s3.BucketPublicAccessBlock( + "co2-reports-bucket-public-access-block", + bucket=co2_reports_bucket.id, + block_public_acls=True, + block_public_policy=True, + ignore_public_acls=True, + restrict_public_buckets=True, + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Create IAM user for CI/CD +ci_user = aws.iam.User( + "co2-reports-ci-user", + name="nf-core-co2-reports-ci", + path="/", + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Create access keys for the CI user +ci_user_access_key = aws.iam.AccessKey( + "co2-reports-ci-access-key", + user=ci_user.name, + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Create IAM policy for bucket access +bucket_access_policy = aws.iam.Policy( + "co2-reports-bucket-access-policy", + name="nf-core-co2-reports-bucket-access", + description="Write access to modules/ prefix in nf-core-co2-reports S3 bucket for CI/CD", + policy=co2_reports_bucket.arn.apply( + lambda bucket_arn: f"""{{ + "Version": "2012-10-17", + "Statement": [ + {{ + "Effect": "Allow", + "Action": [ + "s3:ListBucket", + "s3:GetBucketLocation" + ], + "Resource": "{bucket_arn}", + "Condition": {{ + "StringLike": {{ + "s3:prefix": ["modules/*"] + }} + }} + }}, + {{ + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:GetObject" + ], + "Resource": "{bucket_arn}/modules/*" + }} + ] + }}""" + ), + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Attach policy to CI user +aws.iam.UserPolicyAttachment( + "ci-user-bucket-policy-attachment", + user=ci_user.name, + policy_arn=bucket_access_policy.arn, + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Create GitHub Actions secrets for the modules repository +aws_access_key_secret = github.ActionsSecret( + "co2-reports-aws-access-key-id-secret", + repository="modules", + secret_name="CO2_REPORTS_AWS_ACCESS_KEY_ID", + plaintext_value=ci_user_access_key.id, + opts=pulumi.ResourceOptions(provider=github_provider), +) + +aws_secret_access_key_secret = github.ActionsSecret( + "co2-reports-aws-secret-access-key-secret", + repository="modules", + secret_name="CO2_REPORTS_AWS_SECRET_ACCESS_KEY", + plaintext_value=ci_user_access_key.secret, + opts=pulumi.ResourceOptions(provider=github_provider), +) + +aws_region_secret = github.ActionsSecret( + "co2-reports-aws-region-secret", + repository="modules", + secret_name="CO2_REPORTS_AWS_REGION", + plaintext_value=pulumi.Config("aws").get("region") or "us-east-1", + opts=pulumi.ResourceOptions(provider=github_provider), +) + +# Exports +pulumi.export("bucket_name", co2_reports_bucket.bucket) +pulumi.export("bucket_arn", co2_reports_bucket.arn) +pulumi.export("ci_user_name", ci_user.name) +pulumi.export("ci_user_access_key_id", ci_user_access_key.id) +pulumi.export( + "ci_user_secret_access_key", pulumi.Output.secret(ci_user_access_key.secret) +) +pulumi.export( + "github_secrets_created", + { + "CO2_REPORTS_AWS_ACCESS_KEY_ID": aws_access_key_secret.secret_name, + "CO2_REPORTS_AWS_SECRET_ACCESS_KEY": aws_secret_access_key_secret.secret_name, + "CO2_REPORTS_AWS_REGION": aws_region_secret.secret_name, + }, +) +pulumi.export( + "usage", + { + "s3_uri": "s3://nf-core-co2-reports/", + "upload_path_pattern": "s3://nf-core-co2-reports/modules/YYYY-MM-DD/branch-name/profile/shard/", + }, +) diff --git a/pulumi/co2_reports/pyproject.toml b/pulumi/co2_reports/pyproject.toml new file mode 100644 index 00000000..a97a50f9 --- /dev/null +++ b/pulumi/co2_reports/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "co2-reports" +version = "0.1.0" +description = "Pulumi infrastructure for nf-core CO2 reports" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "pulumi>=3.205.0", + "pulumi-aws>=7.10.0", + "pulumi-github>=6.8.0", + "pulumi-onepassword>=1.1.3", +] diff --git a/pulumi/co2_reports/uv.lock b/pulumi/co2_reports/uv.lock new file mode 100644 index 00000000..6691a242 --- /dev/null +++ b/pulumi/co2_reports/uv.lock @@ -0,0 +1,266 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "arpeggio" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/58/ba011f3cf8291804ce80f9d81289ac15f0319a27f9d7e3c124aa5e4981cc/Arpeggio-2.0.3.tar.gz", hash = "sha256:9e85ad35cfc6c938676817c7ae9a1000a7c72a34c71db0c687136c460d12b85e", size = 766566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/4d/53b8186b41842f7a5e971b1d1c28e678364dcf841e4170f5d14d38ac1e2a/Arpeggio-2.0.3-py2.py3-none-any.whl", hash = "sha256:9374d9c531b62018b787635f37fd81c9a6ee69ef2d28c5db3cd18791b1f7db2f", size = 54656 }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, +] + +[[package]] +name = "co2-reports" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pulumi" }, + { name = "pulumi-aws" }, + { name = "pulumi-github" }, + { name = "pulumi-onepassword" }, +] + +[package.metadata] +requires-dist = [ + { name = "pulumi", specifier = ">=3.205.0" }, + { name = "pulumi-aws", specifier = ">=7.10.0" }, + { name = "pulumi-github", specifier = ">=6.8.0" }, + { name = "pulumi-onepassword", specifier = ">=1.1.3" }, +] + +[[package]] +name = "debugpy" +version = "1.8.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522 }, + { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417 }, + { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130 }, + { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053 }, + { url = "https://files.pythonhosted.org/packages/50/76/597e5cb97d026274ba297af8d89138dfd9e695767ba0e0895edb20963f40/debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464", size = 2538386 }, + { url = "https://files.pythonhosted.org/packages/5f/60/ce5c34fcdfec493701f9d1532dba95b21b2f6394147234dce21160bd923f/debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088", size = 4292100 }, + { url = "https://files.pythonhosted.org/packages/e8/95/7873cf2146577ef71d2a20bf553f12df865922a6f87b9e8ee1df04f01785/debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83", size = 5277002 }, + { url = "https://files.pythonhosted.org/packages/46/11/18c79a1cee5ff539a94ec4aa290c1c069a5580fd5cfd2fb2e282f8e905da/debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420", size = 5319047 }, + { url = "https://files.pythonhosted.org/packages/de/45/115d55b2a9da6de812696064ceb505c31e952c5d89c4ed1d9bb983deec34/debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1", size = 2536899 }, + { url = "https://files.pythonhosted.org/packages/5a/73/2aa00c7f1f06e997ef57dc9b23d61a92120bec1437a012afb6d176585197/debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f", size = 4268254 }, + { url = "https://files.pythonhosted.org/packages/86/b5/ed3e65c63c68a6634e3ba04bd10255c8e46ec16ebed7d1c79e4816d8a760/debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670", size = 5277203 }, + { url = "https://files.pythonhosted.org/packages/b0/26/394276b71c7538445f29e792f589ab7379ae70fd26ff5577dfde71158e96/debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c", size = 5318493 }, + { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210 }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668 }, +] + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718 }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627 }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167 }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267 }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963 }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484 }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777 }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014 }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750 }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003 }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716 }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522 }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558 }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990 }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387 }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668 }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928 }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983 }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727 }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799 }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417 }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219 }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826 }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550 }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564 }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236 }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795 }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214 }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961 }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462 }, +] + +[[package]] +name = "parver" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arpeggio" }, + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/e5/1c774688a90f0b76e872e30f6f1ba3f5e14056cd0d96a684047d4a986226/parver-0.5.tar.gz", hash = "sha256:b9fde1e6bb9ce9f07e08e9c4bea8d8825c5e78e18a0052d02e02bf9517eb4777", size = 26908 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/4c/f98024021bef4d44dce3613feebd702c7ad8883f777ff8488384c59e9774/parver-0.5-py3-none-any.whl", hash = "sha256:2281b187276c8e8e3c15634f62287b2fb6fe0efe3010f739a6bd1e45fa2bf2b2", size = 15172 }, +] + +[[package]] +name = "pip" +version = "25.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622 }, +] + +[[package]] +name = "protobuf" +version = "5.29.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963 }, + { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818 }, + { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091 }, + { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824 }, + { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942 }, + { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823 }, +] + +[[package]] +name = "pulumi" +version = "3.205.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "debugpy" }, + { name = "dill" }, + { name = "grpcio" }, + { name = "pip" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "semver" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/71/ad01388f8fb1729fdef1d01ec4cb8e313a19f99ce6b3dccf243ea72c2239/pulumi-3.205.0-py3-none-any.whl", hash = "sha256:9c2469606e978c6694c5a656bd9d986e940076710e607786e1c9d6e5cbd4b81f", size = 383509 }, +] + +[[package]] +name = "pulumi-aws" +version = "7.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parver" }, + { name = "pulumi" }, + { name = "semver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/d5/2152cca6b51b0ed47b375ae4e306e25bdbfc93c797344c4e0975f2a29e22/pulumi_aws-7.10.0.tar.gz", hash = "sha256:07112752c2533d6b7c1fa505a9e3bd39036c8738f48f6dcf98be02026627fe5c", size = 8322711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/c7/5969b9b16dc4761eecc6abe23d983745a5de1431821e6317673e87768710/pulumi_aws-7.10.0-py3-none-any.whl", hash = "sha256:3bc541f1a065df5198ceca51379bf97090984f28a6ea0356e04f5be30e885ad0", size = 11293100 }, +] + +[[package]] +name = "pulumi-github" +version = "6.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parver" }, + { name = "pulumi" }, + { name = "semver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/06/8c428822ebc00e8726f3b3ce392784b27253688a44506824c4c4046f3c0f/pulumi_github-6.8.0.tar.gz", hash = "sha256:16722f99775e1ab1761b602e99f327adb76396c58c95a5cbac11845546b7c8bc", size = 214693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/72/7c3cf7d0ba9193429863bbfe265154d00d6e5d7a92df1baedaae59745fe0/pulumi_github-6.8.0-py3-none-any.whl", hash = "sha256:be4ce509438dd08ffffe274036fd6f13c1d95acc8604d62a8c26defaac63600b", size = 402071 }, +] + +[[package]] +name = "pulumi-onepassword" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parver" }, + { name = "pulumi" }, + { name = "semver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/e3/15ba07147bf1ad602cd6234bb5ff09c29f9206d5ee0bf6845657c593264a/pulumi_onepassword-1.1.3.tar.gz", hash = "sha256:2ebbefa9463d8a9c6faf65491cad418c186d3a604360e78eaa319ef09c2478ba", size = 17968 } + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +] + +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +]