envilder 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
envilder/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ from envilder.application.envilder_client import EnvilderClient
2
+ from envilder.application.map_file_parser import MapFileParser
3
+ from envilder.domain.envilder_options import EnvilderOptions
4
+ from envilder.domain.map_file_config import MapFileConfig
5
+ from envilder.domain.parsed_map_file import ParsedMapFile
6
+ from envilder.domain.ports.secret_provider import ISecretProvider
7
+ from envilder.domain.secret_provider_type import SecretProviderType
8
+ from envilder.infrastructure.aws.aws_ssm_secret_provider import (
9
+ AwsSsmSecretProvider,
10
+ )
11
+ from envilder.infrastructure.azure.azure_key_vault_secret_provider import (
12
+ AzureKeyVaultSecretProvider,
13
+ )
14
+ from envilder.infrastructure.secret_provider_factory import (
15
+ SecretProviderFactory,
16
+ )
17
+
18
+ __all__ = [
19
+ "AwsSsmSecretProvider",
20
+ "AzureKeyVaultSecretProvider",
21
+ "EnvilderClient",
22
+ "EnvilderOptions",
23
+ "ISecretProvider",
24
+ "MapFileConfig",
25
+ "MapFileParser",
26
+ "ParsedMapFile",
27
+ "SecretProviderFactory",
28
+ "SecretProviderType",
29
+ ]
@@ -0,0 +1,7 @@
1
+ from envilder.application.envilder_client import EnvilderClient
2
+ from envilder.application.map_file_parser import MapFileParser
3
+
4
+ __all__ = [
5
+ "EnvilderClient",
6
+ "MapFileParser",
7
+ ]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from envilder.domain.parsed_map_file import ParsedMapFile
6
+ from envilder.domain.ports.secret_provider import ISecretProvider
7
+
8
+
9
+ class EnvilderClient:
10
+ def __init__(self, secret_provider: ISecretProvider) -> None:
11
+ if secret_provider is None:
12
+ raise ValueError("secret_provider cannot be None")
13
+ self._secret_provider = secret_provider
14
+
15
+ def resolve_secrets(self, map_file: ParsedMapFile) -> dict[str, str]:
16
+ result: dict[str, str] = {}
17
+
18
+ for env_var_name, secret_path in map_file.mappings.items():
19
+ secret_value = self._secret_provider.get_secret(secret_path)
20
+ if secret_value is not None:
21
+ result[env_var_name] = secret_value
22
+
23
+ return result
24
+
25
+ @staticmethod
26
+ def inject_into_environment(secrets: dict[str, str]) -> None:
27
+ for key, value in secrets.items():
28
+ os.environ[key] = value
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from envilder.domain.map_file_config import MapFileConfig
6
+ from envilder.domain.parsed_map_file import ParsedMapFile
7
+ from envilder.domain.secret_provider_type import SecretProviderType
8
+
9
+ _CONFIG_KEY = "$config"
10
+
11
+ _PROVIDER_MAP = {
12
+ "aws": SecretProviderType.AWS,
13
+ "azure": SecretProviderType.AZURE,
14
+ }
15
+
16
+
17
+ class MapFileParser:
18
+ def parse(self, json_content: str) -> ParsedMapFile:
19
+ document = json.loads(json_content)
20
+
21
+ if not isinstance(document, dict):
22
+ raise ValueError("Map file must be a JSON object.")
23
+
24
+ mappings: dict[str, str] = {}
25
+ config = MapFileConfig()
26
+
27
+ for key, value in document.items():
28
+ if key == _CONFIG_KEY:
29
+ if isinstance(value, dict):
30
+ config = _deserialize_config(value)
31
+ continue
32
+
33
+ if isinstance(value, str):
34
+ mappings[key] = value
35
+
36
+ return ParsedMapFile(config=config, mappings=mappings)
37
+
38
+
39
+ def _deserialize_config(raw: dict[str, object]) -> MapFileConfig:
40
+ provider_raw = raw.get("provider")
41
+ provider = None
42
+ if provider_raw is not None:
43
+ if not isinstance(provider_raw, str):
44
+ raise ValueError("Provider must be a string.")
45
+ normalized = provider_raw.strip().lower()
46
+ provider = _PROVIDER_MAP.get(normalized)
47
+ if provider is None:
48
+ raise ValueError(
49
+ f"Unsupported provider '{provider_raw}'."
50
+ " Expected one of: aws, azure."
51
+ )
52
+
53
+ vault_url = raw.get("vaultUrl")
54
+ profile = raw.get("profile")
55
+
56
+ return MapFileConfig(
57
+ provider=provider,
58
+ vault_url=vault_url if isinstance(vault_url, str) else None,
59
+ profile=profile if isinstance(profile, str) else None,
60
+ )
@@ -0,0 +1,13 @@
1
+ from envilder.domain.envilder_options import EnvilderOptions
2
+ from envilder.domain.map_file_config import MapFileConfig
3
+ from envilder.domain.parsed_map_file import ParsedMapFile
4
+ from envilder.domain.ports.secret_provider import ISecretProvider
5
+ from envilder.domain.secret_provider_type import SecretProviderType
6
+
7
+ __all__ = [
8
+ "EnvilderOptions",
9
+ "ISecretProvider",
10
+ "MapFileConfig",
11
+ "ParsedMapFile",
12
+ "SecretProviderType",
13
+ ]
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from envilder.domain.secret_provider_type import SecretProviderType
6
+
7
+
8
+ @dataclass
9
+ class EnvilderOptions:
10
+ provider: SecretProviderType | None = field(default=None)
11
+ vault_url: str | None = field(default=None)
12
+ profile: str | None = field(default=None)
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from envilder.domain.secret_provider_type import SecretProviderType
6
+
7
+
8
+ @dataclass
9
+ class MapFileConfig:
10
+ provider: SecretProviderType | None = field(default=None)
11
+ vault_url: str | None = field(default=None)
12
+ profile: str | None = field(default=None)
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from envilder.domain.map_file_config import MapFileConfig
6
+
7
+
8
+ @dataclass
9
+ class ParsedMapFile:
10
+ config: MapFileConfig
11
+ mappings: dict[str, str]
@@ -0,0 +1,5 @@
1
+ from envilder.domain.ports.secret_provider import ISecretProvider
2
+
3
+ __all__ = [
4
+ "ISecretProvider",
5
+ ]
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+
6
+ class ISecretProvider(Protocol):
7
+ def get_secret(self, name: str) -> str | None: ...
@@ -0,0 +1,6 @@
1
+ from enum import Enum
2
+
3
+
4
+ class SecretProviderType(str, Enum):
5
+ AWS = "aws"
6
+ AZURE = "azure"
@@ -0,0 +1,15 @@
1
+ from envilder.infrastructure.aws.aws_ssm_secret_provider import (
2
+ AwsSsmSecretProvider,
3
+ )
4
+ from envilder.infrastructure.azure.azure_key_vault_secret_provider import (
5
+ AzureKeyVaultSecretProvider,
6
+ )
7
+ from envilder.infrastructure.secret_provider_factory import (
8
+ SecretProviderFactory,
9
+ )
10
+
11
+ __all__ = [
12
+ "AwsSsmSecretProvider",
13
+ "AzureKeyVaultSecretProvider",
14
+ "SecretProviderFactory",
15
+ ]
@@ -0,0 +1,5 @@
1
+ from envilder.infrastructure.aws.aws_ssm_secret_provider import (
2
+ AwsSsmSecretProvider,
3
+ )
4
+
5
+ __all__ = ["AwsSsmSecretProvider"]
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from botocore.exceptions import ClientError
4
+ from mypy_boto3_ssm import SSMClient
5
+
6
+ from envilder.domain.ports.secret_provider import ISecretProvider
7
+
8
+
9
+ class AwsSsmSecretProvider(ISecretProvider):
10
+ def __init__(self, ssm_client: SSMClient) -> None:
11
+ if ssm_client is None:
12
+ raise ValueError("ssm_client cannot be None")
13
+ self._ssm_client = ssm_client
14
+
15
+ def get_secret(self, name: str) -> str | None:
16
+ if not name or not name.strip():
17
+ raise ValueError("Secret name cannot be null or whitespace.")
18
+
19
+ try:
20
+ response = self._ssm_client.get_parameter(
21
+ Name=name, WithDecryption=True
22
+ )
23
+ param = response.get("Parameter")
24
+ value = param.get("Value") if param else None
25
+ return str(value) if value is not None else None
26
+ except ClientError as e:
27
+ error = e.response.get("Error", {})
28
+ if error.get("Code") == "ParameterNotFound":
29
+ return None
30
+ raise
@@ -0,0 +1,5 @@
1
+ from envilder.infrastructure.azure.azure_key_vault_secret_provider import (
2
+ AzureKeyVaultSecretProvider,
3
+ )
4
+
5
+ __all__ = ["AzureKeyVaultSecretProvider"]
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from azure.core.exceptions import ResourceNotFoundError
4
+ from azure.keyvault.secrets import SecretClient
5
+
6
+ from envilder.domain.ports.secret_provider import ISecretProvider
7
+
8
+
9
+ class AzureKeyVaultSecretProvider(ISecretProvider):
10
+ def __init__(self, secret_client: SecretClient) -> None:
11
+ if secret_client is None:
12
+ raise ValueError("secret_client cannot be None")
13
+ self._secret_client = secret_client
14
+
15
+ def get_secret(self, name: str) -> str | None:
16
+ if not name or not name.strip():
17
+ raise ValueError("Secret name cannot be null or empty.")
18
+
19
+ try:
20
+ response = self._secret_client.get_secret(name)
21
+ value: str | None = response.value
22
+ return value
23
+ except ResourceNotFoundError:
24
+ return None
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ import boto3
6
+ from azure.identity import DefaultAzureCredential
7
+ from azure.keyvault.secrets import SecretClient
8
+
9
+ from envilder.domain.envilder_options import EnvilderOptions
10
+ from envilder.domain.map_file_config import MapFileConfig
11
+ from envilder.domain.ports.secret_provider import ISecretProvider
12
+ from envilder.domain.secret_provider_type import SecretProviderType
13
+ from envilder.infrastructure.aws.aws_ssm_secret_provider import (
14
+ AwsSsmSecretProvider,
15
+ )
16
+ from envilder.infrastructure.azure.azure_key_vault_secret_provider import (
17
+ AzureKeyVaultSecretProvider,
18
+ )
19
+
20
+
21
+ class SecretProviderFactory:
22
+ @staticmethod
23
+ def create(
24
+ config: MapFileConfig,
25
+ options: EnvilderOptions | None = None,
26
+ ) -> ISecretProvider:
27
+ if config is None:
28
+ raise ValueError("config cannot be None")
29
+
30
+ provider = (
31
+ options.provider
32
+ if options and options.provider
33
+ else config.provider
34
+ )
35
+
36
+ match provider:
37
+ case SecretProviderType.AZURE:
38
+ return _create_azure_provider(config, options)
39
+ case SecretProviderType.AWS | None:
40
+ return _create_aws_provider(config, options)
41
+ case _:
42
+ raise ValueError(f"Unsupported secret provider: {provider!r}")
43
+
44
+
45
+ def _create_azure_provider(
46
+ config: MapFileConfig, options: EnvilderOptions | None
47
+ ) -> AzureKeyVaultSecretProvider:
48
+ vault_url = (
49
+ options.vault_url
50
+ if options and options.vault_url
51
+ else config.vault_url
52
+ )
53
+
54
+ if not vault_url or not vault_url.strip():
55
+ raise ValueError(
56
+ "Vault URL must be provided for Azure Key Vault" " provider."
57
+ )
58
+
59
+ credential = DefaultAzureCredential()
60
+ secret_client = SecretClient(vault_url=vault_url, credential=credential)
61
+ return AzureKeyVaultSecretProvider(secret_client)
62
+
63
+
64
+ def _create_aws_provider(
65
+ config: MapFileConfig, options: EnvilderOptions | None
66
+ ) -> AwsSsmSecretProvider:
67
+ profile = (
68
+ options.profile if options and options.profile else config.profile
69
+ )
70
+
71
+ if profile:
72
+ try:
73
+ region = _resolve_region_from_env()
74
+ session = (
75
+ boto3.Session(profile_name=profile, region_name=region)
76
+ if region
77
+ else boto3.Session(profile_name=profile)
78
+ )
79
+ ssm_client = session.client("ssm")
80
+ except Exception as e:
81
+ raise ValueError(
82
+ f"Failed to create AWS session with profile"
83
+ f" '{profile}': {e}"
84
+ ) from e
85
+ else:
86
+ region = _resolve_region_from_env()
87
+ session = (
88
+ boto3.Session(region_name=region) if region else boto3.Session()
89
+ )
90
+ ssm_client = session.client("ssm")
91
+
92
+ return AwsSsmSecretProvider(ssm_client)
93
+
94
+
95
+ def _resolve_region_from_env() -> str | None:
96
+ return os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION")
envilder/py.typed ADDED
File without changes
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: envilder
3
+ Version: 0.1.0
4
+ Summary: Securely load environment variables from AWS SSM Parameter Store or Azure Key Vault.
5
+ Project-URL: Homepage, https://github.com/macalbert/envilder
6
+ Project-URL: Repository, https://github.com/macalbert/envilder
7
+ Project-URL: Documentation, https://github.com/macalbert/envilder/tree/main/src/sdks/python
8
+ Project-URL: Issues, https://github.com/macalbert/envilder/issues
9
+ Author: macalbert
10
+ License: MIT
11
+ Keywords: aws,azure,envilder,environment,keyvault,secrets,ssm
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: azure-identity>=1.14
23
+ Requires-Dist: azure-keyvault-secrets>=4.7
24
+ Requires-Dist: boto3>=1.26
25
+ Provides-Extra: dev
26
+ Requires-Dist: black>=24.0; extra == 'dev'
27
+ Requires-Dist: boto3-stubs[ssm]>=1.26; extra == 'dev'
28
+ Requires-Dist: isort>=5.13; extra == 'dev'
29
+ Requires-Dist: mypy>=1.10; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: requests>=2.31; extra == 'dev'
32
+ Requires-Dist: testcontainers[localstack]>=4.0; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Envilder Python SDK
36
+
37
+ Securely load environment variables from **AWS SSM Parameter Store** or **Azure Key Vault** directly into your Python application.
38
+ Zero vendor lock-in — secrets stay in your cloud.
39
+
40
+ Part of the [Envilder](https://github.com/macalbert/envilder) project.
41
+
42
+ ## Prerequisites
43
+
44
+ - Python 3.10+
45
+ - **AWS provider**: AWS credentials configured (CLI, environment variables, or IAM role)
46
+ - **Azure provider**: Azure credentials via `az login`, managed identity, or environment variables
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install envilder
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ ### Direct usage
57
+
58
+ ```python
59
+ from envilder import (
60
+ EnvilderClient,
61
+ MapFileParser,
62
+ SecretProviderFactory,
63
+ )
64
+
65
+ json_content = open('secrets-map.json').read()
66
+ map_file = MapFileParser().parse(json_content)
67
+ provider = SecretProviderFactory.create(map_file.config)
68
+ client = EnvilderClient(provider)
69
+ secrets = client.resolve_secrets(map_file)
70
+
71
+ print(secrets['DB_PASSWORD'])
72
+ ```
73
+
74
+ ### Inject into environment
75
+
76
+ ```python
77
+ EnvilderClient.inject_into_environment(secrets)
78
+
79
+ import os
80
+ print(os.environ['DB_PASSWORD'])
81
+ ```
82
+
83
+ ### With runtime overrides (EnvilderOptions)
84
+
85
+ Override the map file's `$config` at runtime — useful for switching providers per environment:
86
+
87
+ ```python
88
+ from envilder import EnvilderOptions, SecretProviderType
89
+
90
+ options = EnvilderOptions(
91
+ provider=SecretProviderType.AZURE,
92
+ vault_url='https://my-vault.vault.azure.net',
93
+ )
94
+ provider = SecretProviderFactory.create(map_file.config, options)
95
+ ```
96
+
97
+ ## Map File Format
98
+
99
+ ```json
100
+ {
101
+ "$config": {
102
+ "provider": "aws",
103
+ "profile": "my-profile"
104
+ },
105
+ "DB_PASSWORD": "/app/prod/db-password",
106
+ "API_KEY": "/app/prod/api-key"
107
+ }
108
+ ```
109
+
110
+ Supported providers: `aws` (default), `azure`.
111
+
112
+ For Azure, add `vaultUrl`:
113
+
114
+ ```json
115
+ {
116
+ "$config": {
117
+ "provider": "azure",
118
+ "vaultUrl": "https://my-vault.vault.azure.net"
119
+ },
120
+ "DB_PASSWORD": "db-password",
121
+ "API_KEY": "api-key"
122
+ }
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
128
+
129
+ ## Development
130
+
131
+ ### Setup
132
+
133
+ ```bash
134
+ # From the repo root
135
+ make install-sdk-python
136
+ ```
137
+
138
+ ### Quality checks
139
+
140
+ ```bash
141
+ make check-sdk-python # black + isort + mypy (no changes)
142
+ make format-sdk-python # auto-format
143
+ ```
144
+
145
+ ### Running tests
146
+
147
+ Unit tests run without any external dependencies:
148
+
149
+ ```bash
150
+ cd tests/sdks/python
151
+ python -m pytest -v -m "not acceptance"
152
+ ```
153
+
154
+ Acceptance tests require **Docker** and a **LocalStack auth token**:
155
+
156
+ ```bash
157
+ export LOCALSTACK_AUTH_TOKEN=<your-token>
158
+ cd tests/sdks/python
159
+ python -m pytest -v -m acceptance
160
+ ```
161
+
162
+ All tests:
163
+
164
+ ```bash
165
+ make test-sdk-python
166
+ ```
@@ -0,0 +1,21 @@
1
+ envilder/__init__.py,sha256=FYL3mKC-Eo--xTdtscDz969_2K-31RS8IjTa6USTEXc,998
2
+ envilder/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ envilder/application/__init__.py,sha256=9qHF5WPYPHkVNI3aI0_P00IhGRgq8SFy_A_VDVip6wA,185
4
+ envilder/application/envilder_client.py,sha256=SuCtF6ffoFWYl-YpmQs7giD6e5QzVFZlZCYCBdxmnSk,941
5
+ envilder/application/map_file_parser.py,sha256=dhFGrlP8ftB5D3c9blwGCkRKLjsI0YSgoJ7NEHYwqd4,1809
6
+ envilder/domain/__init__.py,sha256=CDNs7nHin6FbYlm5nYcbdP3PuZOoKWMjUF4WtSEXLjM,440
7
+ envilder/domain/envilder_options.py,sha256=Glul_NZlyooOu9RFQWgleHWQajBLy5z-bZ4kKKUDzPw,338
8
+ envilder/domain/map_file_config.py,sha256=rBUGyUZgPLI3HF-NP2k6ZMhfGAfQ46WctPHHfEjoPWU,336
9
+ envilder/domain/parsed_map_file.py,sha256=bKcbTUOPZk1ptZY_e5VL3C-_kkuT2aQLjae7P3bvDA8,218
10
+ envilder/domain/secret_provider_type.py,sha256=CjCD36Cv0RazWbbP8I3mBrKcX5TlmSHWwEeGnpA1eyM,97
11
+ envilder/domain/ports/__init__.py,sha256=fbMM9haUZu49JBBBTYkRmu4eRO8xsjOiPOxqVbO1SBU,104
12
+ envilder/domain/ports/secret_provider.py,sha256=wCoSVZOp7tSD4KEoUOMxEXkE3z9gZ5anynVaKWbXREw,154
13
+ envilder/infrastructure/__init__.py,sha256=v9lKtr0qRrdmSPAKa0edcLq_M1ObZZr5aH5fJDqY4BA,403
14
+ envilder/infrastructure/secret_provider_factory.py,sha256=_FbXa2X-JcdipL8gnweQuJq6IoxgVBJHS2SUNjCjxks,3007
15
+ envilder/infrastructure/aws/__init__.py,sha256=P8aZbG2x5d-qqcUKBFFoXFiQNAD3DNtWhRkrjVS67WQ,130
16
+ envilder/infrastructure/aws/aws_ssm_secret_provider.py,sha256=T1Zmpet3Y2ulnaqPHe11t-MtcHPA0aJgBe4TBddoLgM,1059
17
+ envilder/infrastructure/azure/__init__.py,sha256=GDtqMa_NYMbA9TESaAnq-HfPpr5ggLZoYC7oxiBhv4g,154
18
+ envilder/infrastructure/azure/azure_key_vault_secret_provider.py,sha256=xZcY6raehOIeQVRt7_5b28PTJFT4EnZ6nyBeDvtFo2E,831
19
+ envilder-0.1.0.dist-info/METADATA,sha256=mFZkgvJMizFt74rPlWJUBTH6rwS9PQBZontciWmJ-Qk,3980
20
+ envilder-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
21
+ envilder-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any