Python Pulumi Wrapper - Dataclass-based configuration framework for Pulumi infrastructure as code
Project description
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
- Sync — Generates
Pulumi.<env>.yamlfrom Python config, preserving existing encrypted secrets - Secrets — Detects new
secret()fields and prompts with arrow-key selection (all pre-selected) - Deploy — Runs
pulumi upon 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
TrueorNoneto continue - Return
Falseto 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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pypuwa-0.2.1.tar.gz.
File metadata
- Download URL: pypuwa-0.2.1.tar.gz
- Upload date:
- Size: 27.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f3f297a9c4652bae98c0f00c21e51e189ab8b363dd7245e159dfa56cf0cf52a
|
|
| MD5 |
071b61f8d8230654782ff253c77b70c3
|
|
| BLAKE2b-256 |
858249433ab6768d43dacce3ae404fe27d34e3b77363fe5d14c71cc2235b4f3d
|
Provenance
The following attestation bundles were made for pypuwa-0.2.1.tar.gz:
Publisher:
publish.yml on cafadev/pypuwa
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pypuwa-0.2.1.tar.gz -
Subject digest:
6f3f297a9c4652bae98c0f00c21e51e189ab8b363dd7245e159dfa56cf0cf52a - Sigstore transparency entry: 1598864151
- Sigstore integration time:
-
Permalink:
cafadev/pypuwa@fd3c7b5c0f94f6a6f9eacdd3003e9075f9fc21ff -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/cafadev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fd3c7b5c0f94f6a6f9eacdd3003e9075f9fc21ff -
Trigger Event:
release
-
Statement type:
File details
Details for the file pypuwa-0.2.1-py3-none-any.whl.
File metadata
- Download URL: pypuwa-0.2.1-py3-none-any.whl
- Upload date:
- Size: 23.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
21ec0a1ff153b169bc003c3c917bb98edd37a9c129c435e49e5a3b2c67c397ec
|
|
| MD5 |
3c6f6899e252e7131a7c49abc1968a01
|
|
| BLAKE2b-256 |
4b8a82aea55dffb4e837bc925b70fd4896f5df5ba12282c57e82e8b78a87c478
|
Provenance
The following attestation bundles were made for pypuwa-0.2.1-py3-none-any.whl:
Publisher:
publish.yml on cafadev/pypuwa
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pypuwa-0.2.1-py3-none-any.whl -
Subject digest:
21ec0a1ff153b169bc003c3c917bb98edd37a9c129c435e49e5a3b2c67c397ec - Sigstore transparency entry: 1598864319
- Sigstore integration time:
-
Permalink:
cafadev/pypuwa@fd3c7b5c0f94f6a6f9eacdd3003e9075f9fc21ff -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/cafadev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fd3c7b5c0f94f6a6f9eacdd3003e9075f9fc21ff -
Trigger Event:
release
-
Statement type: