Skip to content

Config Reference

API reference for everything you put in config.py.


ProjectConfig

from small_config import ProjectConfig

Abstract base class. Subclass it, set default_env, implement project(), and expose an instance as CONFIG.

Attribute / Method Type Description
default_env str Environment used when --env is not passed. Default: "dev"
project(env: str) → Project abstract Return the Project for the given env name
class AppConfig(ProjectConfig):
    default_env = "dev"

    def project(self, env: str) -> Project:
        return Project(name="my-app", env=env, components=[...])

CONFIG = AppConfig()

Project

from small_config import Project

Dataclass holding the full description of a stack for one environment.

Field Type Default Description
name str required Stack name — used in lock names
env str required Active environment (passed in from project())
components list[BaseComponent] required Ordered or unordered list of components
tests list[HttpTest] [] Smoke tests run after sc build
lock str "local" Lock strategy: "local" or "git-tag"

TerraformComponent

from small_config import TerraformComponent

Runs terraform init + apply/plan/destroy. Captures state via terraform show -json and outputs via terraform output -json.

Inherits all base parameters.

Default steps by action:

action Commands
"create" (default) terraform init, terraform apply -auto-approve
"plan" terraform init, terraform plan
"validate" terraform init, terraform validate
"destroy" terraform init, terraform destroy -auto-approve

You rarely set action directly — the CLI commands (build, destroy, plan) set it for you.

Refs from Terraform

# Named Terraform output
url = infra.output("ip")
url_with_transform = infra.output("ip", transform=lambda ip: f"http://{ip}:8000")

# Value from terraform show -json via JMESPath
zone = infra.state(jmes="values.root_module.resources[0].values.zone")

# Value via Python function
name = infra.state(fn=lambda s: s["values"]["root_module"]["resources"][0]["values"]["name"])

AnsibleComponent

from small_config import AnsibleComponent

Runs ansible-playbook. No state capture.

Parameter Type Default Description
playbook str "main.yml" Playbook filename (relative to path)

Plus all base parameters.

AnsibleComponent(
    name="configure",
    path="./ansible",
    playbook="site.yml",
    vars={"host": infra.output("ip")},
    depends_on=[infra],
)

ShellComponent

from small_config import ShellComponent

Escape hatch for anything that doesn't fit Terraform or Ansible: gcloud, kubectl, custom scripts, etc. No state capture.

Parameter Type Default Description
commands list[str] required Shell commands to run in order

Plus all base parameters.

ShellComponent(
    name="register",
    path="./scripts",
    commands=["./register.sh {{ vars.ip }}"],
    vars={"ip": infra.output("ip")},
    depends_on=[infra],
)

Base component parameters

All component types share these fields:

Parameter Type Default Description
name str required Unique identifier within a Project
path str \| None ./{name} Directory containing the component's files; omit to use ./{name}. An empty string resolves the same way but logs a warning
action str "create" Lifecycle verb; usually set by the CLI
vars dict[str, Any] {} Template variables; values may be Ref objects
steps list[str] \| None None Override default commands entirely
depends_on list[BaseComponent] [] Components that must run first
tests list[Any] [] Component-scoped HttpTest entries (often with SelfOutputRef); see Concepts — Tests on components

When the on-disk directory is not ./{name} (for example name="configure" but files live under ./ansible), pass path explicitly.

vars and templates

Values in vars are injected into .j2 Jinja2 templates before commands run. Refs are resolved first:

vars={
    "project_id": "my-project",          # literal
    "api_key": sc.secret("api_key"),      # SecretRef → SC_SECRET_API_KEY
    "db_host": db.output("host"),         # OutputRef → from prior component
}

In a Jinja2 template file:

project = "{{ vars.project_id }}"


HttpTest

from small_config import HttpTest

HTTP smoke test. Retries on transport errors; resolves Ref URLs at test time.

Parameter Type Default Description
url str \| Ref required URL to GET — string, OutputRef, StateRef, or component-scoped SelfOutputRef
name str \| None None Label shown in result table; auto-generated if unset
expect_status int 200 Expected HTTP status code
expect_body_contains str \| None None Substring that must appear in the response body
attempts int 6 Max attempts before failing
retry_delay_s float 5.0 Seconds between retries (transport errors only)
trust_env bool False If False, ignores HTTP_PROXY / ALL_PROXY. Set True only if you intentionally need a proxy.
timeout_s float 30.0 Per-attempt connect + read timeout
component BaseComponent \| None None Owning component for this test; also set automatically for tests listed in TerraformComponent(..., tests=[...]) or when using with TerraformComponent(...) as infra
HttpTest(
    name="api health",
    url=infra.output("ip", transform=lambda ip: f"http://{ip}:8000"),
    expect_status=200,
    expect_body_contains="Hello",
    attempts=10,
    retry_delay_s=10.0,
)

Component-local URL without naming the component twice (see Concepts):

from small_config import HttpTest, SelfOutputRef

HttpTest(
    name="api health",
    url=SelfOutputRef("ip", transform=lambda ip: f"http://{ip}:8000"),
    expect_status=200,
)

Refs

sc.secret(name) → SecretRef

from small_config import sc
ssh_pub_key = sc.secret("ssh_pub_key")   # reads SC_SECRET_SSH_PUB_KEY

The environment variable name is SC_SECRET_<NAME_UPPER>. The value is never written to disk.

component.output(key, *, transform=None) → OutputRef

Reads a named Terraform output (from terraform output -json) after the component is applied. The ref includes the component name (use everywhere: vars, project-level tests, cross-stack refs).

ip = infra.output("ip")
url = infra.output("ip", transform=lambda ip: f"http://{ip}:8000")

SelfOutputRef

from small_config import SelfOutputRef

SelfOutputRef(key, *, transform=None)

Holds only the Terraform output key (and optional transform). Resolution uses the owning HttpTest’s component — for component-scoped tests only (tests=[...] on the component, with TerraformComponent(...) as infra, or HttpTest(component=...)).

Invalid in vars= and invalid for resolve() outside an HttpTest (raises TypeError). For project-level or explicit targets, use OutputRef or component.output(...).

component.state(*, jmes=None, fn=None, strict=True) → StateRef

Reads a value from terraform show -json via JMESPath or a Python function.

zone = infra.state(jmes="values.root_module.resources[0].values.zone")
name = infra.state(fn=lambda s: s["values"]["root_module"]["resources"][0]["values"]["name"])
Parameter Type Default Description
jmes str \| None — JMESPath expression
fn callable \| None — Python function receiving the raw state dict
strict bool True Raise if the result is None

Exactly one of jmes or fn must be provided.