CLI argument and config-file parsing into dataclasses, plus logging and PRNG bootstrap helpers
Project description
initially
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=valueoverrides 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d944125154a9b22764353fbb8f13c2fa1c3f2524a362d7e312dc52c7bbfab8fe
|
|
| MD5 |
3905aaebdc9921f7a9638f2152109e94
|
|
| BLAKE2b-256 |
92ca9ae3913dee3a4ab0cd89863f22e4e6b72da34099c856f4a3e5dd34a6bdfd
|
Provenance
The following attestation bundles were made for initially-1.0.0.tar.gz:
Publisher:
publish-pypi.yml on miriada-io/initially
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
initially-1.0.0.tar.gz -
Subject digest:
d944125154a9b22764353fbb8f13c2fa1c3f2524a362d7e312dc52c7bbfab8fe - Sigstore transparency entry: 1559889869
- Sigstore integration time:
-
Permalink:
miriada-io/initially@90b2b87c97d24abd217a67e2e779543445ad3f09 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/miriada-io
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@90b2b87c97d24abd217a67e2e779543445ad3f09 -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
87c64cd3ded3afe7a91e51dc166a602c5e1d1ec4226873e4f90d52704a75b828
|
|
| MD5 |
b45f48534f2eea5aac8055b5dfe9ad6d
|
|
| BLAKE2b-256 |
2ae36aafecd33dcc79ab12ce1f3572e3f43c1b34e5123dc5feadaed0540ac86d
|
Provenance
The following attestation bundles were made for initially-1.0.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on miriada-io/initially
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
initially-1.0.0-py3-none-any.whl -
Subject digest:
87c64cd3ded3afe7a91e51dc166a602c5e1d1ec4226873e4f90d52704a75b828 - Sigstore transparency entry: 1559890004
- Sigstore integration time:
-
Permalink:
miriada-io/initially@90b2b87c97d24abd217a67e2e779543445ad3f09 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/miriada-io
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@90b2b87c97d24abd217a67e2e779543445ad3f09 -
Trigger Event:
release
-
Statement type: