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.
@@ -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
+