dotenvmodel 0.1.1__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.
- dotenvmodel/__init__.py +42 -0
- dotenvmodel/coercion.py +398 -0
- dotenvmodel/config.py +483 -0
- dotenvmodel/exceptions.py +131 -0
- dotenvmodel/fields.py +298 -0
- dotenvmodel/loading.py +145 -0
- dotenvmodel/logging_config.py +92 -0
- dotenvmodel/py.typed +0 -0
- dotenvmodel/types.py +480 -0
- dotenvmodel/validation.py +182 -0
- dotenvmodel-0.1.1.dist-info/METADATA +950 -0
- dotenvmodel-0.1.1.dist-info/RECORD +14 -0
- dotenvmodel-0.1.1.dist-info/WHEEL +4 -0
- dotenvmodel-0.1.1.dist-info/licenses/LICENSE +21 -0
dotenvmodel/fields.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Field descriptor and Required sentinel for dotenvmodel."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
|
+
|
|
8
|
+
# Type variable for generic field types
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _MissingSentinel:
|
|
13
|
+
"""Sentinel to indicate a required field with no default."""
|
|
14
|
+
|
|
15
|
+
def __repr__(self) -> str:
|
|
16
|
+
return "..."
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_MISSING = _MissingSentinel()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _RequiredSentinel:
|
|
23
|
+
"""Sentinel to indicate a required field (explicit alternative to no default)."""
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
return "Required"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Public sentinel value for required fields
|
|
30
|
+
# Type checkers will see this as Any to avoid type errors
|
|
31
|
+
Required: Any = _RequiredSentinel()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FieldInfo:
|
|
35
|
+
"""
|
|
36
|
+
Information about a configuration field.
|
|
37
|
+
|
|
38
|
+
This class holds all metadata about a field including its default value,
|
|
39
|
+
validation constraints, and documentation.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
default: Any = _MISSING,
|
|
45
|
+
*,
|
|
46
|
+
default_factory: Callable[[], Any] | None = None,
|
|
47
|
+
alias: str | None = None,
|
|
48
|
+
description: str | None = None,
|
|
49
|
+
# Numeric validation
|
|
50
|
+
ge: int | float | None = None,
|
|
51
|
+
le: int | float | None = None,
|
|
52
|
+
gt: int | float | None = None,
|
|
53
|
+
lt: int | float | None = None,
|
|
54
|
+
# String validation
|
|
55
|
+
min_length: int | None = None,
|
|
56
|
+
max_length: int | None = None,
|
|
57
|
+
regex: str | None = None,
|
|
58
|
+
# General validation
|
|
59
|
+
choices: list[Any] | None = None,
|
|
60
|
+
# Collection validation
|
|
61
|
+
min_items: int | None = None,
|
|
62
|
+
max_items: int | None = None,
|
|
63
|
+
# UUID validation
|
|
64
|
+
uuid_version: int | None = None,
|
|
65
|
+
# Collection parsing
|
|
66
|
+
separator: str = ",",
|
|
67
|
+
# SecretStr options
|
|
68
|
+
url_unquote: bool = True,
|
|
69
|
+
) -> None:
|
|
70
|
+
# Validate that only one default mechanism is used
|
|
71
|
+
if default is not _MISSING and default is not ... and default_factory is not None:
|
|
72
|
+
raise ValueError("Cannot specify both 'default' and 'default_factory'")
|
|
73
|
+
|
|
74
|
+
# Treat ellipsis as _MISSING (Pydantic-style required indicator)
|
|
75
|
+
if default is ...:
|
|
76
|
+
default = _MISSING
|
|
77
|
+
|
|
78
|
+
# Validate numeric constraint types
|
|
79
|
+
for param_name, param_value in [("ge", ge), ("le", le), ("gt", gt), ("lt", lt)]:
|
|
80
|
+
if param_value is not None and not isinstance(param_value, (int, float, Decimal)):
|
|
81
|
+
raise TypeError(
|
|
82
|
+
f"{param_name} must be int, float, or Decimal, got {type(param_value).__name__}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Validate length/size constraint types
|
|
86
|
+
for param_name, param_value in [
|
|
87
|
+
("min_length", min_length),
|
|
88
|
+
("max_length", max_length),
|
|
89
|
+
("min_items", min_items),
|
|
90
|
+
("max_items", max_items),
|
|
91
|
+
]:
|
|
92
|
+
if param_value is not None and (not isinstance(param_value, int) or param_value < 0):
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"{param_name} must be a non-negative integer, got {param_value!r}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Validate UUID version
|
|
98
|
+
if uuid_version is not None and uuid_version not in (1, 3, 4, 5):
|
|
99
|
+
raise ValueError(f"uuid_version must be 1, 3, 4, or 5, got {uuid_version}")
|
|
100
|
+
|
|
101
|
+
# Validate contradictory constraints
|
|
102
|
+
if ge is not None and le is not None and ge > le:
|
|
103
|
+
raise ValueError(f"ge ({ge}) cannot be greater than le ({le})")
|
|
104
|
+
if gt is not None and lt is not None and gt >= lt:
|
|
105
|
+
raise ValueError(f"gt ({gt}) must be less than lt ({lt})")
|
|
106
|
+
if min_length is not None and max_length is not None and min_length > max_length:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"min_length ({min_length}) cannot be greater than max_length ({max_length})"
|
|
109
|
+
)
|
|
110
|
+
if min_items is not None and max_items is not None and min_items > max_items:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"min_items ({min_items}) cannot be greater than max_items ({max_items})"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self.default = default
|
|
116
|
+
self.default_factory = default_factory
|
|
117
|
+
self.alias = alias
|
|
118
|
+
self.description = description
|
|
119
|
+
|
|
120
|
+
# Numeric constraints
|
|
121
|
+
self.ge = ge
|
|
122
|
+
self.le = le
|
|
123
|
+
self.gt = gt
|
|
124
|
+
self.lt = lt
|
|
125
|
+
|
|
126
|
+
# String constraints
|
|
127
|
+
self.min_length = min_length
|
|
128
|
+
self.max_length = max_length
|
|
129
|
+
self.regex = regex
|
|
130
|
+
# Compile regex pattern with error handling
|
|
131
|
+
if regex:
|
|
132
|
+
try:
|
|
133
|
+
self._compiled_regex = re.compile(regex)
|
|
134
|
+
except re.error as e:
|
|
135
|
+
raise ValueError(f"Invalid regex pattern: {regex!r} - {e}") from e
|
|
136
|
+
else:
|
|
137
|
+
self._compiled_regex = None
|
|
138
|
+
|
|
139
|
+
# General constraints
|
|
140
|
+
self.choices = choices
|
|
141
|
+
|
|
142
|
+
# Collection constraints
|
|
143
|
+
self.min_items = min_items
|
|
144
|
+
self.max_items = max_items
|
|
145
|
+
|
|
146
|
+
# UUID constraints
|
|
147
|
+
self.uuid_version = uuid_version
|
|
148
|
+
|
|
149
|
+
# Collection parsing
|
|
150
|
+
self.separator = separator
|
|
151
|
+
|
|
152
|
+
# SecretStr options
|
|
153
|
+
self.url_unquote = url_unquote
|
|
154
|
+
|
|
155
|
+
# Mark if field is required
|
|
156
|
+
self.required = default is _MISSING and default_factory is None
|
|
157
|
+
|
|
158
|
+
def get_default(self) -> Any:
|
|
159
|
+
"""Get the default value for this field."""
|
|
160
|
+
if self.default_factory is not None:
|
|
161
|
+
return self.default_factory()
|
|
162
|
+
if self.default is _MISSING:
|
|
163
|
+
return _MISSING
|
|
164
|
+
return self.default
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def has_default(self) -> bool:
|
|
168
|
+
"""Check if this field has a default value."""
|
|
169
|
+
return not self.required
|
|
170
|
+
|
|
171
|
+
def __repr__(self) -> str:
|
|
172
|
+
parts = []
|
|
173
|
+
if self.default is not _MISSING:
|
|
174
|
+
parts.append(f"default={self.default!r}")
|
|
175
|
+
if self.default_factory is not None:
|
|
176
|
+
parts.append(f"default_factory={self.default_factory!r}")
|
|
177
|
+
if self.alias:
|
|
178
|
+
parts.append(f"alias={self.alias!r}")
|
|
179
|
+
if self.description:
|
|
180
|
+
parts.append(f"description={self.description!r}")
|
|
181
|
+
|
|
182
|
+
# Add constraints
|
|
183
|
+
if self.ge is not None:
|
|
184
|
+
parts.append(f"ge={self.ge}")
|
|
185
|
+
if self.le is not None:
|
|
186
|
+
parts.append(f"le={self.le}")
|
|
187
|
+
if self.gt is not None:
|
|
188
|
+
parts.append(f"gt={self.gt}")
|
|
189
|
+
if self.lt is not None:
|
|
190
|
+
parts.append(f"lt={self.lt}")
|
|
191
|
+
if self.min_length is not None:
|
|
192
|
+
parts.append(f"min_length={self.min_length}")
|
|
193
|
+
if self.max_length is not None:
|
|
194
|
+
parts.append(f"max_length={self.max_length}")
|
|
195
|
+
if self.regex is not None:
|
|
196
|
+
parts.append(f"regex={self.regex!r}")
|
|
197
|
+
if self.choices is not None:
|
|
198
|
+
parts.append(f"choices={self.choices!r}")
|
|
199
|
+
if self.min_items is not None:
|
|
200
|
+
parts.append(f"min_items={self.min_items}")
|
|
201
|
+
if self.max_items is not None:
|
|
202
|
+
parts.append(f"max_items={self.max_items}")
|
|
203
|
+
if self.uuid_version is not None:
|
|
204
|
+
parts.append(f"uuid_version={self.uuid_version}")
|
|
205
|
+
if self.separator != ",": # Only show if non-default
|
|
206
|
+
parts.append(f"separator={self.separator!r}")
|
|
207
|
+
|
|
208
|
+
return f"FieldInfo({', '.join(parts)})"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def Field(
|
|
212
|
+
default: Any = _MISSING,
|
|
213
|
+
*,
|
|
214
|
+
default_factory: Callable[[], Any] | None = None,
|
|
215
|
+
alias: str | None = None,
|
|
216
|
+
description: str | None = None,
|
|
217
|
+
ge: int | float | None = None,
|
|
218
|
+
le: int | float | None = None,
|
|
219
|
+
gt: int | float | None = None,
|
|
220
|
+
lt: int | float | None = None,
|
|
221
|
+
min_length: int | None = None,
|
|
222
|
+
max_length: int | None = None,
|
|
223
|
+
regex: str | None = None,
|
|
224
|
+
choices: list[Any] | None = None,
|
|
225
|
+
min_items: int | None = None,
|
|
226
|
+
max_items: int | None = None,
|
|
227
|
+
uuid_version: int | None = None,
|
|
228
|
+
separator: str = ",",
|
|
229
|
+
url_unquote: bool = True,
|
|
230
|
+
) -> Any:
|
|
231
|
+
"""
|
|
232
|
+
Define a configuration field with validation and default values.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
default: Default value if environment variable not set
|
|
236
|
+
default_factory: Callable that returns default value (for mutable defaults)
|
|
237
|
+
alias: Alternative environment variable name to read from
|
|
238
|
+
description: Human-readable description for documentation
|
|
239
|
+
ge: Greater than or equal to (>=)
|
|
240
|
+
le: Less than or equal to (<=)
|
|
241
|
+
gt: Greater than (>)
|
|
242
|
+
lt: Less than (<)
|
|
243
|
+
min_length: Minimum string length
|
|
244
|
+
max_length: Maximum string length
|
|
245
|
+
regex: Regular expression pattern to match
|
|
246
|
+
choices: List of allowed values
|
|
247
|
+
min_items: Minimum number of items in collection (list, set, tuple, dict)
|
|
248
|
+
max_items: Maximum number of items in collection (list, set, tuple, dict)
|
|
249
|
+
uuid_version: Required UUID version (1, 3, 4, or 5)
|
|
250
|
+
separator: Separator for list/set/tuple parsing (default: ",")
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
FieldInfo instance containing field metadata
|
|
254
|
+
|
|
255
|
+
Example:
|
|
256
|
+
```python
|
|
257
|
+
class Config(DotEnvConfig):
|
|
258
|
+
# Required field (no default)
|
|
259
|
+
database_url: str = Field()
|
|
260
|
+
|
|
261
|
+
# Optional with default
|
|
262
|
+
debug: bool = Field(default=False)
|
|
263
|
+
|
|
264
|
+
# With validation
|
|
265
|
+
port: int = Field(default=8000, ge=1, le=65535)
|
|
266
|
+
|
|
267
|
+
# With alias
|
|
268
|
+
postgres_dsn: str = Field(alias="DATABASE_URL")
|
|
269
|
+
|
|
270
|
+
# List with custom separator
|
|
271
|
+
tags: list[str] = Field(default_factory=list, separator=";")
|
|
272
|
+
|
|
273
|
+
# Collection size constraints
|
|
274
|
+
allowed_ips: list[str] = Field(min_items=1, max_items=10)
|
|
275
|
+
|
|
276
|
+
# UUID version constraint
|
|
277
|
+
tenant_id: UUID = Field(uuid_version=4)
|
|
278
|
+
```
|
|
279
|
+
"""
|
|
280
|
+
return FieldInfo(
|
|
281
|
+
default=default,
|
|
282
|
+
default_factory=default_factory,
|
|
283
|
+
alias=alias,
|
|
284
|
+
description=description,
|
|
285
|
+
ge=ge,
|
|
286
|
+
le=le,
|
|
287
|
+
gt=gt,
|
|
288
|
+
lt=lt,
|
|
289
|
+
min_length=min_length,
|
|
290
|
+
max_length=max_length,
|
|
291
|
+
regex=regex,
|
|
292
|
+
choices=choices,
|
|
293
|
+
min_items=min_items,
|
|
294
|
+
max_items=max_items,
|
|
295
|
+
uuid_version=uuid_version,
|
|
296
|
+
separator=separator,
|
|
297
|
+
url_unquote=url_unquote,
|
|
298
|
+
)
|
dotenvmodel/loading.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Environment variable and .env file loading logic."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
# Module-level logger
|
|
10
|
+
logger = logging.getLogger("dotenvmodel")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_env_files(
|
|
14
|
+
env: str | None = None,
|
|
15
|
+
*,
|
|
16
|
+
override: bool = True,
|
|
17
|
+
env_dir: Path | None = None,
|
|
18
|
+
) -> dict[str, str]:
|
|
19
|
+
"""
|
|
20
|
+
Load environment variables from cascading .env files.
|
|
21
|
+
|
|
22
|
+
This function implements Node.js-style .env file cascading, loading files
|
|
23
|
+
in the following order (later files override earlier):
|
|
24
|
+
1. .env (base configuration)
|
|
25
|
+
2. .env.local (local base overrides)
|
|
26
|
+
3. .env.{env} (environment-specific)
|
|
27
|
+
4. .env.{env}.local (local environment overrides)
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
env: Environment name (e.g., "dev", "prod", "test"). If None, reads from
|
|
31
|
+
ENV environment variable, defaults to "dev"
|
|
32
|
+
override: If True, .env file values override existing environment variables.
|
|
33
|
+
If False, existing env vars take precedence
|
|
34
|
+
env_dir: Optional custom base directory for .env files. If None, uses
|
|
35
|
+
DOTENV_DIR environment variable or current working directory
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dictionary of all environment variables after loading
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
>>> load_env_files(env="dev", override=True)
|
|
42
|
+
{'DATABASE_URL': 'postgresql://localhost/myapp_dev', ...}
|
|
43
|
+
"""
|
|
44
|
+
# Determine environment
|
|
45
|
+
if env is None:
|
|
46
|
+
env = os.getenv("ENV", "dev")
|
|
47
|
+
|
|
48
|
+
# Validate env parameter to prevent path traversal attacks
|
|
49
|
+
# Only allow alphanumeric characters, hyphens, and underscores
|
|
50
|
+
if not env or not all(c.isalnum() or c in ("-", "_") for c in env):
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Invalid environment name: {env!r}. "
|
|
53
|
+
"Environment names must only contain alphanumeric characters, hyphens, and underscores."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
logger.info(f"Loading configuration for environment: {env}")
|
|
57
|
+
|
|
58
|
+
# Determine base directory
|
|
59
|
+
if env_dir is None:
|
|
60
|
+
env_dir_str = os.getenv("DOTENV_DIR")
|
|
61
|
+
base_dir = Path(env_dir_str) if env_dir_str else Path.cwd()
|
|
62
|
+
else:
|
|
63
|
+
base_dir = env_dir
|
|
64
|
+
|
|
65
|
+
logger.debug(f"Base directory for .env files: {base_dir}")
|
|
66
|
+
|
|
67
|
+
# Validate base directory exists
|
|
68
|
+
if not base_dir.exists():
|
|
69
|
+
logger.error(f"Environment file directory does not exist: {base_dir}")
|
|
70
|
+
raise FileNotFoundError(f"Environment file directory does not exist: {base_dir}")
|
|
71
|
+
|
|
72
|
+
# Define file loading order
|
|
73
|
+
env_files = [
|
|
74
|
+
base_dir / ".env", # Base shared configuration
|
|
75
|
+
base_dir / ".env.local", # Local base overrides
|
|
76
|
+
base_dir / f".env.{env}", # Environment-specific config
|
|
77
|
+
base_dir / f".env.{env}.local", # Local environment overrides
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
logger.debug(f"Searching for .env files in order: {[str(f) for f in env_files]}")
|
|
81
|
+
|
|
82
|
+
# Load each file in order
|
|
83
|
+
loaded_files = []
|
|
84
|
+
missing_files = []
|
|
85
|
+
|
|
86
|
+
for file_path in env_files:
|
|
87
|
+
if file_path.exists():
|
|
88
|
+
logger.info(f"Loading environment variables from {file_path}")
|
|
89
|
+
load_dotenv(file_path, override=override)
|
|
90
|
+
loaded_files.append(str(file_path))
|
|
91
|
+
else:
|
|
92
|
+
logger.debug(f"{file_path} not found (skipping)")
|
|
93
|
+
missing_files.append(str(file_path))
|
|
94
|
+
|
|
95
|
+
if loaded_files:
|
|
96
|
+
logger.info(f"Successfully loaded {len(loaded_files)} file(s): {', '.join(loaded_files)}")
|
|
97
|
+
else:
|
|
98
|
+
logger.warning(f"No .env files found in {base_dir}")
|
|
99
|
+
|
|
100
|
+
# Return all current environment variables
|
|
101
|
+
return dict(os.environ)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_env_var(field_name: str, alias: str | None = None, prefix: str | None = None) -> str | None:
|
|
105
|
+
"""
|
|
106
|
+
Get environment variable value by field name or alias.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
field_name: Name of the field
|
|
110
|
+
alias: Optional alias for the environment variable
|
|
111
|
+
prefix: Optional prefix to prepend to the environment variable name
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Environment variable value or None if not set
|
|
115
|
+
"""
|
|
116
|
+
# Use alias if provided, otherwise convert field_name to UPPER_CASE
|
|
117
|
+
env_var_name = alias if alias else field_name.upper()
|
|
118
|
+
|
|
119
|
+
# Prepend prefix if provided (and alias is not used, since alias is absolute)
|
|
120
|
+
if prefix and not alias:
|
|
121
|
+
env_var_name = f"{prefix}{env_var_name}"
|
|
122
|
+
|
|
123
|
+
return os.getenv(env_var_name)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_env_var_name(field_name: str, alias: str | None = None, prefix: str | None = None) -> str:
|
|
127
|
+
"""
|
|
128
|
+
Get the environment variable name for a field.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
field_name: Name of the field
|
|
132
|
+
alias: Optional alias for the environment variable
|
|
133
|
+
prefix: Optional prefix to prepend to the environment variable name
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Environment variable name
|
|
137
|
+
"""
|
|
138
|
+
# Use alias if provided, otherwise convert field_name to UPPER_CASE
|
|
139
|
+
env_var_name = alias if alias else field_name.upper()
|
|
140
|
+
|
|
141
|
+
# Prepend prefix if provided (and alias is not used, since alias is absolute)
|
|
142
|
+
if prefix and not alias:
|
|
143
|
+
env_var_name = f"{prefix}{env_var_name}"
|
|
144
|
+
|
|
145
|
+
return env_var_name
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Logging configuration utilities for dotenvmodel."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def configure_logging(
|
|
9
|
+
level: str | int | None = None,
|
|
10
|
+
*,
|
|
11
|
+
format_string: str | None = None,
|
|
12
|
+
handler: logging.Handler | None = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Configure logging for dotenvmodel.
|
|
16
|
+
|
|
17
|
+
This is a convenience function to quickly enable dotenvmodel logging.
|
|
18
|
+
For more control, configure the 'dotenvmodel' logger directly using
|
|
19
|
+
the standard logging module.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
level: Logging level. Can be a string ("DEBUG", "INFO", "WARNING", "ERROR")
|
|
23
|
+
or an int (logging.DEBUG, etc.). If None, reads from DOTENVMODEL_LOG_LEVEL
|
|
24
|
+
environment variable, defaults to WARNING.
|
|
25
|
+
format_string: Custom format string for log messages. If None, uses a
|
|
26
|
+
default format with timestamp, level, and message.
|
|
27
|
+
handler: Custom logging handler. If None, uses StreamHandler (stdout).
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
```python
|
|
31
|
+
from dotenvmodel.logging_config import configure_logging
|
|
32
|
+
|
|
33
|
+
# Enable INFO level logging
|
|
34
|
+
configure_logging("INFO")
|
|
35
|
+
|
|
36
|
+
# Or use DEBUG for more verbose output
|
|
37
|
+
configure_logging("DEBUG")
|
|
38
|
+
|
|
39
|
+
# Custom format
|
|
40
|
+
configure_logging(
|
|
41
|
+
"DEBUG",
|
|
42
|
+
format_string="[%(levelname)s] %(message)s"
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
"""
|
|
46
|
+
# Determine log level
|
|
47
|
+
if level is None:
|
|
48
|
+
env_level = os.getenv("DOTENVMODEL_LOG_LEVEL", "WARNING").upper()
|
|
49
|
+
level = getattr(logging, env_level, logging.WARNING)
|
|
50
|
+
elif isinstance(level, str):
|
|
51
|
+
level = getattr(logging, level.upper(), logging.WARNING)
|
|
52
|
+
|
|
53
|
+
# Get the dotenvmodel logger
|
|
54
|
+
logger = logging.getLogger("dotenvmodel")
|
|
55
|
+
logger.setLevel(level)
|
|
56
|
+
|
|
57
|
+
# Remove existing handlers to avoid duplicates
|
|
58
|
+
logger.handlers.clear()
|
|
59
|
+
|
|
60
|
+
# Create handler
|
|
61
|
+
if handler is None:
|
|
62
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
63
|
+
|
|
64
|
+
# Set format
|
|
65
|
+
if format_string is None:
|
|
66
|
+
format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
67
|
+
|
|
68
|
+
formatter = logging.Formatter(format_string)
|
|
69
|
+
handler.setFormatter(formatter)
|
|
70
|
+
|
|
71
|
+
# Add handler to logger
|
|
72
|
+
logger.addHandler(handler)
|
|
73
|
+
|
|
74
|
+
# Prevent propagation to root logger to avoid duplicate logs
|
|
75
|
+
logger.propagate = False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def disable_logging() -> None:
|
|
79
|
+
"""
|
|
80
|
+
Disable all dotenvmodel logging.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
```python
|
|
84
|
+
from dotenvmodel.logging_config import disable_logging
|
|
85
|
+
|
|
86
|
+
disable_logging()
|
|
87
|
+
```
|
|
88
|
+
"""
|
|
89
|
+
logger = logging.getLogger("dotenvmodel")
|
|
90
|
+
logger.setLevel(logging.CRITICAL + 1) # Above CRITICAL
|
|
91
|
+
logger.handlers.clear()
|
|
92
|
+
logger.propagate = False
|
dotenvmodel/py.typed
ADDED
|
File without changes
|