wry


Namewry JSON
Version 0.1.5.post1 PyPI version JSON
download
home_pageNone
SummaryWhy Repeat Yourself? - Define your CLI once with Pydantic models
upload_time2025-09-08 17:59:02
maintainerNone
docs_urlNone
authorNone
requires_python>=3.10
licenseMIT
keywords cli pydantic click wry dry configuration type-safe
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # wry - Why Repeat Yourself? CLI

[![PyPI version](https://badge.fury.io/py/wry.svg)](https://badge.fury.io/py/wry)
[![Python versions](https://img.shields.io/pypi/pyversions/wry.svg)](https://pypi.org/project/wry/)
[![License](https://img.shields.io/pypi/l/wry.svg)](https://github.com/tahouse/wry/blob/main/LICENSE)
[![CI/CD](https://github.com/tahouse/wry/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/tahouse/wry/actions/workflows/ci-cd.yml)
[![codecov](https://codecov.io/gh/tahouse/wry/branch/main/graph/badge.svg)](https://codecov.io/gh/tahouse/wry)

**wry** (Why Repeat Yourself?) is a Python library that combines the power of Pydantic models with Click CLI framework, enabling you to define your CLI arguments and options in one place using type annotations. Following the DRY (Don't Repeat Yourself) principle, it eliminates the repetition of defining arguments, types, and validation rules separately.

## Features

### Core Features

- 🎯 **Single Source of Truth**: Define your CLI structure using Pydantic models with type annotations
- 🔍 **Type Safety**: Full type checking and validation using Pydantic
- 🌍 **Multiple Input Sources**: Automatically handles CLI arguments, environment variables, and config files
- 📊 **Value Source Tracking**: Know whether each config value came from CLI, env, config file, or defaults
- 🎨 **Auto-Generated CLI**: Automatically generates Click options and arguments from your Pydantic models
- 📝 **Rich Help Text**: Auto-generated help includes type information, constraints, and defaults
- 🔧 **Validation**: Leverage Pydantic's validation system with helpful error messages
- 🌳 **Environment Variable Support**: Automatic env var discovery with customizable prefixes
- 📁 **Config File Support**: Load configuration from JSON files with proper precedence

## Installation

```bash
pip install wry
```

## Quick Start

The simplest way to use wry is with `AutoWryModel`, which automatically generates CLI options for all fields:

```python
import click
from pydantic import Field
from wry import AutoWryModel, generate_click_parameters

class AppArgs(AutoWryModel):
    """Configuration for my app."""

    name: str = Field(description="Your name")
    age: int = Field(default=25, ge=0, le=120, description="Your age")
    verbose: bool = Field(default=False, description="Verbose output")

@click.command()
@generate_click_parameters(AppArgs)
def main(**kwargs):
    """My simple CLI application."""
    config = AppArgs(**kwargs)
    click.echo(f"Hello {config.name}, you are {config.age} years old!")

if __name__ == "__main__":
    main()
```

Run it:

```bash
$ python app.py --name Alice --age 30 --verbose
Hello Alice, you are 30 years old!
Name was provided via: ValueSource.CLI

# Also supports environment variables
$ export WRY_NAME=Bob
$ python app.py --age 35
Hello Bob, you are 35 years old!
```

## Value Source Tracking

wry can track where each configuration value came from. You have two options:

### Option 1: Direct Instantiation (No Source Tracking)

```python
@click.command()
@generate_click_parameters(AppArgs)
def main(**kwargs):
    # Simple instantiation - no source tracking
    config = AppArgs(**kwargs)
    # config.source.* will always show CLI regardless of actual source
```

### Option 2: With @click.pass_context (Full Source Tracking)

```python
@click.command()
@generate_click_parameters(AppArgs)
@click.pass_context
def main(ctx, **kwargs):
    # Full source tracking with context
    config = AppArgs.from_click_context(ctx, **kwargs)

    # Now sources are accurate
    print(config.source.name)     # ValueSource.CLI
    print(config.source.age)      # ValueSource.ENV
    print(config.source.verbose)  # ValueSource.DEFAULT

    # Get summary of all sources
    summary = config.get_sources_summary()
    # {
    #     ValueSource.CLI: ['name'],
    #     ValueSource.ENV: ['age'],
    #     ValueSource.DEFAULT: ['verbose']
    # }
```

**Note**: `from_click_context()` requires a Click context. If you don't need source tracking, use direct instantiation.

## Configuration Precedence

Values are resolved in the following order (highest to lowest priority):

1. CLI arguments
2. Environment variables
3. Config file values
4. Default values

## Environment Variables

wry automatically generates environment variable names from field names:

```bash
# Set environment variables
export WRY_NAME="Alice"
export WRY_AGE=25

# These will be picked up automatically
python myapp.py --verbose
```

View supported environment variables:

```bash
python myapp.py --show-env-vars
```

## Config Files

Load configuration from JSON files:

```bash
python myapp.py --config settings.json
```

Where `settings.json` contains:

```json
{
    "name": "Bob",
    "age": 35,
    "verbose": true
}
```

## Advanced Usage

### Multi-Model Commands

Use multiple Pydantic models in a single command:

```python
from typing import Annotated
import click
from wry import WryModel, AutoOption, generate_click_parameters, multi_model

class ServerConfig(WryModel):
    host: Annotated[str, AutoOption] = "localhost"
    port: Annotated[int, AutoOption] = 8080

class DatabaseArgs(WryModel):
    db_url: Annotated[str, AutoOption] = "sqlite:///app.db"
    pool_size: Annotated[int, AutoOption] = 5

@click.command()
@multi_model(ServerConfig, DatabaseConfig)
def serve(server: ServerConfig, database: DatabaseConfig):
    print(f"Starting server at {server.host}:{server.port}")
    print(f"Database: {database.db_url} (pool size: {database.pool_size})")
```

### AutoWryModel - Zero Configuration

Automatically generate options for all fields:

```python
import click
from wry import AutoWryModel, generate_click_parameters
from pydantic import Field

class QuickConfig(AutoWryModel):
    """All fields automatically become CLI options!"""

    name: str = Field(description="Your name")
    age: int = Field(default=30, ge=0, le=120)
    email: str = Field(description="Your email")

    # No need for Annotated[..., AutoOption]!

@click.command()
@generate_click_parameters(QuickConfig)
def quickstart(config: QuickConfig):
    print(f"Hello {config.name}!")
```

### Direct Configuration Creation

Create configs without decorators:

```python
from wry import WryModel

class Config(WryModel):
    name: str = "default"
    verbose: bool = False

# Create with source tracking
config = Config.create_with_sources(
    name="Alice",  # Will be tracked as programmatic source
    verbose=True
)

# Or from Click context (in a command)
config = Config.from_click_context(ctx, **kwargs)
```

## Advanced Features

### Multi-Model Commands

Use multiple configuration models in a single command:

```python
from wry import WryModel, multi_model, create_models

class DatabaseArgs(WryModel):
    host: str = Field(default="localhost")
    port: int = Field(default=5432)

class AppArgs(WryModel):
    debug: bool = Field(default=False)
    workers: int = Field(default=4)

@click.command()
@multi_model(DatabaseConfig, AppArgs)
@click.pass_context
def main(ctx, **kwargs):
    # Automatically splits kwargs between models
    configs = create_models(ctx, kwargs, DatabaseConfig, AppArgs)

    db_config = configs[DatabaseConfig]
    app_config = configs[AppArgs]

    click.echo(f"Connecting to {db_config.host}:{db_config.port}")
    click.echo(f"Running with {app_config.workers} workers")
```

### Strict Mode (Default)

By default, `generate_click_parameters` runs in strict mode to prevent common mistakes:

```python
@click.command()
@generate_click_parameters(Config)  # strict=True by default
@generate_click_parameters(Config)  # ERROR: Duplicate decorator detected!
def main(**kwargs):
    pass
```

To allow multiple decorators (not recommended):

```python
@generate_click_parameters(Config, strict=False)
```

### Manual Field Control

For more control over CLI generation, use the traditional `WryModel` with annotations:

```python
from typing import Annotated
from wry import WryModel, AutoOption, AutoArgument

class Config(WryModel):
    # Environment variable prefix
    env_prefix = "MYAPP_"

    # Required positional argument
    input_file: Annotated[str, AutoArgument] = Field(
        description="Input file path"
    )

    # Optional flag with short option
    verbose: Annotated[bool, AutoOption] = Field(
        default=False,
        description="Enable verbose output"
    )

    # Option with validation
    timeout: Annotated[int, AutoOption] = Field(
        default=30,
        ge=1,
        le=300,
        description="Timeout in seconds"
    )
```

## Development

### Prerequisites

- Python 3.10+
- Git with SSH key configured for signing

### Setup

```bash
# Clone the repository
git clone git@github.com:tahouse/wry.git
cd wry

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install in development mode
pip install -e ".[dev,test]"

# Install pre-commit hooks
pre-commit install
```

### Running Tests

```bash
# Run all tests with coverage
pytest

# Run with coverage report
pytest --cov=wry --cov-report=html

# Run specific test
pytest tests/test_core.py::TestWryModel::test_basic_model_creation

# Run all checks (recommended before pushing)
./check.sh
```

### Testing Across Python Versions

wry supports Python 3.10, 3.11, and 3.12. To ensure compatibility:

```bash
# Test with all available Python versions locally
./scripts/test_all_versions.sh

# Test in CI-like environment using Docker
./scripts/test_ci_locally.sh

# Run GitHub Actions locally with act (requires act to be installed)
./scripts/test_with_act.sh
```

### Code Quality

This project uses pre-commit hooks to ensure code quality:

- **ruff**: Linting and code formatting
- **mypy**: Type checking (pinned to >=1.17.1)
- **pytest**: Tests with 90% coverage requirement
- **bandit**: Security checks
- **safety**: Dependency vulnerability scanning

Pre-commit will run automatically on `git commit`. To run manually:

```bash
pre-commit run --all-files
```

### Version Compatibility

To ensure consistent behavior between local development and CI:

- **pydantic**: >=2.9.2 (for proper type inference)
- **mypy**: >=1.17.1 (for accurate type checking)
- **Python**: 3.10+ (we test against 3.10, 3.11, and 3.12)

Install the exact versions used in CI:

```bash
pip install -e ".[dev,test]" --upgrade
```

### Coverage Requirements

This project enforces 90% code coverage. To check coverage locally:

```bash
pytest --cov=wry --cov-report=term-missing --cov-fail-under=90
```

## Release Process

This project uses Git tags and GitHub Actions for releases. Only maintainers can create releases.

### Creating a Release

1. Ensure all changes are committed and pushed to `main`
2. Create and push a signed tag:

   ```bash
   git tag -s v0.1.0 -m "Release version 0.1.0"
   git push origin v0.1.0
   ```

3. The CI/CD pipeline will automatically:
   - Run all tests
   - Build source and wheel distributions
   - Upload to PyPI
   - Create a GitHub release
   - Sign artifacts with Sigstore

### Versioning

This project follows [Semantic Versioning](https://semver.org/):

- MAJOR version for incompatible API changes
- MINOR version for new functionality (backwards compatible)
- PATCH version for backwards compatible bug fixes

Version numbers are managed by `setuptools-scm` and derived from git tags.

### Development Releases

Every push to `main` creates a development release on PyPI:

```bash
pip install --pre wry  # Install latest dev version
```

## Architecture

### Code Organization

The wry codebase is organized into focused modules:

**Main Package:**

- **`wry/__init__.py`**: Package exports and version handling
- **`wry/click_integration.py`**: Click-specific decorators and parameter generation
- **`wry/multi_model.py`**: Support for multiple models in single commands
- **`wry/auto_model.py`**: Zero-configuration model with automatic option generation

**Core Subpackage (`wry/core/`):**

- **`model.py`**: Core `WryModel` implementation with value tracking
- **`sources.py`**: Value source definitions and tracking
- **`accessors.py`**: Property accessors for field metadata
- **`field_utils.py`**: Field constraint extraction and utilities
- **`env_utils.py`**: Environment variable handling

### Design Principles

1. **WRY (Why Repeat Yourself?)**: Define CLI structure once using Pydantic models
2. **Type Safety**: Leverage Python's type system for validation and IDE support
3. **Explicit is Better**: Users must opt-in to features like source tracking via `@click.pass_context`
4. **Composability**: Mix and match models, decorators, and configurations
5. **Source Tracking**: Always know where configuration values came from

## Contributing

We welcome contributions! Please follow these guidelines to ensure a smooth process.

### Getting Started

1. **Fork the repository** on GitHub
2. **Clone your fork** locally:

   ```bash
   git clone git@github.com:YOUR_USERNAME/wry.git
   cd wry
   ```

3. **Add upstream remote**:

   ```bash
   git remote add upstream git@github.com:tahouse/wry.git
   ```

4. **Create a feature branch**:

   ```bash
   git checkout -b feature/your-feature-name
   ```

### Development Workflow

1. **Set up development environment**:

   ```bash
   python -m venv venv
   source venv/bin/activate  # On Windows: venv\Scripts\activate
   pip install -e ".[dev,test]"
   pre-commit install
   ```

2. **Make your changes**:
   - Follow existing code style and patterns
   - Add/update tests for new functionality
   - Update documentation as needed
   - Add docstrings to all new functions/classes

3. **Test your changes**:

   ```bash
   # Run tests
   pytest

   # Check coverage (must be 100%)
   pytest --cov=wry --cov-report=term-missing

   # Run linting
   pre-commit run --all-files
   ```

4. **Commit your changes**:
   - Use [Conventional Commits](https://www.conventionalcommits.org/) format
   - Examples:
     - `feat: add support for YAML config files`
     - `fix: handle empty config files gracefully`
     - `docs: update examples for new API`
     - `test: add tests for edge cases`
     - `refactor: simplify value source tracking`

### Pull Request Guidelines

1. **Update your branch**:

   ```bash
   git fetch upstream
   git rebase upstream/main
   ```

2. **Push to your fork**:

   ```bash
   git push origin feature/your-feature-name
   ```

3. **Create Pull Request**:
   - Use a clear, descriptive title
   - Reference any related issues
   - Describe what changes you made and why
   - Include examples if applicable
   - Ensure all CI checks pass

### Code Style

- Use type hints for all function arguments and return values
- Follow PEP 8 (enforced by ruff)
- Maximum line length: 88 characters (Black's default)
- Use descriptive variable names
- Add docstrings to all public functions/classes/modules

### Testing Guidelines

- Write tests for all new functionality
- Maintain 100% code coverage
- Use pytest fixtures for common test setups
- Test both happy paths and edge cases
- Include tests for error conditions

### Documentation

- Update README.md if adding new features
- Add/update docstrings
- Include usage examples in docstrings
- Update type hints

### What We're Looking For

- **Bug fixes**: Always welcome!
- **New features**: Please open an issue first to discuss
- **Documentation**: Improvements always appreciated
- **Tests**: Additional test cases for edge conditions
- **Performance**: Optimizations with benchmarks
- **Examples**: More usage examples

### Questions?

- Open an issue for bugs or feature requests
- Start a discussion for general questions
- Check existing issues/PRs before creating new ones

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Acknowledgments

- Built on top of [Click](https://click.palletsprojects.com/) and [Pydantic](https://pydantic-docs.helpmanual.io/)
- Inspired by the DRY (Don't Repeat Yourself) principle

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "wry",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "cli, pydantic, click, wry, dry, configuration, type-safe",
    "author": null,
    "author_email": "Tyler House <26489166+tahouse@users.noreply.github.com>",
    "download_url": "https://files.pythonhosted.org/packages/3c/62/492f35fa73377df626f52551bf41efe57f56b1c43762e7ff0fcb48dfe15b/wry-0.1.5.post1.tar.gz",
    "platform": null,
    "description": "# wry - Why Repeat Yourself? CLI\n\n[![PyPI version](https://badge.fury.io/py/wry.svg)](https://badge.fury.io/py/wry)\n[![Python versions](https://img.shields.io/pypi/pyversions/wry.svg)](https://pypi.org/project/wry/)\n[![License](https://img.shields.io/pypi/l/wry.svg)](https://github.com/tahouse/wry/blob/main/LICENSE)\n[![CI/CD](https://github.com/tahouse/wry/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/tahouse/wry/actions/workflows/ci-cd.yml)\n[![codecov](https://codecov.io/gh/tahouse/wry/branch/main/graph/badge.svg)](https://codecov.io/gh/tahouse/wry)\n\n**wry** (Why Repeat Yourself?) is a Python library that combines the power of Pydantic models with Click CLI framework, enabling you to define your CLI arguments and options in one place using type annotations. Following the DRY (Don't Repeat Yourself) principle, it eliminates the repetition of defining arguments, types, and validation rules separately.\n\n## Features\n\n### Core Features\n\n- \ud83c\udfaf **Single Source of Truth**: Define your CLI structure using Pydantic models with type annotations\n- \ud83d\udd0d **Type Safety**: Full type checking and validation using Pydantic\n- \ud83c\udf0d **Multiple Input Sources**: Automatically handles CLI arguments, environment variables, and config files\n- \ud83d\udcca **Value Source Tracking**: Know whether each config value came from CLI, env, config file, or defaults\n- \ud83c\udfa8 **Auto-Generated CLI**: Automatically generates Click options and arguments from your Pydantic models\n- \ud83d\udcdd **Rich Help Text**: Auto-generated help includes type information, constraints, and defaults\n- \ud83d\udd27 **Validation**: Leverage Pydantic's validation system with helpful error messages\n- \ud83c\udf33 **Environment Variable Support**: Automatic env var discovery with customizable prefixes\n- \ud83d\udcc1 **Config File Support**: Load configuration from JSON files with proper precedence\n\n## Installation\n\n```bash\npip install wry\n```\n\n## Quick Start\n\nThe simplest way to use wry is with `AutoWryModel`, which automatically generates CLI options for all fields:\n\n```python\nimport click\nfrom pydantic import Field\nfrom wry import AutoWryModel, generate_click_parameters\n\nclass AppArgs(AutoWryModel):\n    \"\"\"Configuration for my app.\"\"\"\n\n    name: str = Field(description=\"Your name\")\n    age: int = Field(default=25, ge=0, le=120, description=\"Your age\")\n    verbose: bool = Field(default=False, description=\"Verbose output\")\n\n@click.command()\n@generate_click_parameters(AppArgs)\ndef main(**kwargs):\n    \"\"\"My simple CLI application.\"\"\"\n    config = AppArgs(**kwargs)\n    click.echo(f\"Hello {config.name}, you are {config.age} years old!\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\nRun it:\n\n```bash\n$ python app.py --name Alice --age 30 --verbose\nHello Alice, you are 30 years old!\nName was provided via: ValueSource.CLI\n\n# Also supports environment variables\n$ export WRY_NAME=Bob\n$ python app.py --age 35\nHello Bob, you are 35 years old!\n```\n\n## Value Source Tracking\n\nwry can track where each configuration value came from. You have two options:\n\n### Option 1: Direct Instantiation (No Source Tracking)\n\n```python\n@click.command()\n@generate_click_parameters(AppArgs)\ndef main(**kwargs):\n    # Simple instantiation - no source tracking\n    config = AppArgs(**kwargs)\n    # config.source.* will always show CLI regardless of actual source\n```\n\n### Option 2: With @click.pass_context (Full Source Tracking)\n\n```python\n@click.command()\n@generate_click_parameters(AppArgs)\n@click.pass_context\ndef main(ctx, **kwargs):\n    # Full source tracking with context\n    config = AppArgs.from_click_context(ctx, **kwargs)\n\n    # Now sources are accurate\n    print(config.source.name)     # ValueSource.CLI\n    print(config.source.age)      # ValueSource.ENV\n    print(config.source.verbose)  # ValueSource.DEFAULT\n\n    # Get summary of all sources\n    summary = config.get_sources_summary()\n    # {\n    #     ValueSource.CLI: ['name'],\n    #     ValueSource.ENV: ['age'],\n    #     ValueSource.DEFAULT: ['verbose']\n    # }\n```\n\n**Note**: `from_click_context()` requires a Click context. If you don't need source tracking, use direct instantiation.\n\n## Configuration Precedence\n\nValues are resolved in the following order (highest to lowest priority):\n\n1. CLI arguments\n2. Environment variables\n3. Config file values\n4. Default values\n\n## Environment Variables\n\nwry automatically generates environment variable names from field names:\n\n```bash\n# Set environment variables\nexport WRY_NAME=\"Alice\"\nexport WRY_AGE=25\n\n# These will be picked up automatically\npython myapp.py --verbose\n```\n\nView supported environment variables:\n\n```bash\npython myapp.py --show-env-vars\n```\n\n## Config Files\n\nLoad configuration from JSON files:\n\n```bash\npython myapp.py --config settings.json\n```\n\nWhere `settings.json` contains:\n\n```json\n{\n    \"name\": \"Bob\",\n    \"age\": 35,\n    \"verbose\": true\n}\n```\n\n## Advanced Usage\n\n### Multi-Model Commands\n\nUse multiple Pydantic models in a single command:\n\n```python\nfrom typing import Annotated\nimport click\nfrom wry import WryModel, AutoOption, generate_click_parameters, multi_model\n\nclass ServerConfig(WryModel):\n    host: Annotated[str, AutoOption] = \"localhost\"\n    port: Annotated[int, AutoOption] = 8080\n\nclass DatabaseArgs(WryModel):\n    db_url: Annotated[str, AutoOption] = \"sqlite:///app.db\"\n    pool_size: Annotated[int, AutoOption] = 5\n\n@click.command()\n@multi_model(ServerConfig, DatabaseConfig)\ndef serve(server: ServerConfig, database: DatabaseConfig):\n    print(f\"Starting server at {server.host}:{server.port}\")\n    print(f\"Database: {database.db_url} (pool size: {database.pool_size})\")\n```\n\n### AutoWryModel - Zero Configuration\n\nAutomatically generate options for all fields:\n\n```python\nimport click\nfrom wry import AutoWryModel, generate_click_parameters\nfrom pydantic import Field\n\nclass QuickConfig(AutoWryModel):\n    \"\"\"All fields automatically become CLI options!\"\"\"\n\n    name: str = Field(description=\"Your name\")\n    age: int = Field(default=30, ge=0, le=120)\n    email: str = Field(description=\"Your email\")\n\n    # No need for Annotated[..., AutoOption]!\n\n@click.command()\n@generate_click_parameters(QuickConfig)\ndef quickstart(config: QuickConfig):\n    print(f\"Hello {config.name}!\")\n```\n\n### Direct Configuration Creation\n\nCreate configs without decorators:\n\n```python\nfrom wry import WryModel\n\nclass Config(WryModel):\n    name: str = \"default\"\n    verbose: bool = False\n\n# Create with source tracking\nconfig = Config.create_with_sources(\n    name=\"Alice\",  # Will be tracked as programmatic source\n    verbose=True\n)\n\n# Or from Click context (in a command)\nconfig = Config.from_click_context(ctx, **kwargs)\n```\n\n## Advanced Features\n\n### Multi-Model Commands\n\nUse multiple configuration models in a single command:\n\n```python\nfrom wry import WryModel, multi_model, create_models\n\nclass DatabaseArgs(WryModel):\n    host: str = Field(default=\"localhost\")\n    port: int = Field(default=5432)\n\nclass AppArgs(WryModel):\n    debug: bool = Field(default=False)\n    workers: int = Field(default=4)\n\n@click.command()\n@multi_model(DatabaseConfig, AppArgs)\n@click.pass_context\ndef main(ctx, **kwargs):\n    # Automatically splits kwargs between models\n    configs = create_models(ctx, kwargs, DatabaseConfig, AppArgs)\n\n    db_config = configs[DatabaseConfig]\n    app_config = configs[AppArgs]\n\n    click.echo(f\"Connecting to {db_config.host}:{db_config.port}\")\n    click.echo(f\"Running with {app_config.workers} workers\")\n```\n\n### Strict Mode (Default)\n\nBy default, `generate_click_parameters` runs in strict mode to prevent common mistakes:\n\n```python\n@click.command()\n@generate_click_parameters(Config)  # strict=True by default\n@generate_click_parameters(Config)  # ERROR: Duplicate decorator detected!\ndef main(**kwargs):\n    pass\n```\n\nTo allow multiple decorators (not recommended):\n\n```python\n@generate_click_parameters(Config, strict=False)\n```\n\n### Manual Field Control\n\nFor more control over CLI generation, use the traditional `WryModel` with annotations:\n\n```python\nfrom typing import Annotated\nfrom wry import WryModel, AutoOption, AutoArgument\n\nclass Config(WryModel):\n    # Environment variable prefix\n    env_prefix = \"MYAPP_\"\n\n    # Required positional argument\n    input_file: Annotated[str, AutoArgument] = Field(\n        description=\"Input file path\"\n    )\n\n    # Optional flag with short option\n    verbose: Annotated[bool, AutoOption] = Field(\n        default=False,\n        description=\"Enable verbose output\"\n    )\n\n    # Option with validation\n    timeout: Annotated[int, AutoOption] = Field(\n        default=30,\n        ge=1,\n        le=300,\n        description=\"Timeout in seconds\"\n    )\n```\n\n## Development\n\n### Prerequisites\n\n- Python 3.10+\n- Git with SSH key configured for signing\n\n### Setup\n\n```bash\n# Clone the repository\ngit clone git@github.com:tahouse/wry.git\ncd wry\n\n# Create virtual environment\npython -m venv venv\nsource venv/bin/activate  # On Windows: venv\\Scripts\\activate\n\n# Install in development mode\npip install -e \".[dev,test]\"\n\n# Install pre-commit hooks\npre-commit install\n```\n\n### Running Tests\n\n```bash\n# Run all tests with coverage\npytest\n\n# Run with coverage report\npytest --cov=wry --cov-report=html\n\n# Run specific test\npytest tests/test_core.py::TestWryModel::test_basic_model_creation\n\n# Run all checks (recommended before pushing)\n./check.sh\n```\n\n### Testing Across Python Versions\n\nwry supports Python 3.10, 3.11, and 3.12. To ensure compatibility:\n\n```bash\n# Test with all available Python versions locally\n./scripts/test_all_versions.sh\n\n# Test in CI-like environment using Docker\n./scripts/test_ci_locally.sh\n\n# Run GitHub Actions locally with act (requires act to be installed)\n./scripts/test_with_act.sh\n```\n\n### Code Quality\n\nThis project uses pre-commit hooks to ensure code quality:\n\n- **ruff**: Linting and code formatting\n- **mypy**: Type checking (pinned to >=1.17.1)\n- **pytest**: Tests with 90% coverage requirement\n- **bandit**: Security checks\n- **safety**: Dependency vulnerability scanning\n\nPre-commit will run automatically on `git commit`. To run manually:\n\n```bash\npre-commit run --all-files\n```\n\n### Version Compatibility\n\nTo ensure consistent behavior between local development and CI:\n\n- **pydantic**: >=2.9.2 (for proper type inference)\n- **mypy**: >=1.17.1 (for accurate type checking)\n- **Python**: 3.10+ (we test against 3.10, 3.11, and 3.12)\n\nInstall the exact versions used in CI:\n\n```bash\npip install -e \".[dev,test]\" --upgrade\n```\n\n### Coverage Requirements\n\nThis project enforces 90% code coverage. To check coverage locally:\n\n```bash\npytest --cov=wry --cov-report=term-missing --cov-fail-under=90\n```\n\n## Release Process\n\nThis project uses Git tags and GitHub Actions for releases. Only maintainers can create releases.\n\n### Creating a Release\n\n1. Ensure all changes are committed and pushed to `main`\n2. Create and push a signed tag:\n\n   ```bash\n   git tag -s v0.1.0 -m \"Release version 0.1.0\"\n   git push origin v0.1.0\n   ```\n\n3. The CI/CD pipeline will automatically:\n   - Run all tests\n   - Build source and wheel distributions\n   - Upload to PyPI\n   - Create a GitHub release\n   - Sign artifacts with Sigstore\n\n### Versioning\n\nThis project follows [Semantic Versioning](https://semver.org/):\n\n- MAJOR version for incompatible API changes\n- MINOR version for new functionality (backwards compatible)\n- PATCH version for backwards compatible bug fixes\n\nVersion numbers are managed by `setuptools-scm` and derived from git tags.\n\n### Development Releases\n\nEvery push to `main` creates a development release on PyPI:\n\n```bash\npip install --pre wry  # Install latest dev version\n```\n\n## Architecture\n\n### Code Organization\n\nThe wry codebase is organized into focused modules:\n\n**Main Package:**\n\n- **`wry/__init__.py`**: Package exports and version handling\n- **`wry/click_integration.py`**: Click-specific decorators and parameter generation\n- **`wry/multi_model.py`**: Support for multiple models in single commands\n- **`wry/auto_model.py`**: Zero-configuration model with automatic option generation\n\n**Core Subpackage (`wry/core/`):**\n\n- **`model.py`**: Core `WryModel` implementation with value tracking\n- **`sources.py`**: Value source definitions and tracking\n- **`accessors.py`**: Property accessors for field metadata\n- **`field_utils.py`**: Field constraint extraction and utilities\n- **`env_utils.py`**: Environment variable handling\n\n### Design Principles\n\n1. **WRY (Why Repeat Yourself?)**: Define CLI structure once using Pydantic models\n2. **Type Safety**: Leverage Python's type system for validation and IDE support\n3. **Explicit is Better**: Users must opt-in to features like source tracking via `@click.pass_context`\n4. **Composability**: Mix and match models, decorators, and configurations\n5. **Source Tracking**: Always know where configuration values came from\n\n## Contributing\n\nWe welcome contributions! Please follow these guidelines to ensure a smooth process.\n\n### Getting Started\n\n1. **Fork the repository** on GitHub\n2. **Clone your fork** locally:\n\n   ```bash\n   git clone git@github.com:YOUR_USERNAME/wry.git\n   cd wry\n   ```\n\n3. **Add upstream remote**:\n\n   ```bash\n   git remote add upstream git@github.com:tahouse/wry.git\n   ```\n\n4. **Create a feature branch**:\n\n   ```bash\n   git checkout -b feature/your-feature-name\n   ```\n\n### Development Workflow\n\n1. **Set up development environment**:\n\n   ```bash\n   python -m venv venv\n   source venv/bin/activate  # On Windows: venv\\Scripts\\activate\n   pip install -e \".[dev,test]\"\n   pre-commit install\n   ```\n\n2. **Make your changes**:\n   - Follow existing code style and patterns\n   - Add/update tests for new functionality\n   - Update documentation as needed\n   - Add docstrings to all new functions/classes\n\n3. **Test your changes**:\n\n   ```bash\n   # Run tests\n   pytest\n\n   # Check coverage (must be 100%)\n   pytest --cov=wry --cov-report=term-missing\n\n   # Run linting\n   pre-commit run --all-files\n   ```\n\n4. **Commit your changes**:\n   - Use [Conventional Commits](https://www.conventionalcommits.org/) format\n   - Examples:\n     - `feat: add support for YAML config files`\n     - `fix: handle empty config files gracefully`\n     - `docs: update examples for new API`\n     - `test: add tests for edge cases`\n     - `refactor: simplify value source tracking`\n\n### Pull Request Guidelines\n\n1. **Update your branch**:\n\n   ```bash\n   git fetch upstream\n   git rebase upstream/main\n   ```\n\n2. **Push to your fork**:\n\n   ```bash\n   git push origin feature/your-feature-name\n   ```\n\n3. **Create Pull Request**:\n   - Use a clear, descriptive title\n   - Reference any related issues\n   - Describe what changes you made and why\n   - Include examples if applicable\n   - Ensure all CI checks pass\n\n### Code Style\n\n- Use type hints for all function arguments and return values\n- Follow PEP 8 (enforced by ruff)\n- Maximum line length: 88 characters (Black's default)\n- Use descriptive variable names\n- Add docstrings to all public functions/classes/modules\n\n### Testing Guidelines\n\n- Write tests for all new functionality\n- Maintain 100% code coverage\n- Use pytest fixtures for common test setups\n- Test both happy paths and edge cases\n- Include tests for error conditions\n\n### Documentation\n\n- Update README.md if adding new features\n- Add/update docstrings\n- Include usage examples in docstrings\n- Update type hints\n\n### What We're Looking For\n\n- **Bug fixes**: Always welcome!\n- **New features**: Please open an issue first to discuss\n- **Documentation**: Improvements always appreciated\n- **Tests**: Additional test cases for edge conditions\n- **Performance**: Optimizations with benchmarks\n- **Examples**: More usage examples\n\n### Questions?\n\n- Open an issue for bugs or feature requests\n- Start a discussion for general questions\n- Check existing issues/PRs before creating new ones\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n## Acknowledgments\n\n- Built on top of [Click](https://click.palletsprojects.com/) and [Pydantic](https://pydantic-docs.helpmanual.io/)\n- Inspired by the DRY (Don't Repeat Yourself) principle\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Why Repeat Yourself? - Define your CLI once with Pydantic models",
    "version": "0.1.5.post1",
    "project_urls": {
        "Documentation": "https://github.com/tahouse/wry#readme",
        "Homepage": "https://github.com/tahouse/wry",
        "Issues": "https://github.com/tahouse/wry/issues",
        "Repository": "https://github.com/tahouse/wry"
    },
    "split_keywords": [
        "cli",
        " pydantic",
        " click",
        " wry",
        " dry",
        " configuration",
        " type-safe"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "a90723ed6290ad84a749660f23053e4e650e7bf70e4cef82eb50ae477e4448d1",
                "md5": "96ec666d1e769e64868871a7454c994d",
                "sha256": "f7d6ab8557168288c10467eb5c33f3d3c966615828cdcd9af1678ed01753f6b8"
            },
            "downloads": -1,
            "filename": "wry-0.1.5.post1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "96ec666d1e769e64868871a7454c994d",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 31881,
            "upload_time": "2025-09-08T17:59:00",
            "upload_time_iso_8601": "2025-09-08T17:59:00.716438Z",
            "url": "https://files.pythonhosted.org/packages/a9/07/23ed6290ad84a749660f23053e4e650e7bf70e4cef82eb50ae477e4448d1/wry-0.1.5.post1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "3c62492f35fa73377df626f52551bf41efe57f56b1c43762e7ff0fcb48dfe15b",
                "md5": "c896adad731219f935eb88cf4843805f",
                "sha256": "7786fa7e750f957c78229a8bd2cd1c466f5a98cc82e55a4358d8f81c938ac54c"
            },
            "downloads": -1,
            "filename": "wry-0.1.5.post1.tar.gz",
            "has_sig": false,
            "md5_digest": "c896adad731219f935eb88cf4843805f",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 98228,
            "upload_time": "2025-09-08T17:59:02",
            "upload_time_iso_8601": "2025-09-08T17:59:02.224794Z",
            "url": "https://files.pythonhosted.org/packages/3c/62/492f35fa73377df626f52551bf41efe57f56b1c43762e7ff0fcb48dfe15b/wry-0.1.5.post1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-09-08 17:59:02",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "tahouse",
    "github_project": "wry#readme",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "wry"
}
        
Elapsed time: 1.27224s