Skip to main content

Small Python CLI library

Project description

Stand With Ukraine

Kliamka

A small Python CLI library that provides Pydantic-based argument parser with type safety.

For detailed specifications, see CLAUDE.md.

PyPI - Version

Features

  • Type-safe CLI arguments with Pydantic validation
  • Decorator-based design for clean, readable code
  • Automatic argument parsing from class definitions
  • Enum support — string and integer valued enums, parsed by name or value
  • Positional arguments — with optional defaults
  • List/array argumentsList[str], List[int], List[Enum]
  • Environment variable fallback — CLI > ENV > default priority
  • Subcommands — git-style CLI with @kliamka_subcommands
  • Short flags-v alongside --verbose
  • Mutually exclusive groups — e.g. --json vs --csv
  • --version flag — automatic version display
  • Help customization — program name, usage, epilog
  • Custom type converters — per-argument converter= or a global register_converter(type, fn) registry
  • Pydantic validators — range checks, cross-field validation, regex patterns
  • Programmatic argv — pass custom argument lists for testing/embedding
  • PEP 561 compatible — ships py.typed marker
  • Modern Python 3.11+ with full type hints

Installation

pip install kliamka

Quick Start

from kliamka import KliamkaArg, KliamkaArgClass, kliamka_cli

class MyArgs(KliamkaArgClass):
    """My CLI application arguments."""
    verbose: bool | None = KliamkaArg("--verbose", "Enable verbose output", short="-v")
    count: int | None = KliamkaArg("--count", "Number of iterations", default=1, short="-c")

@kliamka_cli(MyArgs)
def main(args: MyArgs) -> None:
    if args.verbose:
        print("Verbose mode enabled")
    for i in range(args.count or 1):
        print(f"Iteration {i + 1}")

if __name__ == "__main__":
    main()
python my_app.py -v -c 3

API Reference

KliamkaArg

Descriptor for defining CLI arguments.

KliamkaArg(
    flag: str,              # Flag name: "--verbose" or "filename"
    help_text: str = "",    # Help text
    default: Any = None,    # Default value
    positional: bool = False,  # Positional argument
    env: str | None = None,    # Environment variable fallback
    short: str | None = None,  # Short flag: "-v"
    mutually_exclusive: str | None = None,  # Exclusion group name
)

KliamkaArgClass

Base class for CLI argument definitions using Pydantic models.

class MyArgs(KliamkaArgClass):
    """Description shown in --help."""
    debug: bool | None = KliamkaArg("--debug", "Enable debug mode", short="-d")
    config: str | None = KliamkaArg("--config", "Config file path")

Single-value annotations and optional unions (Optional[T] or T | None) are supported. Wider unions such as int | str are rejected when the parser is built.

ParserMeta

Customize help output and add --version:

from kliamka import ParserMeta

class MyArgs(KliamkaArgClass):
    """My application."""
    parser_meta = ParserMeta(
        prog="myapp",
        usage="myapp [options] FILE",
        epilog="See https://example.com for docs.",
        version="myapp 1.0.0",
    )
    verbose: bool | None = KliamkaArg("--verbose", "Verbose", short="-v")

@kliamka_cli

Decorator that parses CLI arguments and injects them as the first parameter.

@kliamka_cli(MyArgs)
def main(args: MyArgs) -> None:
    pass

# Or with custom argv for testing:
@kliamka_cli(MyArgs, argv=["--verbose", "--count", "5"])
def main(args: MyArgs) -> None:
    pass

@kliamka_subcommands

Decorator for git-style subcommand CLIs.

from kliamka import kliamka_subcommands

class MainArgs(KliamkaArgClass):
    verbose: bool | None = KliamkaArg("--verbose", "Verbose output", short="-v")

class AddArgs(KliamkaArgClass):
    """Add a new item."""
    name: str = KliamkaArg("name", "Item name", positional=True)

class RemoveArgs(KliamkaArgClass):
    """Remove an item."""
    id: int = KliamkaArg("id", "Item ID", positional=True)
    force: bool | None = KliamkaArg("--force", "Force removal", short="-f")

@kliamka_subcommands(MainArgs, {"add": AddArgs, "remove": RemoveArgs})
def main(args: MainArgs, command: str, cmd_args) -> None:
    if command == "add":
        print(f"Adding {cmd_args.name}")
    elif command == "remove":
        print(f"Removing {cmd_args.id} (force={cmd_args.force})")

The main class and each subcommand class may define their own ParserMeta. For subcommands, prog, usage, epilog, and version apply to that subcommand's parser and help output.

Feature Examples

Enum Arguments

from enum import Enum

class LogLevel(Enum):
    DEBUG = "debug"
    INFO = "info"
    ERROR = "error"

class MyArgs(KliamkaArgClass):
    level: LogLevel = KliamkaArg("--level", "Log level", LogLevel.INFO)

Accepts --level debug, --level DEBUG, or --level info.

Environment Variable Fallback

class MyArgs(KliamkaArgClass):
    api_key: str | None = KliamkaArg("--api-key", "API key", env="MY_API_KEY")
    debug: bool | None = KliamkaArg("--debug", "Debug mode", env="DEBUG")

Priority: CLI argument > environment variable > default value. An environment variable counts as set even when its value is empty.

Environment booleans are case-insensitive and accept true, 1, yes, on, false, 0, no, and off. Other spellings are rejected with an error naming the environment variable. List values use comma-separated items (for example, FILES=a.txt,b.txt); comma escaping is not supported.

List Arguments

from typing import List

class MyArgs(KliamkaArgClass):
    files: List[str] = KliamkaArg("--files", "Input files")
    counts: List[int] = KliamkaArg("--counts", "Counts")
python app.py --files a.txt b.txt --counts 1 2 3

Positional Arguments

class MyArgs(KliamkaArgClass):
    source: str = KliamkaArg("source", "Source file", positional=True)
    dest: str = KliamkaArg("dest", "Destination file", positional=True)

Mutually Exclusive Arguments

class MyArgs(KliamkaArgClass):
    json_out: bool | None = KliamkaArg(
        "--json", "JSON output", mutually_exclusive="format"
    )
    csv_out: bool | None = KliamkaArg(
        "--csv", "CSV output", mutually_exclusive="format"
    )

--json and --csv cannot be used together.

Custom Type Converters

kliamka has built-in support for bool, int, float, str, Enum, and List[...]. For anything else — pathlib.Path, datetime, uuid.UUID, custom classes — you can plug in a converter.

Two ways, first match wins:

1. Per-argument converter (explicit, scoped):

from pathlib import Path
from kliamka import KliamkaArg, KliamkaArgClass

class MyArgs(KliamkaArgClass):
    config: Path = KliamkaArg(
        "--config",
        "Path to config file",
        converter=lambda s: Path(s).expanduser().resolve(),
    )

2. Global registry (reusable across all arguments of a given type):

from datetime import datetime
from kliamka import KliamkaArgClass, KliamkaArg, register_converter

register_converter(datetime, datetime.fromisoformat)

class MyArgs(KliamkaArgClass):
    since: datetime = KliamkaArg("--since", "Start timestamp")
    until: datetime = KliamkaArg("--until", "End timestamp")

Resolution order (first match wins, used by both CLI and env var parsing):

  1. Explicit KliamkaArg(converter=...) on the field
  2. A type registered via register_converter(...)
  3. Enum subclass — built-in enum parser
  4. List[T] — recursive lookup on element type T
  5. Fallback: the annotation itself (e.g. int, float, str)

Converters can raise ValueError or TypeError on invalid input; kliamka wraps that into a clean CLI error like:

error: argument --port: invalid int value: '99999' (out of range: 99999)

Use unregister_converter(type) to remove a registered converter. See examples/custom_converters.py for a runnable demo.

Pydantic Validation

from pydantic import model_validator

class MyArgs(KliamkaArgClass):
    port: int | None = KliamkaArg("--port", "Port number", default=8080)

    @model_validator(mode="after")
    def validate_port(self) -> "MyArgs":
        if self.port is not None and not (1 <= self.port <= 65535):
            raise ValueError(f"Port must be 1-65535, got {self.port}")
        return self

When validation fails during from_args(), kliamka raises KliamkaError with a simplified message. When validation fails inside @kliamka_cli or @kliamka_subcommands, the library renders that message through argparse, so users see standard CLI output such as:

error: Port must be 1-65535, got 99999

Documentation

  • CLAUDE.md — source of truth for project specifications
  • docs/TODO.md — development log and task tracker
  • docs/20260408_product_requirements.md — current product requirements and roadmap baseline

Development

Requirements

  • Python 3.11+
  • Pydantic 2.0+

Setup

git clone https://github.com/hotsyk/kliamka.git
cd kliamka
make init-dev
make install

Available Make Commands

Command Description
make install Install package in development mode
make test Run unit tests with pytest (-m "not packaging")
make test-package Build wheel/sdist and run packaging smoke tests
make test-all Run the full pytest suite, including packaging smoke coverage
make test-docker VERSION=3.11 Run tests in Docker for a specific Python version
make test-docker-all Run Docker test matrix for Python 3.11–3.14
make lint Run type checking and linting
make format Format code with ruff
make clean Clean build artifacts

Packaging and Compatibility Workflow

The repository now uses a modular package layout internally. The public import surface remains from kliamka import ..., while the active implementation lives under src/kliamka/ and the legacy top-level module path is kept as a compatibility facade.

Validation failures are also split more cleanly by context:

  • KliamkaArgClass.from_args() raises KliamkaError with simplified, joined validation messages for programmatic use.
  • @kliamka_cli and @kliamka_subcommands hand those messages back to argparse, so CLI users still see standard error: output.

GitHub Actions verifies both compatibility and packaging confidence on Python 3.11, 3.12, 3.13, and 3.14. The default local workflow mirrors that split:

  • make test for unit coverage excluding packaging smoke markers
  • make test-package for built wheel/sdist installation smoke tests
  • make test-all for the complete pytest suite

Docker Test Matrix

To mirror the pytest compatibility check locally across interpreter versions, use Dockerfile.test.

Run the full local Docker matrix:

make test-docker-all

This target runs four isolated Docker test jobs sequentially for Python 3.11, 3.12, 3.13, and 3.14.

Run one Dockerized test job for a specific Python version:

make test-docker VERSION=3.12

An optional docker-compose.test.yml file is included as a simple service definition reference for the supported Python versions.

These Docker targets bootstrap their own isolated environment inside the container and do not rely on the host .venv or on make test. Each container installs the package and runs:

pip install -e .
pip install pytest
pytest tests/ -v --tb=short

Success means the selected container exits cleanly and pytest passes for that Python version with no test failures. make test-docker-all is successful only when all four Python versions pass.

This intentionally keeps the Docker scope minimal and aligned with the CI pytest step, without pulling in linting, typing, or benchmark jobs.

Versions

See VERSIONS.md for detailed version history and changelog.

Examples

See the examples/ directory:

  • examples/basic_usage.py — Basic CLI argument handling
  • examples/enums.py — Enum types
  • examples/positional_args.py — Positional arguments
  • examples/list_args.py — List arguments
  • examples/env_vars.py — Environment variable fallback
  • examples/subcommands.py — Git-style subcommands
  • examples/custom_converters.py — Custom type converters (per-arg and global registry)

Performance

Workload argparse kliamka click typer
Creation · simple 102.42 µs 6.25 µs (0.06×) 11.08 µs (0.11×)
Creation · complex 119.04 µs 7.33 µs (0.06×)
Parsing · simple 10.04 µs 10.92 µs (1.09×) 64.21 µs (6.39×) 239.56 µs (23.86×)
Parsing · list 8.54 µs 9.50 µs (1.11×) 61.38 µs (7.19×) 235.13 µs (27.53×)
Parsing · complex 16.13 µs 17.17 µs (1.06×) 81.75 µs (5.07×) 300.46 µs (18.63×)
Full workflow · simple 129.42 µs 17.88 µs (0.14×) 126.25 µs (0.98×)
Full workflow · complex 154.50 µs 25.79 µs (0.17×)
Validation 183.67 ns 1.42 µs (7.72×)
Repeated parsing · 100 990.46 µs 1.08 ms (1.09×)

License

MIT-NORUS License — see LICENSE file for details.

Author

Volodymyr Hotsyk — https://github.com/hotsyk

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

kliamka-0.8.0.tar.gz (37.7 kB view details)

Uploaded Source

Built Distribution

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

kliamka-0.8.0-py3-none-any.whl (21.4 kB view details)

Uploaded Python 3

File details

Details for the file kliamka-0.8.0.tar.gz.

File metadata

  • Download URL: kliamka-0.8.0.tar.gz
  • Upload date:
  • Size: 37.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for kliamka-0.8.0.tar.gz
Algorithm Hash digest
SHA256 1d861bc16f3a3cb237cd6d6fbec048ef02c81ca7a5b4845e470efefe35bb865f
MD5 78c842c99b9f6bb274be52a33dfe1d14
BLAKE2b-256 6da155948f957586bdfc625bdea2e432dd145d872d0fea2e145b485210faefd6

See more details on using hashes here.

Provenance

The following attestation bundles were made for kliamka-0.8.0.tar.gz:

Publisher: publish.yml on hotsyk/kliamka

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

File details

Details for the file kliamka-0.8.0-py3-none-any.whl.

File metadata

  • Download URL: kliamka-0.8.0-py3-none-any.whl
  • Upload date:
  • Size: 21.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for kliamka-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f48472816f2ee23da6b0516bdee1fcf9fd62071fb32c8569ed323f960d785ba7
MD5 b6b3601aee91978442c9904cb74abc68
BLAKE2b-256 7d3b733c95f24eb90c5d593eba23b84c89c67267eb7d23956a1567791654d374

See more details on using hashes here.

Provenance

The following attestation bundles were made for kliamka-0.8.0-py3-none-any.whl:

Publisher: publish.yml on hotsyk/kliamka

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