diff --git a/.travis.yml b/.travis.yml index 32e5877..f2c8029 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: python python: + - "3.13" - "3.12" - "3.11" - "3.10" diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cbdad5..71251bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v1.2.5 (2025-06-05) + +### Fixes + +- add python 3.13 as list of supported versions + ## v1.2.4 (2025-05-07) ### Fixes diff --git a/README.md b/README.md index 2e8f6d1..a234e42 100644 --- a/README.md +++ b/README.md @@ -1,177 +1,195 @@ - # runenv -Manage your application’s settings with `runenv`, using the [12-factor](http://12factor.net/) principles. This library provides both a CLI tool and a Python API to simplify the management of environment variables in your projects. +Manage application settings with ease using `runenv`, a lightweight tool inspired by [The Twelve-Factor App](https://12factor.net/config) methodology for configuration through environment variables. -| Section | Details | -|----------|---------| -| CI/CD | [![CI - Test](https://github.com/onjin/runenv/actions/workflows/test.yml/badge.svg)](https://github.com/onjin/runenv/actions/workflows/test.yml) | -| Package | [![PyPI - Version](https://img.shields.io/pypi/v/runenv.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/runenv/) | -| Downloads | [![PyPI - Downloads](https://img.shields.io/pypi/dm/runenv.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold)](https://pypi.org/project/runenv/) | -| Python Version | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/runenv.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/runenv/) | -| Meta | [![Linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Code Style - Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Types - MyPy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/python/mypy) | -| License | [![License - MIT](https://img.shields.io/badge/license-MIT-9400d3.svg)](https://spdx.org/licenses/) | -| Changes | [CHANGELOG.md](CHANGELOG.md) | +`runenv` provides: +- A CLI for language-agnostic `.env` profile execution +- A Python API for programmatic `.env` loading +> “Store config in the environment” — [12factor.net/config](https://12factor.net/config) + +| Section | Status | +|----------|--------| +| CI/CD | [![CI - Test](https://github.com/onjin/runenv/actions/workflows/test.yml/badge.svg)](https://github.com/onjin/runenv/actions/workflows/test.yml) | +| PyPI | [![PyPI - Version](https://img.shields.io/pypi/v/runenv.svg?logo=pypi&label=PyPI)](https://pypi.org/project/runenv/) [![Downloads](https://img.shields.io/pypi/dm/runenv.svg?color=blue)](https://pypi.org/project/runenv/) | +| Python | [![Python Versions](https://img.shields.io/pypi/pyversions/runenv.svg?logo=python&label=Python)](https://pypi.org/project/runenv/) | +| Style | [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Mypy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/python/mypy) | +| License | [![License - MIT](https://img.shields.io/badge/license-MIT-9400d3.svg)](https://spdx.org/licenses/) | +| Docs | [CHANGELOG.md](CHANGELOG.md) | --- ## Table of Contents -- [Features at a Glance](#features-at-a-glance) -- [Getting Started](#getting-started) +- [Key Features](#key-features) +- [Quick Start](#quick-start) - [Installation](#installation) - - [Quick CLI Usage](#quick-cli-usage) - - [Python API Overview](#python-api-overview) -- [In-Depth Usage and Examples](#in-depth-usage-and-examples) - - [Using the CLI Tool](#using-the-cli-tool) - - [Python API Details](#python-api-details) - - [Framework Integration](#framework-integration) -- [Example `.env` File](#example-env-file) -- [Similar Projects](#similar-projects) + - [CLI Usage](#cli-usage) + - [Python API](#python-api) +- [Multiple Profiles](#multiple-profiles) +- [Framework Integrations](#framework-integrations) +- [Sample `.env` File](#sample-env-file) +- [Similar Tools](#similar-tools) + +--- -## Features at a Glance +## Key Features -- **CLI Tool**: Run programs with customized environment variables from a `.env` file. -- **Python API**: Load and manage environment variables programmatically. -- **Integration**: Easily integrate with frameworks like Django and Flask. +- 🚀 **CLI-First**: Use `.env` files across any language or platform. +- 🐍 **Python-native API**: Load and transform environment settings inside Python. +- ⚙️ **Multiple Profiles**: Switch easily between `.env.dev`, `.env.prod`, etc. +- 🧩 **Framework-Friendly**: Works well with Django, Flask, FastAPI, and more. --- -## Getting Started +## Quick Start ### Installation -To install `runenv` along with its CLI tool, run: - -```console +```bash pip install runenv ``` -### Quick CLI Usage +### CLI Usage -1. Create a `.env` file in your project’s root directory: +Run any command with a specified environment: -The `.env` file can contain simple key-value pairs, comment lines, and inline comments: +```bash +runenv .env.dev python manage.py runserver +runenv .env.prod uvicorn app:app --host 0.0.0.0 +``` -```ini -# Base settings -BASE_URL=http://127.0.0.1:8000 -DATABASE_URI=postgres://postgres:password@localhost/dbname - -# Email configuration -EMAIL_HOST=smtp.example.com -EMAIL_PORT=587 # Port for SMTP -EMAIL_USER="user@example.com" -EMAIL_PASSWORD='password' -EMAIL_USE_TLS=1 - -# Reusing variables -EMAIL_FROM=user@${EMAIL_HOST} +View options: + +```bash +runenv --help ``` -- Variables are set in `KEY=VALUE` pairs. -- Use `#` for comments. -- Inline comments are also supported after a `#`. +Key CLI features: +- `--prefix`, `--strip-prefix`: Use selective environments +- `--dry-run`: Inspect loaded environment +- `-v`: Verbosity control -2. Run a command with the environment loaded from the `.env` file: +--- - ```console - runenv .env ./your_command - ``` +## Python API -### Python API Overview +### Load `.env` into `os.environ` -You can load environment variables directly in Python: +> **Note**: The `load_env` will not parse env_file if the `runenv` CLI was used, unless you `force=True` it. ```python from runenv import load_env -# Load variables from the specified .env file -load_env(".env") +load_env() # loads .env +load_env( + env_file=".env.dev", # file to load + prefix='APP_', # load only APP_.* variables from file + strip_prefix=True, # strip ^ prefix when loading variables + force=True, # load env_file even if the `runvenv` CLI was used + search_parent=1 # look for env_file in current dir and its parent dir +) ``` -## In-Depth Usage and Examples +### Read `.env` as a dictionary + +```python +from runenv import create_env + +config = create_env() # parse .env content into dictionary +config = create_env( + env_file=".env.dev", # file to load + prefix='APP_', # parse only APP_.* variables from file + strip_prefix=True, # strip ^ prefix when parsing variables +) +print(config) +``` + +Options include: +- Filtering by prefix +- Automatic prefix stripping +- Searching parent directories + +--- -### Using the CLI Tool +## Multiple Profiles -The `runenv` CLI provides flexibility to run any command with custom environment settings: +Use separate `.env` files per environment: -```console -runenv .env.development ./manage.py runserver +```bash +runenv .env.dev flask run +runenv .env.staging python main.py +runenv .env.production uvicorn app.main:app ``` -Full help and options: -```console -runenv --help -usage: runenv [-h] [-V] [-v {1,2,3}] [-p PREFIX] [-s] [--dry-run] env_file command - -Run program with given environment file loaded - -positional arguments: - env_file Environment file to load - command Command to run with loaded environment - -options: - -h, --help show this help message and exit - -V, --version show program's version number and exit - -v {1,2,3}, --verbosity {1,2,3} - verbosity level, 1 - (ERROR, default), 2 - (INFO) or 3 - (DEBUG) - -p PREFIX, --prefix PREFIX - Load only variables with given prefix - -s, --strip-prefix Strip prefix given with --prefix from environment variables names - --dry-run Return parsed .env instead of running command +Recommended structure: +``` +.env.dev +.env.test +.env.staging +.env.production ``` -### Python API Details +--- -#### `load_env` +## Framework Integrations -Load variables into the environment: +> **Note**: If you're using `runenv .env [./manage.py, ...]` CLI then you do not need change your code. Use these integrations only if you're using Python API. + +### Django ```python -load_env(env_file=".env", prefix="DJANGO_", strip_prefix=True, force=False, search_parent=0) +# manage.py or wsgi.py +from runenv import load_env +load_env(".env") ``` -**Parameters:** +### Flask -- `env_file` (str, optional): The environment file to read from (default is `.env`). -- `prefix` (str, optional): Load only variables that start with this prefix. -- `strip_prefix` (bool, optional): If True, removes the prefix from variable names when loaded (default is True). -- `force` (bool, optional): Force loading the `.env` file again even if already loaded by `runenv` CLI (default is False). -- `search_parent` (int, optional): Number of parent directories to search for `.env` file (default is 0). +```python +from flask import Flask +from runenv import load_env -#### `create_env` +load_env(".env") +app = Flask(__name__) +``` -Parse `.env` contents into a dictionary without modifying the environment: +### FastAPI ```python -env_vars = create_env(env_file=".env", prefix="APP_", strip_prefix=True) -print(env_vars) +from fastapi import FastAPI +from runenv import load_env + +load_env(".env") +app = FastAPI() ``` -**Parameters:** +--- -- `env_file` (str, optional): The environment file to read from (default is `.env`). -- `prefix` (str, optional): Load only variables that start with this prefix. -- `strip_prefix` (bool, optional): If True, removes the prefix from variable names when loaded (default is True). +## Sample `.env` File -### Framework Integration +```ini +# Basic +DEBUG=1 +PORT=8000 -Easily integrate `runenv` with web frameworks: +# Nested variable +HOST=localhost +URL=http://${HOST}:${PORT} -```python -# In Django's manage.py or Flask's app setup -from runenv import load_env -load_env(".env") +# Quotes and comments +EMAIL="admin@example.com" # Inline comment +SECRET='s3cr3t' ``` +--- -## Similar Projects +## Similar Tools -- [envdir](https://github.com/jezdez/envdir): Run programs with a modified environment based on files in a directory. -- [python-dotenv](https://github.com/theskumar/python-dotenv): Reads key-value pairs from `.env` files and adds them to the environment. +- [python-dotenv](https://github.com/theskumar/python-dotenv) – Python-focused, lacks CLI tool +- [envdir](https://github.com/jezdez/envdir) – Directory-based env manager +- [dotenv-linter](https://github.com/dotenv-linter/dotenv-linter) – Linter for `.env` files --- -With `runenv`, managing environment variables becomes simpler and more consistent, making it easier to develop and deploy applications across different environments. +With `runenv`, you get portable, scalable, and explicit configuration management that aligns with modern deployment standards. Ideal for CLI usage, Python projects, and multi-environment pipelines. diff --git a/pyproject.toml b/pyproject.toml index 5387477..12b8552 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "runenv" description = '' dynamic = ["version"] readme = "README.md" -requires-python = ">=2.7" +requires-python = ">=3.7" license = "MIT" keywords = [] authors = [{ name = "Marek Wywiał", email = "onjinx@gmail.com" }] @@ -16,6 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] @@ -35,9 +36,13 @@ devel-test = ["coverage[toml]", "pytest", "pytest-cov"] devel-docs = ["mkdocs", "mkdocs-material"] devel = [ "runenv[devel-docs,devel-types,devel-test]", - "ruff==0.7.1", + "ruff==0.11.12", "mkchangelog", ] +[dependency-groups] +dev = [ + "runenv[devel]", +] ################################################################################ ## Hatch Build Configuration @@ -75,7 +80,7 @@ installer = "uv" features = ["devel-test"] [[tool.hatch.envs.test.matrix]] -python = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] +python = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] ################################################################################ ## PyTest Configuration @@ -144,8 +149,6 @@ ignore = [ "D200", # Require single line docstrings to be on one line. "D203", # Require blank line before class docstring "D212", # Multi-line docstring summary must start at the first line - "ANN101", # `self` must be typed - "ANN102", # `cls` must be typed "FIX002", # Forbid TODO in comments "TD002", # Assign someone to 'TODO' comments @@ -192,3 +195,6 @@ docstring-code-format = true [tool.ruff.lint.per-file-ignores] # Tests can use magic values, assertions, and relative imports "tests/**/*" = ["PLR2004", "S101", "TID252"] + +[tool.basedpyright] +pythonVersion = "3.7" diff --git a/src/runenv/__about__.py b/src/runenv/__about__.py index fab066c..ddea128 100644 --- a/src/runenv/__about__.py +++ b/src/runenv/__about__.py @@ -3,4 +3,4 @@ # SPDX-License-Identifier: MIT __author__ = "Marek Wywiał" __email__ = "onjinx@gmail.com" -__version__ = "1.2.4" +__version__ = "1.2.5" diff --git a/src/runenv/__init__.py b/src/runenv/__init__.py index 7bfcd95..79a8160 100644 --- a/src/runenv/__init__.py +++ b/src/runenv/__init__.py @@ -130,7 +130,7 @@ def create_env( prefix: Union[str, None] = None, strip_prefix: bool = True, # noqa: FBT001,FBT002 ) -> Dict[str, str]: - """Create environ dictionary from current os.environ and variables got from given `env_file`.""" + """Create environ dictionary from current variables got from given `env_file`.""" environ: Dict[str, str] = {} with open(env_file) as f: for raw_line in f: diff --git a/src/runenv/py.typed b/src/runenv/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/env.test b/tests/env.test index c0c58ef..023c68c 100644 --- a/tests/env.test +++ b/tests/env.test @@ -9,6 +9,7 @@ DOUBLE_QUOTE="so'me" DOUBLE_QUOTE_WITH_COMMENT="so'me either" # comment QUOTED_WITH_HASH='some#one' DOUBLE_QUOTED_WITH_HASH="some#one" +FROM_ENV="MAYBE-${ALREADY_SET}" # COMMENT diff --git a/tests/test_runenv.py b/tests/test_runenv.py index 8fb1802..5822dfe 100755 --- a/tests/test_runenv.py +++ b/tests/test_runenv.py @@ -1,5 +1,4 @@ #!/usr/bin/env python - """ test_runenv. ---------------------------------- @@ -58,6 +57,8 @@ def tearDown(self) -> None: del os.environ[k] def test_create_env(self) -> None: + os.environ["ALREADY_SET"] = "YES" + environ = create_env(self.env_file) assert environ.get("VARIABLED") == "some_lazy_variable_12" assert environ.get("STRING") == "some string with spaces" @@ -75,6 +76,11 @@ def test_create_env(self) -> None: assert "COMMENTED" not in environ assert "# COMMENTED" not in environ + # external variable is not visible + assert environ.get("ALREADY_SET", None) is None + # but is loaded into our interpolation + assert environ.get("FROM_ENV") == "MAYBE-YES" + @pytest.mark.skipif( "linux" not in sys.platform, reason="works on linux",