From d4cbad1ae5b525415729e991c69da962d39e14ee Mon Sep 17 00:00:00 2001 From: David Nugent Date: Tue, 17 Dec 2024 17:28:54 +1100 Subject: [PATCH] EVX-3 Add encryption and stream support Introduced AES-256 encryption support for `.env` files, enabling better support for secure handling of sensitive data. Added `envcrypt` CLI utility to encrypt and decrypt `.env` files, with corresponding tests for validation. Updated the `Env` class and associated modules to support decryption, improved environment variable loading, and adjusted Python requirements to >=3.11. --- .envrc | 4 +- .gitignore | 2 +- .idea/envex.iml | 2 +- .idea/misc.xml | 5 +- .idea/ruff.xml | 4 + CHANGELOG.md | 15 ++ README.md | 45 +++--- envex/dot_env.py | 161 +++++++++++++++------ envex/env_crypto.py | 140 ++++++++++++++++++ envex/env_wrapper.py | 54 ++++++- pyproject.toml | 29 +++- scripts/envcrypt.py | 193 +++++++++++++++++++++++++ scripts/envsecrets.py | 8 +- scripts/example/example.sh | 2 +- scripts/lib/decr_action.py | 11 +- scripts/lib/log.py | 4 +- scripts/seal.py | 17 ++- tests/conftest.py | 69 +++++---- tests/test_env_crypto.py | 99 +++++++++++++ tests/test_env_hvac.py | 36 +++-- tests/test_env_secrets.py | 7 +- tests/test_load_env.py | 8 +- tests/test_wrapper.py | 81 +++++++++-- uv.lock | 281 +++++++++++++++++-------------------- 24 files changed, 979 insertions(+), 298 deletions(-) create mode 100644 envex/env_crypto.py create mode 100755 scripts/envcrypt.py create mode 100644 tests/test_env_crypto.py diff --git a/.envrc b/.envrc index e4b4a6a..c68fc65 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1,5 @@ -layout virtualenv .venv +layout uv +source_up dotenv_if_exists PATH_add scripts +export TEST_PASSWORD="jV4cl:aPx2D9s" diff --git a/.gitignore b/.gitignore index 9e218d4..27c9047 100644 --- a/.gitignore +++ b/.gitignore @@ -301,4 +301,4 @@ Session.vim .env requirements.txt -output.env +test.env* diff --git a/.idea/envex.iml b/.idea/envex.iml index fa73b3a..5710b03 100644 --- a/.idea/envex.iml +++ b/.idea/envex.iml @@ -2,7 +2,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 0dc1ab3..4291e66 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,8 @@ - + + + + diff --git a/.idea/ruff.xml b/.idea/ruff.xml index 98323b9..9d32810 100644 --- a/.idea/ruff.xml +++ b/.idea/ruff.xml @@ -1,6 +1,10 @@ + diff --git a/CHANGELOG.md b/CHANGELOG.md index 510bf9d..e395ecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # ChangeLog +### v4.0.0 + +- :warning: BREAKING CHANGE: kwargs passed to Env() are no longer added to the env if readenv=False. This is probably of no consequence as it is a (mis?)feature that rarely (if ever) was used. +- Bugfix: dicts passed in *args the Env() are correctly converted to str->str mappings. +- Feature: Env can now take BytesIO and StringIO objects in Env(*args). Since these are immediate objects, they are handled as priority variables, different to variables set via `.env` files in that they overwrite existing variables by default. Explicitly using the overwrite=False changes this behaviour. +- Warning: To provide support for different types of streams, environment files are now handled internally as bytes, however before evaluation they are converted via an encoding parameter that defaults to utf-8. +- Feature: Encrypted `.env` files (`.env.enc`) are now supported, meaing that you don't need to implement a Hashicorp Vault in order to avoid having plain text secrets on the filesystem, you can simply encrypt the `.env` file directly]. Use the `decrypt=True` parameter and provide - directly or indirectly - the encryption password used to derive the key: + + - `password=` + - `password=$` + - `password=/` +- If `decrypt=True` is used with a password (see previous item) `envex` will look for `.env.enc` (or more correctly `${DOTENV:-.env}.enc`) and use that if it exists. Regardless, encryped content is supported in both `.env` and `.env.enc` files, but the `.enc` file is used in preference. Encrypted files have a special initial 4-byte signature that distinguishes them from unencrypted files. +- A utility cli script `envcrypt` is provided to support both encryption and decryption. + Use `envcrypt -h` for usage. + ### v3.2.0 - upgrade dependencies (several) including vulnerability fixes diff --git a/README.md b/README.md index 5652ee2..8355b31 100644 --- a/README.md +++ b/README.md @@ -7,23 +7,21 @@ ## Overview -This module provides a convenient interface for handling the environment, and therefore configuration of any application -using 12factor.net principals removing many environment-specific variables and security sensitive information from -application code. - -An `Env` instance delivers a lot of functionality by providing a type-smart front-end to `os.environ`, -providing a superset of `os.environ` functionality, including setting default values. - -From version 2.0, this module also supports transparently fetching values from Hashicorp vault, -which reduces the need to store secrets in plain text on the filesystem. -This functionality is optional, activated automatically when the `hvac` module is installed, and connection and -authentication to Vault succeed. -Only get (no set) operations to Vault are supported. +This module provides a convenient interface for handling the environment, and therefore configuration of any application using 12factor.net principals removing many environment-specific variables and security-sensitive information from application code. + +An `Env` instance delivers a lot of functionality by providing a type-smart front-end to `os.environ`, providing a superset of `os.environ` functionality, including setting default values. + +`envex` supports AES-256 encrypted environment files, and if enabled by using the `decrypt=True` argument and providing the decryption password searches for `.env.enc` files first but falls back to `.env`. +This avoids having plain text files on the filesystem that contain sensitive information. +The provided `env_crypto` utility allows conversion between encrypted and non-encrypted formats. + +Alternatively, `envex` handles transparently fetching values from Hashicorp vault, reducing the need to store secrets in plain text on the filesystem and depending on the environment may present a more convenient way of managing application secrets. +Hashicorp vault functionality is optional, activated automatically when the `hvac` module is installed into the active virtual environment, and connection and authentication to Vault succeed. Values fetched from Vault are cached by default to reduce the overhead of the api call. -If this is of concern to security, caching can be disabled using the `enable_cache=False` parameter to Env. +If this is of concern to security, caching can be disabled using the`enable_cache=False` parameter to Env. -This module provides some features not supported by other dotenv handlers (python-dotenv, etc.) including recursive -expansion of template variables, which can be very useful for DRY. +This module provides man y features not supported by other dotenv handlers (python-dotenv, etc.) including recursive +expansion of template variables, supporting the don't-repeat-yourself (DRY) principle. ```python from envex import env @@ -148,11 +146,12 @@ The SecretsManager and Vault client leverage environment variables for their con This ensures a degree of transparency as it allows the client to use them but mitigates the need for the client code to be aware of their presence. A summary of these variables is in the following table: -| Variable | Description | -|-------------------|--------------------------------------------------------------------------| -| VAULT_ADDR | The URL of the vault server | -| VAULT_TOKEN | The vault token to use for authentication | -| VAULT_CACERT | The path to the CA certificate to use for TLS verification | -| VAULT_CAPATH | The path to a directory of CA certificates to use for TLS verification | -| VAULT_CLIENT_CERT | The path to the client certificate to use for TLS connection | -| VAULT_CLIENT_KEY | The path to the client key to use for TLS connection | + +| Variable | Description | +| ----------------- | ---------------------------------------------------------------------- | +| VAULT_ADDR | The URL of the vault server | +| VAULT_TOKEN | The vault token to use for authentication | +| VAULT_CACERT | The path to the CA certificate to use for TLS verification | +| VAULT_CAPATH | The path to a directory of CA certificates to use for TLS verification | +| VAULT_CLIENT_CERT | The path to the client certificate to use for TLS connection | +| VAULT_CLIENT_KEY | The path to the client key to use for TLS connection | diff --git a/envex/dot_env.py b/envex/dot_env.py index 6e23561..9c323a0 100644 --- a/envex/dot_env.py +++ b/envex/dot_env.py @@ -2,18 +2,26 @@ import os import sys import contextlib +from io import TextIOBase, BytesIO from pathlib import Path from string import Template -from typing import List, MutableMapping, Union +from typing import Dict, List, MutableMapping, Union, Optional, ContextManager, BinaryIO + +from .env_crypto import decrypt_data, DecryptError __all__ = ( "load_env", + "load_stream", "load_dotenv", # alias + "update_env", "unquote", ) + DEFAULT_ENVKEY = "DOTENV" DEFAULT_DOTENV = ".env" +ENCRYPTED_EXT = ".enc" +DEFAULT_ENCODING = "utf-8" def unquote(line, quotes="\"'"): @@ -22,6 +30,11 @@ def unquote(line, quotes="\"'"): return line +def update_env(env: MutableMapping[str, str], mapping: Dict): + for k, v in mapping.items(): + env[str(k)] = str(v) + + def _env_default( environ: MutableMapping[str, str], key: str, val: str, overwrite: bool = False ): @@ -38,26 +51,38 @@ def _env_export( def _env_files( - env_file: str, search_path: List[Path], parents: bool, errors: bool + env_file: str, search_path: List[Path], parents: bool, decrypt: bool, errors: bool ) -> List[str]: """expand env_file with the full search path, optionally parents as well""" - searched = [] + + def search_dotpath(base: Path, name: str): + _path = os.path.join(base, name) + return _path if os.access(_path, os.R_OK) else None + for path in search_path: path = path.resolve() if not path.is_dir(): path = path.parent searched.append(path) paths = [path] + list(path.parents) + # search a path and parents for sub_path in paths: - # stop at first found, or ... - # fail fast unless searching parents - env_path = os.path.join(sub_path, env_file) - if os.access(env_path, os.R_OK): + # if decryption is enabled, encrypted .env.enc files take priority + env_path = ( + search_dotpath(sub_path, env_file + ENCRYPTED_EXT) if decrypt else None + ) + if env_path: yield env_path - elif not parents: - break + else: + # but allow fallback to standard .env + env_path = search_dotpath(sub_path, env_file) + if env_path: + yield env_path + # quit search unless we are searching up + elif not parents: + break if errors: raise FileNotFoundError(f"{env_file} in {[s.as_posix() for s in searched]}") else: @@ -65,9 +90,9 @@ def _env_files( @contextlib.contextmanager -def open_env(path: Union[str, Path]): +def open_env(path: Union[str, Path]) -> ContextManager[BinaryIO]: """same as open, allow monkeypatch""" - fp = open(path, "r") + fp = open(path, "rb") try: yield fp finally: @@ -79,6 +104,41 @@ def open_env(path: Union[str, Path]): } +def _process_line(_lineno: int, string: str, errors: bool, _env_path: Path | None): + """process a single line""" + _func, _key, _val = _env_default, None, None + parts = string.split("=", 1) + if len(parts) == 2: + _key, _val = parts + elif len(parts) == 1: + _key = parts[0] + if _key: + words = _key.split(maxsplit=1) + if len(words) > 1: + command, _key = words + try: + _func = ENV_COMMANDS[command] + except KeyError: + if errors: + path = _env_path.as_posix() if _env_path else "stream" + print( + f"unknown command {command} {path}({_lineno})", + file=sys.stderr, + ) + return _func, unquote(_key), unquote(_val) + + +def _process_stream( + stream: BytesIO, environ, overwrite, errors, encoding=DEFAULT_ENCODING, env_path=None +): + for lineno, line in enumerate(stream.readlines(), start=1): + line = line.decode(encoding).strip() + if line and line[0] != "#": + func, key, val = _process_line(lineno, line, errors, env_path) + if func is not None: + func(environ, key, val, overwrite=overwrite) + + def _process_env( env_file: str, search_path: List[Path], @@ -87,6 +147,9 @@ def _process_env( parents: bool, errors: bool, working_dirs: bool, + decrypt: bool, + password: Optional[str] = None, + encoding: str = DEFAULT_ENCODING, ) -> MutableMapping[str, str]: """ search for any env_files in the given dir list and populate environ dict @@ -96,50 +159,32 @@ def _process_env( :param environ: environment to update :param overwrite: whether to overwrite existing values :param parents: whether to search upwards until a file is found + :param decrypt: whether to attempt decryption :param errors: whether to raise FileNotFoundError if the env_file is not found :param working_dirs: whether to add the env file's directory + :param encoding: text encoding """ - def process_line(_env_path: Path, _lineno: int, string: str): - """process a single line""" - _func, _key, _val = _env_default, None, None - parts = string.split("=", 1) - if len(parts) == 2: - _key, _val = parts - elif len(parts) == 1: - _key = parts[0] - if _key: - words = _key.split(maxsplit=1) - if len(words) > 1: - command, _key = words - try: - _func = ENV_COMMANDS[command] - except KeyError: - if errors: - print( - f"unknown command {command} {_env_path.as_posix()}({_lineno})", - file=sys.stderr, - ) - return _func, unquote(_key), unquote(_val) - files_not_found = [] files_found = False - for env_path in _env_files(env_file, search_path, parents, errors): + for env_path in _env_files(env_file, search_path, parents, decrypt, errors): # insert PWD as container of env file env_path = Path(env_path).resolve() if working_dirs: environ["PWD"] = str(env_path.parent) try: with open_env(env_path) as f: - lineno = 0 - for line in f.readlines(): - line = line.strip() - lineno += 1 - if line and line[0] != "#": - func, key, val = process_line(env_path, lineno, line) - if func is not None: - func(environ, key, val, overwrite=overwrite) - files_found = True + load_stream( + BytesIO(f.read()), + environ, + overwrite, + errors, + decrypt, + password, + encoding, + env_path, + ) + files_found = True except FileNotFoundError: files_not_found.append(env_path) if errors and not files_found and files_not_found: @@ -180,17 +225,23 @@ def load_env( update: bool = True, errors: bool = False, working_dirs: bool = True, + decrypt: bool = False, + password: str = None, + encoding: Optional[str] = DEFAULT_ENCODING, ) -> MutableMapping[str, str]: """ Loads one or more .env files with optional nesting, updating os.environ :param env_file: name of the environment file (.env or $ENV default) :param search_path: single or list of directories in order of precedence - str, bytes or Path + :param environ: environment mapping to process :param overwrite: whether to overwrite existing values :param parents: whether to search upwards until a file is found :param update: option to update os.environ, default=True :param errors: whether to raise FileNotFoundError if env_file not found :param working_dirs: whether to add the env file's directory - :param environ: environment mapping to process + :param decrypt: whether to support encrypted .env.enc + :param password: decryption password + :param encoding: text encoding (default utf-8) :returns the new environment """ if environ is None: @@ -229,10 +280,32 @@ def load_env( parents, errors, working_dirs, + decrypt, + password, + encoding, ) ) # optionally update the actual environment return _update_os_env(environ) if update else environ +def load_stream( + stream: Union[BytesIO, TextIOBase], + environ: MutableMapping[str, str] = None, + overwrite: bool = False, + errors: bool = False, + decrypt: bool = False, + password: Optional[str] = None, + encoding: Optional[str] = DEFAULT_ENCODING, + env_path: Optional[Path] = None, +): + if isinstance(stream, TextIOBase): + stream.seek(0) + stream = BytesIO(stream.read().encode(encoding)) + elif password and decrypt: + with contextlib.suppress(DecryptError): + stream = decrypt_data(stream, password) + _process_stream(stream, environ, overwrite, errors, encoding, env_path) + + load_dotenv = load_env diff --git a/envex/env_crypto.py b/envex/env_crypto.py new file mode 100644 index 0000000..68d9924 --- /dev/null +++ b/envex/env_crypto.py @@ -0,0 +1,140 @@ +""" +Block data encryption using +""" + +import logging +import secrets +from io import BytesIO, TextIOBase + +__all__ = ("encrypt_data", "decrypt_data", "EncryptError", "DecryptError") + +from typing import Union + +# Magic bytes to identify an encrypted files +MAGIC_BYTES = b"SECF" # "Secure Encrypted File" +ITERATIONS = 4800000 +AES_KEY_LENGTH = 32 # max bytes for AES256 + +logger = logging.getLogger(__file__) + + +class DecryptError(ValueError): + pass + + +class EncryptError(ValueError): + pass + + +try: + from Crypto.Cipher import AES + from Crypto.Hash import SHA256 + from Crypto.Protocol.KDF import PBKDF2 + + def _pad(data: bytes) -> bytes: + """ + Pad data to be a multiple of 16 bytes (AES block size) + """ + padding_length = 16 - (len(data) % 16) + padding = bytes([padding_length] * padding_length) + return data + padding + + def _unpad(data: bytes) -> bytes: + """ + Check and remove PKCS7 padding + """ + padding_length = data[-1] + if padding_length < 1 or padding_length > AES.block_size: + raise ValueError("Invalid padding length") + if data[-padding_length:] != bytes([padding_length]) * padding_length: + raise ValueError("Invalid padding bytes") + return data[:-padding_length] + + def generate_key_from_password( + password: str, salt: bytes = None + ) -> tuple[bytes, bytes]: + """ + Generate an AES key from a password using PBKDF2 + Returns the key and salt used + """ + if salt is None: + salt = secrets.token_bytes(16) + + key = PBKDF2( + password, + salt, + dkLen=AES_KEY_LENGTH, # AES-256 key size + count=ITERATIONS, # High iteration count for security + hmac_hash_module=SHA256, + ) + return key, salt + + def encrypt_data( + input_stream: Union[BytesIO, TextIOBase], password: str, encoding: str = "utf-8" + ) -> BytesIO: + """ + Encrypt a file using AES-256 in CBC mode with a password-derived key + """ + if not isinstance(input_stream, BytesIO): + input_stream.seek(0) + input_stream = BytesIO(input_stream.read().encode(encoding)) + first_bytes = input_stream.read(len(MAGIC_BYTES)) + if first_bytes == MAGIC_BYTES: + logger.debug("Attempted to encrypt an already encrypted stream") + raise EncryptError("This data is already encrypted") + input_stream.seek(0) + + if not password: + logger.debug("No or blank password provided") + raise EncryptError("No or blank password provided") + + key, salt = generate_key_from_password(password) + + # Generate random IV + iv = secrets.token_bytes(16) + + # Create cipher + cipher = AES.new(key, AES.MODE_CBC, iv) + + # Pad and encrypt the data + encrypted_data = cipher.encrypt(_pad(input_stream.getvalue())) + logger.debug(f"Encryption successful ({len(encrypted_data)} + 36 bytes)") + + # Write magic bytes, salt, IV, and encrypted data + return BytesIO(MAGIC_BYTES + salt + iv + encrypted_data) + + def decrypt_data(input_stream: BytesIO, password: str) -> BytesIO: + """ + Decrypt data that was encrypted using encrypt_data() + """ + # Read the magic bytes, salt, IV, and encrypted data + magic = input_stream.read(len(MAGIC_BYTES)) + if magic != MAGIC_BYTES: + logger.debug("Attempted to decrypt a non-encrypted stream") + raise DecryptError("This data does not look to be encrypted") + salt = input_stream.read(16) # salt + iv = input_stream.read(16) # IV + encrypted_data = input_stream.read() + + # Regenerate the key using the same password and salt + key, _ = generate_key_from_password(password, salt) + + # Create cipher + cipher = AES.new(key, AES.MODE_CBC, iv) + padded_decrypted_data = cipher.decrypt(encrypted_data) + # Decrypt and unpad the data + try: + decrypted_data = _unpad(padded_decrypted_data) + except ValueError as e: + raise DecryptError("Incorrect password or invalid data") from e + logger.debug(f"Decryption successful ({len(decrypted_data)} bytes)") + return BytesIO(decrypted_data) + + +except ImportError: + + def encrypt_data(_input_stream: BytesIO, _password: str) -> BytesIO: + raise EncryptError("Encryption not supported") + + def decrypt_data(_input_stream: BytesIO, _password: str) -> BytesIO: + raise DecryptError("Decryption not supported") diff --git a/envex/env_wrapper.py b/envex/env_wrapper.py index 7f01548..2b305b3 100644 --- a/envex/env_wrapper.py +++ b/envex/env_wrapper.py @@ -5,11 +5,13 @@ import contextlib import re +from pathlib import Path +from io import TextIOBase, BytesIO from typing import Any, List, MutableMapping, Type from envex.env_hvac import SecretsManager -from .dot_env import load_env, unquote +from .dot_env import load_env, unquote, load_stream, update_env class Env: @@ -44,6 +46,9 @@ def __init__( @param environ: dict | None default base environment (os.environ is default) @param exception: (optional) Exception class to raise on error (default=KeyError) @param readenv: read values from .env files (default=False) + @param decrypt: attempt decryption if password is used + @param encoding: text encoding + @param password: password to use for decryption - if readenv=True, the following additional args may be used @param env_file: str name of the environment file (.env or $ENV default) @param search_path: None | Union[List[str], List[Path]], str] path(s) to search for env_file @@ -64,12 +69,35 @@ def __init__( @param kwargs: (optional) environment variables to add/override """ self._env = self.os_env() if environ is None else environ - self._env.update(args) + + streams = [] + for arg in args: + if isinstance(arg, dict): + update_env(self._env, arg) + elif isinstance(arg, (BytesIO, TextIOBase)): + streams.append(arg) + + if "streams" in kwargs and isinstance(kwargs["streams"], (tuple, list)): + streams.extend(kwargs.pop("streams")) + + password = kwargs.get("password") + if kwargs.get("decrypt", False) and password: + if password[0] == "$": # use environment variable (pre-.env) + password = self._env.pop(password[1:], None) # also remove it + elif password[0] == "/": # read a file + pw_file = Path(password[1:]) + password = pw_file.read_text().rstrip() if pw_file.exists() else None + if not password: + kwargs["decrypt"] = False + else: + kwargs["password"] = password + + kwargs.setdefault("environ", self._env) if readenv: self.read_env(**kwargs) - else: - self._env.update({k: v for k, v in kwargs.items() if isinstance(v, str)}) + self.read_streams(*streams, **kwargs) self.env_source = self.env.get("ENVEX_SOURCE", "env") == "env" + self.exception = exception or self._EXCEPTION_CLS self.secret_manager = SecretsManager( url=url, token=token, @@ -82,7 +110,6 @@ def __init__( mount_point=mount_point, timeout=kwargs.get("timeout", None), ) - self.exception = exception or self._EXCEPTION_CLS @staticmethod def os_env(): @@ -99,11 +126,24 @@ def read_env(self, **kwargs): parents: bool update: bool errors: bool + decrypt: bool + password: str + encoding: str kwargs: MutableMapping[str, str] """ - kwargs.setdefault("environ", self._env) self._env = load_env(**kwargs) + def read_streams(self, *streams, **kwargs): + environ = kwargs["environ"] + # default overwrite is different for streams + overwrite = kwargs.get("overwrite", True) + errors = kwargs.get("errors", False) + decrypt = kwargs.get("decrypt", True) + password = kwargs.get("password", None) + encoding = kwargs.get("encoding", "utf-8") + for stream in streams: + load_stream(stream, environ, overwrite, errors, decrypt, password, encoding) + @property def exception(self) -> Type[Exception]: return self._exception @@ -117,7 +157,7 @@ def env(self): return self._env def get(self, var: str, default=None): - # getting from environment is cheapest + # getting from the environment is the least expensive value = self.env.get(var, None) # not set or isn't primary, check secrets manager if value is None or not self.env_source: diff --git a/pyproject.toml b/pyproject.toml index e18281e..fceca65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,31 @@ [project] name = "envex" -version = "3.3.1" -description = "Environment interface with .env and hashicorp vault support" +version = "4.0.0" +description = "Environment interface with (optionally encrypted) .env with hashicorp vault support" readme = "README.md" license = { file = "LICENSE.md" } -requires-python = ">=3.10" +requires-python = ">=3.11" authors = [ { name = "David Nugent", email = "davidn@uniquode.io" } ] classifiers = [ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", ] -scripts = {env2hvac = "scripts.env2hvac:main", envsecrets = "scripts.envsecrets:main", seal = "scripts.seal:main"} +scripts = {envcrypt = "scripts.envcrypt:main", env2hvac = "scripts.env2hvac:main", envsecrets = "scripts.envsecrets:main", seal = "scripts.seal:main"} + +[project.urls] +homepage = "https://github.com/deeprave/envex" +repository = "https://github.com/deeprave/envex" [tool.ruff] namespace-packages = ["envex"] [tool.pytest.ini_options] -minversion = "7.0" +minversion = "8.0" markers = "integration: mark a test as an integration test" addopts = "-m 'not integration'" pythonpath = ["."] @@ -28,9 +36,18 @@ dev = [ "pytest-cov>=4.1,<7.0", "pytest-mock>=3.11.1", "pytest>=7.0", - "ruff>=0.4.4", "testcontainers>=4.4.0", ] vault = [ "hvac>=1.1.1", ] +crypto = [ + "pycryptodome>=3.21.0", +] + +[tool.uv] +default-groups = ["dev", "crypto"] + +[build-system] +requires = ["flit-core"] +build-backend = "flit_core.buildapi" diff --git a/scripts/envcrypt.py b/scripts/envcrypt.py new file mode 100755 index 0000000..958f369 --- /dev/null +++ b/scripts/envcrypt.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +Block data encryption using +""" + +import os +import sys +import logging +import string +from io import BytesIO +from pathlib import Path + +from envex.env_crypto import encrypt_data, decrypt_data, DecryptError + +ENCRYPTED_EXT = ".enc" + +logging.basicConfig( + format="%(asctime)s %(levelname).3s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + level=logging.INFO, + handlers=[logging.StreamHandler(sys.stdout)], +) +logger = logging.getLogger(__file__) + + +def check_password_simple(password: str) -> bool: + """ + Check if a password is too simple based on length, + character variety, and basic complexity rules. + Returns True if the password is too simple. + """ + # Minimum length check + if len(password) < 8: + return True + + # Check for character variety + has_upper = any(char.isupper() for char in password) + has_lower = any(char.islower() for char in password) + has_digit = any(char.isdigit() for char in password) + all_digit = all(char.isdigit() for char in password) + has_special = any(char in string.punctuation for char in password) + + if not has_upper or not has_lower or not has_digit or not has_special or all_digit: + return True + + # Check for repeated/sequential characters + if len(set(password)) <= 3: # Mostly repeated characters + return True + + repeated_patterns = ["12345", "abcde", "password", "qwerty", "asdf"] + return any((pattern in password.lower() for pattern in repeated_patterns)) + + +def main(): + import io + import argparse + + class CustomParser(argparse.ArgumentParser): + def error(self, message): + logger.error(f"{self.prog}: error: {message}") + super().error() + + def warning(self, message): + logger.warning(f"{self.prog}: error: {message}") + super().warning() + + def print_help(self, file=None): + text = io.StringIO() + super().print_help(file=text) + text.seek(0) + for line in text.readlines(): + logger.info(line.strip()) + + class CustomHelpFormatter(argparse.ArgumentDefaultsHelpFormatter): + def __init__(self, *args, **kwargs): + kwargs["max_help_position"] = 45 + kwargs["width"] = 1000 + super().__init__(*args, **kwargs) + + parser = CustomParser(description=__doc__, formatter_class=CustomHelpFormatter) + password_opts = parser.add_mutually_exclusive_group() + password_opts.add_argument( + "-P", "--password", action="store", help="Use given password" + ) + password_opts.add_argument( + "-E", + "--environ", + action="store", + help="Read password from provided environment variable", + ) + password_opts.add_argument( + "-F", "--file", action="store", help="Read password from a given file" + ) + + encrypt_opts = parser.add_mutually_exclusive_group() + encrypt_opts.add_argument( + "-e", "--encrypt", action="store_true", default=False, help="Use given password" + ) + encrypt_opts.add_argument( + "-d", + "--decrypt", + action="store_true", + default=False, + help="Read password from provided environment variable", + ) + + parser.add_argument( + "-r", + "--rm", + action="store_true", + default=False, + help="Remove input file after successful conversion", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + default=False, + help="Increase output verbosity", + ) + parser.add_argument("input", action="store", help="File to encrypt or decrypt") + parser.add_argument( + "output", nargs="?", default=None, action="store", help="Output file (optional)" + ) + + args = parser.parse_args() + + if not (_password := args.password): + if args.environ: + _password = os.environ.get(args.environ) + elif args.file: + _password = Path(args.file).read_text() + else: + logger.error(f"{parser.prog}: password or password source is not provided") + exit(2) + + if check_password_simple(_password): + logger.warning("""WARNING: Password appears to be too short, simple or can easily be guessed. + Recommended: + - at least 1 each of uppercase, lowercase, digit, punctuation + - at least 8 characters in length + - does not contain common sequences used in passwords""") + + if not args.encrypt and not args.decrypt: + logger.error( + "{parser.prog}: operation to perform (--encrypt or --decrypt) must be specified" + ) + exit(3) + + encrypt = args.encrypt + + if args.verbose: + logger.setLevel(logging.DEBUG) + + _input = Path(args.input) + if not _input.exists(): + logger.error(f"{parser.prog}: {_input} does not exist") + exit(1) + + if not args.output: + if encrypt: + args.output = f"{args.input}{ENCRYPTED_EXT}" + elif args.input.endswith(ENCRYPTED_EXT): + args.output = args.input[: -len(ENCRYPTED_EXT)] + else: + logger.error( + f"{parser.prog}: cannot automatically determine output file name" + ) + exit(1) + + _output = Path(args.output) + if _output.exists() and _input.samefile(_output): + logger.error(f"{parser.prog}: Input and Output files cannot be the same") + exit(1) + + logger.debug( + f"{parser.prog}: {'encrypt' if encrypt else 'decrypt'} {_input} -> {_output}" + ) + + func = encrypt_data if encrypt else decrypt_data + try: + _output.write_bytes(func(BytesIO(_input.read_bytes()), _password).getvalue()) + except DecryptError as e: + logger.error(f"{parser.prog}: {e.args}") + exit(4) + + if args.rm: + logger.debug(f"{parser.prog}: removing {_input}") + _input.unlink(missing_ok=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/envsecrets.py b/scripts/envsecrets.py index 443c0ed..82a28b2 100755 --- a/scripts/envsecrets.py +++ b/scripts/envsecrets.py @@ -9,6 +9,7 @@ Output: """ + import argparse import re import sys @@ -62,7 +63,12 @@ def cache_regex(rx: str) -> re.Pattern: def env_match(var, regexlist, is_value=False): if regexlist: for regex in regexlist: - if is_value and var == regex or not is_value and cache_regex(regex).match(var): + if ( + is_value + and var == regex + or not is_value + and cache_regex(regex).match(var) + ): break else: return False diff --git a/scripts/example/example.sh b/scripts/example/example.sh index 8dbb26c..c58d8f4 100755 --- a/scripts/example/example.sh +++ b/scripts/example/example.sh @@ -7,6 +7,6 @@ TEMPLATE_NAME=example_env_template.env TEMPLATE="$SCRIPT_DIR/$TEMPLATE_NAME" SOURCE="$SCRIPT_DIR/$SOURCE_NAME" -OUTPUT=output.env +OUTPUT=test.env $SCRIPT_DIR/../envsecrets.py -e -s "$SCRIPT_DIR" -d "$SOURCE_NAME" -t "$TEMPLATE" -k example -C "~/.certs/cacert.pem" -v "$OUTPUT" diff --git a/scripts/lib/decr_action.py b/scripts/lib/decr_action.py index 450207a..30554b0 100644 --- a/scripts/lib/decr_action.py +++ b/scripts/lib/decr_action.py @@ -7,9 +7,16 @@ # noinspection PyShadowingBuiltins class Decrement(argparse.Action): def __init__( - self, option_strings, dest: str | NoneType, default: int = None, required: bool = False, help: str = None + self, + option_strings, + dest: str | NoneType, + default: int = None, + required: bool = False, + help: str = None, ): - super().__init__(option_strings, dest, nargs=0, default=default, required=required, help=help) + super().__init__( + option_strings, dest, nargs=0, default=default, required=required, help=help + ) # noinspection PyShadowingNames def __call__(self, parser, namespace, values, option_string=None): diff --git a/scripts/lib/log.py b/scripts/lib/log.py index 267ec95..c69579f 100644 --- a/scripts/lib/log.py +++ b/scripts/lib/log.py @@ -19,8 +19,8 @@ def fatal(msg, *args, **kwargs) -> NoReturn: def config(**kwargs): - kwargs.setdefault("level", logging.INFO), - kwargs.setdefault("format", "%(asctime)s %(message)s (%(levelname)s)"), + (kwargs.setdefault("level", logging.INFO),) + (kwargs.setdefault("format", "%(asctime)s %(message)s (%(levelname)s)"),) logging.basicConfig(**kwargs) diff --git a/scripts/seal.py b/scripts/seal.py index 9792e55..8819613 100755 --- a/scripts/seal.py +++ b/scripts/seal.py @@ -3,6 +3,7 @@ """ Seal or [-u] unseal a vault """ + import logging import os import textwrap @@ -39,7 +40,9 @@ class CustomFormatter(argparse.RawDescriptionHelpFormatter): def _split_lines(self, text, width): return textwrap.wrap(text, width=250) - parser = argparse.ArgumentParser(description=description, formatter_class=CustomFormatter) + parser = argparse.ArgumentParser( + description=description, formatter_class=CustomFormatter + ) parser.add_argument( "-a", "--address", @@ -65,12 +68,18 @@ def _split_lines(self, text, width): help="Comma separated list of unseal keys", ) action_group = parser.add_mutually_exclusive_group(required=False) - action_group.add_argument("-s", "--seal", action="store_true", default=False, help="Seal the vault") - action_group.add_argument("-u", "--unseal", action="store_true", default=False, help="Unseal the vault") + action_group.add_argument( + "-s", "--seal", action="store_true", default=False, help="Seal the vault" + ) + action_group.add_argument( + "-u", "--unseal", action="store_true", default=False, help="Unseal the vault" + ) args = parser.parse_args() - client = hvac.Client(url=args.address, token=args.token, verify=expand(args.cacert) or False) + client = hvac.Client( + url=args.address, token=args.token, verify=expand(args.cacert) or False + ) try: if args.seal: diff --git a/tests/conftest.py b/tests/conftest.py index f528361..ce73f64 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,42 +1,55 @@ # -*- coding: utf-8 -*- import pytest -import hvac -from hvac.exceptions import InvalidRequest -from testcontainers.vault import VaultContainer +try: + import hvac + from hvac.exceptions import InvalidRequest -container_name = "hashicorp/vault:1.14.4" + use_hvac = True -test_data = { - "ABC": "123", - "DEF": "456", -} + from testcontainers.vault import VaultContainer + container_name = "hashicorp/vault:1.14.4" -@pytest.fixture(scope="session") -def vault(request) -> VaultContainer: - vault = VaultContainer(container_name) + test_data = { + "ABC": "123", + "DEF": "456", + } - vault.start() - connection_url = vault.get_connection_url() - client = hvac.Client(url=connection_url, token=vault.root_token) - assert client.is_authenticated() + @pytest.fixture(scope="session") + def vault(request) -> VaultContainer: + vault = VaultContainer(container_name) - try: - client.sys.enable_secrets_engine("kv", path="secret") - except InvalidRequest as e: - if "path is already in use at" not in str(e): - raise + vault.start() + connection_url = vault.get_connection_url() + client = hvac.Client(url=connection_url, token=vault.root_token) + assert client.is_authenticated() - client.write_data("secret/data/test", data=dict(data=test_data)) + try: + client.sys.enable_secrets_engine("kv", path="secret") + except InvalidRequest as e: + if "path is already in use at" not in str(e): + raise - def vault_stop(): - vault.stop() + client.write_data("secret/data/test", data=dict(data=test_data)) - request.addfinalizer(vault_stop) - return vault + def vault_stop(): + vault.stop() + request.addfinalizer(vault_stop) + return vault -@pytest.fixture(scope="session") -def vault_client(vault) -> hvac.Client: - return hvac.Client(url=vault.get_connection_url(), token=vault.root_token) + @pytest.fixture(scope="session") + def vault_client(vault) -> hvac.Client: + return hvac.Client(url=vault.get_connection_url(), token=vault.root_token) + +except ImportError: + use_hvac = False + + +def pytest_configure(config): + config.addinivalue_line("markers", "vault: vault module is available") + # Register the slow marker + pytest.mark.vault = pytest.mark.skipif( + not use_hvac, reason="Test skipped because hvac_module is not available" + ) diff --git a/tests/test_env_crypto.py b/tests/test_env_crypto.py new file mode 100644 index 0000000..39da900 --- /dev/null +++ b/tests/test_env_crypto.py @@ -0,0 +1,99 @@ +# tests/test_env_crypto.py + +from io import BytesIO + +import pytest +from envex.env_crypto import encrypt_data, decrypt_data, EncryptError, DecryptError + + +@pytest.fixture +def password(): + # Provide a test password fixture + return "#pretEnD_stR0ng_pAs$woRd" + + +@pytest.fixture +def incorrect_password(): + # Provide an incorrect test password + return "wrong_password" + + +@pytest.fixture +def encrypted_stream_with_invalid_magic_bytes(): + # Mock BytesIO stream with invalid magic bytes + return BytesIO(b"DATA_WITH_INVALID_MAGIC_BYTES") + + +@pytest.mark.unit +def test_encrypt_data_success(password): + input_data = BytesIO(b"test data") + result = encrypt_data(input_data, password) + assert isinstance(result, BytesIO) + assert result.getvalue() != b"test data" # Ensure data is encrypted + + +@pytest.mark.unit +def test_encrypt_data_no_password(): + input_data = BytesIO(b"test data") + password = "" + with pytest.raises(EncryptError): + encrypt_data(input_data, password) + + +@pytest.mark.unit +def test_encrypt_data_already_encrypted(password): + input_data = BytesIO(b"Some Test data data") + input_enc = encrypt_data(input_data, password) + with pytest.raises(EncryptError): + encrypt_data(input_enc, password) + + +@pytest.mark.unit +def test_encrypt_empty_data(): + input_data = BytesIO(b"") + password = "strongpassword123" + result = encrypt_data(input_data, password) + assert isinstance(result, BytesIO) + assert len(result.getvalue()) > 0 # Ensure outputs exist even for empty input + + +def test_encrypt_large_data(password): + input_data = BytesIO(b"a" * 10**6) # 1MB of data + result = encrypt_data(input_data, password) + assert isinstance(result, BytesIO) + assert result.getvalue() != b"a" * 10**6 # Ensure data is encrypted + assert input_data.getvalue() == decrypt_data(result, password).getvalue() + + +@pytest.mark.unit +def test_valid_decryption(password): + encrypted_stream = encrypt_data(BytesIO(b"VALID_ENCRYPTED_DATA"), password) + result = decrypt_data(encrypted_stream, password) + assert isinstance(result, BytesIO) + assert result.getvalue() == b"VALID_ENCRYPTED_DATA" + + +@pytest.mark.unit +def test_invalid_magic_bytes(encrypted_stream_with_invalid_magic_bytes, password): + # Ensure decryption fails on invalid magic bytes + with pytest.raises(DecryptError) as e: + decrypt_data(encrypted_stream_with_invalid_magic_bytes, password) + assert "does not look to be encrypted" in str(e.value) + + +@pytest.mark.unit +def test_invalid_password(incorrect_password, password): + data = b"VALID_ENCRYPTED_DATA" + encrypted_data = encrypt_data(BytesIO(data), password) + # Ensure decryption fails with an incorrect password + with pytest.raises(DecryptError) as e: + decrypt_data(encrypted_data, incorrect_password) + assert "Incorrect password or invalid data" in str(e.value) + + +@pytest.mark.unit +def test_empty_stream(password): + # Test with an empty BytesIO stream + empty_stream = BytesIO() + with pytest.raises(DecryptError): + decrypt_data(empty_stream, password) diff --git a/tests/test_env_hvac.py b/tests/test_env_hvac.py index 6648b9e..6cf9a90 100644 --- a/tests/test_env_hvac.py +++ b/tests/test_env_hvac.py @@ -37,7 +37,9 @@ def delete(self, path): self.secrets.pop(path, None) def sys(self): - return MagicMock(list_mounted_secrets_engines=MagicMock(return_value={"data": {}})) + return MagicMock( + list_mounted_secrets_engines=MagicMock(return_value={"data": {}}) + ) with patch("hvac.Client", new=MockClient): yield MockClient() @@ -91,6 +93,7 @@ def sys(self): ] +@pytest.mark.vault @pytest.mark.parametrize( "url, token, cert, verify, base_path, engine, mount_point, expected_base_path", test_params, @@ -134,7 +137,9 @@ class MockClient: def read(self, path): mock_responses = { - "secret/data/valid/path": {"data": {"data": {"secret_key": "secret_value"}}}, + "secret/data/valid/path": { + "data": {"data": {"secret_key": "secret_value"}} + }, "secret/data/valid/empty": {"data": {"data": {}}}, "secret/data/no/data": {}, None: None, @@ -182,6 +187,7 @@ def client(self): # Parametrized test cases +@pytest.mark.vault @pytest.mark.parametrize( "test_input, test_input_key, expected_output, test_id", [ @@ -200,12 +206,16 @@ def client(self): (None, None, {}, "error_case_none_path"), ], ) -def test_get_secrets(secrets_manager, test_input, test_input_key, expected_output, test_id): +def test_get_secrets( + secrets_manager, test_input, test_input_key, expected_output, test_id +): # Act result = secrets_manager.get_secrets(test_input) # Assert - assert result == expected_output, f"get_secrets({test_input}) failed for test_id: {test_id}" + assert ( + result == expected_output + ), f"get_secrets({test_input}) failed for test_id: {test_id}" # Act result = secrets_manager.get_secret(test_input_key) @@ -221,6 +231,7 @@ def test_get_secrets(secrets_manager, test_input, test_input_key, expected_outpu assert secrets_manager.secrets == {} +@pytest.mark.vault @pytest.mark.parametrize( "test_input_key, expected_inital_output, modified_value, test_id", [ @@ -239,25 +250,34 @@ def test_get_secrets(secrets_manager, test_input, test_input_key, expected_outpu (None, None, None, "error_case_none_path"), ], ) -def test_get_set_secret(secrets_manager, test_input_key, expected_inital_output, modified_value, test_id): +def test_get_set_secret( + secrets_manager, test_input_key, expected_inital_output, modified_value, test_id +): result = secrets_manager.get_secret(test_input_key) - assert result == expected_inital_output, f"get_secret({test_input_key}) failed for test_id: {test_id}" + assert ( + result == expected_inital_output + ), f"get_secret({test_input_key}) failed for test_id: {test_id}" secrets_manager.set_secret(test_input_key, modified_value) result = secrets_manager.get_secret(test_input_key) - assert result == modified_value, f"get_secret({test_input_key}) failed for test_id: {test_id}" + assert ( + result == modified_value + ), f"get_secret({test_input_key}) failed for test_id: {test_id}" result = list(secrets_manager.list_secrets()) expected_result = [test_input_key] if modified_value else [] assert result == expected_result, f"list_secrets() failed for test_id: {test_id}" secrets_manager.delete_secret(test_input_key) - assert secrets_manager.secrets == {}, f"delete_secret({test_input_key}) failed for test_id: {test_id}" + assert ( + secrets_manager.secrets == {} + ), f"delete_secret({test_input_key}) failed for test_id: {test_id}" result = list(secrets_manager.list_secrets()) assert not result +@pytest.mark.vault def test_seal_unseal(secrets_manager): secrets_manager.seal() assert secrets_manager.sealed diff --git a/tests/test_env_secrets.py b/tests/test_env_secrets.py index a73e8ba..e88c413 100644 --- a/tests/test_env_secrets.py +++ b/tests/test_env_secrets.py @@ -5,7 +5,12 @@ @pytest.mark.integration def test_vault_secrets(vault): - env = Env(url=vault.get_connection_url(), base_path="test", token=vault.root_token, engine="kv") + env = Env( + url=vault.get_connection_url(), + base_path="test", + token=vault.root_token, + engine="kv", + ) assert env is not None assert env.secret_manager is not None assert env.secret_manager.client is not None diff --git a/tests/test_load_env.py b/tests/test_load_env.py index 95a3590..706583d 100644 --- a/tests/test_load_env.py +++ b/tests/test_load_env.py @@ -20,11 +20,11 @@ def envmap(): @contextlib.contextmanager def dotenv(ignored): _ = ignored - yield io.StringIO( - """ + yield io.BytesIO( + b""" # This is an example .env file SECOND=a-second-value -THIRD=altnernative-third +THIRD=alternative-third export FIFTH=fifth-value COMBINED=${FIRST}:${THIRD}:${FIFTH} DOUBLE_QUOTED="a quoted value" @@ -48,7 +48,7 @@ def test_load_env_overwrite(monkeypatch, envmap): for var in envmap.keys(): assert var in env assert "FIFTH" in env - assert env["COMBINED"] == "first-value:altnernative-third:fifth-value" + assert env["COMBINED"] == "first-value:alternative-third:fifth-value" def test_quoted_value(monkeypatch, envmap): diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py index 74fd919..63b0f2a 100644 --- a/tests/test_wrapper.py +++ b/tests/test_wrapper.py @@ -5,6 +5,7 @@ import pytest import envex +from envex.env_crypto import encrypt_data TEST_ENV = [ "# This is an example .env file", @@ -20,11 +21,18 @@ "ALISTOFIPS=::1,127.0.0.1,mydomain.com", ] +TEST_ENV_STREAM = io.BytesIO("\n".join(TEST_ENV).encode("utf-8")) + + +@pytest.fixture +def password(): + return "ajf4vDFa_849&s" + @contextlib.contextmanager -def dotenv(ignored): - _ = ignored - yield io.StringIO("\n".join(TEST_ENV)) +def dotenv(_ignored): + TEST_ENV_STREAM.seek(0) + yield TEST_ENV_STREAM def test_env_wrapper(): @@ -33,6 +41,30 @@ def test_env_wrapper(): assert "USER" in env +def test_env_wrapper_dict(): + values = dict(TEST="one", ARG2="two", ENABLED=3) + env = envex.Env(values, environ={}) + assert env("TEST") == "one" + assert env("ARG2") == "two" + assert env("ENABLED") == "3" + + +def test_env_wrapper_stream_bytes(): + stream = io.BytesIO(b"ONE=1\nARG2=two\nENABLED=true\n") + env = envex.Env(stream, environ={}) + assert env("ONE") == "1" + assert env("ARG2") == "two" + assert env("ENABLED") == "true" + + +def test_env_wrapper_stream_text(): + stream = io.StringIO("ONE=one\nARG2=2\nENABLED=false\n") + env = envex.Env(stream, environ={}) + assert env("ONE") == "one" + assert env("ARG2") == "2" + assert env("ENABLED") == "false" + + def test_env_get(): env = envex.Env(environ={}) var, val = "MY_VARIABLE", "MY_VARIABLE_VALUE" @@ -184,21 +216,22 @@ def test_env_export(): values = dict(MYVARIABLE="somevalue", MYVARIABLE2=1, MYVARIABLE3="...") env.export(values) + # sourcery skip: no-loop-in-tests for k, v in values.items(): assert env[k] == str(v) - assert env.is_all_set([k for k in values.keys()]) + assert env.is_all_set(list(values.keys())) assert not env.is_all_set("NOTSETVAR") - env.export({k: None for k in values.keys()}) - assert not env.is_any_set([k for k in values.keys()]) + env.export({k: None for k in values}) + assert not env.is_any_set(list(values.keys())) env["NOT_MYVARIABLE"] = "somevalue" assert env.is_any_set("NOT_MYVARIABLE") env.export(**values) for k, v in values.items(): assert env[k] == str(v) - assert env.is_all_set([k for k in values.keys()]) - env.export({k: None for k in values.keys()}) - assert not env.is_any_set([k for k in values.keys()]) + assert env.is_all_set(list(values.keys())) + env.export({k: None for k in values}) + assert not env.is_any_set(list(values.keys())) import os @@ -262,3 +295,33 @@ def test_setdefault_exists(): result = env.setdefault("var3", "def") assert result == "abc" assert env.env["var3"] == "abc" + + +def test_encrypted_stream_bytes(password): + data = b"ONE=1\nARG2=two\nENABLED=true\n" + stream = encrypt_data(io.BytesIO(data), password) + env = envex.Env(stream, decrypt=True, password=password) + assert env("ONE") == "1" + assert env("ARG2") == "two" + assert env("ENABLED") == "true" + + +def test_encrypted_stream_text(password): + data = "ONE=one\nARG2=2\nENABLED=false\n" + stream = encrypt_data(io.StringIO(data), password) + env = envex.Env(stream, decrypt=True, password=password) + assert env("ONE") == "one" + assert env("ARG2") == "2" + assert env("ENABLED") == "false" + + +def test_encrypted_stream_bytes_env(password): + import os + + os.environ["TEST_PASSWORD"] = password + data = b"ONE=1\nARG2=two\nENABLED=true\n" + stream = encrypt_data(io.BytesIO(data), password) + env = envex.Env(stream, decrypt=True, password="$TEST_PASSWORD") + assert env("ONE") == "1" + assert env("ARG2") == "two" + assert env("ENABLED") == "true" diff --git a/uv.lock b/uv.lock index 58f5115..b2399ed 100644 --- a/uv.lock +++ b/uv.lock @@ -1,13 +1,13 @@ version = 1 -requires-python = ">=3.10" +requires-python = ">=3.11" [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, ] [[package]] @@ -16,21 +16,6 @@ version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, - { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, - { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, - { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, - { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, - { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, - { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, - { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, - { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, - { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, - { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, - { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, - { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, - { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, - { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, @@ -90,61 +75,50 @@ wheels = [ [[package]] name = "coverage" -version = "7.6.4" +version = "7.6.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/d2/c25011f4d036cf7e8acbbee07a8e09e9018390aee25ba085596c4b83d510/coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", size = 801710 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/93/4ad92f71e28ece5c0326e5f4a6630aa4928a8846654a65cfff69b49b95b9/coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07", size = 206713 }, - { url = "https://files.pythonhosted.org/packages/01/ae/747a580b1eda3f2e431d87de48f0604bd7bc92e52a1a95185a4aa585bc47/coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0", size = 207149 }, - { url = "https://files.pythonhosted.org/packages/07/1a/1f573f8a6145f6d4c9130bbc120e0024daf1b24cf2a78d7393fa6eb6aba7/coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72", size = 235584 }, - { url = "https://files.pythonhosted.org/packages/40/42/c8523f2e4db34aa9389caee0d3688b6ada7a84fcc782e943a868a7f302bd/coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/8d/95/565c310fffa16ede1a042e9ea1ca3962af0d8eb5543bc72df6b91dc0c3d5/coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491", size = 234649 }, - { url = "https://files.pythonhosted.org/packages/d5/81/3b550674d98968ec29c92e3e8650682be6c8b1fa7581a059e7e12e74c431/coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b", size = 233744 }, - { url = "https://files.pythonhosted.org/packages/0d/70/d66c7f51b3e33aabc5ea9f9624c1c9d9655472962270eb5e7b0d32707224/coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea", size = 232204 }, - { url = "https://files.pythonhosted.org/packages/23/2d/2b3a2dbed7a5f40693404c8a09e779d7c1a5fbed089d3e7224c002129ec8/coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a", size = 233335 }, - { url = "https://files.pythonhosted.org/packages/5a/4f/92d1d2ad720d698a4e71c176eacf531bfb8e0721d5ad560556f2c484a513/coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa", size = 209435 }, - { url = "https://files.pythonhosted.org/packages/c7/b9/cdf158e7991e2287bcf9082670928badb73d310047facac203ff8dcd5ff3/coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172", size = 210243 }, - { url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819 }, - { url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263 }, - { url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205 }, - { url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612 }, - { url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479 }, - { url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405 }, - { url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038 }, - { url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812 }, - { url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400 }, - { url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243 }, - { url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013 }, - { url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251 }, - { url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268 }, - { url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298 }, - { url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367 }, - { url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853 }, - { url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160 }, - { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824 }, - { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639 }, - { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428 }, - { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 }, - { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 }, - { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 }, - { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 }, - { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 }, - { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 }, - { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 }, - { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 }, - { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 }, - { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 }, - { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 }, - { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 }, - { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 }, - { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 }, - { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 }, - { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 }, - { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 }, - { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 }, - { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 }, - { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 }, - { url = "https://files.pythonhosted.org/packages/cc/56/e1d75e8981a2a92c2a777e67c26efa96c66da59d645423146eb9ff3a851b/coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", size = 198954 }, + { url = "https://files.pythonhosted.org/packages/b1/91/b3dc2f7f38b5cca1236ab6bbb03e84046dd887707b4ec1db2baa47493b3b/coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9", size = 207133 }, + { url = "https://files.pythonhosted.org/packages/0d/2b/53fd6cb34d443429a92b3ec737f4953627e38b3bee2a67a3c03425ba8573/coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c", size = 207577 }, + { url = "https://files.pythonhosted.org/packages/74/f2/68edb1e6826f980a124f21ea5be0d324180bf11de6fd1defcf9604f76df0/coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7", size = 239524 }, + { url = "https://files.pythonhosted.org/packages/d3/83/8fec0ee68c2c4a5ab5f0f8527277f84ed6f2bd1310ae8a19d0c5532253ab/coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9", size = 236925 }, + { url = "https://files.pythonhosted.org/packages/8b/20/8f50e7c7ad271144afbc2c1c6ec5541a8c81773f59352f8db544cad1a0ec/coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4", size = 238792 }, + { url = "https://files.pythonhosted.org/packages/6f/62/4ac2e5ad9e7a5c9ec351f38947528e11541f1f00e8a0cdce56f1ba7ae301/coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1", size = 237682 }, + { url = "https://files.pythonhosted.org/packages/58/2f/9d2203f012f3b0533c73336c74134b608742be1ce475a5c72012573cfbb4/coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b", size = 236310 }, + { url = "https://files.pythonhosted.org/packages/33/6d/31f6ab0b4f0f781636075f757eb02141ea1b34466d9d1526dbc586ed7078/coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3", size = 237096 }, + { url = "https://files.pythonhosted.org/packages/7d/fb/e14c38adebbda9ed8b5f7f8e03340ac05d68d27b24397f8d47478927a333/coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0", size = 209682 }, + { url = "https://files.pythonhosted.org/packages/a4/11/a782af39b019066af83fdc0e8825faaccbe9d7b19a803ddb753114b429cc/coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b", size = 210542 }, + { url = "https://files.pythonhosted.org/packages/60/52/b16af8989a2daf0f80a88522bd8e8eed90b5fcbdecf02a6888f3e80f6ba7/coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8", size = 207325 }, + { url = "https://files.pythonhosted.org/packages/0f/79/6b7826fca8846c1216a113227b9f114ac3e6eacf168b4adcad0cb974aaca/coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a", size = 207563 }, + { url = "https://files.pythonhosted.org/packages/a7/07/0bc73da0ccaf45d0d64ef86d33b7d7fdeef84b4c44bf6b85fb12c215c5a6/coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015", size = 240580 }, + { url = "https://files.pythonhosted.org/packages/71/8a/9761f409910961647d892454687cedbaccb99aae828f49486734a82ede6e/coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3", size = 237613 }, + { url = "https://files.pythonhosted.org/packages/8b/10/ee7d696a17ac94f32f2dbda1e17e730bf798ae9931aec1fc01c1944cd4de/coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae", size = 239684 }, + { url = "https://files.pythonhosted.org/packages/16/60/aa1066040d3c52fff051243c2d6ccda264da72dc6d199d047624d395b2b2/coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4", size = 239112 }, + { url = "https://files.pythonhosted.org/packages/4e/e5/69f35344c6f932ba9028bf168d14a79fedb0dd4849b796d43c81ce75a3c9/coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6", size = 237428 }, + { url = "https://files.pythonhosted.org/packages/32/20/adc895523c4a28f63441b8ac645abd74f9bdd499d2d175bef5b41fc7f92d/coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f", size = 239098 }, + { url = "https://files.pythonhosted.org/packages/a9/a6/e0e74230c9bb3549ec8ffc137cfd16ea5d56e993d6bffed2218bff6187e3/coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692", size = 209940 }, + { url = "https://files.pythonhosted.org/packages/3e/18/cb5b88349d4aa2f41ec78d65f92ea32572b30b3f55bc2b70e87578b8f434/coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97", size = 210726 }, + { url = "https://files.pythonhosted.org/packages/35/26/9abab6539d2191dbda2ce8c97b67d74cbfc966cc5b25abb880ffc7c459bc/coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664", size = 207356 }, + { url = "https://files.pythonhosted.org/packages/44/da/d49f19402240c93453f606e660a6676a2a1fbbaa6870cc23207790aa9697/coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c", size = 207614 }, + { url = "https://files.pythonhosted.org/packages/da/e6/93bb9bf85497816082ec8da6124c25efa2052bd4c887dd3b317b91990c9e/coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014", size = 240129 }, + { url = "https://files.pythonhosted.org/packages/df/65/6a824b9406fe066835c1274a9949e06f084d3e605eb1a602727a27ec2fe3/coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00", size = 237276 }, + { url = "https://files.pythonhosted.org/packages/9f/79/6c7a800913a9dd23ac8c8da133ebb556771a5a3d4df36b46767b1baffd35/coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d", size = 239267 }, + { url = "https://files.pythonhosted.org/packages/57/e7/834d530293fdc8a63ba8ff70033d5182022e569eceb9aec7fc716b678a39/coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a", size = 238887 }, + { url = "https://files.pythonhosted.org/packages/15/05/ec9d6080852984f7163c96984444e7cd98b338fd045b191064f943ee1c08/coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077", size = 236970 }, + { url = "https://files.pythonhosted.org/packages/0a/d8/775937670b93156aec29f694ce37f56214ed7597e1a75b4083ee4c32121c/coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb", size = 238831 }, + { url = "https://files.pythonhosted.org/packages/f4/58/88551cb7fdd5ec98cb6044e8814e38583436b14040a5ece15349c44c8f7c/coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba", size = 210000 }, + { url = "https://files.pythonhosted.org/packages/b7/12/cfbf49b95120872785ff8d56ab1c7fe3970a65e35010c311d7dd35c5fd00/coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1", size = 210753 }, + { url = "https://files.pythonhosted.org/packages/7c/68/c1cb31445599b04bde21cbbaa6d21b47c5823cdfef99eae470dfce49c35a/coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419", size = 208091 }, + { url = "https://files.pythonhosted.org/packages/11/73/84b02c6b19c4a11eb2d5b5eabe926fb26c21c080e0852f5e5a4f01165f9e/coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a", size = 208369 }, + { url = "https://files.pythonhosted.org/packages/de/e0/ae5d878b72ff26df2e994a5c5b1c1f6a7507d976b23beecb1ed4c85411ef/coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4", size = 251089 }, + { url = "https://files.pythonhosted.org/packages/ab/9c/0aaac011aef95a93ef3cb2fba3fde30bc7e68a6635199ed469b1f5ea355a/coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae", size = 246806 }, + { url = "https://files.pythonhosted.org/packages/f8/19/4d5d3ae66938a7dcb2f58cef3fa5386f838f469575b0bb568c8cc9e3a33d/coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030", size = 249164 }, + { url = "https://files.pythonhosted.org/packages/b3/0b/4ee8a7821f682af9ad440ae3c1e379da89a998883271f088102d7ca2473d/coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be", size = 248642 }, + { url = "https://files.pythonhosted.org/packages/8a/12/36ff1d52be18a16b4700f561852e7afd8df56363a5edcfb04cf26a0e19e0/coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e", size = 246516 }, + { url = "https://files.pythonhosted.org/packages/43/d0/8e258f6c3a527c1655602f4f576215e055ac704de2d101710a71a2affac2/coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9", size = 247783 }, + { url = "https://files.pythonhosted.org/packages/a9/0d/1e4a48d289429d38aae3babdfcadbf35ca36bdcf3efc8f09b550a845bdb5/coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b", size = 210646 }, + { url = "https://files.pythonhosted.org/packages/26/74/b0729f196f328ac55e42b1e22ec2f16d8bcafe4b8158a26ec9f1cdd1d93e/coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611", size = 211815 }, ] [package.optional-dependencies] @@ -168,15 +142,17 @@ wheels = [ [[package]] name = "envex" -version = "3.3.0" -source = { virtual = "." } +version = "4.0.0" +source = { editable = "." } [package.dev-dependencies] +crypto = [ + { name = "pycryptodome" }, +] dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-mock" }, - { name = "ruff" }, { name = "testcontainers" }, ] vault = [ @@ -186,24 +162,15 @@ vault = [ [package.metadata] [package.metadata.requires-dev] +crypto = [{ name = "pycryptodome", specifier = ">=3.21.0" }] dev = [ { name = "pytest", specifier = ">=7.0" }, { name = "pytest-cov", specifier = ">=4.1,<7.0" }, { name = "pytest-mock", specifier = ">=3.11.1" }, - { name = "ruff", specifier = ">=0.4.4" }, { name = "testcontainers", specifier = ">=4.4.0" }, ] vault = [{ name = "hvac", specifier = ">=1.1.1" }] -[[package]] -name = "exceptiongroup" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, -] - [[package]] name = "hvac" version = "2.3.0" @@ -252,21 +219,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "pycryptodome" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/52/13b9db4a913eee948152a079fe58d035bd3d1a519584155da8e786f767e6/pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", size = 4818071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/88/5e83de10450027c96c79dc65ac45e9d0d7a7fef334f39d3789a191f33602/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", size = 2495937 }, + { url = "https://files.pythonhosted.org/packages/66/e1/8f28cd8cf7f7563319819d1e172879ccce2333781ae38da61c28fe22d6ff/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", size = 1634629 }, + { url = "https://files.pythonhosted.org/packages/6a/c1/f75a1aaff0c20c11df8dc8e2bf8057e7f73296af7dfd8cbb40077d1c930d/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", size = 2168708 }, + { url = "https://files.pythonhosted.org/packages/ea/66/6f2b7ddb457b19f73b82053ecc83ba768680609d56dd457dbc7e902c41aa/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", size = 2254555 }, + { url = "https://files.pythonhosted.org/packages/2c/2b/152c330732a887a86cbf591ed69bd1b489439b5464806adb270f169ec139/pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", size = 2294143 }, + { url = "https://files.pythonhosted.org/packages/55/92/517c5c498c2980c1b6d6b9965dffbe31f3cd7f20f40d00ec4069559c5902/pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", size = 2160509 }, + { url = "https://files.pythonhosted.org/packages/39/1f/c74288f54d80a20a78da87df1818c6464ac1041d10988bb7d982c4153fbc/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", size = 2329480 }, + { url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 }, + { url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 }, + { url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 }, + { url = "https://files.pythonhosted.org/packages/25/b3/09ff7072e6d96c9939c24cf51d3c389d7c345bf675420355c22402f71b68/pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", size = 1691593 }, + { url = "https://files.pythonhosted.org/packages/a8/91/38e43628148f68ba9b68dedbc323cf409e537fd11264031961fd7c744034/pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", size = 1765997 }, +] + [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, ] [[package]] @@ -294,14 +279,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, ] +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + [[package]] name = "pywin32" version = "308" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/a6/3e9f2c474895c1bb61b11fa9640be00067b5c5b363c501ee9c3fa53aec01/pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e", size = 5927028 }, - { url = "https://files.pythonhosted.org/packages/d9/b4/84e2463422f869b4b718f79eb7530a4c1693e96b8a4e5e968de38be4d2ba/pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e", size = 6558484 }, - { url = "https://files.pythonhosted.org/packages/9f/8f/fb84ab789713f7c6feacaa08dad3ec8105b88ade8d1c4f0f0dfcaaa017d6/pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c", size = 7971454 }, { url = "https://files.pythonhosted.org/packages/eb/e2/02652007469263fe1466e98439831d65d4ca80ea1a2df29abecedf7e47b7/pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a", size = 5928156 }, { url = "https://files.pythonhosted.org/packages/48/ef/f4fb45e2196bc7ffe09cad0542d9aff66b0e33f6c0954b43e49c33cad7bd/pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b", size = 6559559 }, { url = "https://files.pythonhosted.org/packages/79/ef/68bb6aa865c5c9b11a35771329e95917b5559845bd75b65549407f9fc6b4/pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6", size = 7972495 }, @@ -328,44 +319,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] -[[package]] -name = "ruff" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/51/231bb3790e5b0b9fd4131f9a231d73d061b3667522e3f406fd9b63334d0e/ruff-0.7.2.tar.gz", hash = "sha256:2b14e77293380e475b4e3a7a368e14549288ed2931fce259a6f99978669e844f", size = 3210036 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/56/0caa2b5745d66a39aa239c01059f6918fc76ed8380033d2f44bf297d141d/ruff-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:b73f873b5f52092e63ed540adefc3c36f1f803790ecf2590e1df8bf0a9f72cb8", size = 10373973 }, - { url = "https://files.pythonhosted.org/packages/1a/33/cad6ff306731f335d481c50caa155b69a286d5b388e87ff234cd2a4b3557/ruff-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5b813ef26db1015953daf476202585512afd6a6862a02cde63f3bafb53d0b2d4", size = 10171140 }, - { url = "https://files.pythonhosted.org/packages/97/f5/6a2ca5c9ba416226eac9cf8121a1baa6f06655431937e85f38ffcb9d0d01/ruff-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:853277dbd9675810c6826dad7a428d52a11760744508340e66bf46f8be9701d9", size = 9809333 }, - { url = "https://files.pythonhosted.org/packages/16/83/e3e87f13d1a1dc205713632978cd7bc287a59b08bc95780dbe359b9aefcb/ruff-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21aae53ab1490a52bf4e3bf520c10ce120987b047c494cacf4edad0ba0888da2", size = 10622987 }, - { url = "https://files.pythonhosted.org/packages/22/16/97ccab194480e99a2e3c77ae132b3eebfa38c2112747570c403a4a13ba3a/ruff-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc7e0fc6e0cb3168443eeadb6445285abaae75142ee22b2b72c27d790ab60ba", size = 10184640 }, - { url = "https://files.pythonhosted.org/packages/97/1b/82ff05441b036f68817296c14f24da47c591cb27acfda473ee571a5651ac/ruff-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd77877a4e43b3a98e5ef4715ba3862105e299af0c48942cc6d51ba3d97dc859", size = 11210203 }, - { url = "https://files.pythonhosted.org/packages/a6/96/7ecb30a7ef7f942e2d8e0287ad4c1957dddc6c5097af4978c27cfc334f97/ruff-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e00163fb897d35523c70d71a46fbaa43bf7bf9af0f4534c53ea5b96b2e03397b", size = 11870894 }, - { url = "https://files.pythonhosted.org/packages/06/6a/c716bb126218227f8e604a9c484836257708a05ee3d2ebceb666ff3d3867/ruff-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3c54b538633482dc342e9b634d91168fe8cc56b30a4b4f99287f4e339103e88", size = 11449533 }, - { url = "https://files.pythonhosted.org/packages/e6/2f/3a5f9f9478904e5ae9506ea699109070ead1e79aac041e872cbaad8a7458/ruff-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b792468e9804a204be221b14257566669d1db5c00d6bb335996e5cd7004ba80", size = 12607919 }, - { url = "https://files.pythonhosted.org/packages/a0/57/4642e57484d80d274750dcc872ea66655bbd7e66e986fede31e1865b463d/ruff-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba53ed84ac19ae4bfb4ea4bf0172550a2285fa27fbb13e3746f04c80f7fa088", size = 11016915 }, - { url = "https://files.pythonhosted.org/packages/4d/6d/59be6680abee34c22296ae3f46b2a3b91662b8b18ab0bf388b5eb1355c97/ruff-0.7.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b19fafe261bf741bca2764c14cbb4ee1819b67adb63ebc2db6401dcd652e3748", size = 10625424 }, - { url = "https://files.pythonhosted.org/packages/82/e7/f6a643683354c9bc7879d2f228ee0324fea66d253de49273a0814fba1927/ruff-0.7.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28bd8220f4d8f79d590db9e2f6a0674f75ddbc3847277dd44ac1f8d30684b828", size = 10233692 }, - { url = "https://files.pythonhosted.org/packages/d7/48/b4e02fc835cd7ed1ee7318d9c53e48bcf6b66301f55925a7dcb920e45532/ruff-0.7.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9fd67094e77efbea932e62b5d2483006154794040abb3a5072e659096415ae1e", size = 10751825 }, - { url = "https://files.pythonhosted.org/packages/1e/06/6c5ee6ab7bb4cbad9e8bb9b2dd0d818c759c90c1c9e057c6ed70334b97f4/ruff-0.7.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:576305393998b7bd6c46018f8104ea3a9cb3fa7908c21d8580e3274a3b04b691", size = 11074811 }, - { url = "https://files.pythonhosted.org/packages/a1/16/8969304f25bcd0e4af1778342e63b715e91db8a2dbb51807acd858cba915/ruff-0.7.2-py3-none-win32.whl", hash = "sha256:fa993cfc9f0ff11187e82de874dfc3611df80852540331bc85c75809c93253a8", size = 8650268 }, - { url = "https://files.pythonhosted.org/packages/d9/18/c4b00d161def43fe5968e959039c8f6ce60dca762cec4a34e4e83a4210a0/ruff-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dd8800cbe0254e06b8fec585e97554047fb82c894973f7ff18558eee33d1cb88", size = 9433693 }, - { url = "https://files.pythonhosted.org/packages/7f/7b/c920673ac01c19814dd15fc617c02301c522f3d6812ca2024f4588ed4549/ruff-0.7.2-py3-none-win_arm64.whl", hash = "sha256:bb8368cd45bba3f57bb29cbb8d64b4a33f8415d0149d2655c5c8539452ce7760", size = 8735845 }, -] - [[package]] name = "testcontainers" -version = "4.8.2" +version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docker" }, + { name = "python-dotenv" }, { name = "typing-extensions" }, { name = "urllib3" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/72/c58d84f5704c6caadd9f803a3adad5ab54ac65328c02d13295f40860cf33/testcontainers-4.8.2.tar.gz", hash = "sha256:dd4a6a2ea09e3c3ecd39e180b6548105929d0bb78d665ce9919cb3f8c98f9853", size = 63590 } +sdist = { url = "https://files.pythonhosted.org/packages/2e/9a/e1ac5231231192b39302fcad7de2c0dbfc718c0636d7e28917c30ec57c41/testcontainers-4.9.0.tar.gz", hash = "sha256:2cd6af070109ff68c1ab5389dc89c86c2dc3ab30a21ca734b2cb8f0f80ad479e", size = 64612 } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/77/5ac0dff2903a033d83d971fd85957356abdb66a327f3589df2b3d1a586b4/testcontainers-4.8.2-py3-none-any.whl", hash = "sha256:9e19af077cd96e1957c13ee466f1f32905bc6c5bc1bc98643eb18be1a989bfb0", size = 104326 }, + { url = "https://files.pythonhosted.org/packages/3e/f8/6425ff800894784160290bcb9737878d910b6da6a08633bfe7f2ed8c9ae3/testcontainers-4.9.0-py3-none-any.whl", hash = "sha256:c6fee929990972c40bf6b91b7072c94064ff3649b405a14fde0274c8b2479d32", size = 105324 }, ] [[package]] @@ -397,39 +364,45 @@ wheels = [ [[package]] name = "wrapt" -version = "1.16.0" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972 } +sdist = { url = "https://files.pythonhosted.org/packages/24/a1/fc03dca9b0432725c2e8cdbf91a349d2194cf03d8523c124faebe581de09/wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801", size = 55542 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/c6/5375258add3777494671d8cec27cdf5402abd91016dee24aa2972c61fedf/wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4", size = 37315 }, - { url = "https://files.pythonhosted.org/packages/32/12/e11adfde33444986135d8881b401e4de6cbb4cced046edc6b464e6ad7547/wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020", size = 38160 }, - { url = "https://files.pythonhosted.org/packages/70/7d/3dcc4a7e96f8d3e398450ec7703db384413f79bd6c0196e0e139055ce00f/wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440", size = 80419 }, - { url = "https://files.pythonhosted.org/packages/d1/c4/8dfdc3c2f0b38be85c8d9fdf0011ebad2f54e40897f9549a356bebb63a97/wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487", size = 72669 }, - { url = "https://files.pythonhosted.org/packages/49/83/b40bc1ad04a868b5b5bcec86349f06c1ee1ea7afe51dc3e46131e4f39308/wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf", size = 80271 }, - { url = "https://files.pythonhosted.org/packages/19/d4/cd33d3a82df73a064c9b6401d14f346e1d2fb372885f0295516ec08ed2ee/wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72", size = 84748 }, - { url = "https://files.pythonhosted.org/packages/ef/58/2fde309415b5fa98fd8f5f4a11886cbf276824c4c64d45a39da342fff6fe/wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0", size = 77522 }, - { url = "https://files.pythonhosted.org/packages/07/44/359e4724a92369b88dbf09878a7cde7393cf3da885567ea898e5904049a3/wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136", size = 84780 }, - { url = "https://files.pythonhosted.org/packages/88/8f/706f2fee019360cc1da652353330350c76aa5746b4e191082e45d6838faf/wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d", size = 35335 }, - { url = "https://files.pythonhosted.org/packages/19/2b/548d23362e3002ebbfaefe649b833fa43f6ca37ac3e95472130c4b69e0b4/wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2", size = 37528 }, - { url = "https://files.pythonhosted.org/packages/fd/03/c188ac517f402775b90d6f312955a5e53b866c964b32119f2ed76315697e/wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", size = 37313 }, - { url = "https://files.pythonhosted.org/packages/0f/16/ea627d7817394db04518f62934a5de59874b587b792300991b3c347ff5e0/wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", size = 38164 }, - { url = "https://files.pythonhosted.org/packages/7f/a7/f1212ba098f3de0fd244e2de0f8791ad2539c03bef6c05a9fcb03e45b089/wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", size = 80890 }, - { url = "https://files.pythonhosted.org/packages/b7/96/bb5e08b3d6db003c9ab219c487714c13a237ee7dcc572a555eaf1ce7dc82/wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", size = 73118 }, - { url = "https://files.pythonhosted.org/packages/6e/52/2da48b35193e39ac53cfb141467d9f259851522d0e8c87153f0ba4205fb1/wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", size = 80746 }, - { url = "https://files.pythonhosted.org/packages/11/fb/18ec40265ab81c0e82a934de04596b6ce972c27ba2592c8b53d5585e6bcd/wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", size = 85668 }, - { url = "https://files.pythonhosted.org/packages/0f/ef/0ecb1fa23145560431b970418dce575cfaec555ab08617d82eb92afc7ccf/wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", size = 78556 }, - { url = "https://files.pythonhosted.org/packages/25/62/cd284b2b747f175b5a96cbd8092b32e7369edab0644c45784871528eb852/wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", size = 85712 }, - { url = "https://files.pythonhosted.org/packages/e5/a7/47b7ff74fbadf81b696872d5ba504966591a3468f1bc86bca2f407baef68/wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", size = 35327 }, - { url = "https://files.pythonhosted.org/packages/cf/c3/0084351951d9579ae83a3d9e38c140371e4c6b038136909235079f2e6e78/wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", size = 37523 }, - { url = "https://files.pythonhosted.org/packages/92/17/224132494c1e23521868cdd57cd1e903f3b6a7ba6996b7b8f077ff8ac7fe/wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", size = 37614 }, - { url = "https://files.pythonhosted.org/packages/6a/d7/cfcd73e8f4858079ac59d9db1ec5a1349bc486ae8e9ba55698cc1f4a1dff/wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", size = 38316 }, - { url = "https://files.pythonhosted.org/packages/7e/79/5ff0a5c54bda5aec75b36453d06be4f83d5cd4932cc84b7cb2b52cee23e2/wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", size = 86322 }, - { url = "https://files.pythonhosted.org/packages/c4/81/e799bf5d419f422d8712108837c1d9bf6ebe3cb2a81ad94413449543a923/wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", size = 79055 }, - { url = "https://files.pythonhosted.org/packages/62/62/30ca2405de6a20448ee557ab2cd61ab9c5900be7cbd18a2639db595f0b98/wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", size = 87291 }, - { url = "https://files.pythonhosted.org/packages/49/4e/5d2f6d7b57fc9956bf06e944eb00463551f7d52fc73ca35cfc4c2cdb7aed/wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", size = 90374 }, - { url = "https://files.pythonhosted.org/packages/a6/9b/c2c21b44ff5b9bf14a83252a8b973fb84923764ff63db3e6dfc3895cf2e0/wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", size = 83896 }, - { url = "https://files.pythonhosted.org/packages/14/26/93a9fa02c6f257df54d7570dfe8011995138118d11939a4ecd82cb849613/wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", size = 91738 }, - { url = "https://files.pythonhosted.org/packages/a2/5b/4660897233eb2c8c4de3dc7cefed114c61bacb3c28327e64150dc44ee2f6/wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", size = 35568 }, - { url = "https://files.pythonhosted.org/packages/5c/cc/8297f9658506b224aa4bd71906447dea6bb0ba629861a758c28f67428b91/wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", size = 37653 }, - { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 }, + { url = "https://files.pythonhosted.org/packages/0e/40/def56538acddc2f764c157d565b9f989072a1d2f2a8e384324e2e104fc7d/wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a", size = 38766 }, + { url = "https://files.pythonhosted.org/packages/89/e2/8c299f384ae4364193724e2adad99f9504599d02a73ec9199bf3f406549d/wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed", size = 83730 }, + { url = "https://files.pythonhosted.org/packages/29/ef/fcdb776b12df5ea7180d065b28fa6bb27ac785dddcd7202a0b6962bbdb47/wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489", size = 75470 }, + { url = "https://files.pythonhosted.org/packages/55/b5/698bd0bf9fbb3ddb3a2feefbb7ad0dea1205f5d7d05b9cbab54f5db731aa/wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9", size = 83168 }, + { url = "https://files.pythonhosted.org/packages/ce/07/701a5cee28cb4d5df030d4b2649319e36f3d9fdd8000ef1d84eb06b9860d/wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339", size = 82307 }, + { url = "https://files.pythonhosted.org/packages/42/92/c48ba92cda6f74cb914dc3c5bba9650dc80b790e121c4b987f3a46b028f5/wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d", size = 75101 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/9276d3269334138b88a2947efaaf6335f61d547698e50dff672ade24f2c6/wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b", size = 81835 }, + { url = "https://files.pythonhosted.org/packages/b9/4c/39595e692753ef656ea94b51382cc9aea662fef59d7910128f5906486f0e/wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346", size = 36412 }, + { url = "https://files.pythonhosted.org/packages/63/bb/c293a67fb765a2ada48f48cd0f2bb957da8161439da4c03ea123b9894c02/wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a", size = 38744 }, + { url = "https://files.pythonhosted.org/packages/85/82/518605474beafff11f1a34759f6410ab429abff9f7881858a447e0d20712/wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569", size = 38904 }, + { url = "https://files.pythonhosted.org/packages/80/6c/17c3b2fed28edfd96d8417c865ef0b4c955dc52c4e375d86f459f14340f1/wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504", size = 88622 }, + { url = "https://files.pythonhosted.org/packages/4a/11/60ecdf3b0fd3dca18978d89acb5d095a05f23299216e925fcd2717c81d93/wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451", size = 80920 }, + { url = "https://files.pythonhosted.org/packages/d2/50/dbef1a651578a3520d4534c1e434989e3620380c1ad97e309576b47f0ada/wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1", size = 89170 }, + { url = "https://files.pythonhosted.org/packages/44/a2/78c5956bf39955288c9e0dd62e807b308c3aa15a0f611fbff52aa8d6b5ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106", size = 86748 }, + { url = "https://files.pythonhosted.org/packages/99/49/2ee413c78fc0bdfebe5bee590bf3becdc1fab0096a7a9c3b5c9666b2415f/wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada", size = 79734 }, + { url = "https://files.pythonhosted.org/packages/c0/8c/4221b7b270e36be90f0930fe15a4755a6ea24093f90b510166e9ed7861ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4", size = 87552 }, + { url = "https://files.pythonhosted.org/packages/4c/6b/1aaccf3efe58eb95e10ce8e77c8909b7a6b0da93449a92c4e6d6d10b3a3d/wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635", size = 36647 }, + { url = "https://files.pythonhosted.org/packages/b3/4f/243f88ac49df005b9129194c6511b3642818b3e6271ddea47a15e2ee4934/wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7", size = 38830 }, + { url = "https://files.pythonhosted.org/packages/67/9c/38294e1bb92b055222d1b8b6591604ca4468b77b1250f59c15256437644f/wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181", size = 38904 }, + { url = "https://files.pythonhosted.org/packages/78/b6/76597fb362cbf8913a481d41b14b049a8813cd402a5d2f84e57957c813ae/wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393", size = 88608 }, + { url = "https://files.pythonhosted.org/packages/bc/69/b500884e45b3881926b5f69188dc542fb5880019d15c8a0df1ab1dfda1f7/wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4", size = 80879 }, + { url = "https://files.pythonhosted.org/packages/52/31/f4cc58afe29eab8a50ac5969963010c8b60987e719c478a5024bce39bc42/wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b", size = 89119 }, + { url = "https://files.pythonhosted.org/packages/aa/9c/05ab6bf75dbae7a9d34975fb6ee577e086c1c26cde3b6cf6051726d33c7c/wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721", size = 86778 }, + { url = "https://files.pythonhosted.org/packages/0e/6c/4b8d42e3db355603d35fe5c9db79c28f2472a6fd1ccf4dc25ae46739672a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90", size = 79793 }, + { url = "https://files.pythonhosted.org/packages/69/23/90e3a2ee210c0843b2c2a49b3b97ffcf9cad1387cb18cbeef9218631ed5a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a", size = 87606 }, + { url = "https://files.pythonhosted.org/packages/5f/06/3683126491ca787d8d71d8d340e775d40767c5efedb35039d987203393b7/wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045", size = 36651 }, + { url = "https://files.pythonhosted.org/packages/f1/bc/3bf6d2ca0d2c030d324ef9272bea0a8fdaff68f3d1fa7be7a61da88e51f7/wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838", size = 38835 }, + { url = "https://files.pythonhosted.org/packages/ce/b5/251165c232d87197a81cd362eeb5104d661a2dd3aa1f0b33e4bf61dda8b8/wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b", size = 40146 }, + { url = "https://files.pythonhosted.org/packages/89/33/1e1bdd3e866eeb73d8c4755db1ceb8a80d5bd51ee4648b3f2247adec4e67/wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379", size = 113444 }, + { url = "https://files.pythonhosted.org/packages/9f/7c/94f53b065a43f5dc1fbdd8b80fd8f41284315b543805c956619c0b8d92f0/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d", size = 101246 }, + { url = "https://files.pythonhosted.org/packages/62/5d/640360baac6ea6018ed5e34e6e80e33cfbae2aefde24f117587cd5efd4b7/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f", size = 109320 }, + { url = "https://files.pythonhosted.org/packages/e3/cf/6c7a00ae86a2e9482c91170aefe93f4ccda06c1ac86c4de637c69133da59/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c", size = 110193 }, + { url = "https://files.pythonhosted.org/packages/cd/cc/aa718df0d20287e8f953ce0e2f70c0af0fba1d3c367db7ee8bdc46ea7003/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b", size = 100460 }, + { url = "https://files.pythonhosted.org/packages/f7/16/9f3ac99fe1f6caaa789d67b4e3c562898b532c250769f5255fa8b8b93983/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab", size = 106347 }, + { url = "https://files.pythonhosted.org/packages/64/85/c77a331b2c06af49a687f8b926fc2d111047a51e6f0b0a4baa01ff3a673a/wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf", size = 37971 }, + { url = "https://files.pythonhosted.org/packages/05/9b/b2469f8be9efed24283fd7b9eeb8e913e9bc0715cf919ea8645e428ab7af/wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a", size = 40755 }, + { url = "https://files.pythonhosted.org/packages/4b/d9/a8ba5e9507a9af1917285d118388c5eb7a81834873f45df213a6fe923774/wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371", size = 23592 }, ]