Skip to main content
Yanked

This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.5.4 instead.
Reason given by maintainers: API is not stable.

spectrik

A generic specification and blueprint pattern for declarative configuration-as-code tools.

Overview

spectrik provides a reusable framework for building tools that apply declarative configurations to external systems. It includes:

  • Specification — an abstract base class for defining desired-state resources
  • SpecOp strategies — Present, Ensure, and Absent wrappers that control when specs are applied or removed
  • Blueprint — a named, ordered collection of spec operations
  • Project — a top-level build target that orchestrates blueprints, with support for multiple project types in a single workspace
  • Workspace — a lazy-resolving collection of projects built from parsed configuration files
  • Lifecycle hooks — @pre_build and @post_build decorators for setup and teardown on project subclasses
  • HCL loading engine — parse .hcl files into blueprints and projects with decorator-based registration

Installation

pip install spectrik

Quick Start

import spectrik
from spectrik.hcl import scan

# Register a custom project type
@spectrik.project("myapp")
class MyProject(spectrik.Project):
    api_key: str = ""

# Register specs for HCL block decoding
@spectrik.spec("widget")
class Widget(spectrik.Specification["MyProject"]):
    color: str = "red"

    def equals(self, ctx):
        return current_color() == self.color

    def apply(self, ctx):
        set_color(self.color)

    def remove(self, ctx):
        reset_color()

# Load from HCL files and build
ws = scan("./configs")
ws["myapp"].build(dry_run=True)

Core Concepts

Specifications

A Specification[P] defines a single desired-state resource. Subclasses implement:

  • apply(ctx) — create or update the resource (required)
  • equals(ctx) — return True if current state matches desired state (optional; defaults to NotImplemented)
  • exists(ctx) — return True if the resource exists (optional; defaults to the result of equals())
  • remove(ctx) — delete the resource (optional; raises NotImplementedError by default for irreversible resources)

When equals() is not implemented (e.g., for specs managing secrets or opaque values), the Ensure strategy always applies and logs that equality is unknown. Present and Absent fall back to exists().

SpecOp Strategies

Strategies wrap a spec with conditional execution logic:

Strategy Behavior
Present Apply only if the resource doesn't exist
Ensure Apply if current state doesn't match (or equality is unknown)
Absent Remove if the resource exists

All strategies support dry-run mode, fire Context events, and handle errors consistently.

Projects and Registration

The base Project class is a Pydantic model that orchestrates blueprints. Consumer apps subclass it with domain-specific fields and register it using @spectrik.project():

@spectrik.project("railway")
class RailwayProject(spectrik.Project):
    token: str = ""
    service_id: str = ""

The block type name in HCL maps to the registered project class. Multiple project types can coexist in a single workspace:

railway "api" {
    token      = "${env.RAILWAY_TOKEN}"
    service_id = "abc123"

    use = ["deploy"]
}

project "docs" {
    use = ["static-site"]
}

The base Project class is automatically registered as the "project" block type.

Lifecycle Hooks

Project subclasses can define lifecycle hooks using decorators. Hooks run during Project.build() — after construction but before (or after) spec execution.

@spectrik.project("railway")
class RailwayProject(spectrik.Project):
    token: str = ""

    @spectrik.pre_build
    def resolve_secrets(self, ctx: spectrik.Context) -> None:
        if not self.token:
            raise RuntimeError(f"Project '{self.name}' requires 'token'")
        self.token = resolve_secret(self.token)

    @spectrik.post_build
    def cleanup(self, ctx: spectrik.Context) -> None:
        close_api_client()
  • @pre_build — runs before any specs execute. Raising an exception aborts the build. Use this for credential resolution, connection setup, or field validation that can't happen at construction time.
  • @post_build — always runs after specs complete (even on failure), like a finally block. Use this for cleanup.

Multiple hooks of the same type run in MRO order (base class first).

Context and Events

Context carries runtime state through the build pipeline:

ctx = spectrik.Context(target=project, dry_run=True, continue_on_error=False)
  • target — the project instance being built
  • dry_run — when True, specs report what they would do without making changes
  • continue_on_error — when True, the build continues past spec failures

Context provides events for observing spec execution:

ctx.on_spec_start += lambda ctx, op: print(f"Starting {op.spec}")
ctx.on_spec_applied += lambda ctx, op: print(f"Applied {op.spec}")
ctx.on_spec_skipped += lambda ctx, op, reason: print(f"Skipped: {reason}")
ctx.on_spec_failed += lambda ctx, op, err: print(f"Failed: {err}")
ctx.on_spec_finish += lambda ctx, op: print(f"Finished {op.spec}")
ctx.on_spec_removed += lambda ctx, op: print(f"Removed {op.spec}")

Events are for external observation (progress bars, logging, metrics). For project-level setup and teardown, use lifecycle hooks instead.

Workspace

A Workspace is a lazy-resolving Mapping[str, Project] built from parsed HCL files. Projects are resolved fresh on each access.

ws = scan("./configs")

# Access by name
project = ws["myapp"]

# Query by type or name
railway_projects = ws.select(project_type=RailwayProject)
specific = ws.select(name="api")
subset = ws.select(names=["api", "docs"])

HCL Support

spectrik uses HCL as its configuration language.

import spectrik.hcl as hcl

ws = hcl.scan("./configs", context={
    "env": os.environ,
    "name": "myapp",
    "cwd": os.getcwd,
})

ws["myapp"].build()

Interpolation

String values in HCL files support ${...} variable interpolation. Pass a context dict when loading, and spectrik resolves references after parsing.

project "app" {
    description = "${name}"
    home        = "${env.HOME}/.config/${name}"
    workdir     = "${cwd}/data"
}

Dotted references walk the context using attribute or key access, so dicts, dataclasses, and Pydantic models all work naturally. If a context value is callable, it is invoked at resolution time — useful for values like "cwd": os.getcwd.

Escaping

Use $$ to produce a literal $ in the output. This is needed when HCL values contain template syntax meant for other tools:

You write in HCL Output after interpolation
${name} Resolved from context
$${name} Literal ${name}
$${{ secrets.TOKEN }} Literal ${{ secrets.TOKEN }}

For example, to embed a GitHub Actions workflow that mixes spectrik variables with Actions expressions:

blueprint "deploy" {
    present "file" {
        path    = ".github/workflows/deploy.yaml"
        content = <<-EOF
        name: Deploy
        on: [push]
        jobs:
          build:
            runs-on: ubuntu-latest
            steps:
              - uses: actions/checkout@v4
              - run: echo "Deploying ${app_name}"
                env:
                  TOKEN: $${{ secrets.GITHUB_TOKEN }}
        EOF
    }
}

In this example, ${app_name} is resolved by spectrik while $${{ secrets.GITHUB_TOKEN }} produces the literal ${{ secrets.GITHUB_TOKEN }} that GitHub Actions expects.

License

MIT

Release files for spectrik 1.0.0

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

Source distribution (sdist)

Source distribution for spectrik 1.0.0
File Size Uploaded
spectrik-1.0.0.tar.gz 69.5 kB Details

Built distribution (wheel)

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

Total release size: 85.7 kB

Release files / spectrik-1.0.0.tar.gz

Download URL spectrik-1.0.0.tar.gz
Size 69.5 kB
Tags Source
SHA-256 checksum
How to use checksums
53ce8da05f046598a640637bf393721d1739db91e4b00e29ab7f6d5090ec49a1
BLAKE2b-256 checksum
How to use checksums
9c52105c1998050a6d247d5658d5a41c2fe335d00f66e71328733a6751526d12
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / spectrik-1.0.0-py3-none-any.whl

Download URL spectrik-1.0.0-py3-none-any.whl
Size 16.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
db389eb6e197584317601f862731683638713842c3b3fa178c322bce54fe2745
BLAKE2b-256 checksum
How to use checksums
f0be56242b00a2c601f21ed51d1637666f2f6b8d150be36cb12b98d23e9f932d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.5.4

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

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