Development¶
Setup¶
The project uses uv for dependency management, ruff for linting and formatting, mypy for type checking and pytest for tests.
make help lists every available target. The most used ones:
| Target | What it does |
|---|---|
make lint |
Ruff lint plus a formatting check |
make format |
Apply ruff autofixes and formatting |
make typecheck |
Run mypy in strict mode |
make test |
Run the test suite |
make coverage |
Run the test suite with a coverage report |
make check |
Everything CI runs |
make build |
Build the sdist and the wheel into dist/ |
make docs |
Build this documentation into site/ |
Running the CLI from a checkout:
Project layout¶
src/chaotic/
├── cli.py # argument parsing, env fallbacks, exit codes
├── runner.py # one-shot and periodic execution
├── config.py # loading and validating a chaos plan
├── excludes.py # "do not touch anything right now" windows
├── factory.py # config kind -> provider class
├── errors.py # ChaoticError and friends
├── log.py # logging setup
└── providers/
├── base.py # Chaotic, RestartChaotic, Target
└── <provider>.py # one module per API
tests/ # mirrors the layout above
Adding a provider¶
Most providers do the same thing: pick a random machine, stop it, wait, start it
again. That experiment lives in RestartChaotic, so a new provider only answers
three questions:
from functools import cached_property
from typing import Sequence
from chaotic.log import log
from chaotic.providers.base import RestartChaotic, Target
class ExampleChaotic(RestartChaotic):
"""Stop and start a random Example Cloud server."""
target_noun = "server" # only used in log messages
@cached_property
def client(self) -> ExampleSDK:
# Built lazily, so importing the module never touches the network
# and never requires credentials.
return ExampleSDK(token=os.getenv("EXAMPLE_API_TOKEN", ""))
def list_targets(self) -> Sequence[Target]:
tag = self.configs.get("tag")
log.info("Querying with tag: %s", tag)
return [Target(id=s.id, name=s.name, raw=s) for s in self.client.servers(tag=tag)]
def stop(self, target: Target) -> None:
self.client.stop(target.id)
def start(self, target: Target) -> None:
self.client.start(target.id)
Override skip_reason() to veto a target after it has been chosen — Proxmox uses
it for min_uptime. Providers with a genuinely different experiment subclass
Chaotic directly and implement action(); NomadChaotic is the example.
Then:
- Register the class in
PROVIDERSinsrc/chaotic/factory.py. - Add an
examples/config_<kind>.yaml. - Add
docs/configs/<kind>.mdand list it in thenavofmkdocs.yml. - Add tests.
tests/conftest.pyprovidesmake_provider(), which swaps the lazyclientfor a fake, and theno_sleepfixture.
tests/test_package.py fails if a provider has no example config, and
tests/test_factory.py fails if the registry and the documented kinds drift
apart.
Conventions¶
- No import side effects. Modules must not read credentials, open sockets or configure logging at import time. Credentials are read when the API client is first used, which is what makes the providers testable.
- Lazy log formatting. Use
log.info("Selected %s", name), not an f-string — the JSON formatter and the linter both expect it. - Errors. Raise a
ChaoticErrorsubclass for anything an operator can fix. The CLI logs those as a single line and exits 1; anything else gets a traceback.