Skip to main content

CLI argument and config-file parsing into dataclasses, plus logging and PRNG bootstrap helpers

Project description

initially

PyPI Python Tests License

Bootstrapping utilities for Python services: parse argv and a config file (.toml/.ini) straight into a dataclass, set up root logging, and a few small helpers around them — without writing the same fifty lines at the top of every main.

Installation

pip install initially

Requires Python 3.11+.

Why initially?

The first hundred lines of every if __name__ == "__main__": look the same:

  • read a config file, fall back to defaults, optionally accept -v key=value overrides on the CLI;
  • shovel the resulting dict into a typed dataclass;
  • print a usable error and exit if something is missing instead of dumping a stacktrace;
  • configure root logging so library messages are visible.

initially is exactly that, factored into one import. It treats your @dataclass as the source of truth: field types drive parsing and validation, and the same dataclass shape is also what gets printed back to the user as a sample INI on misuse.

Quick Start

The recommended layout: one top-level config dataclass per service, a LogsConfig field, parse, init logs, run.

from dataclasses import dataclass

from initially import LogsConfig, init_logs, parse_args_as_dataclass_or_exit


@dataclass
class AppConfig:
    @dataclass
    class Database:
        url: str
        timeout: float = 5.0

    logging: LogsConfig
    db: Database


def main() -> None:
    config = parse_args_as_dataclass_or_exit(AppConfig)
    init_logs(config.logging)
    ...


if __name__ == "__main__":
    main()

Run it with a TOML file:

python app.py config.toml

…or override individual fields without a file:

python app.py -v db.url=postgres://... -v db.timeout=10 -v logging.level=DEBUG

…or mix both — file values are loaded first, then CLI -v key=value flags overwrite them.

If a required field is missing or types don't fit, descriptive errors are logged and the process exits with the configured exit code. Run the binary with no arguments at all and you'll additionally see a minimal config.ini example derived from your dataclass — handy in CI to see exactly what the binary expects.

LogsConfig as a config field

Putting logging: LogsConfig as a field of your top-level config makes the [logging] INI section line up with the dataclass automatically:

[logging]
level = INFO
filename = /var/log/app.log
@dataclass
class AppConfig:
    logging: LogsConfig
    ...

init_logs(config.logging) then takes care of the rest. This is the intended idiom — there is no separate "logging config" path.

Overview

Argument parsing: parse_args_as_dataclass_or_exit | parse_args | Arg | parse_args_as_file_with_kwargs

Config files: read_file_as_dict | read_config | read_toml_file_by_path | read_ini_file_by_path | parse_ini_content

Logging: init_logs | LogsConfig | CustomFormatter

Dataclass introspection: get_hint_from_dataclass | StructureHint | FieldHint | make_ini_from_hint

File I/O: read_file_text

Errors: FileTextReadingFailure | ParseError | IniParseError

Other: init_random


Argument parsing

parse_args_as_dataclass_or_exit

def parse_args_as_dataclass_or_exit(
    dataclass_type: type[DP],
    args: Iterable[str] | None = None,
    flag_name: str | None = None,           # default: '-v'
    key_value_separator: str | None = None, # default: '='
    key_depth_separator: str | None = None, # default: '.'
    exit_code: int = 2,
) -> DP

The recommended entry point for the standard case: one config file → one dataclass, optionally with -v key=value overrides. Reach for parse_args instead when you need multiple positional arguments or per-slot custom casters.

Reads an optional positional config file (.toml or .ini, picked by extension), applies any number of -v key=value overrides, and converts the merged dict into dataclass_type. Nested dataclasses are addressed with dotted keys: -v db.url=....

If the file can't be read, fields are missing, or types don't fit, the function logs descriptive errors via the standard logging module and calls exit(exit_code). When neither a file nor any overrides were passed, it also logs a minimal config.ini example derived from the dataclass — useful in CI to see what the binary expects.

parse_args & Arg

The lower-level positional-argument parser. Use it when you need more than one positional argument, or when you want per-slot custom casters. For the typical case of one config file → one dataclass, prefer parse_args_as_dataclass_or_exit — it's shorter, validates against your dataclass, and adds -v key=value overrides on top.

Each keyword argument to parse_args declares one CLI slot, in order. Each value is a cast callable: a plain type (int, float, your own constructor) or an Arg for richer behaviour, including a hint string used in error output.

from dataclasses import dataclass

from initially import Arg, parse_args


@dataclass
class Settings:
    url: str
    timeout: int = 5


# Two positional arguments: a config file and a JSON report
config, report = parse_args(
    config_file=Arg.toml_file_to_dataclass(Settings),
    report_file=Arg.json_file(hint="path to a JSON report"),
)

Run it as python app.py config.toml report.json.

parse_args returns a single value when called with one keyword argument, and a list in the order they were declared otherwise.

Arg ships with helpers for common cases:

Helper Behaviour
Arg.json_file(hint=None) argument is a path to a JSON file → parsed as dict/list
Arg.ini_file(hint=None) argument is a path to an INI file → parsed as dict[str, dict]
Arg.ini_file_to_dataclass(DC) INI file → DC instance; auto-generates a hint from DC fields
Arg.toml_file_to_dataclass(DC) same for TOML

On error, missing/failed casts are reported via logging.critical and exit(2) is called.

parse_args_as_file_with_kwargs

def parse_args_as_file_with_kwargs(
    args: Iterable[str] | None = None,
    kwarg_flag_name: str | None = None,        # default: '-v'
    key_value_separator: str | None = None,    # default: '='
) -> tuple[str | None, dict[str, str]]

The low-level building block underneath parse_args_as_dataclass_or_exit: returns (config_file_path_or_None, dict_of_overrides). Useful when you want to merge the parsed flags into something other than a dataclass.

parse_args_as_file_with_kwargs(args=["./some.ini", "-v", "key=42", "-v", "foo=bar"])
# ('./some.ini', {'key': '42', 'foo': 'bar'})

Config files

read_file_as_dict / read_config

def read_file_as_dict(path, encoding=None) -> dict[str, dict[str, str]]

Dispatches to read_toml_file_by_path or read_ini_file_by_path based on the file's suffix; raises TypeError for any other extension. read_config is a single-argument alias.

read_toml_file_by_path

Reads and parses a .toml file via the stdlib tomllib. Errors propagate as raised by tomllib. The file is read through read_file_text, so a missing or unreadable path raises FileTextReadingFailure.

read_ini_file_by_path

Reads a .ini file and returns dict[str, dict[str, str]]. INI parsing problems become IniParseError. The file is read through read_file_text, so a missing or unreadable path raises FileTextReadingFailure.

parse_ini_content

Same as read_ini_file_by_path, but operates on an in-memory string.

Logging

init_logs / LogsConfig / CustomFormatter

@dataclass
class LogsConfig:
    level: str = "DEBUG"
    filename: str | None = None
    max_file_size_megabytes: int = 10
    max_files_amount: int = 10

def init_logs(settings: LogsConfig | None = None,
              logging_formatter: logging.Formatter | None = None) -> None

Configures the root logger. With no filename, logs go to stdout; with a filename, a RotatingFileHandler is attached using the configured size/retention. The default CustomFormatter rewrites record.name to include the process pid and the module name, which makes log lines from forked workers easy to attribute. Pass your own logging.Formatter to override the format.

The intended way to wire this in: keep LogsConfig as a field of your top-level config dataclass (so [logging] in the INI lines up automatically), then call init_logs(config.logging, logging_formatter=...) once at startup.

Dataclass introspection

get_hint_from_dataclass

Walks a dataclass type and returns a StructureHint describing fields, their types, and defaults. Nested dataclasses become nested StructureHints. Fields with init=False are skipped (they are computed in __post_init__, not configured).

StructureHint & FieldHint

Frozen dataclasses describing the shape of another dataclass. FieldHint carries name, type, and an optional default (which may be a default_factory callable). StructureHint carries an optional name and a sequence of fields/sub-structures.

make_ini_from_hint

Renders a StructureHint as a sample INI document. This is what parse_args_as_dataclass_or_exit uses internally to build the "MINIMAL config.ini EXAMPLE" hint that is printed when a required field is missing — the example you see in the error logs is generated from your dataclass, so it always matches what the binary actually accepts.

It's also exposed for direct use: when you want to ship an example_config.ini alongside your service that stays in sync with the code, render it once from the dataclass:

from initially import get_hint_from_dataclass, make_ini_from_hint

ini_text = make_ini_from_hint(get_hint_from_dataclass(AppConfig))

Required fields appear as name = REQUIRED; optional fields appear as commented-out lines with the default. Defaults that are callables (e.g. time.time) appear as ; name = ... with a default=module.qualname annotation. Pass add_optional=False to omit optional fields entirely.

INI itself does not allow more than one nesting level — passing a deeper structure raises ValueError.

File I/O

read_file_text

def read_file_text(path, encoding: str | None = None) -> str

Reads a text file, raising FileTextReadingFailure with a message that names the failing path and the underlying cause for every failure mode (empty path, bad path type, missing file, not-a-file, decode error, lookup error). Used internally by the INI/TOML readers; exposed because it's frequently useful in its own right.

Errors

ParseError / IniParseError

ParseError is the base for parser-level failures; IniParseError is its INI-specific subclass.

Other

init_random

def init_random(seed=NoValue) -> None | int | float | str | bytes | bytearray

Seeds random.seed. With no argument (or the NoValue sentinel), it uses os.getpid() + time.time() so two processes started in the same second still get different sequences. Useful when you want unique-per-process startup randomness without rolling your own seeding.

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

initially-1.0.0.tar.gz (19.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

initially-1.0.0-py3-none-any.whl (18.6 kB view details)

Uploaded Python 3

File details

Details for the file initially-1.0.0.tar.gz.

File metadata

  • Download URL: initially-1.0.0.tar.gz
  • Upload date:
  • Size: 19.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for initially-1.0.0.tar.gz
Algorithm Hash digest
SHA256 d944125154a9b22764353fbb8f13c2fa1c3f2524a362d7e312dc52c7bbfab8fe
MD5 3905aaebdc9921f7a9638f2152109e94
BLAKE2b-256 92ca9ae3913dee3a4ab0cd89863f22e4e6b72da34099c856f4a3e5dd34a6bdfd

See more details on using hashes here.

Provenance

The following attestation bundles were made for initially-1.0.0.tar.gz:

Publisher: publish-pypi.yml on miriada-io/initially

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file initially-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: initially-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 18.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for initially-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 87c64cd3ded3afe7a91e51dc166a602c5e1d1ec4226873e4f90d52704a75b828
MD5 b45f48534f2eea5aac8055b5dfe9ad6d
BLAKE2b-256 2ae36aafecd33dcc79ab12ce1f3572e3f43c1b34e5123dc5feadaed0540ac86d

See more details on using hashes here.

Provenance

The following attestation bundles were made for initially-1.0.0-py3-none-any.whl:

Publisher: publish-pypi.yml on miriada-io/initially

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page