django-env-loader 1.0.3__tar.gz
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.
- django_env_loader-1.0.3/LICENSE +21 -0
- django_env_loader-1.0.3/PKG-INFO +655 -0
- django_env_loader-1.0.3/README.md +626 -0
- django_env_loader-1.0.3/pyproject.toml +118 -0
- django_env_loader-1.0.3/src/django_env_loader/__init__.py +19 -0
- django_env_loader-1.0.3/src/django_env_loader/exceptions.py +29 -0
- django_env_loader-1.0.3/src/django_env_loader/loader.py +517 -0
- django_env_loader-1.0.3/src/django_env_loader/py.typed +2 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Felipe Abreu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-env-loader
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Gerenciador robusto de variáveis de ambiente e secrets para Django
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: django,env,environment,secrets,docker
|
|
8
|
+
Author: Felipe Abreu
|
|
9
|
+
Author-email: felipeabreu.rj@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: Django
|
|
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: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
|
25
|
+
Project-URL: Homepage, https://github.com/felipeabreu86/django-env-loader
|
|
26
|
+
Project-URL: Repository, https://github.com/felipeabreu86/django-env-loader
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Django Env Loader
|
|
30
|
+
|
|
31
|
+
**Gerenciador robusto e type-safe de variáveis de ambiente para projetos Python e Django.**
|
|
32
|
+
|
|
33
|
+
`django-env-loader` é uma biblioteca moderna que simplifica o gerenciamento de variáveis de ambiente e Docker secrets, oferecendo validação rigorosa, conversão automática de tipos e integração nativa com Django.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## ✨ Características
|
|
38
|
+
|
|
39
|
+
- 🔒 **Type-safe**: Suporte completo a type hints e validação de tipos
|
|
40
|
+
- 🐳 **Docker Secrets**: Suporte nativo a Docker secrets com fallback automático
|
|
41
|
+
- ⚡ **Performance**: Sistema de cache inteligente para secrets
|
|
42
|
+
- 🎯 **Django-ready**: Helpers especializados para configurações Django
|
|
43
|
+
- 🛡️ **Validação robusta**: Conversores seguros com tratamento de erros
|
|
44
|
+
- 🔧 **Configurável**: Prefixos, encoding, strict mode e muito mais
|
|
45
|
+
- 📝 **Bem documentado**: Type hints completos e docstrings detalhadas
|
|
46
|
+
- 🧪 **Testado**: Cobertura de testes > 95%
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 📦 Instalação
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install django-env-loader
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Para uso com Django (instala dependências adicionais):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install django-env-loader[django]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 🚀 Quick Start
|
|
65
|
+
|
|
66
|
+
### Uso básico
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# Forma mais simples - usa instância singleton pré-configurada
|
|
70
|
+
from django_env_loader import env_loader
|
|
71
|
+
|
|
72
|
+
# Obtém variáveis simples
|
|
73
|
+
api_key = env_loader.get("API_KEY", required=True)
|
|
74
|
+
debug = env_loader.get_bool("DEBUG", default=False)
|
|
75
|
+
port = env_loader.get_int("PORT", default=8000)
|
|
76
|
+
|
|
77
|
+
# Listas e dicionários
|
|
78
|
+
allowed_hosts = env_loader.get_list("ALLOWED_HOSTS", default=["localhost"])
|
|
79
|
+
database_options = env_loader.get_dict("DB_OPTIONS") # FORMAT: key1=val1,key2=val2
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Ou crie uma instância customizada:**
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from django_env_loader import EnvLoader, EnvConfig
|
|
86
|
+
from pathlib import Path
|
|
87
|
+
|
|
88
|
+
config = EnvConfig(env_file=Path(".env.production"), prefix="MYAPP_")
|
|
89
|
+
loader = EnvLoader(config)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Uso com Django
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# settings.py
|
|
96
|
+
# Opção 1: Singleton simples
|
|
97
|
+
from django_env_loader import env_loader
|
|
98
|
+
|
|
99
|
+
SECRET_KEY = env_loader.get("SECRET_KEY", required=True)
|
|
100
|
+
DEBUG = env_loader.get_bool("DEBUG", default=False)
|
|
101
|
+
ALLOWED_HOSTS = env_loader.get_list("ALLOWED_HOSTS", default=["localhost"])
|
|
102
|
+
|
|
103
|
+
# Opção 2: DjangoEnvLoader com helpers especializados
|
|
104
|
+
from django_env_loader import DjangoEnvLoader
|
|
105
|
+
|
|
106
|
+
env = DjangoEnvLoader()
|
|
107
|
+
|
|
108
|
+
# Configurações Django com validação automática
|
|
109
|
+
SECRET_KEY = env.get_secret_key() # Obrigatória, busca em secrets primeiro
|
|
110
|
+
DEBUG = env.get_debug(default=False)
|
|
111
|
+
ALLOWED_HOSTS = env.get_allowed_hosts()
|
|
112
|
+
DATABASE_URL = env.get_database_url()
|
|
113
|
+
|
|
114
|
+
# Outras configurações
|
|
115
|
+
DATABASES = {
|
|
116
|
+
'default': dj_database_url.parse(DATABASE_URL)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
EMAIL_HOST = env.get("EMAIL_HOST", default="localhost")
|
|
120
|
+
EMAIL_PORT = env.get_int("EMAIL_PORT", default=587)
|
|
121
|
+
EMAIL_USE_TLS = env.get_bool("EMAIL_USE_TLS", default=True)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 📚 Documentação Completa
|
|
127
|
+
|
|
128
|
+
### Configuração Avançada
|
|
129
|
+
|
|
130
|
+
O `EnvLoader` aceita uma configuração personalizada através da classe `EnvConfig`:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from django_env_loader import EnvLoader, EnvConfig
|
|
134
|
+
from pathlib import Path
|
|
135
|
+
|
|
136
|
+
config = EnvConfig(
|
|
137
|
+
env_file=Path(".env.production"), # Arquivo .env customizado
|
|
138
|
+
secrets_dir=Path("/run/secrets"), # Diretório de Docker secrets
|
|
139
|
+
encoding="utf-8", # Encoding dos arquivos
|
|
140
|
+
prefix="MYAPP_", # Prefixo para todas as variáveis
|
|
141
|
+
override_existing=False, # Não sobrescreve vars já definidas
|
|
142
|
+
auto_cast=True, # Conversão automática de tipos
|
|
143
|
+
cache_secrets=True, # Cache de secrets
|
|
144
|
+
strict_mode=False, # False = warnings, True = exceções
|
|
145
|
+
warn_on_missing=True, # Avisa sobre variáveis não encontradas
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
loader = EnvLoader(config)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Métodos de Obtenção de Variáveis
|
|
152
|
+
|
|
153
|
+
#### `get()` - Método base
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# Obter string simples
|
|
157
|
+
value = loader.get("MY_VAR", default="default_value")
|
|
158
|
+
|
|
159
|
+
# Obrigatória (levanta SecretNotFoundError se não encontrada)
|
|
160
|
+
value = loader.get("REQUIRED_VAR", required=True)
|
|
161
|
+
|
|
162
|
+
# Sem buscar em Docker secrets
|
|
163
|
+
value = loader.get("ENV_ONLY", use_secrets=False)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### `get_bool()` - Booleanos
|
|
167
|
+
|
|
168
|
+
Aceita múltiplos formatos: `true/false`, `1/0`, `yes/no`, `on/off`, `sim/não`
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
debug = loader.get_bool("DEBUG", default=False)
|
|
172
|
+
maintenance = loader.get_bool("MAINTENANCE_MODE", required=True)
|
|
173
|
+
|
|
174
|
+
# Valores aceitos para True
|
|
175
|
+
# "true", "1", "yes", "y", "on", "t", "sim", "s"
|
|
176
|
+
|
|
177
|
+
# Valores aceitos para False
|
|
178
|
+
# "false", "0", "no", "n", "off", "f", "não", "nao", ""
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### `get_int()` - Números inteiros
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
port = loader.get_int("PORT", default=8000)
|
|
185
|
+
workers = loader.get_int("WORKERS", required=True)
|
|
186
|
+
timeout = loader.get_int("TIMEOUT", default=30)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `get_float()` - Números decimais
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
tax_rate = loader.get_float("TAX_RATE", default=0.15)
|
|
193
|
+
temperature = loader.get_float("MAX_TEMP", default=98.6)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### `get_list()` - Listas
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
# Formato: item1,item2,item3
|
|
200
|
+
hosts = loader.get_list("ALLOWED_HOSTS", default=["localhost"])
|
|
201
|
+
|
|
202
|
+
# Delimitador customizado
|
|
203
|
+
emails = loader.get_list("ADMIN_EMAILS", delimiter=";")
|
|
204
|
+
|
|
205
|
+
# De variável de ambiente:
|
|
206
|
+
# ALLOWED_HOSTS=example.com,api.example.com,www.example.com
|
|
207
|
+
# Resultado: ["example.com", "api.example.com", "www.example.com"]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### `get_dict()` - Dicionários
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
# Formato: key1=value1,key2=value2
|
|
214
|
+
options = loader.get_dict("DB_OPTIONS")
|
|
215
|
+
|
|
216
|
+
# Delimitador customizado
|
|
217
|
+
config = loader.get_dict("FEATURE_FLAGS", delimiter=";")
|
|
218
|
+
|
|
219
|
+
# De variável de ambiente:
|
|
220
|
+
# DB_OPTIONS=timeout=30,pool_size=10,ssl=true
|
|
221
|
+
# Resultado: {"timeout": "30", "pool_size": "10", "ssl": "true"}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### `get_with_validator()` - Validação customizada
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from typing import Callable
|
|
228
|
+
from datetime import datetime
|
|
229
|
+
|
|
230
|
+
# Validador de email
|
|
231
|
+
def validate_email(value: str) -> str:
|
|
232
|
+
if "@" not in value:
|
|
233
|
+
raise ValueError(f"Email inválido: {value}")
|
|
234
|
+
return value.lower()
|
|
235
|
+
|
|
236
|
+
admin_email = loader.get_with_validator(
|
|
237
|
+
"ADMIN_EMAIL",
|
|
238
|
+
validator=validate_email,
|
|
239
|
+
required=True
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Validador de data
|
|
243
|
+
def parse_date(value: str) -> datetime:
|
|
244
|
+
return datetime.fromisoformat(value)
|
|
245
|
+
|
|
246
|
+
launch_date = loader.get_with_validator(
|
|
247
|
+
"LAUNCH_DATE",
|
|
248
|
+
validator=parse_date,
|
|
249
|
+
default=datetime.now()
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Validador de URL
|
|
253
|
+
def validate_url(value: str) -> str:
|
|
254
|
+
if not value.startswith(("http://", "https://")):
|
|
255
|
+
raise ValueError(f"URL deve começar com http:// ou https://")
|
|
256
|
+
return value.rstrip("/")
|
|
257
|
+
|
|
258
|
+
api_url = loader.get_with_validator(
|
|
259
|
+
"API_URL",
|
|
260
|
+
validator=validate_url,
|
|
261
|
+
required=True
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Docker Secrets
|
|
266
|
+
|
|
267
|
+
O loader busca automaticamente em Docker secrets antes de tentar variáveis de ambiente:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
# Ordem de busca:
|
|
271
|
+
# 1. /run/secrets/DATABASE_PASSWORD (Docker secret)
|
|
272
|
+
# 2. DATABASE_PASSWORD (variável de ambiente)
|
|
273
|
+
|
|
274
|
+
db_password = loader.get("DATABASE_PASSWORD", required=True, use_secrets=True)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Exemplo com Docker Compose:**
|
|
278
|
+
|
|
279
|
+
```yaml
|
|
280
|
+
# docker-compose.yml
|
|
281
|
+
version: '3.8'
|
|
282
|
+
|
|
283
|
+
services:
|
|
284
|
+
web:
|
|
285
|
+
image: myapp:latest
|
|
286
|
+
secrets:
|
|
287
|
+
- db_password
|
|
288
|
+
- secret_key
|
|
289
|
+
environment:
|
|
290
|
+
- DEBUG=false
|
|
291
|
+
- DATABASE_HOST=db
|
|
292
|
+
|
|
293
|
+
secrets:
|
|
294
|
+
db_password:
|
|
295
|
+
file: ./secrets/db_password.txt
|
|
296
|
+
secret_key:
|
|
297
|
+
file: ./secrets/secret_key.txt
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
# settings.py
|
|
302
|
+
from django_env_loader import DjangoEnvLoader
|
|
303
|
+
|
|
304
|
+
env = DjangoEnvLoader()
|
|
305
|
+
|
|
306
|
+
# Busca automaticamente em /run/secrets/SECRET_KEY
|
|
307
|
+
SECRET_KEY = env.get_secret_key()
|
|
308
|
+
|
|
309
|
+
# Busca em /run/secrets/DATABASE_PASSWORD
|
|
310
|
+
db_password = env.get("DATABASE_PASSWORD", required=True)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Verificação de Variáveis
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
# Verifica se variável existe e não está vazia
|
|
317
|
+
if loader.is_set("FEATURE_FLAG"):
|
|
318
|
+
enable_feature()
|
|
319
|
+
|
|
320
|
+
# Obtém todas as variáveis carregadas
|
|
321
|
+
all_vars = loader.get_all(include_secrets=False)
|
|
322
|
+
print(all_vars)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Gerenciamento de Cache
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
# Limpar cache de secrets (útil para recarregar valores)
|
|
329
|
+
loader.clear_cache()
|
|
330
|
+
|
|
331
|
+
# Reset completo do singleton (útil em testes)
|
|
332
|
+
from django_env_loader import EnvLoader
|
|
333
|
+
EnvLoader.reset_singleton()
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## 🎯 Casos de Uso Comuns
|
|
339
|
+
|
|
340
|
+
### 1. Configuração Multi-ambiente
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
# .env.development
|
|
344
|
+
DEBUG=true
|
|
345
|
+
DATABASE_URL=sqlite:///db.sqlite3
|
|
346
|
+
ALLOWED_HOSTS=localhost,127.0.0.1
|
|
347
|
+
|
|
348
|
+
# .env.production
|
|
349
|
+
DEBUG=false
|
|
350
|
+
DATABASE_URL=postgresql://user:pass@db:5432/mydb
|
|
351
|
+
ALLOWED_HOSTS=myapp.com,www.myapp.com
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
# settings.py
|
|
356
|
+
import os
|
|
357
|
+
from pathlib import Path
|
|
358
|
+
from django_env_loader import env_loader, EnvLoader, EnvConfig
|
|
359
|
+
|
|
360
|
+
# Opção 1: Usando singleton com config customizada
|
|
361
|
+
ENVIRONMENT = os.getenv("ENVIRONMENT", "development")
|
|
362
|
+
env_file = Path(f".env.{ENVIRONMENT}")
|
|
363
|
+
|
|
364
|
+
# Reconfigura o singleton (faça isso antes de qualquer uso)
|
|
365
|
+
EnvLoader.reset_singleton()
|
|
366
|
+
config = EnvConfig(env_file=env_file, strict_mode=True)
|
|
367
|
+
env = EnvLoader(config)
|
|
368
|
+
|
|
369
|
+
# Opção 2: Criar nova instância (não recomendado se usar singleton em outros lugares)
|
|
370
|
+
# config = EnvConfig(env_file=env_file, strict_mode=True)
|
|
371
|
+
# env = EnvLoader(config)
|
|
372
|
+
|
|
373
|
+
DEBUG = env.get_bool("DEBUG")
|
|
374
|
+
DATABASE_URL = env.get("DATABASE_URL", required=True)
|
|
375
|
+
ALLOWED_HOSTS = env.get_list("ALLOWED_HOSTS")
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### 2. Validação de Configurações Obrigatórias
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
from django_env_loader import env_loader, SecretNotFoundError
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
# Garante que variáveis críticas existem
|
|
385
|
+
api_key = env_loader.get("API_KEY", required=True)
|
|
386
|
+
secret_key = env_loader.get("SECRET_KEY", required=True)
|
|
387
|
+
database_url = env_loader.get("DATABASE_URL", required=True)
|
|
388
|
+
|
|
389
|
+
except SecretNotFoundError as e:
|
|
390
|
+
print(f"❌ Configuração faltando: {e}")
|
|
391
|
+
print(f" Locais buscados: {e.searched_locations}")
|
|
392
|
+
import sys
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 3. Feature Flags
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
# .env
|
|
400
|
+
FEATURE_FLAGS=new_ui=true,beta_api=false,analytics=true
|
|
401
|
+
|
|
402
|
+
# app.py
|
|
403
|
+
from django_env_loader import env_loader
|
|
404
|
+
|
|
405
|
+
features = env_loader.get_dict("FEATURE_FLAGS")
|
|
406
|
+
|
|
407
|
+
if features.get("new_ui") == "true":
|
|
408
|
+
enable_new_ui()
|
|
409
|
+
|
|
410
|
+
if features.get("beta_api") == "true":
|
|
411
|
+
enable_beta_api()
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### 4. Configuração de Múltiplos Serviços
|
|
415
|
+
|
|
416
|
+
```python
|
|
417
|
+
from django_env_loader import EnvLoader, EnvConfig
|
|
418
|
+
|
|
419
|
+
# Serviço de email
|
|
420
|
+
email_config = EnvConfig(prefix="EMAIL_")
|
|
421
|
+
email_loader = EnvLoader(email_config)
|
|
422
|
+
|
|
423
|
+
EMAIL_CONFIG = {
|
|
424
|
+
"host": email_loader.get("HOST", default="localhost"),
|
|
425
|
+
"port": email_loader.get_int("PORT", default=587),
|
|
426
|
+
"username": email_loader.get("USERNAME"),
|
|
427
|
+
"password": email_loader.get("PASSWORD", use_secrets=True),
|
|
428
|
+
"use_tls": email_loader.get_bool("USE_TLS", default=True),
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
# Serviço de cache
|
|
432
|
+
cache_config = EnvConfig(prefix="REDIS_")
|
|
433
|
+
cache_loader = EnvLoader(cache_config)
|
|
434
|
+
|
|
435
|
+
REDIS_CONFIG = {
|
|
436
|
+
"host": cache_loader.get("HOST", default="localhost"),
|
|
437
|
+
"port": cache_loader.get_int("PORT", default=6379),
|
|
438
|
+
"db": cache_loader.get_int("DB", default=0),
|
|
439
|
+
"password": cache_loader.get("PASSWORD", use_secrets=True),
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### 5. Integração com Pydantic Settings
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
from pydantic_settings import BaseSettings
|
|
447
|
+
from django_env_loader import env_loader
|
|
448
|
+
|
|
449
|
+
class Settings(BaseSettings):
|
|
450
|
+
app_name: str = env_loader.get("APP_NAME", default="MyApp")
|
|
451
|
+
debug: bool = env_loader.get_bool("DEBUG", default=False)
|
|
452
|
+
database_url: str = env_loader.get("DATABASE_URL", required=True)
|
|
453
|
+
secret_key: str = env_loader.get("SECRET_KEY", required=True)
|
|
454
|
+
allowed_hosts: list[str] = env_loader.get_list("ALLOWED_HOSTS")
|
|
455
|
+
|
|
456
|
+
class Config:
|
|
457
|
+
case_sensitive = False
|
|
458
|
+
|
|
459
|
+
settings = Settings()
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## 🔒 Segurança
|
|
465
|
+
|
|
466
|
+
### Boas Práticas
|
|
467
|
+
|
|
468
|
+
1. **Nunca commite arquivos `.env`** com dados sensíveis
|
|
469
|
+
```bash
|
|
470
|
+
# .gitignore
|
|
471
|
+
.env
|
|
472
|
+
.env.*
|
|
473
|
+
!.env.example
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
2. **Use Docker secrets** para dados sensíveis em produção
|
|
477
|
+
```python
|
|
478
|
+
# Prioriza secrets sobre env vars
|
|
479
|
+
secret_key = loader.get("SECRET_KEY", use_secrets=True, required=True)
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
3. **Valide configurações** no startup
|
|
483
|
+
```python
|
|
484
|
+
# manage.py ou app startup
|
|
485
|
+
from django_env_loader import DjangoEnvLoader, SecretNotFoundError
|
|
486
|
+
|
|
487
|
+
env = DjangoEnvLoader()
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
env.get_secret_key()
|
|
491
|
+
env.get_database_url()
|
|
492
|
+
except SecretNotFoundError as e:
|
|
493
|
+
logger.critical(f"Configuração crítica faltando: {e}")
|
|
494
|
+
sys.exit(1)
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
4. **Use strict_mode** em produção
|
|
498
|
+
```python
|
|
499
|
+
config = EnvConfig(strict_mode=True)
|
|
500
|
+
loader = EnvLoader(config)
|
|
501
|
+
# Agora erros de validação levantam exceções em vez de warnings
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## 🧪 Testes
|
|
507
|
+
|
|
508
|
+
### Testando código que usa EnvLoader
|
|
509
|
+
|
|
510
|
+
```python
|
|
511
|
+
import pytest
|
|
512
|
+
from django_env_loader import EnvLoader, EnvConfig
|
|
513
|
+
from pathlib import Path
|
|
514
|
+
|
|
515
|
+
@pytest.fixture
|
|
516
|
+
def loader():
|
|
517
|
+
"""Fixture que cria loader limpo para cada teste."""
|
|
518
|
+
EnvLoader.reset_singleton()
|
|
519
|
+
config = EnvConfig(env_file=Path("tests/.env.test"))
|
|
520
|
+
return EnvLoader(config)
|
|
521
|
+
|
|
522
|
+
def test_get_database_url(loader, monkeypatch):
|
|
523
|
+
"""Testa obtenção de DATABASE_URL."""
|
|
524
|
+
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/testdb")
|
|
525
|
+
|
|
526
|
+
url = loader.get("DATABASE_URL", required=True)
|
|
527
|
+
assert url == "postgresql://localhost/testdb"
|
|
528
|
+
|
|
529
|
+
def test_missing_required_variable(loader):
|
|
530
|
+
"""Testa erro ao buscar variável obrigatória inexistente."""
|
|
531
|
+
from django_env_loader import SecretNotFoundError
|
|
532
|
+
|
|
533
|
+
with pytest.raises(SecretNotFoundError) as exc_info:
|
|
534
|
+
loader.get("NONEXISTENT", required=True)
|
|
535
|
+
|
|
536
|
+
assert "NONEXISTENT" in str(exc_info.value)
|
|
537
|
+
|
|
538
|
+
def test_bool_conversion(loader, monkeypatch):
|
|
539
|
+
"""Testa conversão de valores booleanos."""
|
|
540
|
+
monkeypatch.setenv("FEATURE_ENABLED", "true")
|
|
541
|
+
assert loader.get_bool("FEATURE_ENABLED") is True
|
|
542
|
+
|
|
543
|
+
monkeypatch.setenv("FEATURE_DISABLED", "0")
|
|
544
|
+
assert loader.get_bool("FEATURE_DISABLED") is False
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## 📖 API Reference
|
|
550
|
+
|
|
551
|
+
### Classes Principais
|
|
552
|
+
|
|
553
|
+
#### `EnvLoader`
|
|
554
|
+
Gerenciador principal de variáveis de ambiente.
|
|
555
|
+
|
|
556
|
+
**Métodos:**
|
|
557
|
+
- `get(key, *, default, required, use_secrets)` → `str | T`
|
|
558
|
+
- `get_bool(key, *, default, required, use_secrets)` → `bool`
|
|
559
|
+
- `get_int(key, *, default, required, use_secrets)` → `int`
|
|
560
|
+
- `get_float(key, *, default, required, use_secrets)` → `float`
|
|
561
|
+
- `get_list(key, *, default, delimiter, required, use_secrets)` → `list[str]`
|
|
562
|
+
- `get_dict(key, *, default, delimiter, required, use_secrets)` → `dict[str, str]`
|
|
563
|
+
- `get_with_validator(key, validator, *, default, required, use_secrets)` → `T | None`
|
|
564
|
+
- `is_set(key, *, use_secrets)` → `bool`
|
|
565
|
+
- `get_all(*, include_secrets)` → `dict[str, str]`
|
|
566
|
+
- `clear_cache()` → `None`
|
|
567
|
+
- `reset_singleton()` → `None` (class method)
|
|
568
|
+
|
|
569
|
+
#### `DjangoEnvLoader`
|
|
570
|
+
Subclasse especializada para Django.
|
|
571
|
+
|
|
572
|
+
**Métodos adicionais:**
|
|
573
|
+
- `get_database_url(default)` → `str`
|
|
574
|
+
- `get_allowed_hosts()` → `list[str]`
|
|
575
|
+
- `get_debug(default)` → `bool`
|
|
576
|
+
- `get_secret_key()` → `str`
|
|
577
|
+
|
|
578
|
+
#### `EnvConfig`
|
|
579
|
+
Configuração do EnvLoader.
|
|
580
|
+
|
|
581
|
+
**Atributos:**
|
|
582
|
+
- `env_file: Path | str | None`
|
|
583
|
+
- `secrets_dir: Path`
|
|
584
|
+
- `encoding: str`
|
|
585
|
+
- `prefix: str`
|
|
586
|
+
- `override_existing: bool`
|
|
587
|
+
- `auto_cast: bool`
|
|
588
|
+
- `cache_secrets: bool`
|
|
589
|
+
- `strict_mode: bool`
|
|
590
|
+
- `warn_on_missing: bool`
|
|
591
|
+
|
|
592
|
+
### Exceções
|
|
593
|
+
|
|
594
|
+
#### `SecretNotFoundError`
|
|
595
|
+
Levantada quando um secret obrigatório não é encontrado.
|
|
596
|
+
|
|
597
|
+
**Atributos:**
|
|
598
|
+
- `key: str` - Nome da variável
|
|
599
|
+
- `searched_locations: list[str]` - Locais onde foi buscada
|
|
600
|
+
|
|
601
|
+
#### `ValidationError`
|
|
602
|
+
Levantada quando a validação de uma variável falha.
|
|
603
|
+
|
|
604
|
+
**Atributos:**
|
|
605
|
+
- `key: str` - Nome da variável
|
|
606
|
+
- `value: Any` - Valor que falhou na validação
|
|
607
|
+
- `reason: str` - Motivo da falha
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## 🤝 Contribuindo
|
|
612
|
+
|
|
613
|
+
Contribuições são bem-vindas! Por favor:
|
|
614
|
+
|
|
615
|
+
1. Faça um fork do projeto
|
|
616
|
+
2. Crie uma branch para sua feature (`git checkout -b feature/AmazingFeature`)
|
|
617
|
+
3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`)
|
|
618
|
+
4. Push para a branch (`git push origin feature/AmazingFeature`)
|
|
619
|
+
5. Abra um Pull Request
|
|
620
|
+
|
|
621
|
+
### Diretrizes de Desenvolvimento
|
|
622
|
+
|
|
623
|
+
- Escreva testes para novas funcionalidades
|
|
624
|
+
- Mantenha cobertura de testes > 95%
|
|
625
|
+
- Use type hints em todo o código
|
|
626
|
+
- Siga o guia de estilo PEP 8
|
|
627
|
+
- Atualize a documentação
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## 📝 Changelog
|
|
632
|
+
|
|
633
|
+
Veja o arquivo [CHANGELOG](CHANGELOG.md) para histórico de mudanças.
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## 📄 Licença
|
|
638
|
+
|
|
639
|
+
Este projeto está licenciado sob a Licença MIT - veja o arquivo [LICENSE](LICENSE) para detalhes.
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## 📬 Suporte
|
|
644
|
+
|
|
645
|
+
- **Issues**: [GitHub Issues](https://github.com/felipeabreu86/django-env-loader/issues)
|
|
646
|
+
- **Discussões**: [GitHub Discussions](https://github.com/felipeabreu86/django-env-loader/discussions)
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## 🔗 Links Úteis
|
|
651
|
+
|
|
652
|
+
- [PyPI](https://pypi.org/project/django-env-loader/)
|
|
653
|
+
- [Código fonte](https://github.com/felipeabreu86/django-env-loader)
|
|
654
|
+
- [Exemplos](https://github.com/felipeabreu86/django-env-loader/tree/main/examples)
|
|
655
|
+
|