diff --git a/.env.example b/.env.example index 536a738..f5427fc 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,4 @@ MINIO_ENDPOINT=localhost:9000 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_BUCKET=my-bucket +MINIO_SECURE=false diff --git a/README.md b/README.md index 2dfdd26..45593f9 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Requires Redis with the RedisJSON module (e.g. redis-stack). | `MINIO_ACCESS_KEY` | Access key | | `MINIO_SECRET_KEY` | Secret key | | `MINIO_BUCKET` | Bucket name (created on connect if missing) | +| `MINIO_SECURE` | Use HTTPS (`true`/`false`; default: `true`) | Copy [`.env.example`](.env.example) to `.env` for local development. `RedisConfig.from_env()` and `MinioConfig.from_env()` load `.env` automatically when resolving configuration from the environment. @@ -59,6 +60,7 @@ minio = MinioAdapter( access_key="minioadmin", secret_key="minioadmin", bucket="my-bucket", + secure=False, ) ) ``` diff --git a/python_repositories/adapters/minio_adapter.py b/python_repositories/adapters/minio_adapter.py index 947b952..02775af 100644 --- a/python_repositories/adapters/minio_adapter.py +++ b/python_repositories/adapters/minio_adapter.py @@ -25,6 +25,7 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter): access_key_env_var_name: str = "MINIO_ACCESS_KEY" secret_key_env_var_name: str = "MINIO_SECRET_KEY" bucket_env_var_name: str = "MINIO_BUCKET" + secure_env_var_name: str = "MINIO_SECURE" chunk_size: int = 5 * 2**20 # 5 MiB connection_name: str = "Minio" @@ -35,16 +36,16 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter): client: minio.Minio | None = None, ) -> None: super().__init__() - if client is not None and config is None: - raise ValueError("config is required when client is provided") - if config is None and client is None: + if config is None: + if client is not None: + raise ValueError("config is required when client is provided") config = MinioConfig.from_env( self.endpoint_env_var_name, self.access_key_env_var_name, self.secret_key_env_var_name, self.bucket_env_var_name, + self.secure_env_var_name, ) - assert config is not None self._config = config self._client_injected = client is not None self._client: minio.Minio | None = client diff --git a/python_repositories/adapters/redis_adapter.py b/python_repositories/adapters/redis_adapter.py index e056b4c..47a88a4 100644 --- a/python_repositories/adapters/redis_adapter.py +++ b/python_repositories/adapters/redis_adapter.py @@ -34,11 +34,10 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter): client: redis.Redis | None = None, ) -> None: super().__init__() - if client is not None and config is None: - raise ValueError("config is required when client is provided") - if config is None and client is None: + if config is None: + if client is not None: + raise ValueError("config is required when client is provided") config = RedisConfig.from_env(self.uri_env_var_name) - assert config is not None self._config = config self._client_injected = client is not None self._client: redis.Redis | None = client diff --git a/python_repositories/config/minio_config.py b/python_repositories/config/minio_config.py index 4cdd9d4..0405ddb 100644 --- a/python_repositories/config/minio_config.py +++ b/python_repositories/config/minio_config.py @@ -9,6 +9,21 @@ from python_utils import check_env from python_repositories.config.dotenv_loader import load_dotenv +_TRUTHY = frozenset({"1", "true", "yes", "on"}) +_FALSY = frozenset({"0", "false", "no", "off"}) + + +def _env_bool(name: str, default: bool) -> bool: + raw = os.getenv(name) + if raw is None: + return default + normalized = raw.strip().lower() + if normalized in _TRUTHY: + return True + if normalized in _FALSY: + return False + raise ValueError(f"Invalid boolean value for {name}: {raw!r}") + @dataclass(frozen=True) class MinioConfig: @@ -18,7 +33,7 @@ class MinioConfig: access_key: str secret_key: str bucket: str - secure: bool = False + secure: bool = True @classmethod def from_env( @@ -27,6 +42,7 @@ class MinioConfig: access_key_env_var_name: str = "MINIO_ACCESS_KEY", secret_key_env_var_name: str = "MINIO_SECRET_KEY", bucket_env_var_name: str = "MINIO_BUCKET", + secure_env_var_name: str = "MINIO_SECURE", *, use_dotenv: bool = True, ) -> MinioConfig: @@ -45,4 +61,5 @@ class MinioConfig: access_key=str(os.getenv(access_key_env_var_name)), secret_key=str(os.getenv(secret_key_env_var_name)), bucket=str(os.getenv(bucket_env_var_name)), + secure=_env_bool(secure_env_var_name, default=True), ) diff --git a/tests/conftest.py b/tests/conftest.py index 0274983..09b99f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,4 +8,5 @@ TEST_MINIO_CONFIG = MinioConfig( access_key="minioadmin", secret_key="minioadmin", bucket="test-bucket", + secure=False, ) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b67e399..cf1e821 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -70,6 +70,7 @@ def minio_container() -> Generator[dict[str, str], None, None]: "MINIO_ACCESS_KEY": MINIO_ACCESS_KEY, "MINIO_SECRET_KEY": MINIO_SECRET_KEY, "MINIO_BUCKET": MINIO_BUCKET, + "MINIO_SECURE": "false", } yield env_vars @@ -91,6 +92,7 @@ def minio_config(minio_container: dict[str, str]) -> MinioConfig: access_key=minio_container["MINIO_ACCESS_KEY"], secret_key=minio_container["MINIO_SECRET_KEY"], bucket=minio_container["MINIO_BUCKET"], + secure=False, ) diff --git a/tests/unit/minio_config_test.py b/tests/unit/minio_config_test.py index 8afaec1..e530c6f 100644 --- a/tests/unit/minio_config_test.py +++ b/tests/unit/minio_config_test.py @@ -7,17 +7,59 @@ import pytest from python_repositories.config import MinioConfig -def test_from_env_loads_all_fields(monkeypatch: pytest.MonkeyPatch) -> None: +def _set_required_minio_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("MINIO_ENDPOINT", "localhost:9000") monkeypatch.setenv("MINIO_ACCESS_KEY", "access") monkeypatch.setenv("MINIO_SECRET_KEY", "secret") monkeypatch.setenv("MINIO_BUCKET", "my-bucket") + + +def test_from_env_loads_all_fields(monkeypatch: pytest.MonkeyPatch) -> None: + _set_required_minio_env(monkeypatch) config = MinioConfig.from_env(use_dotenv=False) assert config.endpoint == "localhost:9000" assert config.access_key == "access" assert config.secret_key == "secret" assert config.bucket == "my-bucket" - assert config.secure is False + assert config.secure is True + + +def test_from_env_defaults_secure_to_true_when_unset( + monkeypatch: pytest.MonkeyPatch, +) -> None: + _set_required_minio_env(monkeypatch) + monkeypatch.delenv("MINIO_SECURE", raising=False) + config = MinioConfig.from_env(use_dotenv=False) + assert config.secure is True + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("true", True), + ("1", True), + ("yes", True), + ("false", False), + ("0", False), + ("no", False), + ], +) +def test_from_env_parses_secure( + monkeypatch: pytest.MonkeyPatch, + value: str, + expected: bool, +) -> None: + _set_required_minio_env(monkeypatch) + monkeypatch.setenv("MINIO_SECURE", value) + config = MinioConfig.from_env(use_dotenv=False) + assert config.secure is expected + + +def test_from_env_raises_for_invalid_secure(monkeypatch: pytest.MonkeyPatch) -> None: + _set_required_minio_env(monkeypatch) + monkeypatch.setenv("MINIO_SECURE", "not-a-bool") + with pytest.raises(ValueError, match="MINIO_SECURE"): + MinioConfig.from_env(use_dotenv=False) def test_from_env_raises_when_endpoint_missing(