dotenvmodel 0.3.2__tar.gz → 0.4.0__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.
- dotenvmodel-0.4.0/.release-please-manifest.json +3 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/CHANGELOG.md +7 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/PKG-INFO +78 -2
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/README.md +77 -1
- dotenvmodel-0.4.0/SECURITY.md +30 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/guides/fields.md +47 -1
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/guides/validation.md +50 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/index.md +2 -1
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/__init__.py +4 -2
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/coercion.py +69 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/config.py +254 -5
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/describe/formatters.py +40 -3
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/fields.py +152 -2
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/metaclass.py +15 -1
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/types.py +35 -1
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/validation.py +20 -3
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/pyproject.toml +4 -4
- dotenvmodel-0.4.0/tests/test_defaults_coercion.py +245 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_describe.py +269 -0
- dotenvmodel-0.4.0/tests/test_string_affixes.py +310 -0
- dotenvmodel-0.4.0/tests/test_strip.py +558 -0
- dotenvmodel-0.4.0/tests/test_strip_integration.py +124 -0
- dotenvmodel-0.4.0/tests/test_validator.py +730 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/uv.lock +120 -120
- dotenvmodel-0.3.2/.release-please-manifest.json +0 -3
- dotenvmodel-0.3.2/SECURITY.md +0 -23
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/.github/CODEOWNERS +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/.github/dependabot.yml +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/.github/pull_request_template.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/.github/workflows/ci.yml +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/.github/workflows/docs.yml +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/.github/workflows/publish.yml +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/.github/workflows/release-please.yml +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/.gitignore +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/.python-version +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/CODE_OF_CONDUCT.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/CONTRIBUTING.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/LICENSE +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/Makefile +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/coercion.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/config.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/constants.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/describe-formatters.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/describe-renderers.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/describe.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/dotenvmodel.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/exceptions.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/fields.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/loading.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/logging-config.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/metaclass.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/types.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/api-reference/validation.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/changelog.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/examples/complete-app.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/getting-started/installation.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/getting-started/quick-start.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/guides/configuration-docs.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/guides/error-handling.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/guides/loading.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/guides/logging.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/guides/prefixes.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/docs/guides/types.md +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/_constants.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/_redaction.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/describe/__init__.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/describe/renderers.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/exceptions.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/loading.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/logging_config.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/dotenvmodel/py.typed +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/examples/advanced_types.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/examples/basic_usage.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/examples/describe_documentation.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/examples/generate_env_example.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/examples/logging_example.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/mkdocs.yml +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/release-please-config.json +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/__init__.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/conftest.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_basic.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_coercion.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_collection_validators.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_edge_adoption.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_edge_datetime.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_edge_describe_output.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_edge_enum.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_edge_field_options.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_edge_numbers.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_edge_path_options.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_edge_strings.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_empty_collection_items.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_empty_strings.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_enum.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_errors.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_field_constraints.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_future_annotations.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_inheritance.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_json.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_json_and_optional.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_loading.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_logging_config.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_nested_config.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_prefix.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_reload.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_secret_redaction.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_secretstr_security.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_types.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_union_types.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_url_dsn.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_url_password_decoding.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_url_unquote.py +0 -0
- {dotenvmodel-0.3.2 → dotenvmodel-0.4.0}/tests/test_urls.py +0 -0
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0](https://github.com/AZX-PBC-OSS/dotenvmodel/compare/v0.3.2...v0.4.0) (2026-07-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add strip, starts_with/ends_with, and custom validator hook to Field ([#36](https://github.com/AZX-PBC-OSS/dotenvmodel/issues/36)) ([8e4210e](https://github.com/AZX-PBC-OSS/dotenvmodel/commit/8e4210e80e5888ef0b769bbf966f486c7b4b526e))
|
|
14
|
+
|
|
8
15
|
## [0.3.2](https://github.com/AZX-PBC-OSS/dotenvmodel/compare/v0.3.1...v0.3.2) (2026-07-15)
|
|
9
16
|
|
|
10
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotenvmodel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Type-safe environment configuration with automatic .env file loading
|
|
5
5
|
Project-URL: Homepage, https://github.com/AZX-PBC-OSS/dotenvmodel
|
|
6
6
|
Project-URL: Repository, https://github.com/AZX-PBC-OSS/dotenvmodel
|
|
@@ -44,7 +44,8 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
- **.env.example Generation**: Automatically generate `.env.example` files with type hints, constraints, and examples
|
|
45
45
|
- **File Export**: Save documentation directly to files for integration with build tools and wikis
|
|
46
46
|
- **Environment Prefixes**: Class-level `env_prefix` to namespace environment variables
|
|
47
|
-
- **Validation**: Numeric constraints (ge, le, gt, lt), string constraints (min_length, max_length, regex), choice validation, and collection size constraints (min_items, max_items)
|
|
47
|
+
- **Validation**: Numeric constraints (ge, le, gt, lt), string constraints (min_length, max_length, regex, starts_with, ends_with), choice validation, custom validator hooks, and collection size constraints (min_items, max_items)
|
|
48
|
+
- **String Stripping**: Per-field and class-level whitespace/char-set/regex stripping of raw values before coercion
|
|
48
49
|
- **Clear Error Messages**: Helpful validation errors that guide you to fixes
|
|
49
50
|
- **Optional Logging**: Built-in logging support to debug configuration loading
|
|
50
51
|
- **Zero Runtime Overhead**: All validation happens at startup/load time
|
|
@@ -362,6 +363,8 @@ class Config(DotEnvConfig):
|
|
|
362
363
|
tags: dict[str, str] = Field(default_factory=dict)
|
|
363
364
|
```
|
|
364
365
|
|
|
366
|
+
A `str` default for a non-`str` field type is **coerced to the declared type and validated at load** — e.g. a `bool` default of `'false'` becomes `False`, a `SecretStr` default becomes a masked `SecretStr`, and a `PostgresDsn` default is validated (a bad scheme raises at load) with its password redacted in `repr`. Non-`str` defaults (e.g. `int 8000`, `default_factory=list`) and `str` defaults for `str`-typed fields are left untouched. Constraints fire on the coerced default just as they do on env-provided values.
|
|
367
|
+
|
|
365
368
|
### Field Aliases
|
|
366
369
|
|
|
367
370
|
Use a different environment variable name than the field name:
|
|
@@ -389,8 +392,44 @@ class Config(DotEnvConfig):
|
|
|
389
392
|
)
|
|
390
393
|
```
|
|
391
394
|
|
|
395
|
+
### String Stripping
|
|
396
|
+
|
|
397
|
+
Raw environment values often come with stray whitespace or wrapping quotes. Use `strip` to clean string-like values (`str`, `SecretStr`, their `Optional` forms, `str` subclasses like `HttpUrl`, and `Literal["a", "b"]` fields whose every member is `str`) **before** coercion and validation:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
import re
|
|
401
|
+
|
|
402
|
+
class Config(DotEnvConfig):
|
|
403
|
+
# Whitespace strip: " hello " -> "hello"
|
|
404
|
+
name: str = Field(strip=True)
|
|
405
|
+
|
|
406
|
+
# Char-set strip (str.strip(chars) semantics): ",'hello'," -> "hello"
|
|
407
|
+
tag: str = Field(strip=",'\"")
|
|
408
|
+
|
|
409
|
+
# Regex strip: removes every match, anywhere in the string
|
|
410
|
+
key: SecretStr = Field(strip=re.compile(r"^['\"]+|['\"]+$"))
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Set `strip_strings = True` on the class to strip every string-like field by default, and opt out per field with `strip=False`:
|
|
414
|
+
|
|
415
|
+
```python
|
|
416
|
+
class Config(DotEnvConfig):
|
|
417
|
+
strip_strings: bool = True
|
|
418
|
+
|
|
419
|
+
name: str = Field() # stripped (inherits class setting)
|
|
420
|
+
literal: str = Field(strip=False) # per-field override wins
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Stripping is value processing, not validation: it runs even with `validate=False`, and constraints see the stripped value (e.g. `min_length` checks the final length, and a whitespace-only value for an `Optional[str]` field strips to `""`, which maps to `None`).
|
|
424
|
+
|
|
425
|
+
For `SecretStr` fields, `strip` runs **before** `url_unquote`, so percent-encoded whitespace (e.g. `%20`) survives stripping — it is removed while still percent-encoded, then unquoted. Use a `re.Pattern` strip if you need to strip decoded whitespace.
|
|
426
|
+
|
|
427
|
+
> **Warning — use linear-time regex patterns.** Both the `regex` constraint and `strip` with an `re.Pattern` run developer-supplied patterns against env values, which can be operator-controlled. Avoid patterns with nested quantifiers (e.g. `(a+)+`, `(a*)*`) that can cause catastrophic backtracking (ReDoS). Prefer anchored, linear-time patterns.
|
|
428
|
+
|
|
392
429
|
## Validation
|
|
393
430
|
|
|
431
|
+
> **Note — empty vs missing:** `Optional[T]` fields map missing **and** empty values to `None` and skip constraints, while plain `str` preserves empty strings as real values. So "validate only if present" is spelled `str | None = Field(default=None, min_length=...)`.
|
|
432
|
+
|
|
394
433
|
### Numeric Validation
|
|
395
434
|
|
|
396
435
|
```python
|
|
@@ -425,6 +464,10 @@ class Config(DotEnvConfig):
|
|
|
425
464
|
# Regex pattern
|
|
426
465
|
email: str = Field(regex=r'^[\w\.-]+@[\w\.-]+\.\w+$')
|
|
427
466
|
|
|
467
|
+
# Required prefix / suffix
|
|
468
|
+
client_key: str = Field(starts_with="sk-")
|
|
469
|
+
signed_token: str = Field(ends_with=".sig")
|
|
470
|
+
|
|
428
471
|
# Combined constraints
|
|
429
472
|
password: str = Field(
|
|
430
473
|
min_length=8,
|
|
@@ -449,6 +492,39 @@ class Config(DotEnvConfig):
|
|
|
449
492
|
)
|
|
450
493
|
```
|
|
451
494
|
|
|
495
|
+
### Custom Validators
|
|
496
|
+
|
|
497
|
+
For logic that built-in constraints can't express, attach a `validator` hook. It receives the **coerced, constraint-validated** value plus a `ValidatorContext` (field name and env var name), and its return value becomes the final field value — so it can also transform:
|
|
498
|
+
|
|
499
|
+
```python
|
|
500
|
+
from dotenvmodel import DotEnvConfig, Field, SecretStr, ValidatorContext
|
|
501
|
+
|
|
502
|
+
def check_api_key(value: SecretStr, ctx: ValidatorContext) -> SecretStr:
|
|
503
|
+
# The hook receives the coerced value: a SecretStr stays wrapped,
|
|
504
|
+
# so call get_secret_value() to inspect the plaintext.
|
|
505
|
+
if not value.get_secret_value().startswith("sk-"):
|
|
506
|
+
# ValueError/TypeError are wrapped in ConstraintViolationError
|
|
507
|
+
# and aggregate into MultipleValidationErrors like any other failure
|
|
508
|
+
raise ValueError(f"{ctx.env_var_name} must start with 'sk-'")
|
|
509
|
+
return value
|
|
510
|
+
|
|
511
|
+
class Config(DotEnvConfig):
|
|
512
|
+
api_key: SecretStr = Field(validator=check_api_key)
|
|
513
|
+
|
|
514
|
+
# Transform example: normalize to lowercase
|
|
515
|
+
region: str = Field(default="us-east-1", validator=lambda v, ctx: v.lower())
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Semantics to know:
|
|
519
|
+
|
|
520
|
+
- The hook receives the **coerced** value. A `SecretStr` stays wrapped — use `get_secret_value()` to inspect the plaintext.
|
|
521
|
+
- Runs **after** built-in constraints, on non-`None` values only, and even with `validate=False` (transformation is part of loading).
|
|
522
|
+
- Built-in constraints are **not** re-run on a transformed value.
|
|
523
|
+
- For sensitive fields (`SecretStr`, DSN types), returning a plain `str` re-wraps it in the declared type so the secret stays masked in `repr`.
|
|
524
|
+
- Returning `None` on a non-`Optional` field raises `TypeCoercionError`.
|
|
525
|
+
- For sensitive fields, **any** exception from the hook is masked to a generic `ConstraintViolationError` with no exception chaining — the hook's message is never embedded, so a carelessly written hook can't leak the secret. For non-sensitive fields, `str(e)` is embedded in the error message and the original exception is chained.
|
|
526
|
+
- Raise `ConstraintViolationError` directly for a fully custom message on non-sensitive fields (it passes through untouched).
|
|
527
|
+
|
|
452
528
|
## Loading Configuration
|
|
453
529
|
|
|
454
530
|
### From Environment Variables
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
- **.env.example Generation**: Automatically generate `.env.example` files with type hints, constraints, and examples
|
|
17
17
|
- **File Export**: Save documentation directly to files for integration with build tools and wikis
|
|
18
18
|
- **Environment Prefixes**: Class-level `env_prefix` to namespace environment variables
|
|
19
|
-
- **Validation**: Numeric constraints (ge, le, gt, lt), string constraints (min_length, max_length, regex), choice validation, and collection size constraints (min_items, max_items)
|
|
19
|
+
- **Validation**: Numeric constraints (ge, le, gt, lt), string constraints (min_length, max_length, regex, starts_with, ends_with), choice validation, custom validator hooks, and collection size constraints (min_items, max_items)
|
|
20
|
+
- **String Stripping**: Per-field and class-level whitespace/char-set/regex stripping of raw values before coercion
|
|
20
21
|
- **Clear Error Messages**: Helpful validation errors that guide you to fixes
|
|
21
22
|
- **Optional Logging**: Built-in logging support to debug configuration loading
|
|
22
23
|
- **Zero Runtime Overhead**: All validation happens at startup/load time
|
|
@@ -334,6 +335,8 @@ class Config(DotEnvConfig):
|
|
|
334
335
|
tags: dict[str, str] = Field(default_factory=dict)
|
|
335
336
|
```
|
|
336
337
|
|
|
338
|
+
A `str` default for a non-`str` field type is **coerced to the declared type and validated at load** — e.g. a `bool` default of `'false'` becomes `False`, a `SecretStr` default becomes a masked `SecretStr`, and a `PostgresDsn` default is validated (a bad scheme raises at load) with its password redacted in `repr`. Non-`str` defaults (e.g. `int 8000`, `default_factory=list`) and `str` defaults for `str`-typed fields are left untouched. Constraints fire on the coerced default just as they do on env-provided values.
|
|
339
|
+
|
|
337
340
|
### Field Aliases
|
|
338
341
|
|
|
339
342
|
Use a different environment variable name than the field name:
|
|
@@ -361,8 +364,44 @@ class Config(DotEnvConfig):
|
|
|
361
364
|
)
|
|
362
365
|
```
|
|
363
366
|
|
|
367
|
+
### String Stripping
|
|
368
|
+
|
|
369
|
+
Raw environment values often come with stray whitespace or wrapping quotes. Use `strip` to clean string-like values (`str`, `SecretStr`, their `Optional` forms, `str` subclasses like `HttpUrl`, and `Literal["a", "b"]` fields whose every member is `str`) **before** coercion and validation:
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
import re
|
|
373
|
+
|
|
374
|
+
class Config(DotEnvConfig):
|
|
375
|
+
# Whitespace strip: " hello " -> "hello"
|
|
376
|
+
name: str = Field(strip=True)
|
|
377
|
+
|
|
378
|
+
# Char-set strip (str.strip(chars) semantics): ",'hello'," -> "hello"
|
|
379
|
+
tag: str = Field(strip=",'\"")
|
|
380
|
+
|
|
381
|
+
# Regex strip: removes every match, anywhere in the string
|
|
382
|
+
key: SecretStr = Field(strip=re.compile(r"^['\"]+|['\"]+$"))
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Set `strip_strings = True` on the class to strip every string-like field by default, and opt out per field with `strip=False`:
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
class Config(DotEnvConfig):
|
|
389
|
+
strip_strings: bool = True
|
|
390
|
+
|
|
391
|
+
name: str = Field() # stripped (inherits class setting)
|
|
392
|
+
literal: str = Field(strip=False) # per-field override wins
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Stripping is value processing, not validation: it runs even with `validate=False`, and constraints see the stripped value (e.g. `min_length` checks the final length, and a whitespace-only value for an `Optional[str]` field strips to `""`, which maps to `None`).
|
|
396
|
+
|
|
397
|
+
For `SecretStr` fields, `strip` runs **before** `url_unquote`, so percent-encoded whitespace (e.g. `%20`) survives stripping — it is removed while still percent-encoded, then unquoted. Use a `re.Pattern` strip if you need to strip decoded whitespace.
|
|
398
|
+
|
|
399
|
+
> **Warning — use linear-time regex patterns.** Both the `regex` constraint and `strip` with an `re.Pattern` run developer-supplied patterns against env values, which can be operator-controlled. Avoid patterns with nested quantifiers (e.g. `(a+)+`, `(a*)*`) that can cause catastrophic backtracking (ReDoS). Prefer anchored, linear-time patterns.
|
|
400
|
+
|
|
364
401
|
## Validation
|
|
365
402
|
|
|
403
|
+
> **Note — empty vs missing:** `Optional[T]` fields map missing **and** empty values to `None` and skip constraints, while plain `str` preserves empty strings as real values. So "validate only if present" is spelled `str | None = Field(default=None, min_length=...)`.
|
|
404
|
+
|
|
366
405
|
### Numeric Validation
|
|
367
406
|
|
|
368
407
|
```python
|
|
@@ -397,6 +436,10 @@ class Config(DotEnvConfig):
|
|
|
397
436
|
# Regex pattern
|
|
398
437
|
email: str = Field(regex=r'^[\w\.-]+@[\w\.-]+\.\w+$')
|
|
399
438
|
|
|
439
|
+
# Required prefix / suffix
|
|
440
|
+
client_key: str = Field(starts_with="sk-")
|
|
441
|
+
signed_token: str = Field(ends_with=".sig")
|
|
442
|
+
|
|
400
443
|
# Combined constraints
|
|
401
444
|
password: str = Field(
|
|
402
445
|
min_length=8,
|
|
@@ -421,6 +464,39 @@ class Config(DotEnvConfig):
|
|
|
421
464
|
)
|
|
422
465
|
```
|
|
423
466
|
|
|
467
|
+
### Custom Validators
|
|
468
|
+
|
|
469
|
+
For logic that built-in constraints can't express, attach a `validator` hook. It receives the **coerced, constraint-validated** value plus a `ValidatorContext` (field name and env var name), and its return value becomes the final field value — so it can also transform:
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
from dotenvmodel import DotEnvConfig, Field, SecretStr, ValidatorContext
|
|
473
|
+
|
|
474
|
+
def check_api_key(value: SecretStr, ctx: ValidatorContext) -> SecretStr:
|
|
475
|
+
# The hook receives the coerced value: a SecretStr stays wrapped,
|
|
476
|
+
# so call get_secret_value() to inspect the plaintext.
|
|
477
|
+
if not value.get_secret_value().startswith("sk-"):
|
|
478
|
+
# ValueError/TypeError are wrapped in ConstraintViolationError
|
|
479
|
+
# and aggregate into MultipleValidationErrors like any other failure
|
|
480
|
+
raise ValueError(f"{ctx.env_var_name} must start with 'sk-'")
|
|
481
|
+
return value
|
|
482
|
+
|
|
483
|
+
class Config(DotEnvConfig):
|
|
484
|
+
api_key: SecretStr = Field(validator=check_api_key)
|
|
485
|
+
|
|
486
|
+
# Transform example: normalize to lowercase
|
|
487
|
+
region: str = Field(default="us-east-1", validator=lambda v, ctx: v.lower())
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Semantics to know:
|
|
491
|
+
|
|
492
|
+
- The hook receives the **coerced** value. A `SecretStr` stays wrapped — use `get_secret_value()` to inspect the plaintext.
|
|
493
|
+
- Runs **after** built-in constraints, on non-`None` values only, and even with `validate=False` (transformation is part of loading).
|
|
494
|
+
- Built-in constraints are **not** re-run on a transformed value.
|
|
495
|
+
- For sensitive fields (`SecretStr`, DSN types), returning a plain `str` re-wraps it in the declared type so the secret stays masked in `repr`.
|
|
496
|
+
- Returning `None` on a non-`Optional` field raises `TypeCoercionError`.
|
|
497
|
+
- For sensitive fields, **any** exception from the hook is masked to a generic `ConstraintViolationError` with no exception chaining — the hook's message is never embedded, so a carelessly written hook can't leak the secret. For non-sensitive fields, `str(e)` is embedded in the error message and the original exception is chained.
|
|
498
|
+
- Raise `ConstraintViolationError` directly for a fully custom message on non-sensitive fields (it passes through untouched).
|
|
499
|
+
|
|
424
500
|
## Loading Configuration
|
|
425
501
|
|
|
426
502
|
### From Environment Variables
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
We take security vulnerabilities seriously. If you discover a security vulnerability in dotenvmodel, please report it responsibly.
|
|
6
|
+
|
|
7
|
+
**Do NOT open a public GitHub issue for security vulnerabilities.**
|
|
8
|
+
|
|
9
|
+
Instead, please use [GitHub Security Advisories](https://github.com/AZX-PBC-OSS/dotenvmodel/security/advisories/new) to report vulnerabilities privately.
|
|
10
|
+
|
|
11
|
+
## Response Timeline
|
|
12
|
+
|
|
13
|
+
- **Acknowledgment:** Within 48 hours
|
|
14
|
+
- **Initial Assessment:** Within 5 business days
|
|
15
|
+
- **Fix or Mitigation:** Depends on severity, typically within 30 days for high-severity issues
|
|
16
|
+
|
|
17
|
+
## Scope
|
|
18
|
+
|
|
19
|
+
This policy covers the dotenvmodel Python package and its CI/CD pipeline. Vulnerabilities in third-party dependencies should be reported to their respective maintainers.
|
|
20
|
+
|
|
21
|
+
## Disclosure
|
|
22
|
+
|
|
23
|
+
We follow coordinated disclosure. Once a fix is released, we will publish a GitHub Security Advisory with credit to the reporter (unless they prefer to remain anonymous).
|
|
24
|
+
|
|
25
|
+
## Residual Secret-Exposure Channels
|
|
26
|
+
|
|
27
|
+
dotenvmodel masks secrets in the common display paths (`repr`, error messages, and exception chains for `SecretStr` and DSN-typed fields). Two residual channels remain by design and are the caller's responsibility to mitigate:
|
|
28
|
+
|
|
29
|
+
- **Traceback frame locals.** When a load-time error is raised, library internals hold the raw env value in local variables (e.g. `raw_value` in `_process_field`). Error-reporting tools that capture locals will record these — Sentry with `include_locals=True`, `pytest --showlocals`, or rich tracebacks. Do not enable local capture for processes that load configs containing secrets.
|
|
30
|
+
- **`FieldInfo.__repr__`.** `FieldInfo.__repr__` prints `default` values verbatim (the raw default stored on the descriptor, before load-time coercion). A `str` default supplied for a `SecretStr` field is visible in `repr(field_info)` and in `describe()`/`generate_env_example()` introspection. Do not log `FieldInfo` reprs, or pass secret defaults as `SecretStr(...)` instances (which mask themselves) rather than plain strings.
|
|
@@ -39,7 +39,11 @@ class Config(DotEnvConfig):
|
|
|
39
39
|
| `min_length` | `int \| None` | Minimum string length (inclusive). For `str` and `SecretStr`. |
|
|
40
40
|
| `max_length` | `int \| None` | Maximum string length (inclusive). For `str` and `SecretStr`. |
|
|
41
41
|
| `regex` | `str \| None` | Regular expression pattern the string must match. |
|
|
42
|
+
| `starts_with` | `str \| None` | Required string prefix. For `str` and `SecretStr`. |
|
|
43
|
+
| `ends_with` | `str \| None` | Required string suffix. For `str` and `SecretStr`. |
|
|
44
|
+
| `strip` | `bool \| str \| re.Pattern \| None` | Strip mode applied to the raw string before coercion. `None` inherits the class-level `strip_strings`; `True` strips whitespace; `False` disables; a non-empty `str` is a char set (`value.strip(chars)`); a compiled pattern removes every match. |
|
|
42
45
|
| `choices` | `list[Any] \| None` | List of allowed values (validated after type coercion). |
|
|
46
|
+
| `validator` | `Callable[[Any, ValidatorContext], Any] \| None` | Custom hook receiving the coerced, validated value plus context; its return value becomes the final value. |
|
|
43
47
|
| `min_items` | `int \| None` | Minimum items in a collection (`list`, `set`, `tuple`, `dict`). |
|
|
44
48
|
| `max_items` | `int \| None` | Maximum items in a collection (`list`, `set`, `tuple`, `dict`). |
|
|
45
49
|
| `uuid_version` | `int \| None` | Required UUID version (`1`, `3`, `4`, or `5`). |
|
|
@@ -49,7 +53,7 @@ class Config(DotEnvConfig):
|
|
|
49
53
|
| `require_exists` | `bool` | Whether a `Path` field must point to an existing path. Default: `False`. |
|
|
50
54
|
|
|
51
55
|
!!! info "Validation parameters"
|
|
52
|
-
The validation parameters (`ge`, `le`, `gt`, `lt`, `min_length`, `max_length`, `regex`, `choices`, `min_items`, `max_items`, `uuid_version`) are documented in detail in the [Validation guide](validation.md).
|
|
56
|
+
The validation parameters (`ge`, `le`, `gt`, `lt`, `min_length`, `max_length`, `regex`, `starts_with`, `ends_with`, `choices`, `validator`, `min_items`, `max_items`, `uuid_version`) are documented in detail in the [Validation guide](validation.md).
|
|
53
57
|
|
|
54
58
|
---
|
|
55
59
|
|
|
@@ -130,6 +134,9 @@ There are three equivalent ways to mark a field as required. All produce identic
|
|
|
130
134
|
!!! warning "Never use mutable defaults"
|
|
131
135
|
Using `default=[]` or `default={}` shares the same object across all instances. Always use `default_factory=list` or `default_factory=dict` instead. `Field()` raises a `ValueError` if you specify both `default` and `default_factory`.
|
|
132
136
|
|
|
137
|
+
!!! note "`str` defaults are coerced and validated for non-`str` field types"
|
|
138
|
+
A `str` default for a non-`str` field type is coerced to the declared type and run through validation at load — e.g. a `bool` default of `'false'` becomes `False`, a `SecretStr` default becomes a masked `SecretStr`, and a `PostgresDsn` default is validated (a bad scheme raises at load) with its password redacted in `repr`. Non-`str` defaults (e.g. `int 8000`, `default_factory=list`) and `str` defaults for `str`-typed fields are left untouched.
|
|
139
|
+
|
|
133
140
|
---
|
|
134
141
|
|
|
135
142
|
## Aliases
|
|
@@ -186,6 +193,45 @@ See the [Configuration Documentation guide](configuration-docs.md) for examples
|
|
|
186
193
|
|
|
187
194
|
---
|
|
188
195
|
|
|
196
|
+
## String Stripping
|
|
197
|
+
|
|
198
|
+
The `strip` parameter cleans raw string values **before** coercion and validation. It applies to string-like fields: `str`, `SecretStr`, their `Optional` forms, `str` subclasses (`HttpUrl`, `PostgresDsn`, `RedisDsn`), and `Literal["a", "b"]` fields whose every member is `str`.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
import re
|
|
202
|
+
|
|
203
|
+
class Config(DotEnvConfig):
|
|
204
|
+
# Whitespace strip: " hello " -> "hello"
|
|
205
|
+
name: str = Field(strip=True)
|
|
206
|
+
|
|
207
|
+
# Char-set strip (str.strip(chars) semantics): ",'hello'," -> "hello"
|
|
208
|
+
tag: str = Field(strip=",'\"")
|
|
209
|
+
|
|
210
|
+
# Regex strip: removes every match, anywhere in the string
|
|
211
|
+
key: SecretStr = Field(strip=re.compile(r"^['\"]+|['\"]+$"))
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Set the `strip_strings` class attribute to strip every string-like field by default; per-field `strip` overrides it:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
class Config(DotEnvConfig):
|
|
218
|
+
strip_strings: bool = True
|
|
219
|
+
|
|
220
|
+
name: str = Field() # stripped (inherits class setting)
|
|
221
|
+
literal: str = Field(strip=False) # per-field override wins
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
!!! note "Stripping is processing, not validation"
|
|
225
|
+
`strip` runs even with `validate=False`, and constraints see the stripped value — `min_length` checks the final length, and a whitespace-only value for an `Optional[str]` field strips to `""`, which maps to `None`.
|
|
226
|
+
|
|
227
|
+
!!! note "Strip runs before URL-unquoting on `SecretStr`"
|
|
228
|
+
For `SecretStr` fields, `strip` is applied to the raw value **before** `url_unquote`, so percent-encoded whitespace (e.g. `%20`) survives stripping — it is removed while still percent-encoded, then unquoted. Use a `re.Pattern` strip if you need to strip decoded whitespace.
|
|
229
|
+
|
|
230
|
+
!!! warning "Use linear-time regex patterns"
|
|
231
|
+
Both the `regex` constraint and `strip` with an `re.Pattern` run developer-supplied patterns against env values, which can be operator-controlled. Avoid patterns with nested quantifiers (e.g. `(a+)+`, `(a*)*`) that can cause catastrophic backtracking (ReDoS). Prefer anchored, linear-time patterns.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
189
235
|
## Path Options
|
|
190
236
|
|
|
191
237
|
Two parameters control `Path` field behavior:
|
|
@@ -6,6 +6,9 @@ Constraints are defined as parameters to `Field()`. This page covers every suppo
|
|
|
6
6
|
|
|
7
7
|
For the complete API reference, see [Validation API](../api-reference/validation.md).
|
|
8
8
|
|
|
9
|
+
!!! note "Empty vs missing values"
|
|
10
|
+
`Optional[T]` fields map missing **and** empty values to `None` and skip constraints, while plain `str` preserves empty strings as real values. So "validate only if present" is spelled `str | None = Field(default=None, min_length=...)`.
|
|
11
|
+
|
|
9
12
|
---
|
|
10
13
|
|
|
11
14
|
## Numeric Constraints
|
|
@@ -95,6 +98,8 @@ String constraints apply to `str` and `SecretStr` fields.
|
|
|
95
98
|
| `min_length` | Minimum string length (inclusive) |
|
|
96
99
|
| `max_length` | Maximum string length (inclusive) |
|
|
97
100
|
| `regex` | Regular expression pattern the string must match (uses `re.match`) |
|
|
101
|
+
| `starts_with` | Required string prefix |
|
|
102
|
+
| `ends_with` | Required string suffix |
|
|
98
103
|
|
|
99
104
|
### Examples
|
|
100
105
|
|
|
@@ -112,6 +117,10 @@ class Config(DotEnvConfig):
|
|
|
112
117
|
# Regex pattern
|
|
113
118
|
email: str = Field(regex=r'^[\w\.-]+@[\w\.-]+\.\w+$')
|
|
114
119
|
|
|
120
|
+
# Required prefix / suffix
|
|
121
|
+
client_key: str = Field(starts_with="sk-")
|
|
122
|
+
signed_token: str = Field(ends_with=".sig")
|
|
123
|
+
|
|
115
124
|
# Combined constraints
|
|
116
125
|
password: str = Field(
|
|
117
126
|
min_length=8,
|
|
@@ -159,6 +168,9 @@ class Config(DotEnvConfig):
|
|
|
159
168
|
!!! note "Regex uses `re.match`"
|
|
160
169
|
The `regex` constraint uses `re.match`, which anchors at the **start** of the string. Include `^` and `$` in your pattern to anchor both ends, as shown in the examples above.
|
|
161
170
|
|
|
171
|
+
!!! warning "Use linear-time patterns (ReDoS)"
|
|
172
|
+
`regex` and `strip` with an `re.Pattern` run developer-supplied patterns against env values, which can be operator-controlled. Avoid patterns with nested quantifiers (e.g. `(a+)+`, `(a*)*`) that can cause catastrophic backtracking (ReDoS). Prefer anchored, linear-time patterns.
|
|
173
|
+
|
|
162
174
|
---
|
|
163
175
|
|
|
164
176
|
## Choice Validation
|
|
@@ -214,6 +226,44 @@ class Config(DotEnvConfig):
|
|
|
214
226
|
|
|
215
227
|
---
|
|
216
228
|
|
|
229
|
+
## Custom Validators
|
|
230
|
+
|
|
231
|
+
For logic the built-in constraints can't express, attach a `validator` hook. It receives the **coerced, built-in-constraint-validated** value plus a `ValidatorContext` (field name and resolved env var name), and its return value becomes the final field value — so a hook can also transform.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from dotenvmodel import DotEnvConfig, Field, SecretStr, ValidatorContext
|
|
235
|
+
|
|
236
|
+
def check_api_key(value: SecretStr, ctx: ValidatorContext) -> SecretStr:
|
|
237
|
+
# The hook receives the coerced value: a SecretStr stays wrapped,
|
|
238
|
+
# so call get_secret_value() to inspect the plaintext.
|
|
239
|
+
if not value.get_secret_value().startswith("sk-"):
|
|
240
|
+
# ValueError/TypeError are wrapped in ConstraintViolationError and
|
|
241
|
+
# aggregate into MultipleValidationErrors like any other failure
|
|
242
|
+
raise ValueError(f"{ctx.env_var_name} must start with 'sk-'")
|
|
243
|
+
return value
|
|
244
|
+
|
|
245
|
+
class Config(DotEnvConfig):
|
|
246
|
+
api_key: SecretStr = Field(validator=check_api_key)
|
|
247
|
+
|
|
248
|
+
# Transform example: normalize to lowercase
|
|
249
|
+
region: str = Field(default="us-east-1", validator=lambda v, ctx: v.lower())
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Semantics:
|
|
253
|
+
|
|
254
|
+
- The hook receives the **coerced** value. A `SecretStr` stays wrapped — use `get_secret_value()` to inspect the plaintext.
|
|
255
|
+
- Runs **after** built-in constraints, on non-`None` values only, and even with `validate=False` (transformation is part of loading, not validation).
|
|
256
|
+
- Built-in constraints are **not** re-run on a transformed value (pydantic "after"-mode semantics).
|
|
257
|
+
- For sensitive fields (`SecretStr`, DSN types), returning a plain `str` re-wraps it in the declared type so the secret stays masked in `repr`.
|
|
258
|
+
- Returning `None` on a non-`Optional` field raises `TypeCoercionError`.
|
|
259
|
+
- A `ValueError` or `TypeError` from the hook is wrapped in `ConstraintViolationError` with `constraint="validator=<fn name>"`.
|
|
260
|
+
- Raise `ConstraintViolationError` directly for a fully custom message on non-sensitive fields (passed through untouched).
|
|
261
|
+
|
|
262
|
+
!!! warning "Sensitive-field leak prevention"
|
|
263
|
+
For sensitive fields (`SecretStr`, DSN types), **any** exception from the hook is masked to a generic `ConstraintViolationError` with no exception chaining — the hook's exception text is never embedded in the error or its `__cause__`/`__context__` chain, so a carelessly written hook cannot leak the secret or URL password into logs. For non-sensitive fields, `str(e)` is embedded in the error message and the original exception is chained.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
217
267
|
## Collection Size Constraints
|
|
218
268
|
|
|
219
269
|
The `min_items` and `max_items` parameters constrain the number of items in `list`, `set`, `tuple`, and `dict` fields.
|
|
@@ -44,7 +44,8 @@ safety, validation, and automatic `.env` loading — with only one runtime depen
|
|
|
44
44
|
---
|
|
45
45
|
|
|
46
46
|
Numeric constraints (`ge`, `le`, `gt`, `lt`), string constraints (`min_length`,
|
|
47
|
-
`max_length`, `regex`), choice validation,
|
|
47
|
+
`max_length`, `regex`, `starts_with`, `ends_with`), choice validation, custom
|
|
48
|
+
`validator` hooks, string `strip` processing, and collection size constraints.
|
|
48
49
|
|
|
49
50
|
- :material-file-document-multiple:{ .lg .middle } **Configuration Documentation**
|
|
50
51
|
|
|
@@ -20,6 +20,7 @@ Public API:
|
|
|
20
20
|
- `DotEnvConfig`: Base class for type-safe configuration
|
|
21
21
|
- `Field`: Define fields with defaults, validation, and aliases
|
|
22
22
|
- `Required`: Sentinel for required fields (alternative to `Field()`)
|
|
23
|
+
- `ValidatorContext`: Context passed to `Field(validator=...)` hooks
|
|
23
24
|
- `SecretStr`: String type that hides values in logs
|
|
24
25
|
- `HttpUrl`, `PostgresDsn`, `RedisDsn`: URL/DSN types with validation
|
|
25
26
|
- `Json`: Type for parsing JSON strings
|
|
@@ -31,7 +32,7 @@ Public API:
|
|
|
31
32
|
Exception hierarchy
|
|
32
33
|
"""
|
|
33
34
|
|
|
34
|
-
__version__ = "0.
|
|
35
|
+
__version__ = "0.4.0"
|
|
35
36
|
__author__ = "AZX, PBC."
|
|
36
37
|
__email__ = "oss@azx.io"
|
|
37
38
|
__license__ = "MIT"
|
|
@@ -48,7 +49,7 @@ from dotenvmodel.exceptions import (
|
|
|
48
49
|
TypeCoercionError,
|
|
49
50
|
ValidationError,
|
|
50
51
|
)
|
|
51
|
-
from dotenvmodel.fields import Field, Required
|
|
52
|
+
from dotenvmodel.fields import Field, Required, ValidatorContext
|
|
52
53
|
from dotenvmodel.logging_config import configure_logging, disable_logging
|
|
53
54
|
from dotenvmodel.types import (
|
|
54
55
|
HttpUrl,
|
|
@@ -74,6 +75,7 @@ __all__ = [
|
|
|
74
75
|
"SecretStr",
|
|
75
76
|
"TypeCoercionError",
|
|
76
77
|
"ValidationError",
|
|
78
|
+
"ValidatorContext",
|
|
77
79
|
"__version__",
|
|
78
80
|
"configure_logging",
|
|
79
81
|
"describe_configs",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import logging
|
|
5
|
+
import re
|
|
5
6
|
import types
|
|
6
7
|
from datetime import datetime, timedelta
|
|
7
8
|
from decimal import Decimal
|
|
@@ -14,6 +15,7 @@ from typing_extensions import TypeForm
|
|
|
14
15
|
|
|
15
16
|
from dotenvmodel._constants import LOGGER_NAME
|
|
16
17
|
from dotenvmodel.exceptions import TypeCoercionError
|
|
18
|
+
from dotenvmodel.types import SecretStr
|
|
17
19
|
|
|
18
20
|
if TYPE_CHECKING:
|
|
19
21
|
from dotenvmodel.fields import FieldInfo
|
|
@@ -21,6 +23,73 @@ if TYPE_CHECKING:
|
|
|
21
23
|
logger = logging.getLogger(LOGGER_NAME)
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
def unwrap_optional(field_type: TypeForm[Any]) -> TypeForm[Any]:
|
|
27
|
+
"""Return the non-None member of an ``Optional[T]`` / ``T | None`` type.
|
|
28
|
+
|
|
29
|
+
``Optional[T]`` and ``T | None`` resolve to ``T``. Any other type —
|
|
30
|
+
including non-Optional Unions with multiple non-None members, which this
|
|
31
|
+
library rejects elsewhere — is returned unchanged.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
field_type: The field's type annotation
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The Optional-unwrapped type, or ``field_type`` itself when it is not
|
|
38
|
+
a single-member Optional.
|
|
39
|
+
"""
|
|
40
|
+
origin = get_origin(field_type)
|
|
41
|
+
if origin is types.UnionType or origin is Union:
|
|
42
|
+
non_none = [arg for arg in get_args(field_type) if arg is not type(None)]
|
|
43
|
+
if len(non_none) == 1:
|
|
44
|
+
return non_none[0]
|
|
45
|
+
return field_type
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_string_like_type(field_type: TypeForm[Any]) -> bool:
|
|
49
|
+
"""Check whether a field type is string-like (eligible for ``strip``).
|
|
50
|
+
|
|
51
|
+
Unwraps ``Optional[T]`` and checks for ``str``, ``SecretStr``, a
|
|
52
|
+
``str`` subclass (e.g. ``HttpUrl``, ``PostgresDsn``, ``RedisDsn``), or a
|
|
53
|
+
``Literal[...]`` whose every argument is ``str`` (e.g.
|
|
54
|
+
``Literal["dev", "prod"]``), so that strip is applied before coercion.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
field_type: The field's type annotation
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
True if the field holds a string-like value
|
|
61
|
+
"""
|
|
62
|
+
unwrapped = unwrap_optional(field_type)
|
|
63
|
+
if inspect.isclass(unwrapped):
|
|
64
|
+
return issubclass(unwrapped, (str, SecretStr))
|
|
65
|
+
origin = get_origin(unwrapped)
|
|
66
|
+
if origin is Literal:
|
|
67
|
+
return all(isinstance(arg, str) for arg in get_args(unwrapped))
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def apply_strip(value: str, mode: bool | str | re.Pattern[str]) -> str:
|
|
72
|
+
"""Apply a strip mode to a raw string value.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
value: The raw string from the environment
|
|
76
|
+
mode: Strip mode — ``True`` for whitespace stripping, ``False`` for no
|
|
77
|
+
stripping, a non-empty ``str`` for char-set stripping
|
|
78
|
+
(``value.strip(chars)``), or a compiled ``re.Pattern`` to remove
|
|
79
|
+
every match (``pattern.sub("", value)``)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The stripped string
|
|
83
|
+
"""
|
|
84
|
+
if mode is False:
|
|
85
|
+
return value
|
|
86
|
+
if mode is True:
|
|
87
|
+
return value.strip()
|
|
88
|
+
if isinstance(mode, str):
|
|
89
|
+
return value.strip(mode)
|
|
90
|
+
return mode.sub("", value)
|
|
91
|
+
|
|
92
|
+
|
|
24
93
|
def coerce_value(
|
|
25
94
|
field_name: str,
|
|
26
95
|
value: str | None,
|