python-env-resolver


Namepython-env-resolver JSON
Version 0.1.0 PyPI version JSON
download
home_pageNone
SummaryType-safe environment variable handling for Python with Pydantic
upload_time2025-10-08 22:56:35
maintainerNone
docs_urlNone
authorNone
requires_python>=3.10
licenseMIT
keywords config configuration dotenv env environment pydantic secrets settings validation
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # python-env-resolver

Type-safe environment configuration for Python services, powered by Pydantic. Model the shape of your settings once, enforce policies centrally, and keep secrets fresh without burning latency.

```bash
pip install python-env-resolver
# or
uv add python-env-resolver
```

## Why teams reach for it

- Strong typing and validation with your Pydantic models.
- Resolver pipeline that merges `os.environ`, `.env`, cloud stores, or any async source.
- Built-in policies to lock down where secrets may come from, especially in production.
- Audit trail for compliance or debugging misconfigured environments.
- Smart TTL caching with stale-while-revalidate so background refreshes never block callers.

## Quickstart

### Async

```python
from pydantic import BaseModel, HttpUrl
from python_env_resolver import resolve

class AppConfig(BaseModel):
    port: int = 3000
    database_url: HttpUrl
    debug: bool = False
    api_key: str | None = None

async def main():
    config = await resolve(AppConfig)
    print(config.database_url)
```

### Sync

```python
from pydantic import BaseModel, HttpUrl
from python_env_resolver import resolve_sync

class AppConfig(BaseModel):
    port: int = 3000
    database_url: HttpUrl
    debug: bool = False
    api_key: str | None = None

config = resolve_sync(AppConfig)
print(config.database_url)
```

Both APIs share identical behavior. By default they read from `process_env()` (plain `os.environ`). Pass additional resolvers when you need more.

## Resolvers and merge strategy

```python
from python_env_resolver import dotenv, process_env, resolve, resolve_sync, ResolveOptions

async def load_config():
    return await resolve(
        AppConfig,
        resolvers=[dotenv(".env"), process_env()],
        options=ResolveOptions(priority="last")  # later resolvers override earlier ones
    )

# Or synchronously:
config = resolve_sync(
    AppConfig,
    resolvers=[dotenv(".env"), process_env()],
    options=ResolveOptions(priority="last"),
)
```

Resolvers are async callables that return `dict[str, str]`. Custom sources only need to implement a `.load()` coroutine and a `name`.

## Custom resolvers

Build your own resolver to load from any source—databases, HTTP APIs, vault systems, or custom file formats. The interface is minimal and composable:

```python
from python_env_resolver import BaseResolver

class ConsulResolver(BaseResolver):
    def __init__(self, host: str, prefix: str):
        super().__init__(name="consul")
        self.host = host
        self.prefix = prefix
    
    async def load(self) -> dict[str, str]:
        # Your logic to fetch key-value pairs from Consul
        async with httpx.AsyncClient() as client:
            response = await client.get(f"{self.host}/v1/kv/{self.prefix}?recurse=true")
            data = response.json()
            return {item["Key"]: item["Value"] for item in data}

# Use it like any built-in resolver
config = await resolve(
    AppConfig,
    resolvers=[ConsulResolver("http://localhost:8500", "app/config"), process_env()],
)
```

You can also create simple function-based resolvers for one-off needs:

```python
from python_env_resolver import create_resolver

async def load_from_api():
    # Fetch environment variables from your API
    return {"API_KEY": "secret123", "REGION": "us-west-2"}

api_resolver = create_resolver("my-api", load_from_api)
config = await resolve(AppConfig, resolvers=[api_resolver])
```

## Custom validators

Compose Pydantic validators with built-in utilities or create your own for domain-specific constraints:

```python
from pydantic import BaseModel, field_validator
from python_env_resolver import resolve, validate_url, validate_port

class AppConfig(BaseModel):
    api_url: str
    port: int
    redis_host: str
    
    @field_validator("api_url")
    @classmethod
    def check_api_url(cls, v: str) -> str:
        # Use built-in validator
        return validate_url(v, require_https=True)
    
    @field_validator("port")
    @classmethod
    def check_port(cls, v: int) -> int:
        # Compose with built-in validator
        return validate_port(v, min_port=1024, max_port=65535)
    
    @field_validator("redis_host")
    @classmethod
    def check_redis_host(cls, v: str) -> str:
        # Custom validation logic
        if not v.endswith(".cache.amazonaws.com") and v != "localhost":
            raise ValueError("Redis must be ElastiCache or localhost")
        return v

config = await resolve(AppConfig)
```

Mix and match validators for ultimate flexibility:

```python
from python_env_resolver import validate_email, validate_number_range

class ServiceConfig(BaseModel):
    admin_email: str
    max_connections: int
    timeout_seconds: float
    
    @field_validator("admin_email")
    @classmethod
    def check_email(cls, v: str) -> str:
        return validate_email(v)
    
    @field_validator("max_connections")
    @classmethod
    def check_max_connections(cls, v: int) -> int:
        return validate_number_range(v, min_val=1, max_val=1000)
    
    @field_validator("timeout_seconds")
    @classmethod
    def check_timeout(cls, v: float) -> float:
        if v <= 0 or v > 300:
            raise ValueError("Timeout must be between 0 and 300 seconds")
        return v
```

## Keeping secrets fast with caching

Wrap any resolver with `cached()` to enable TTL caching, including stale-while-revalidate. Fresh data is served instantly while a background refresh updates the cache, even when multiple callers arrive simultaneously.

```python
from python_env_resolver import CacheOptions, TTL, cached
from python_env_resolver_aws import aws_secrets  # example integration

secrets_resolver = cached(
    aws_secrets(secret_id="prod/db"),
    CacheOptions(
        ttl=TTL.minutes5,
        max_age=TTL.hour,
        stale_while_revalidate=True,
    ),
)

async def load_config():
    return await resolve(AppConfig, resolvers=[secrets_resolver])
```

Prefer the helper when you like opinionated defaults tuned for AWS Secrets Manager:

```python
from python_env_resolver import aws_cache

secrets_resolver = cached(aws_secrets(secret_id="prod/db"), aws_cache())
```

## Security policies

Keep risky sources out of production or require critical settings to originate from a specific resolver.

```python
from python_env_resolver import PolicyOptions, ResolveOptions

options = ResolveOptions(
    policies=PolicyOptions(
        allow_dotenv_in_production=["LOG_LEVEL"],  # only allow this key from .env
        enforce_allowed_sources={
            "DATABASE_URL": ["aws-secrets"],
        },
    )
)

config = await resolve(AppConfig, options=options)
```

Policy violations surface as clear `ValueError`s before your application boots.

## Audit trail

Enable auditing to record where every value came from—helpful in staging and production alike.

```python
from python_env_resolver import ResolveOptions, get_audit_log

config = await resolve(
    AppConfig,
    options=ResolveOptions(enable_audit=True),
)

for event in get_audit_log():
    print(event.type, event.source, event.details)
```

## Safer boot paths

Need a non-raising API for CLI tools or tests? Use `safe_resolve`:

```python
from python_env_resolver import safe_resolve, safe_resolve_sync

result = await safe_resolve(AppConfig)
if result.success:
    config = result.data
else:
    raise RuntimeError(result.error)

# Synchronous helper
sync_result = safe_resolve_sync(AppConfig)
if not sync_result.success:
    raise RuntimeError(sync_result.error)
```

## Typed helpers

The package ships with focused validators and utilities for common scenarios:

- `TTL` constants for readable cache configuration (`TTL.minutes5`, `TTL.hour`, etc.).
- Validators: `validate_url`, `validate_port`, `validate_email`, `validate_number_range`.
- Resolver factories: `process_env()`, `dotenv(path)`, `create_resolver()`.
- Base classes: `BaseResolver` for building custom resolvers with full type safety.

Use them directly in your Pydantic models or compose your own domain-specific abstractions on top. The library is designed for maximum flexibility—every component is composable and extensible.

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

### Development Setup

```bash
# Clone the repository
git clone https://github.com/jagreehal/python-env-resolver.git
cd python-env-resolver

# Install dependencies
uv pip install -e ".[dev]"

# Run tests
pytest

# Type check
mypy src

# Lint
ruff check .
```

### Publishing

See [PUBLISHING.md](PUBLISHING.md) for detailed instructions on publishing to PyPI.

## License

MIT License - see [LICENSE](LICENSE) file for details.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "python-env-resolver",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "config, configuration, dotenv, env, environment, pydantic, secrets, settings, validation",
    "author": null,
    "author_email": "Jag Reehal <jag@jagreehal.com>",
    "download_url": "https://files.pythonhosted.org/packages/00/11/1ec879b87bd7d3972bec4b0b5d8df73deb7edadda914062aa03b99f04f8a/python_env_resolver-0.1.0.tar.gz",
    "platform": null,
    "description": "# python-env-resolver\n\nType-safe environment configuration for Python services, powered by Pydantic. Model the shape of your settings once, enforce policies centrally, and keep secrets fresh without burning latency.\n\n```bash\npip install python-env-resolver\n# or\nuv add python-env-resolver\n```\n\n## Why teams reach for it\n\n- Strong typing and validation with your Pydantic models.\n- Resolver pipeline that merges `os.environ`, `.env`, cloud stores, or any async source.\n- Built-in policies to lock down where secrets may come from, especially in production.\n- Audit trail for compliance or debugging misconfigured environments.\n- Smart TTL caching with stale-while-revalidate so background refreshes never block callers.\n\n## Quickstart\n\n### Async\n\n```python\nfrom pydantic import BaseModel, HttpUrl\nfrom python_env_resolver import resolve\n\nclass AppConfig(BaseModel):\n    port: int = 3000\n    database_url: HttpUrl\n    debug: bool = False\n    api_key: str | None = None\n\nasync def main():\n    config = await resolve(AppConfig)\n    print(config.database_url)\n```\n\n### Sync\n\n```python\nfrom pydantic import BaseModel, HttpUrl\nfrom python_env_resolver import resolve_sync\n\nclass AppConfig(BaseModel):\n    port: int = 3000\n    database_url: HttpUrl\n    debug: bool = False\n    api_key: str | None = None\n\nconfig = resolve_sync(AppConfig)\nprint(config.database_url)\n```\n\nBoth APIs share identical behavior. By default they read from `process_env()` (plain `os.environ`). Pass additional resolvers when you need more.\n\n## Resolvers and merge strategy\n\n```python\nfrom python_env_resolver import dotenv, process_env, resolve, resolve_sync, ResolveOptions\n\nasync def load_config():\n    return await resolve(\n        AppConfig,\n        resolvers=[dotenv(\".env\"), process_env()],\n        options=ResolveOptions(priority=\"last\")  # later resolvers override earlier ones\n    )\n\n# Or synchronously:\nconfig = resolve_sync(\n    AppConfig,\n    resolvers=[dotenv(\".env\"), process_env()],\n    options=ResolveOptions(priority=\"last\"),\n)\n```\n\nResolvers are async callables that return `dict[str, str]`. Custom sources only need to implement a `.load()` coroutine and a `name`.\n\n## Custom resolvers\n\nBuild your own resolver to load from any source\u2014databases, HTTP APIs, vault systems, or custom file formats. The interface is minimal and composable:\n\n```python\nfrom python_env_resolver import BaseResolver\n\nclass ConsulResolver(BaseResolver):\n    def __init__(self, host: str, prefix: str):\n        super().__init__(name=\"consul\")\n        self.host = host\n        self.prefix = prefix\n    \n    async def load(self) -> dict[str, str]:\n        # Your logic to fetch key-value pairs from Consul\n        async with httpx.AsyncClient() as client:\n            response = await client.get(f\"{self.host}/v1/kv/{self.prefix}?recurse=true\")\n            data = response.json()\n            return {item[\"Key\"]: item[\"Value\"] for item in data}\n\n# Use it like any built-in resolver\nconfig = await resolve(\n    AppConfig,\n    resolvers=[ConsulResolver(\"http://localhost:8500\", \"app/config\"), process_env()],\n)\n```\n\nYou can also create simple function-based resolvers for one-off needs:\n\n```python\nfrom python_env_resolver import create_resolver\n\nasync def load_from_api():\n    # Fetch environment variables from your API\n    return {\"API_KEY\": \"secret123\", \"REGION\": \"us-west-2\"}\n\napi_resolver = create_resolver(\"my-api\", load_from_api)\nconfig = await resolve(AppConfig, resolvers=[api_resolver])\n```\n\n## Custom validators\n\nCompose Pydantic validators with built-in utilities or create your own for domain-specific constraints:\n\n```python\nfrom pydantic import BaseModel, field_validator\nfrom python_env_resolver import resolve, validate_url, validate_port\n\nclass AppConfig(BaseModel):\n    api_url: str\n    port: int\n    redis_host: str\n    \n    @field_validator(\"api_url\")\n    @classmethod\n    def check_api_url(cls, v: str) -> str:\n        # Use built-in validator\n        return validate_url(v, require_https=True)\n    \n    @field_validator(\"port\")\n    @classmethod\n    def check_port(cls, v: int) -> int:\n        # Compose with built-in validator\n        return validate_port(v, min_port=1024, max_port=65535)\n    \n    @field_validator(\"redis_host\")\n    @classmethod\n    def check_redis_host(cls, v: str) -> str:\n        # Custom validation logic\n        if not v.endswith(\".cache.amazonaws.com\") and v != \"localhost\":\n            raise ValueError(\"Redis must be ElastiCache or localhost\")\n        return v\n\nconfig = await resolve(AppConfig)\n```\n\nMix and match validators for ultimate flexibility:\n\n```python\nfrom python_env_resolver import validate_email, validate_number_range\n\nclass ServiceConfig(BaseModel):\n    admin_email: str\n    max_connections: int\n    timeout_seconds: float\n    \n    @field_validator(\"admin_email\")\n    @classmethod\n    def check_email(cls, v: str) -> str:\n        return validate_email(v)\n    \n    @field_validator(\"max_connections\")\n    @classmethod\n    def check_max_connections(cls, v: int) -> int:\n        return validate_number_range(v, min_val=1, max_val=1000)\n    \n    @field_validator(\"timeout_seconds\")\n    @classmethod\n    def check_timeout(cls, v: float) -> float:\n        if v <= 0 or v > 300:\n            raise ValueError(\"Timeout must be between 0 and 300 seconds\")\n        return v\n```\n\n## Keeping secrets fast with caching\n\nWrap any resolver with `cached()` to enable TTL caching, including stale-while-revalidate. Fresh data is served instantly while a background refresh updates the cache, even when multiple callers arrive simultaneously.\n\n```python\nfrom python_env_resolver import CacheOptions, TTL, cached\nfrom python_env_resolver_aws import aws_secrets  # example integration\n\nsecrets_resolver = cached(\n    aws_secrets(secret_id=\"prod/db\"),\n    CacheOptions(\n        ttl=TTL.minutes5,\n        max_age=TTL.hour,\n        stale_while_revalidate=True,\n    ),\n)\n\nasync def load_config():\n    return await resolve(AppConfig, resolvers=[secrets_resolver])\n```\n\nPrefer the helper when you like opinionated defaults tuned for AWS Secrets Manager:\n\n```python\nfrom python_env_resolver import aws_cache\n\nsecrets_resolver = cached(aws_secrets(secret_id=\"prod/db\"), aws_cache())\n```\n\n## Security policies\n\nKeep risky sources out of production or require critical settings to originate from a specific resolver.\n\n```python\nfrom python_env_resolver import PolicyOptions, ResolveOptions\n\noptions = ResolveOptions(\n    policies=PolicyOptions(\n        allow_dotenv_in_production=[\"LOG_LEVEL\"],  # only allow this key from .env\n        enforce_allowed_sources={\n            \"DATABASE_URL\": [\"aws-secrets\"],\n        },\n    )\n)\n\nconfig = await resolve(AppConfig, options=options)\n```\n\nPolicy violations surface as clear `ValueError`s before your application boots.\n\n## Audit trail\n\nEnable auditing to record where every value came from\u2014helpful in staging and production alike.\n\n```python\nfrom python_env_resolver import ResolveOptions, get_audit_log\n\nconfig = await resolve(\n    AppConfig,\n    options=ResolveOptions(enable_audit=True),\n)\n\nfor event in get_audit_log():\n    print(event.type, event.source, event.details)\n```\n\n## Safer boot paths\n\nNeed a non-raising API for CLI tools or tests? Use `safe_resolve`:\n\n```python\nfrom python_env_resolver import safe_resolve, safe_resolve_sync\n\nresult = await safe_resolve(AppConfig)\nif result.success:\n    config = result.data\nelse:\n    raise RuntimeError(result.error)\n\n# Synchronous helper\nsync_result = safe_resolve_sync(AppConfig)\nif not sync_result.success:\n    raise RuntimeError(sync_result.error)\n```\n\n## Typed helpers\n\nThe package ships with focused validators and utilities for common scenarios:\n\n- `TTL` constants for readable cache configuration (`TTL.minutes5`, `TTL.hour`, etc.).\n- Validators: `validate_url`, `validate_port`, `validate_email`, `validate_number_range`.\n- Resolver factories: `process_env()`, `dotenv(path)`, `create_resolver()`.\n- Base classes: `BaseResolver` for building custom resolvers with full type safety.\n\nUse them directly in your Pydantic models or compose your own domain-specific abstractions on top. The library is designed for maximum flexibility\u2014every component is composable and extensible.\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.\n\n### Development Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/jagreehal/python-env-resolver.git\ncd python-env-resolver\n\n# Install dependencies\nuv pip install -e \".[dev]\"\n\n# Run tests\npytest\n\n# Type check\nmypy src\n\n# Lint\nruff check .\n```\n\n### Publishing\n\nSee [PUBLISHING.md](PUBLISHING.md) for detailed instructions on publishing to PyPI.\n\n## License\n\nMIT License - see [LICENSE](LICENSE) file for details.\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Type-safe environment variable handling for Python with Pydantic",
    "version": "0.1.0",
    "project_urls": {
        "Changelog": "https://github.com/jagreehal/python-env-resolver/releases",
        "Documentation": "https://github.com/jagreehal/python-env-resolver#readme",
        "Homepage": "https://github.com/jagreehal/python-env-resolver",
        "Issues": "https://github.com/jagreehal/python-env-resolver/issues",
        "Repository": "https://github.com/jagreehal/python-env-resolver"
    },
    "split_keywords": [
        "config",
        " configuration",
        " dotenv",
        " env",
        " environment",
        " pydantic",
        " secrets",
        " settings",
        " validation"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "6e9e6b6356532651672e7cd8ded7829068210e5b4d57cdad07a77183a40d1e46",
                "md5": "159abe3c1547e36a12a8f373b7a28e8a",
                "sha256": "64561e228ded56a443e13f86d90555e9a4ad8568110487f5de2e95cc2194232b"
            },
            "downloads": -1,
            "filename": "python_env_resolver-0.1.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "159abe3c1547e36a12a8f373b7a28e8a",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 15990,
            "upload_time": "2025-10-08T22:56:34",
            "upload_time_iso_8601": "2025-10-08T22:56:34.548053Z",
            "url": "https://files.pythonhosted.org/packages/6e/9e/6b6356532651672e7cd8ded7829068210e5b4d57cdad07a77183a40d1e46/python_env_resolver-0.1.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "00111ec879b87bd7d3972bec4b0b5d8df73deb7edadda914062aa03b99f04f8a",
                "md5": "ba19ac85b3efa6d69acccea8d2facca7",
                "sha256": "250bc6e719c45160c48d2f450ca50ee77412c442913d2c94fdf2a7414b40a986"
            },
            "downloads": -1,
            "filename": "python_env_resolver-0.1.0.tar.gz",
            "has_sig": false,
            "md5_digest": "ba19ac85b3efa6d69acccea8d2facca7",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 40354,
            "upload_time": "2025-10-08T22:56:35",
            "upload_time_iso_8601": "2025-10-08T22:56:35.892980Z",
            "url": "https://files.pythonhosted.org/packages/00/11/1ec879b87bd7d3972bec4b0b5d8df73deb7edadda914062aa03b99f04f8a/python_env_resolver-0.1.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-08 22:56:35",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "jagreehal",
    "github_project": "python-env-resolver",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "python-env-resolver"
}
        
Elapsed time: 0.66898s