Skip to main content

pypuwa

Python Pulumi Wrapper — Dataclass-based configuration framework for Pulumi infrastructure as code.

Define your infrastructure config in Python dataclasses. Get interpolation, secret management, environment variable generation, lifecycle hooks, and a deploy CLI for free.

Install

pip install pypuwa

Quick Start

# config/base/services/api.py
from dataclasses import dataclass, field
from typing import FrozenSet
from pypuwa import BaseDatabaseConfig, BaseComputeConfig, Secret, secret


@dataclass(kw_only=True)
class MyDatabaseConfig(BaseDatabaseConfig):
    INSTANCE_ID: str = "{stack}-my-api-db"
    NAME: str = "my_api"
    USERNAME: str = "api_user"


@dataclass(kw_only=True)
class MyAppConfig(BaseComputeConfig):
    SERVICE_NAME: str = "{stack}-my-api"
    CPU: str = "2 vCPU"
    MEMORY: str = "4 GB"

    DEBUG: str = "False"
    DATABASE_NAME: str = "${services.database.NAME}"
    API_SECRET: Secret = secret()

    _NON_ENV_FIELDS: FrozenSet[str] = frozenset(["CPU", "MEMORY"])
# config/environments/staging.py
from pypuwa import BaseStackConfig, ProviderConfig

staging_config = MyStackConfig(
    providers=ProviderConfig(aws={"region": "us-east-1"}),
)
# pypuwaconf.py
from config.environments.staging import staging_config
from config.environments.production import production_config

PROJECT_NAME = "my-infra"

ENVIRONMENTS = {
    "staging": staging_config,
    "production": production_config,
}

Then deploy:

pypuwa deploy staging

pypuwaconf.py

Every project has a pypuwaconf.py at the root. This is the entry point for the CLI.

# pypuwaconf.py
import os
import subprocess
from config.environments.staging import staging_config
from config.environments.production import production_config

PROJECT_NAME = "my-infra"          # Pulumi config prefix
RUNNER = "uv run pulumi"           # Command to run Pulumi (optional, defaults to "pulumi")

ENVIRONMENTS = {
    "staging": staging_config,
    "production": production_config,
}


def _ensure_azure_tenant(environment: str) -> bool:
    result = subprocess.run(
        ["az", "account", "show", "--query", "tenantId", "-o", "tsv"],
        capture_output=True, text=True,
    )
    if result.returncode != 0:
        print("ERROR: Run 'az login' first")
        return False  # Aborts pipeline
    os.environ["AZURE_TENANT_ID"] = result.stdout.strip()
    return True


def _confirm_production(environment: str) -> bool:
    if environment == "production":
        resp = input("Deploy to PRODUCTION? Type 'yes': ")
        return resp == "yes"
    return True


HOOKS = {
    "pre_deploy": _ensure_azure_tenant,
    "pre_deploy": _confirm_production,  # last one wins per key
}

CLI

# Full deploy: hooks → sync → secrets → pulumi up
pypuwa deploy staging

# Preview only (sync + show changes, no deploy)
pypuwa preview production

# Sync config to Pulumi YAML without deploying
pypuwa sync staging

# Options (override pypuwaconf.py values)
pypuwa deploy production --backup
pypuwa deploy staging --dry-run
pypuwa deploy staging --runner "uv run pulumi"
pypuwa deploy staging --project-name my-infra

What pypuwa deploy does

pre_sync → [sync config to YAML] → post_sync → pre_secrets → [detect & set secrets] → post_secrets → pre_deploy → [pulumi up] → post_deploy
  1. Sync — Generates Pulumi.<env>.yaml from Python config, preserving existing encrypted secrets
  2. Secrets — Detects new secret() fields and prompts with arrow-key selection (all pre-selected)
  3. Deploy — Runs pulumi up on the selected stack

Lifecycle Hooks

Six hooks available: pre_sync, post_sync, pre_secrets, post_secrets, pre_deploy, post_deploy.

Each hook receives the environment name and can:

  • Return True or None to continue
  • Return False to abort the pipeline
  • Raise an exception to abort with error

Interactive Secret Management

? Select secrets to set now (space to toggle, enter to confirm):
  ❯ ◉ services.api.database.PASSWORD
    ◉ services.api.app_runner.DJANGO_SECRET_KEY
    ◉ services.redis.AUTH_TOKEN

All new secrets are pre-selected. Hit enter to set all, or space to deselect. Values entered with hidden input.

Features

Interpolation

SERVICE_NAME: str = "{stack}-my-service"        # -> "staging-my-service"
DB_NAME: str = "${services.database.NAME}"      # -> resolved from config tree

Secrets

PASSWORD: Secret = secret()                     # Auto-inferred from field name
API_KEY: Secret = secret("EXTERNAL_API_KEY")    # Explicit key
REDIS_PASS: Secret = secret("${services.redis.AUTH_TOKEN}")  # Cross-service

Environment Variables

@dataclass(kw_only=True)
class MyConfig(BaseComputeConfig):
    DEBUG: str = "True"
    DATABASE_URL: str = "postgres://..."
    CPU: str = "2 vCPU"                         # Infrastructure, excluded from env

    _NON_ENV_FIELDS: FrozenSet[str] = frozenset(["CPU", "MEMORY"])

config = MyConfig(SERVICE_NAME="api")
env_vars = config.env_dict()
# {"SERVICE_NAME": "api", "DEBUG": "True", "DATABASE_URL": "postgres://..."}

Base Components (cloud-agnostic)

Class Purpose
BaseDatabaseConfig Any database (RDS, Azure PostgreSQL, Cloud SQL)
BaseComputeConfig Any compute (App Runner, Container Apps, Cloud Run)
BaseContainerRegistryConfig Any registry (ECR, ACR, GCR)
BaseCacheConfig Any cache (ElastiCache, Azure Cache, Memorystore)
BaseStorageConfig Any object storage (S3, Blob, GCS)
BaseContainerTaskConfig Any container task (ECS, Cloud Run Jobs)
BaseServiceConfig Base service with URL
BaseRepositoryConfig Source code repository reference

Extend these with cloud-specific fields in your project:

@dataclass(kw_only=True)
class MyDatabaseConfig(BaseDatabaseConfig):
    INSTANCE_TYPE: str = "db.t3.micro"   # AWS-specific
    SKIP_FINAL_SNAPSHOT: bool = True     # AWS-specific

Project Structure

my-infra/
├── pypuwaconf.py                   # Project config (environments, hooks, runner)
├── __main__.py                     # Pulumi entry point
├── Pulumi.yaml                     # Pulumi project definition
├── Pulumi.staging.yaml             # Generated (secrets encrypted here)
├── Pulumi.production.yaml          # Generated
├── config/
│   ├── base/
│   │   ├── stack.py                # MyStackConfig(BaseStackConfig)
│   │   └── services/
│   │       ├── api.py              # Database + Compute configs
│   │       ├── worker.py           # Worker config
│   │       └── redis.py            # Cache config
│   └── environments/
│       ├── staging.py              # staging_config = MyStackConfig(...)
│       └── production.py           # production_config = MyStackConfig(...)
├── components/                     # Your cloud-specific Pulumi wrappers
│   ├── database.py
│   ├── container_app.py
│   └── redis.py
└── services/                       # Deployment orchestration
    ├── api/entrypoint.py
    └── worker/entrypoint.py

See examples/basic-project for a complete working example.

Requirements

  • Python >= 3.11
  • Pulumi >= 3.0

License

MIT

Metadata

Release files for pypuwa 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pypuwa 0.2.1
File Size Uploaded
pypuwa-0.2.1.tar.gz 27.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pypuwa 0.2.1
File Interpreter ABI Platform
pypuwa-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 50.2 kB

Release files / pypuwa-0.2.1.tar.gz

Download URL pypuwa-0.2.1.tar.gz
Size 27.0 kB
Tags Source
SHA-256 checksum
How to use checksums
6f3f297a9c4652bae98c0f00c21e51e189ab8b363dd7245e159dfa56cf0cf52a
BLAKE2b-256 checksum
How to use checksums
858249433ab6768d43dacce3ae404fe27d34e3b77363fe5d14c71cc2235b4f3d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 22, 2026.

Transparency log

Release files / pypuwa-0.2.1-py3-none-any.whl

Download URL pypuwa-0.2.1-py3-none-any.whl
Size 23.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
21ec0a1ff153b169bc003c3c917bb98edd37a9c129c435e49e5a3b2c67c397ec
BLAKE2b-256 checksum
How to use checksums
4b8a82aea55dffb4e837bc925b70fd4896f5df5ba12282c57e82e8b78a87c478
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 22, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page