Skip to main content

Configator logo depicting a cool gator

A convenient way to load your app configuration from 1Password.

Ruff CI status Quality Gate Status Test Coverage Lines of Code

This project is licensed under the terms of the MIT license.

Quick Start

import asyncio
import os
from configator import load_config
from pydantic import BaseModel


class DatabaseConfig(BaseModel):
    host: str
    port: int
    username: str
    password: str


class AppConfig(BaseModel):
    api_key: str
    debug: bool
    timeout: int


class Config(BaseModel):
    db: DatabaseConfig
    app: AppConfig
    debug: bool = False


async def main():
    token = os.getenv("OP_SERVICE_ACCOUNT_TOKEN")
    cfg: Config = await load_config(
        schema=Config,
        token=token,
        vault="REPO whatever",
        item="whatever-develop",
    )
    assert cfg.db.port == 5432


asyncio.run(main())

Developer Mode

For local development, you can override configuration values using a .env file by setting the CONFIGATOR_DEV_MODE environment variable:

export CONFIGATOR_DEV_MODE=1

When developer mode is enabled, values are loaded with the following priority (highest to lowest):

  1. .env file
  2. Environment variables
  3. 1Password values (via initialization parameters)

Without developer mode, the standard priority applies (1Password values take precedence over environment variables and .env files).

⚠️ Never enable developer mode in production. A stray .env file would silently override vetted secrets. Configator guards against this: if CONFIGATOR_DEV_MODE is set while ENVIRONMENT (or, as a fallback, APP_ENV) resolves to production — any value case-insensitively starting with product, e.g. product or production — instantiating a config model raises a RuntimeError. Production deployments must ensure CONFIGATOR_DEV_MODE is unset and that no .env file ships in production images.

This feature works with the provided common configuration models (PostgresConfig, SentryConfig). For your own config schemas, you can simply extend ConfigatorSettings to get this behavior.

Installation

uv add "git+https://github.com/Utiligize/configator@v3000.0.0"

or, if you like the bleeding edge:

uv add "git+https://github.com/Utiligize/configator"

For information on how to authenticate uv with GitHub, see https://docs.astral.sh/uv/concepts/authentication/git/.

For information on how to use private repos in GitHub Actions, see https://docs.astral.sh/uv/guides/integration/github/#private-repos. If you create a fine-grained access token, it simply needs the "Content" read permission.

Writing Config Classes

Define your app's config as a class deriving from Pydantic's BaseModel. The field names will be matched against the 1Password item field titles, and the values loaded from them. The field names are treated as lower snake case, and item field names in 1Password are converted accordingly when matching. For example, a Python model with a field called sentry_key will match a 1Password item with a field title of SENTRY_KEY or sentry-key. It is therefore important to ensure that field names are unique, at least within sections.

Nested models are loaded from separate sections in the 1Password item. Fields in these nested models can have the same name as fields in other sections. Fields in the base config class are found by name, no matter their section (but the intention is for them to be added without one), so the names of these must be unique in the full model.

Supported Features

  • Basic types (str, int, float, even complex) are simply parsed from the string in 1Password.
  • decimal.Decimal is also supported and should usually be preferred over float.
  • Booleans are special: since any string is truthy in Python, a bool must have one of 8 (case-insensitive) values:
    • "true", "1", "yes", and "on" are interpreted as True.
    • "false", "0", "no", and "off" are interpreted as False.
    • any other value for a field defined as bool will raise a ValueError.
  • Collections (dict, list, set) are loaded by interpreting the string value in 1Password as JSON and passing that object to the constructor. This means that a set can be constructed from what looks like a list, for example.
  • Any string starting with op:// will be resolved recursively (up to a depth of 10 links). All references in the config item are resolved together, one batched request per level of nesting, so a schema with many referencing fields costs a handful of 1Password requests rather than one per field. The request count for each load is emitted as an info log line.
  • Every 1Password call is retried individually on transient failures (3 attempts, with no further attempt scheduled more than 10 seconds after the first), so a retry costs one request rather than a full reload. The cap bounds retrying, not a single hung request — the SDK exposes no request timeout. Rate-limit errors are never retried: they are logged and raised immediately, because the hourly read budget can be up to an hour from resetting and each retry would spend a request against a budget that is already empty.

Planned Features

Unsupported Features

  • Typed collections are sadly not supported, because it confuses the issubclass matching of fields. This means that fields in your config model must be defined as e.g. plain dict, not dict[str, str].

  • Optional and Union fields are not supported, i.e. you cannot do either of

    foo: str | None = None
    bar: Optional[str]
    baz: int | float
    

    because it confuses the hydrator, who won't know which constructor to call or will try to initialize None.

  • While default values are supported, default_factory is not.

  • Basic Python types bytes and bytearray may work but are not officially supported.

Error Handling

Every failure load_config reports about 1Password or the config item derives from ConfigatorError, so one except clause catches them all — the developer-mode production guard being the one deliberate exception, described at the end of this section. Below the base sit two types that answer the question a caller has to make a decision on — is the config wrong, or is 1Password simply not answering?

Exception Meaning What a caller should do
ConfigUnavailableError 1Password could not be reached or would not answer: authentication failure, network or TLS error, rate limiting, an empty vault or item listing, or a reference that could not be resolved. The config that is there may well be fine. A service that keeps a last-good config snapshot may boot from it rather than fail.
ConfigInvalidError The item was read, but does not fit the schema: a field with no value and no default, a value that will not parse as its annotated type, malformed JSON in a collection field, a Pydantic validation failure, or a reference chain deeper than 10 links. Fail loudly. Retrying and falling back to an older snapshot both serve stale config over a real, unfixed error; someone has to correct the 1Password item or the schema.

An empty vault or item listing counts as unavailable rather than invalid, because a de-permissioned or rotated service-account token looks exactly like a vault that is not there.

Where a failure originates in an underlying exception — an SDK error, a parse failure, a Pydantic validation error — it is chained as __cause__, so the original message and traceback survive. Failures Configator detects itself have no __cause__: a vault or item absent from a listing, a required field with no value, a reference the response omits, and the depth guard all raise on their own. Treat __cause__ as optional:

from configator import ConfigInvalidError, ConfigUnavailableError, load_config

try:
    cfg = await load_config(schema=Config, token=token, vault=vault, item=item)
except ConfigUnavailableError as exc:
    log.warning("1Password unavailable, booting from snapshot: %s", exc.__cause__ or exc)
    cfg = load_snapshot()
except ConfigInvalidError:
    log.exception("config item does not fit the schema")
    raise

The developer-mode production guard described above is deliberately not part of this hierarchy: it still raises a plain RuntimeError, because it is a refusal to start rather than a report about the config item.

Development

Setup

uv sync

Lint and Format

just lint

Run Tests

just test

Run Failed Tests

just test-failed

◼️◼️◼️

Release files for configator-op 3000.7.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 configator-op 3000.7.0
File Size Uploaded
configator_op-3000.7.0.tar.gz 30.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for configator-op 3000.7.0
File Interpreter ABI Platform
configator_op-3000.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 46.0 kB

Release files / configator_op-3000.7.0.tar.gz

Download URL configator_op-3000.7.0.tar.gz
Size 30.8 kB
Tags Source
SHA-256 checksum
How to use checksums
1151194fde8ea2e235a8754d7d565e8c487d7d701d83db9469a52ed7e2135ed9
BLAKE2b-256 checksum
How to use checksums
0eedf894c88d58b3a958483e7aad20542233514f1aa8ee1b42a227ff9fbe1990
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / configator_op-3000.7.0-py3-none-any.whl

Download URL configator_op-3000.7.0-py3-none-any.whl
Size 15.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ac2f7f4d420c3f19a8fd5cb74bc88ce1ceaecde76df990aad55a0a1d2ef72151
BLAKE2b-256 checksum
How to use checksums
56f7c69cba017f2e4dc9241ed95ee10673cdde3cad5029be1cea4de4004007ac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

This release

3000.7.0 This release

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