pax_global_header00006660000000000000000000000064151443334500014514gustar00rootroot0000000000000052 comment=198e71cf0cd570501535de7ae93a70953150f073 pydantic-pydantic-settings-198e71c/000077500000000000000000000000001514433345000173415ustar00rootroot00000000000000pydantic-pydantic-settings-198e71c/.github/000077500000000000000000000000001514433345000207015ustar00rootroot00000000000000pydantic-pydantic-settings-198e71c/.github/FUNDING.yml000066400000000000000000000000251514433345000225130ustar00rootroot00000000000000github: samuelcolvin pydantic-pydantic-settings-198e71c/.github/workflows/000077500000000000000000000000001514433345000227365ustar00rootroot00000000000000pydantic-pydantic-settings-198e71c/.github/workflows/ci.yml000066400000000000000000000100571514433345000240570ustar00rootroot00000000000000name: CI on: push: branches: - main tags: - '**' pull_request: {} jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: set up uv uses: astral-sh/setup-uv@v6 with: python-version: '3.12' - name: Install dependencies # Installing pip is required for the pre-commit action: run: | uv sync --group linting --all-extras uv pip install pip - uses: pre-commit/action@v3.0.1 with: extra_args: --all-files test: name: test py${{ matrix.python }} on ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] python: ['3.10', '3.11', '3.12', '3.13', '3.14'] env: PYTHON: ${{ matrix.python }} OS: ${{ matrix.os }} UV_PYTHON_PREFERENCE: only-managed runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: set up uv uses: astral-sh/setup-uv@v6 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | uv sync --group testing --all-extras - run: mkdir coverage - name: test run: make test env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python }} CONTEXT: ${{ runner.os }}-py${{ matrix.python }} - name: uninstall deps run: uv pip uninstall PyYAML - name: test without deps run: make test env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python }}-without-deps CONTEXT: ${{ runner.os }}-py${{ matrix.python }}-without-deps - name: store coverage files uses: actions/upload-artifact@v4 with: name: coverage-${{ matrix.python }}-${{ runner.os }} path: coverage include-hidden-files: true coverage: runs-on: ubuntu-latest needs: [test] steps: - uses: actions/checkout@v4 with: # needed for diff-cover fetch-depth: 0 - name: get coverage files uses: actions/download-artifact@v4 with: merge-multiple: true path: coverage - uses: astral-sh/setup-uv@v5 with: enable-cache: true - run: uv sync --group testing --all-extras - run: uv run coverage combine coverage - run: uv run coverage html --show-contexts --title "Pydantic Settings coverage for ${{ github.sha }}" - name: Store coverage html uses: actions/upload-artifact@v4 with: name: coverage-html path: htmlcov include-hidden-files: true - run: uv run coverage xml - run: uv run diff-cover coverage.xml --html-report index.html - name: Store diff coverage html uses: actions/upload-artifact@v4 with: name: diff-coverage-html path: index.html - run: uv run coverage report --fail-under 98 check: # This job does nothing and is only used for the branch protection if: always() needs: [lint, test, coverage] runs-on: ubuntu-latest outputs: result: ${{ steps.all-green.outputs.result }} steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 id: all-green with: jobs: ${{ toJSON(needs) }} release: needs: [check] if: needs.check.outputs.result == 'success' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest environment: release permissions: id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install 'build' library run: pip install -U build - name: Check version id: check-tag uses: samuelcolvin/check-python-version@v4.1 with: version_file_path: pydantic_settings/version.py - name: Build library run: python -m build - name: Upload package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 pydantic-pydantic-settings-198e71c/.gitignore000066400000000000000000000004441514433345000213330ustar00rootroot00000000000000.idea/ env/ .envrc venv/ .venv/ env3*/ Pipfile *.lock !uv.lock *.py[cod] *.egg-info/ /build/ dist/ .cache/ .mypy_cache/ test.py .coverage .hypothesis /htmlcov/ /site/ /site.zip .pytest_cache/ .python-version .vscode/ _build/ .auto-format /sandbox/ /.ghtopdep_cache/ /worktrees/ /.ruff_cache/ pydantic-pydantic-settings-198e71c/.pre-commit-config.yaml000066400000000000000000000007101514433345000236200ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: - id: check-yaml args: ['--unsafe'] - id: check-toml - id: end-of-file-fixer - id: trailing-whitespace - repo: local hooks: - id: lint name: Lint entry: make lint types: [python] language: system pass_filenames: false - id: mypy name: Mypy entry: make mypy types: [python] language: system pass_filenames: false pydantic-pydantic-settings-198e71c/LICENSE000066400000000000000000000021171514433345000203470ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2022 Samuel Colvin and other contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pydantic-pydantic-settings-198e71c/Makefile000066400000000000000000000011601514433345000207770ustar00rootroot00000000000000.DEFAULT_GOAL := all sources = pydantic_settings tests .PHONY: install install: uv sync --all-extras --all-groups .PHONY: refresh-lockfiles refresh-lockfiles: @echo "Updating uv.lock file" uv lock -U .PHONY: format format: uv run ruff check --fix $(sources) uv run ruff format $(sources) .PHONY: lint lint: uv run ruff check $(sources) uv run ruff format --check $(sources) .PHONY: mypy mypy: uv run mypy pydantic_settings .PHONY: test test: uv run coverage run -m pytest --durations=10 .PHONY: testcov testcov: test @echo "building coverage html" @uv run coverage html .PHONY: all all: lint mypy testcov pydantic-pydantic-settings-198e71c/README.md000066400000000000000000000017401514433345000206220ustar00rootroot00000000000000# pydantic-settings [![CI](https://github.com/pydantic/pydantic-settings/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/pydantic/pydantic-settings/actions/workflows/ci.yml?query=branch%3Amain) [![Coverage](https://codecov.io/gh/pydantic/pydantic-settings/branch/main/graph/badge.svg)](https://codecov.io/gh/pydantic/pydantic-settings) [![pypi](https://img.shields.io/pypi/v/pydantic-settings.svg)](https://pypi.python.org/pypi/pydantic-settings) [![license](https://img.shields.io/github/license/pydantic/pydantic-settings.svg)](https://github.com/pydantic/pydantic-settings/blob/main/LICENSE) [![downloads](https://static.pepy.tech/badge/pydantic-settings/month)](https://pepy.tech/project/pydantic-settings) [![versions](https://img.shields.io/pypi/pyversions/pydantic-settings.svg)](https://github.com/pydantic/pydantic-settings) Settings management using Pydantic. See [documentation](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for more details. pydantic-pydantic-settings-198e71c/docs/000077500000000000000000000000001514433345000202715ustar00rootroot00000000000000pydantic-pydantic-settings-198e71c/docs/extra/000077500000000000000000000000001514433345000214145ustar00rootroot00000000000000pydantic-pydantic-settings-198e71c/docs/extra/terminal.css000066400000000000000000000010231514433345000237350ustar00rootroot00000000000000.terminal { background: #300a24; border-radius: 4px; padding: 5px 10px; } pre.terminal-content { display: inline-block; line-height: 1.3 !important; white-space: pre-wrap; word-wrap: break-word; background: #300a24 !important; color: #d0d0d0 !important; } .ansi2 { font-weight: lighter; } .ansi3 { font-style: italic; } .ansi32 { color: #00aa00; } .ansi34 { color: #5656fe; } .ansi35 { color: #E850A8; } .ansi38-1 { color: #cf0000; } .ansi38-5 { color: #E850A8; } .ansi38-68 { color: #2a54a8; } pydantic-pydantic-settings-198e71c/docs/extra/tweaks.css000066400000000000000000000021011514433345000234160ustar00rootroot00000000000000.sponsors { display: flex; justify-content: center; flex-wrap: wrap; align-items: center; margin: 1rem 0; } .sponsors > div { text-align: center; width: 33%; padding-bottom: 20px; } .sponsors span { display: block; } @media screen and (max-width: 599px) { .sponsors span { display: none; } } .sponsors img { width: 65%; border-radius: 5px; } /*blog post*/ aside.blog { display: flex; align-items: center; } aside.blog img { width: 50px; height: 50px; border-radius: 25px; margin-right: 20px; } /* Define the company grid layout */ #grid-container { width: 100%; text-align: center; } #company-grid { display: inline-block; margin: 0 auto; gap: 10px; align-content: center; justify-content: center; grid-auto-flow: column; } [data-md-color-scheme="slate"] #company-grid { background-color: #ffffff; border-radius: .5rem; } .tile { display: flex; text-align: center; width: 120px; height: 120px; display: inline-block; margin: 10px; padding: 5px; border-radius: .5rem; } .tile img { width: 100px; } pydantic-pydantic-settings-198e71c/docs/favicon.png000066400000000000000000000015731514433345000224320ustar00rootroot00000000000000PNG  IHDR sgAMA a cHRMz&u0`:pQ<bKGD̿ pHYs ǠtIME % *IDATHǝ?hAO vh*(E,8TER\t̪K NvuK5K0(mRڀZC4{i<7{sdmĔiaφo&fR%w\q*Y仺cmҼuU &ÉbHhVó򶏅[c[^6@x{7;`Uv¤kl9_#`p/| .qgM7k񩝚%:fludxn<2Ü/ы(.o\"uXE^:%K=ތHI QW_7A-Qu]xr>ݡjlװXSZd-oeh T+HP8{eF6=s*|zDv'u.hN%-v Oö(o, im W̛Nnem>+l5?U:g%tEXtdate:create2019-10-07T17:37:10+02:00m%tEXtdate:modify2019-10-07T17:37:10+02:0004tEXtSoftwarewww.inkscape.org<WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`pydantic-pydantic-settings-198e71c/docs/index.md000066400000000000000000002764761514433345000217500ustar00rootroot00000000000000## Installation Installation is as simple as: ```bash pip install pydantic-settings ``` ## Usage If you create a model that inherits from `BaseSettings`, the model initialiser will attempt to determine the values of any fields not passed as keyword arguments by reading from the environment. (Default values will still be used if the matching environment variable is not set.) This makes it easy to: * Create a clearly-defined, type-hinted application configuration class * Automatically read modifications to the configuration from environment variables * Manually override specific settings in the initialiser where desired (e.g. in unit tests) For example: ```py from collections.abc import Callable from typing import Any from pydantic import ( AliasChoices, AmqpDsn, BaseModel, Field, ImportString, PostgresDsn, RedisDsn, ) from pydantic_settings import BaseSettings, SettingsConfigDict class SubModel(BaseModel): foo: str = 'bar' apple: int = 1 class Settings(BaseSettings): auth_key: str = Field(validation_alias='my_auth_key') # (1)! api_key: str = Field(alias='my_api_key') # (2)! redis_dsn: RedisDsn = Field( 'redis://user:pass@localhost:6379/1', validation_alias=AliasChoices('service_redis_dsn', 'redis_url'), # (3)! ) pg_dsn: PostgresDsn = 'postgres://user:pass@localhost:5432/foobar' amqp_dsn: AmqpDsn = 'amqp://user:pass@localhost:5672/' special_function: ImportString[Callable[[Any], Any]] = 'math.cos' # (4)! # to override domains: # export my_prefix_domains='["foo.com", "bar.com"]' domains: set[str] = set() # to override more_settings: # export my_prefix_more_settings='{"foo": "x", "apple": 1}' more_settings: SubModel = SubModel() model_config = SettingsConfigDict(env_prefix='my_prefix_') # (5)! print(Settings().model_dump()) """ { 'auth_key': 'xxx', 'api_key': 'xxx', 'redis_dsn': RedisDsn('redis://user:pass@localhost:6379/1'), 'pg_dsn': PostgresDsn('postgres://user:pass@localhost:5432/foobar'), 'amqp_dsn': AmqpDsn('amqp://user:pass@localhost:5672/'), 'special_function': math.cos, 'domains': set(), 'more_settings': {'foo': 'bar', 'apple': 1}, } """ ``` 1. The environment variable name is overridden using `validation_alias`. In this case, the environment variable `my_auth_key` will be read instead of `auth_key`. Check the [`Field` documentation](fields.md) for more information. 2. The environment variable name is overridden using `alias`. In this case, the environment variable `my_api_key` will be used for both validation and serialization instead of `api_key`. Check the [`Field` documentation](fields.md#field-aliases) for more information. 3. The [`AliasChoices`][pydantic.AliasChoices] class allows to have multiple environment variable names for a single field. The first environment variable that is found will be used. Check the [documentation on alias choices](alias.md#aliaspath-and-aliaschoices) for more information. 4. The [`ImportString`][pydantic.types.ImportString] class allows to import an object from a string. In this case, the environment variable `special_function` will be read and the function [`math.cos`][] will be imported. 5. The `env_prefix` config setting allows to set a prefix for all environment variables. Check the [Environment variable names documentation](#environment-variable-names) for more information. ## Validation of default values Unlike pydantic `BaseModel`, default values of `BaseSettings` fields are validated by default. You can disable this behaviour by setting `validate_default=False` either in `model_config` or on field level by `Field(validate_default=False)`: ```py from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(validate_default=False) # default won't be validated foo: int = 'test' print(Settings()) #> foo='test' class Settings1(BaseSettings): # default won't be validated foo: int = Field('test', validate_default=False) print(Settings1()) #> foo='test' ``` Check the [validation of default values](fields.md#validate-default-values) for more information. ## Environment variable names By default, the environment variable name is the same as the field name. You can change the prefix for all environment variables by setting the `env_prefix` config setting, or via the `_env_prefix` keyword argument on instantiation: ```py from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_prefix='my_prefix_') auth_key: str = 'xxx' # will be read from `my_prefix_auth_key` ``` !!! note The default `env_prefix` is `''` (empty string). `env_prefix` is not only for env settings but also for dotenv files, secrets, and other sources. If you want to change the environment variable name for a single field, you can use an alias. There are two ways to do this: * Using `Field(alias=...)` (see `api_key` above) * Using `Field(validation_alias=...)` (see `auth_key` above) Check the [`Field` aliases documentation](fields.md#field-aliases) for more information about aliases. To apply `env_prefix` not only to variable names but also to aliases, set `env_prefix_target='all'`. To apply `env_prefix` only to aliases and not to variable names, set `env_prefix_target='alias'`. To apply `env_prefix` only to variable names (the default behavior), set `env_prefix_target='variable'`. ```py import os from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class FooBarSettings(BaseSettings): # default model_config = SettingsConfigDict(env_prefix='TARGET_') foo: str = Field(alias='FooAlias') bar: str os.environ['FooAlias'] = 'TARGET_FOO_VALUE' # (1)! os.environ['TARGET_BAR'] = 'TARGET_BAR_VALUE' print(FooBarSettings().model_dump()) #> {'foo': 'TARGET_FOO_VALUE', 'bar': 'TARGET_BAR_VALUE'} class TargetAllSettings(FooBarSettings): model_config = SettingsConfigDict( env_prefix='TARGET_ALL_', env_prefix_target='all', ) os.environ['TARGET_ALL_FooAlias'] = 'TARGET_ALL_FOO_VALUE' os.environ['TARGET_ALL_BAR'] = 'TARGET_ALL_BAR_VALUE' print(TargetAllSettings().model_dump()) #> {'foo': 'TARGET_ALL_FOO_VALUE', 'bar': 'TARGET_ALL_BAR_VALUE'} class TargetAliasSettings(FooBarSettings): model_config = SettingsConfigDict( env_prefix='TARGET_ALIAS_', env_prefix_target='alias', ) os.environ['TARGET_ALIAS_FooAlias'] = 'TARGET_ALIAS_FOO_VALUE' os.environ['BAR'] = 'TARGET_ALL_BAR_VALUE' print(TargetAliasSettings().model_dump()) #> {'foo': 'TARGET_ALIAS_FOO_VALUE', 'bar': 'TARGET_ALL_BAR_VALUE'} class TargetVarSettings(FooBarSettings): model_config = SettingsConfigDict( env_prefix='TARGET_VAR_', env_prefix_target='variable', ) os.environ['FooAlias'] = 'TARGET_VAR_FOO_VALUE' # (1)! os.environ['TARGET_VAR_BAR'] = 'TARGET_VAR_BAR_VALUE' print(TargetVarSettings().model_dump()) #> {'foo': 'TARGET_VAR_FOO_VALUE', 'bar': 'TARGET_VAR_BAR_VALUE'} ``` 1. `env_prefix` will be ignored and the value will be read from `FooAlias` environment variable. ### Case-sensitivity By default, environment variable names are case-insensitive. If you want to make environment variable names case-sensitive, you can set the `case_sensitive` config setting: ```py from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(case_sensitive=True) redis_host: str = 'localhost' ``` When `case_sensitive` is `True`, the environment variable names must match field names (optionally with a prefix), so in this example `redis_host` could only be modified via `export redis_host`. If you want to name environment variables all upper-case, you should name attribute all upper-case too. You can still name environment variables anything you like through `Field(validation_alias=...)`. Case-sensitivity can also be set via the `_case_sensitive` keyword argument on instantiation. In case of nested models, the `case_sensitive` setting will be applied to all nested models. ```py import os from pydantic import BaseModel, ValidationError from pydantic_settings import BaseSettings class RedisSettings(BaseModel): host: str port: int class Settings(BaseSettings, case_sensitive=True): redis: RedisSettings os.environ['redis'] = '{"host": "localhost", "port": 6379}' print(Settings().model_dump()) #> {'redis': {'host': 'localhost', 'port': 6379}} os.environ['redis'] = '{"HOST": "localhost", "port": 6379}' # (1)! try: Settings() except ValidationError as e: print(e) """ 1 validation error for Settings redis.host Field required [type=missing, input_value={'HOST': 'localhost', 'port': 6379}, input_type=dict] For further information visit https://errors.pydantic.dev/2/v/missing """ ``` 1. Note that the `host` field is not found because the environment variable name is `HOST` (all upper-case). !!! note On Windows, Python's `os` module always treats environment variables as case-insensitive, so the `case_sensitive` config setting will have no effect - settings will always be updated ignoring case. ## Parsing environment variable values By default environment variables are parsed verbatim, including if the value is empty. You can choose to ignore empty environment variables by setting the `env_ignore_empty` config setting to `True`. This can be useful if you would prefer to use the default value for a field rather than an empty value from the environment. For most simple field types (such as `int`, `float`, `str`, etc.), the environment variable value is parsed the same way it would be if passed directly to the initialiser (as a string). Complex types like `list`, `set`, `dict`, and sub-models are populated from the environment by treating the environment variable's value as a JSON-encoded string. Another way to populate nested complex variables is to configure your model with the `env_nested_delimiter` config setting, then use an environment variable with a name pointing to the nested module fields. What it does is simply explodes your variable into nested models or dicts. So if you define a variable `FOO__BAR__BAZ=123` it will convert it into `FOO={'BAR': {'BAZ': 123}}` If you have multiple variables with the same structure they will be merged. !!! note Sub model has to inherit from `pydantic.BaseModel`, Otherwise `pydantic-settings` will initialize sub model, collects values for sub model fields separately, and you may get unexpected results. As an example, given the following environment variables: ```bash # your environment export V0=0 export SUB_MODEL='{"v1": "json-1", "v2": "json-2"}' export SUB_MODEL__V2=nested-2 export SUB_MODEL__V3=3 export SUB_MODEL__DEEP__V4=v4 ``` You could load them into the following settings model: ```py from pydantic import BaseModel from pydantic_settings import BaseSettings, SettingsConfigDict class DeepSubModel(BaseModel): # (1)! v4: str class SubModel(BaseModel): # (2)! v1: str v2: bytes v3: int deep: DeepSubModel class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__') v0: str sub_model: SubModel print(Settings().model_dump()) """ { 'v0': '0', 'sub_model': {'v1': 'json-1', 'v2': b'nested-2', 'v3': 3, 'deep': {'v4': 'v4'}}, } """ ``` 1. Sub model has to inherit from `pydantic.BaseModel`. 2. Sub model has to inherit from `pydantic.BaseModel`. `env_nested_delimiter` can be configured via the `model_config` as shown above, or via the `_env_nested_delimiter` keyword argument on instantiation. By default environment variables are split by `env_nested_delimiter` into arbitrarily deep nested fields. You can limit the depth of the nested fields with the `env_nested_max_split` config setting. A common use case this is particularly useful is for two-level deep settings, where the `env_nested_delimiter` (usually a single `_`) may be a substring of model field names. For example: ```bash # your environment export GENERATION_LLM_PROVIDER='anthropic' export GENERATION_LLM_API_KEY='your-api-key' export GENERATION_LLM_API_VERSION='2024-03-15' ``` You could load them into the following settings model: ```py from pydantic import BaseModel from pydantic_settings import BaseSettings, SettingsConfigDict class LLMConfig(BaseModel): provider: str = 'openai' api_key: str api_type: str = 'azure' api_version: str = '2023-03-15-preview' class GenerationConfig(BaseSettings): model_config = SettingsConfigDict( env_nested_delimiter='_', env_nested_max_split=1, env_prefix='GENERATION_' ) llm: LLMConfig ... print(GenerationConfig().model_dump()) """ { 'llm': { 'provider': 'anthropic', 'api_key': 'your-api-key', 'api_type': 'azure', 'api_version': '2024-03-15', } } """ ``` Without `env_nested_max_split=1` set, `GENERATION_LLM_API_KEY` would be parsed as `llm.api.key` instead of `llm.api_key` and it would raise a `ValidationError`. Nested environment variables take precedence over the top-level environment variable JSON (e.g. in the example above, `SUB_MODEL__V2` trumps `SUB_MODEL`). You may also populate a complex type by providing your own source class. ```py import json import os from typing import Any from pydantic.fields import FieldInfo from pydantic_settings import ( BaseSettings, EnvSettingsSource, PydanticBaseSettingsSource, ) class MyCustomSource(EnvSettingsSource): def prepare_field_value( self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool ) -> Any: if field_name == 'numbers': return [int(x) for x in value.split(',')] return json.loads(value) class Settings(BaseSettings): numbers: list[int] @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (MyCustomSource(settings_cls),) os.environ['numbers'] = '1,2,3' print(Settings().model_dump()) #> {'numbers': [1, 2, 3]} ``` ### Disabling JSON parsing pydantic-settings by default parses complex types from environment variables as JSON strings. If you want to disable this behavior for a field and parse the value in your own validator, you can annotate the field with [`NoDecode`](../api/pydantic_settings.md#pydantic_settings.NoDecode): ```py import os from typing import Annotated from pydantic import field_validator from pydantic_settings import BaseSettings, NoDecode class Settings(BaseSettings): numbers: Annotated[list[int], NoDecode] # (1)! @field_validator('numbers', mode='before') @classmethod def decode_numbers(cls, v: str) -> list[int]: return [int(x) for x in v.split(',')] os.environ['numbers'] = '1,2,3' print(Settings().model_dump()) #> {'numbers': [1, 2, 3]} ``` 1. The `NoDecode` annotation disables JSON parsing for the `numbers` field. The `decode_numbers` field validator will be called to parse the value. You can also disable JSON parsing for all fields by setting the `enable_decoding` config setting to `False`: ```py import os from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(enable_decoding=False) numbers: list[int] @field_validator('numbers', mode='before') @classmethod def decode_numbers(cls, v: str) -> list[int]: return [int(x) for x in v.split(',')] os.environ['numbers'] = '1,2,3' print(Settings().model_dump()) #> {'numbers': [1, 2, 3]} ``` You can force JSON parsing for a field by annotating it with [`ForceDecode`](../api/pydantic_settings.md#pydantic_settings.ForceDecode). This will bypass the `enable_decoding` config setting: ```py import os from typing import Annotated from pydantic import field_validator from pydantic_settings import BaseSettings, ForceDecode, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(enable_decoding=False) numbers: Annotated[list[int], ForceDecode] numbers1: list[int] # (1)! @field_validator('numbers1', mode='before') @classmethod def decode_numbers1(cls, v: str) -> list[int]: return [int(x) for x in v.split(',')] os.environ['numbers'] = '["1","2","3"]' os.environ['numbers1'] = '1,2,3' print(Settings().model_dump()) #> {'numbers': [1, 2, 3], 'numbers1': [1, 2, 3]} ``` 1. The `numbers1` field is not annotated with `ForceDecode`, so it will not be parsed as JSON. and we have to provide a custom validator to parse the value. ## Nested model default partial updates By default, Pydantic settings does not allow partial updates to nested model default objects. This behavior can be overriden by setting the `nested_model_default_partial_update` flag to `True`, which will allow partial updates on nested model default object fields. ```py import os from pydantic import BaseModel from pydantic_settings import BaseSettings, SettingsConfigDict class SubModel(BaseModel): val: int = 0 flag: bool = False class SettingsPartialUpdate(BaseSettings): model_config = SettingsConfigDict( env_nested_delimiter='__', nested_model_default_partial_update=True ) nested_model: SubModel = SubModel(val=1) class SettingsNoPartialUpdate(BaseSettings): model_config = SettingsConfigDict( env_nested_delimiter='__', nested_model_default_partial_update=False ) nested_model: SubModel = SubModel(val=1) # Apply a partial update to the default object using environment variables os.environ['NESTED_MODEL__FLAG'] = 'True' # When partial update is enabled, the existing SubModel instance is updated # with nested_model.flag=True change assert SettingsPartialUpdate().model_dump() == { 'nested_model': {'val': 1, 'flag': True} } # When partial update is disabled, a new SubModel instance is instantiated # with nested_model.flag=True change assert SettingsNoPartialUpdate().model_dump() == { 'nested_model': {'val': 0, 'flag': True} } ``` ## Dotenv (.env) support Dotenv files (generally named `.env`) are a common pattern that make it easy to use environment variables in a platform-independent manner. A dotenv file follows the same general principles of all environment variables, and it looks like this: ```bash title=".env" # ignore comment ENVIRONMENT="production" REDIS_ADDRESS=localhost:6379 MEANING_OF_LIFE=42 MY_VAR='Hello world' ``` Once you have your `.env` file filled with variables, *pydantic* supports loading it in two ways: 1. Setting the `env_file` (and `env_file_encoding` if you don't want the default encoding of your OS) on `model_config` in the `BaseSettings` class: ````py hl_lines="4 5" from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8') ```` 2. Instantiating the `BaseSettings` derived class with the `_env_file` keyword argument (and the `_env_file_encoding` if needed): ````py hl_lines="8" from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8') settings = Settings(_env_file='prod.env', _env_file_encoding='utf-8') ```` In either case, the value of the passed argument can be any valid path or filename, either absolute or relative to the current working directory. From there, *pydantic* will handle everything for you by loading in your variables and validating them. !!! note If a filename is specified for `env_file`, Pydantic will only check the current working directory and won't check any parent directories for the `.env` file. Even when using a dotenv file, *pydantic* will still read environment variables as well as the dotenv file, **environment variables will always take priority over values loaded from a dotenv file**. Passing a file path via the `_env_file` keyword argument on instantiation (method 2) will override the value (if any) set on the `model_config` class. If the above snippets were used in conjunction, `prod.env` would be loaded while `.env` would be ignored. If you need to load multiple dotenv files, you can pass multiple file paths as a tuple or list. The files will be loaded in order, with each file overriding the previous one. ```py from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( # `.env.prod` takes priority over `.env` env_file=('.env', '.env.prod') ) ``` You can also use the keyword argument override to tell Pydantic not to load any file at all (even if one is set in the `model_config` class) by passing `None` as the instantiation keyword argument, e.g. `settings = Settings(_env_file=None)`. Because python-dotenv is used to parse the file, bash-like semantics such as `export` can be used which (depending on your OS and environment) may allow your dotenv file to also be used with `source`, see [python-dotenv's documentation](https://saurabh-kumar.com/python-dotenv/#usages) for more details. Pydantic settings consider `extra` config in case of dotenv file. It means if you set the `extra=forbid` (*default*) on `model_config` and your dotenv file contains an entry for a field that is not defined in settings model, it will raise `ValidationError` in settings construction. For compatibility with pydantic 1.x BaseSettings you should use `extra=ignore`: ```py from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file='.env', extra='ignore') ``` !!! note Pydantic settings loads all the values from dotenv file and passes it to the model, regardless of the model's `env_prefix`. So if you provide extra values in a dotenv file, whether they start with `env_prefix` or not, a `ValidationError` will be raised. ## Command Line Support Pydantic settings provides integrated CLI support, making it easy to quickly define CLI applications using Pydantic models. There are two primary use cases for Pydantic settings CLI: 1. When using a CLI to override fields in Pydantic models. 2. When using Pydantic models to define CLIs. By default, the experience is tailored towards use case #1 and builds on the foundations established in [parsing environment variables](#parsing-environment-variable-values). If your use case primarily falls into #2, you will likely want to enable most of the defaults outlined at the end of [creating CLI applications](#creating-cli-applications). ### The Basics To get started, let's revisit the example presented in [parsing environment variables](#parsing-environment-variable-values) but using a Pydantic settings CLI: ```py import sys from pydantic import BaseModel from pydantic_settings import BaseSettings, SettingsConfigDict class DeepSubModel(BaseModel): v4: str class SubModel(BaseModel): v1: str v2: bytes v3: int deep: DeepSubModel class Settings(BaseSettings): model_config = SettingsConfigDict(cli_parse_args=True) v0: str sub_model: SubModel sys.argv = [ 'example.py', '--v0=0', '--sub_model={"v1": "json-1", "v2": "json-2"}', '--sub_model.v2=nested-2', '--sub_model.v3=3', '--sub_model.deep.v4=v4', ] print(Settings().model_dump()) """ { 'v0': '0', 'sub_model': {'v1': 'json-1', 'v2': b'nested-2', 'v3': 3, 'deep': {'v4': 'v4'}}, } """ ``` To enable CLI parsing, we simply set the `cli_parse_args` flag to a valid value, which retains similar connotations as defined in `argparse`. Note that a CLI settings source is [**the topmost source**](#field-value-priority) by default unless its [priority value is customised](#customise-settings-sources): ```py import os import sys from pydantic_settings import ( BaseSettings, CliSettingsSource, PydanticBaseSettingsSource, ) class Settings(BaseSettings): my_foo: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return env_settings, CliSettingsSource(settings_cls, cli_parse_args=True) os.environ['MY_FOO'] = 'from environment' sys.argv = ['example.py', '--my_foo=from cli'] print(Settings().model_dump()) #> {'my_foo': 'from environment'} ``` #### Lists CLI argument parsing of lists supports intermixing of any of the below three styles: * JSON style `--field='[1,2]'` * Argparse style `--field 1 --field 2` * Lazy style `--field=1,2` ```py import sys from pydantic_settings import BaseSettings class Settings(BaseSettings, cli_parse_args=True): my_list: list[int] sys.argv = ['example.py', '--my_list', '[1,2]'] print(Settings().model_dump()) #> {'my_list': [1, 2]} sys.argv = ['example.py', '--my_list', '1', '--my_list', '2'] print(Settings().model_dump()) #> {'my_list': [1, 2]} sys.argv = ['example.py', '--my_list', '1,2'] print(Settings().model_dump()) #> {'my_list': [1, 2]} ``` #### Dictionaries CLI argument parsing of dictionaries supports intermixing of any of the below two styles: * JSON style `--field='{"k1": 1, "k2": 2}'` * Environment variable style `--field k1=1 --field k2=2` These can be used in conjunction with list forms as well, e.g: * `--field k1=1,k2=2 --field k3=3 --field '{"k4": 4}'` etc. ```py import sys from pydantic_settings import BaseSettings class Settings(BaseSettings, cli_parse_args=True): my_dict: dict[str, int] sys.argv = ['example.py', '--my_dict', '{"k1":1,"k2":2}'] print(Settings().model_dump()) #> {'my_dict': {'k1': 1, 'k2': 2}} sys.argv = ['example.py', '--my_dict', 'k1=1', '--my_dict', 'k2=2'] print(Settings().model_dump()) #> {'my_dict': {'k1': 1, 'k2': 2}} ``` #### Literals and Enums CLI argument parsing of literals and enums are converted into CLI choices. ```py import sys from enum import IntEnum from typing import Literal from pydantic_settings import BaseSettings class Fruit(IntEnum): pear = 0 kiwi = 1 lime = 2 class Settings(BaseSettings, cli_parse_args=True): fruit: Fruit pet: Literal['dog', 'cat', 'bird'] sys.argv = ['example.py', '--fruit', 'lime', '--pet', 'cat'] print(Settings().model_dump()) #> {'fruit': , 'pet': 'cat'} ``` #### Aliases Pydantic field aliases are added as CLI argument aliases. Aliases of length one are converted into short options. ```py import sys from pydantic import AliasChoices, AliasPath, Field from pydantic_settings import BaseSettings class User(BaseSettings, cli_parse_args=True): first_name: str = Field( validation_alias=AliasChoices('f', 'fname', AliasPath('fullname', 0)) ) last_name: str = Field( validation_alias=AliasChoices('l', 'lname', AliasPath('fullname', 1)) ) sys.argv = ['example.py', '--fname', 'John', '--lname', 'Doe'] print(User().model_dump()) #> {'first_name': 'John', 'last_name': 'Doe'} sys.argv = ['example.py', '-f', 'John', '-l', 'Doe'] print(User().model_dump()) #> {'first_name': 'John', 'last_name': 'Doe'} sys.argv = ['example.py', '--fullname', 'John,Doe'] print(User().model_dump()) #> {'first_name': 'John', 'last_name': 'Doe'} sys.argv = ['example.py', '--fullname', 'John', '--lname', 'Doe'] print(User().model_dump()) #> {'first_name': 'John', 'last_name': 'Doe'} ``` ### Subcommands and Positional Arguments Subcommands and positional arguments are expressed using the `CliSubCommand` and `CliPositionalArg` annotations. The subcommand annotation can only be applied to required fields (i.e. fields that do not have a default value). Furthermore, subcommands must be a valid type derived from either a pydantic `BaseModel` or pydantic.dataclasses `dataclass`. Parsed subcommands can be retrieved from model instances using the `get_subcommand` utility function. If a subcommand is not required, set the `is_required` flag to `False` to disable raising an error if no subcommand is found. !!! note CLI settings subcommands are limited to a single subparser per model. In other words, all subcommands for a model are grouped under a single subparser; it does not allow for multiple subparsers with each subparser having its own set of subcommands. For more information on subparsers, see [argparse subcommands](https://docs.python.org/3/library/argparse.html#sub-commands). !!! note `CliSubCommand` and `CliPositionalArg` are always case sensitive. ```py import sys from pydantic import BaseModel from pydantic_settings import ( BaseSettings, CliPositionalArg, CliSubCommand, SettingsError, get_subcommand, ) class Init(BaseModel): directory: CliPositionalArg[str] class Clone(BaseModel): repository: CliPositionalArg[str] directory: CliPositionalArg[str] class Git(BaseSettings, cli_parse_args=True, cli_exit_on_error=False): clone: CliSubCommand[Clone] init: CliSubCommand[Init] # Run without subcommands sys.argv = ['example.py'] cmd = Git() assert cmd.model_dump() == {'clone': None, 'init': None} try: # Will raise an error since no subcommand was provided get_subcommand(cmd).model_dump() except SettingsError as err: assert str(err) == 'Error: CLI subcommand is required {clone, init}' # Will not raise an error since subcommand is not required assert get_subcommand(cmd, is_required=False) is None # Run the clone subcommand sys.argv = ['example.py', 'clone', 'repo', 'dest'] cmd = Git() assert cmd.model_dump() == { 'clone': {'repository': 'repo', 'directory': 'dest'}, 'init': None, } # Returns the subcommand model instance (in this case, 'clone') assert get_subcommand(cmd).model_dump() == { 'directory': 'dest', 'repository': 'repo', } ``` The `CliSubCommand` and `CliPositionalArg` annotations also support union operations and aliases. For unions of Pydantic models, it is important to remember the [nuances](https://docs.pydantic.dev/latest/concepts/unions/) that can arise during validation. Specifically, for unions of subcommands that are identical in content, it is recommended to break them out into separate `CliSubCommand` fields to avoid any complications. Lastly, the derived subcommand names from unions will be the names of the Pydantic model classes themselves. When assigning aliases to `CliSubCommand` or `CliPositionalArg` fields, only a single alias can be assigned. For non-union subcommands, aliasing will change the displayed help text and subcommand name. Conversely, for union subcommands, aliasing will have no tangible effect from the perspective of the CLI settings source. Lastly, for positional arguments, aliasing will change the CLI help text displayed for the field. ```py import sys from typing import Union from pydantic import BaseModel, Field from pydantic_settings import ( BaseSettings, CliPositionalArg, CliSubCommand, get_subcommand, ) class Alpha(BaseModel): """Apha Help""" cmd_alpha: CliPositionalArg[str] = Field(alias='alpha-cmd') class Beta(BaseModel): """Beta Help""" opt_beta: str = Field(alias='opt-beta') class Gamma(BaseModel): """Gamma Help""" opt_gamma: str = Field(alias='opt-gamma') class Root(BaseSettings, cli_parse_args=True, cli_exit_on_error=False): alpha_or_beta: CliSubCommand[Union[Alpha, Beta]] = Field(alias='alpha-or-beta-cmd') gamma: CliSubCommand[Gamma] = Field(alias='gamma-cmd') sys.argv = ['example.py', 'Alpha', 'hello'] assert get_subcommand(Root()).model_dump() == {'cmd_alpha': 'hello'} sys.argv = ['example.py', 'Beta', '--opt-beta=hey'] assert get_subcommand(Root()).model_dump() == {'opt_beta': 'hey'} sys.argv = ['example.py', 'gamma-cmd', '--opt-gamma=hi'] assert get_subcommand(Root()).model_dump() == {'opt_gamma': 'hi'} ``` ### Creating CLI Applications The `CliApp` class provides two utility methods, `CliApp.run` and `CliApp.run_subcommand`, that can be used to run a Pydantic `BaseSettings`, `BaseModel`, or `pydantic.dataclasses.dataclass` as a CLI application. Primarily, the methods provide structure for running `cli_cmd` methods associated with models. `CliApp.run` can be used in directly providing the `cli_args` to be parsed, and will run the model `cli_cmd` method (if defined) after instantiation: ```py from pydantic_settings import BaseSettings, CliApp class Settings(BaseSettings): this_foo: str def cli_cmd(self) -> None: # Print the parsed data print(self.model_dump()) #> {'this_foo': 'is such a foo'} # Update the parsed data showing cli_cmd ran self.this_foo = 'ran the foo cli cmd' s = CliApp.run(Settings, cli_args=['--this_foo', 'is such a foo']) print(s.model_dump()) #> {'this_foo': 'ran the foo cli cmd'} ``` Similarly, the `CliApp.run_subcommand` can be used in recursive fashion to run the `cli_cmd` method of a subcommand: ```py from pydantic import BaseModel from pydantic_settings import CliApp, CliPositionalArg, CliSubCommand class Init(BaseModel): directory: CliPositionalArg[str] def cli_cmd(self) -> None: print(f'git init "{self.directory}"') #> git init "dir" self.directory = 'ran the git init cli cmd' class Clone(BaseModel): repository: CliPositionalArg[str] directory: CliPositionalArg[str] def cli_cmd(self) -> None: print(f'git clone from "{self.repository}" into "{self.directory}"') self.directory = 'ran the clone cli cmd' class Git(BaseModel): clone: CliSubCommand[Clone] init: CliSubCommand[Init] def cli_cmd(self) -> None: CliApp.run_subcommand(self) cmd = CliApp.run(Git, cli_args=['init', 'dir']) assert cmd.model_dump() == { 'clone': None, 'init': {'directory': 'ran the git init cli cmd'}, } ``` !!! note Unlike `CliApp.run`, `CliApp.run_subcommand` requires the subcommand model to have a defined `cli_cmd` method. For `BaseModel` and `pydantic.dataclasses.dataclass` types, `CliApp.run` will internally use the following `BaseSettings` configuration defaults: * `nested_model_default_partial_update=True` * `case_sensitive=True` * `cli_hide_none_type=True` * `cli_avoid_json=True` * `cli_enforce_required=True` * `cli_implicit_flags=True` * `cli_kebab_case=True` #### Asynchronous Commands Pydantic settings supports running asynchronous CLI commands via `CliApp.run` and `CliApp.run_subcommand`. With this feature, you can define async def methods within your Pydantic models (including subcommands) and have them executed just like their synchronous counterparts. Specifically: 1. Asynchronous methods are supported: You can now mark your cli_cmd or similar CLI entrypoint methods as async def and have CliApp execute them. 2. Subcommands may also be asynchronous: If you have nested CLI subcommands, the final (lowest-level) subcommand methods can likewise be asynchronous. 3. Limit asynchronous methods to final subcommands: Defining parent commands as asynchronous is not recommended, because it can result in additional threads and event loops being created. For best performance and to avoid unnecessary resource usage, only implement your deepest (child) subcommands as async def. Below is a simple example demonstrating an asynchronous top-level command: ```py from pydantic_settings import BaseSettings, CliApp class AsyncSettings(BaseSettings): async def cli_cmd(self) -> None: print('Hello from an async CLI method!') #> Hello from an async CLI method! # If an event loop is already running, a new thread will be used; # otherwise, asyncio.run() is used to execute this async method. assert CliApp.run(AsyncSettings, cli_args=[]).model_dump() == {} ``` #### Asynchronous Subcommands As mentioned above, you can also define subcommands as async. However, only do so for the leaf (lowest-level) subcommand to avoid spawning new threads and event loops unnecessarily in parent commands: ```py from pydantic import BaseModel from pydantic_settings import ( BaseSettings, CliApp, CliPositionalArg, CliSubCommand, ) class Clone(BaseModel): repository: CliPositionalArg[str] directory: CliPositionalArg[str] async def cli_cmd(self) -> None: # Perform async tasks here, e.g. network or I/O operations print(f'Cloning async from "{self.repository}" into "{self.directory}"') #> Cloning async from "repo" into "dir" class Git(BaseSettings): clone: CliSubCommand[Clone] def cli_cmd(self) -> None: # Run the final subcommand (clone/init). It is recommended to define async methods only at the deepest level. CliApp.run_subcommand(self) CliApp.run(Git, cli_args=['clone', 'repo', 'dir']).model_dump() == { 'repository': 'repo', 'directory': 'dir', } ``` When executing a subcommand with an asynchronous cli_cmd, Pydantic settings automatically detects whether the current thread already has an active event loop. If so, the async command is run in a fresh thread to avoid conflicts. Otherwise, it uses asyncio.run() in the current thread. This handling ensures your asynchronous subcommands "just work" without additional manual setup. #### Printing Help The `print_help` and `format_help` methods are available for printing or formatting help. ```python from pydantic_settings import BaseSettings, CliApp class Settings(BaseSettings, cli_prog_name='example'): def cli_cmd(self) -> None: # Will print help for the current command or subcommand instance. CliApp.print_help(self) # Will return formatted help for the current command or subcommand instance. CliApp.format_help(self) CliApp.run(Settings, cli_args=[]) """ usage: example [-h] options: -h, --help show this help message and exit """ # You can also print or format help on the class itself. print(CliApp.format_help(Settings)) """ usage: example [-h] options: -h, --help show this help message and exit """ ``` #### Serializing Arguments An instantiated Pydantic model can be serialized into its CLI arguments using the `CliApp.serialize` method. Serialization styles can be controlled using the `list_style`, `dict_style`, and `positionals_first` flags. ```py from pydantic import BaseModel from pydantic_settings import CliApp, CliPositionalArg class Nested(BaseModel): that: dict[str, int] class Settings(BaseModel): positional_arg: CliPositionalArg[str] this: list[str] nested: Nested settings = Settings( positional_arg='arg', this=['hello', 'world'], nested=Nested(that={'a': 1, 'b': 2}) ) print(CliApp.serialize(settings)) #> ['--this', '["hello", "world"]', '--nested.that', '{"a": 1, "b": 2}', 'arg'] print(CliApp.serialize(settings, positionals_first=True)) #> ['arg', '--this', '["hello", "world"]', '--nested.that', '{"a": 1, "b": 2}'] print(CliApp.serialize(settings, list_style='lazy')) #> ['--this', 'hello,world', '--nested.that', '{"a": 1, "b": 2}', 'arg'] print(CliApp.serialize(settings, list_style='argparse')) #> ['--this', 'hello', '--this', 'world', '--nested.that', '{"a": 1, "b": 2}', 'arg'] print(CliApp.serialize(settings, dict_style='env')) """ ['--this', '["hello", "world"]', '--nested.that', 'a=1', '--nested.that', 'b=2', 'arg'] """ ``` ### Mutually Exclusive Groups CLI mutually exclusive groups can be created by inheriting from the `CliMutuallyExclusiveGroup` class. !!! note A `CliMutuallyExclusiveGroup` cannot be used in a union or contain nested models. ```py from typing import Optional from pydantic import BaseModel from pydantic_settings import CliApp, CliMutuallyExclusiveGroup, SettingsError class Circle(CliMutuallyExclusiveGroup): radius: Optional[float] = None diameter: Optional[float] = None perimeter: Optional[float] = None class Settings(BaseModel): circle: Circle try: CliApp.run( Settings, cli_args=['--circle.radius=1', '--circle.diameter=2'], cli_exit_on_error=False, ) except SettingsError as e: print(e) """ error parsing CLI: argument --circle.diameter: not allowed with argument --circle.radius """ ``` ### Customizing the CLI Experience The below flags can be used to customise the CLI experience to your needs. #### Change the Displayed Program Name Change the default program name displayed in the help text usage by setting `cli_prog_name`. By default, it will derive the name of the currently executing program from `sys.argv[0]`, just like argparse. ```py import sys from pydantic_settings import BaseSettings class Settings(BaseSettings, cli_parse_args=True, cli_prog_name='appdantic'): pass try: sys.argv = ['example.py', '--help'] Settings() except SystemExit as e: print(e) #> 0 """ usage: appdantic [-h] options: -h, --help show this help message and exit """ ``` #### CLI Boolean Flags Change whether boolean fields should be explicit or implicit by default using the `cli_implicit_flags` setting. By default, boolean fields are "explicit", meaning a boolean value must be explicitly provided on the CLI, e.g. `--flag=True`. Conversely, boolean fields that are "implicit" derive the value from the flag itself, e.g. `--flag,--no-flag`, which removes the need for an explicit value to be passed. Additionally, the provided `CliImplicitFlag` and `CliExplicitFlag` annotations can be used for more granular control when necessary. ```py from pydantic_settings import BaseSettings, CliExplicitFlag, CliImplicitFlag class ExplicitSettings(BaseSettings, cli_parse_args=True): """Boolean fields are explicit by default.""" explicit_req: bool """ --explicit_req bool (required) """ explicit_opt: bool = False """ --explicit_opt bool (default: False) """ # Booleans are explicit by default, so must override implicit flags with annotation implicit_req: CliImplicitFlag[bool] """ --implicit_req, --no-implicit_req (required) """ implicit_opt: CliImplicitFlag[bool] = False """ --implicit_opt, --no-implicit_opt (default: False) """ class ImplicitSettings(BaseSettings, cli_parse_args=True, cli_implicit_flags=True): """With cli_implicit_flags=True, boolean fields are implicit by default.""" # Booleans are implicit by default, so must override explicit flags with annotation explicit_req: CliExplicitFlag[bool] """ --explicit_req bool (required) """ explicit_opt: CliExplicitFlag[bool] = False """ --explicit_opt bool (default: False) """ implicit_req: bool """ --implicit_req, --no-implicit_req (required) """ implicit_opt: bool = False """ --implicit_opt, --no-implicit_opt (default: False) """ ``` Implicit flag behavior can be further refined using the "toggle" or "dual" mode settings. Similarly, the provided `CliToggleFlag` and `CliDualFlag` annotations can be used for more granular control. For "toggle" flags, if default=`False`, `--flag` will store `True`. Otherwise, if default=`True`, `--no-flag` will store `False`. ```py from pydantic_settings import BaseSettings, CliDualFlag, CliToggleFlag class ImplicitDualSettings( BaseSettings, cli_parse_args=True, cli_implicit_flags='dual' ): """With cli_implicit_flags='dual', implicit flags are dual by default.""" implicit_req: bool """ --implicit_req, --no-implicit_req (required) """ implicit_dual_opt: bool = False """ --implicit_dual_opt, --no-implicit_dual_opt (default: False) """ # Implicit flags are dual by default, so must override toggle flags with annotation flag_a: CliToggleFlag[bool] = False """ --flag_a (default: False) """ # Implicit flags are dual by default, so must override toggle flags with annotation flag_b: CliToggleFlag[bool] = True """ --no-flag_b (default: True) """ class ImplicitToggleSettings( BaseSettings, cli_parse_args=True, cli_implicit_flags='toggle' ): """With cli_implicit_flags='toggle', implicit flags are toggle by default.""" implicit_req: bool """ --implicit_req, --no-implicit_req (required) """ # Implicit flags are toggle by default, so must override dual flags with annotation implicit_dual_opt: CliDualFlag[bool] = False """ --implicit_dual_opt, --no-implicit_dual_opt (default: False) """ flag_a: bool = False """ --flag_a (default: False) """ flag_b: bool = True """ --no-flag_b (default: True) """ ``` #### Ignore and Retrieve Unknown Arguments Change whether to ignore unknown CLI arguments and only parse known ones using `cli_ignore_unknown_args`. By default, the CLI does not ignore any args. Ignored arguments can then be retrieved using the `CliUnknownArgs` annotation. ```py import sys from pydantic_settings import BaseSettings, CliUnknownArgs class Settings(BaseSettings, cli_parse_args=True, cli_ignore_unknown_args=True): good_arg: str ignored_args: CliUnknownArgs sys.argv = ['example.py', '--bad-arg=bad', 'ANOTHER_BAD_ARG', '--good_arg=hello world'] print(Settings().model_dump()) #> {'good_arg': 'hello world', 'ignored_args': ['--bad-arg=bad', 'ANOTHER_BAD_ARG']} ``` #### CLI Kebab Case for Arguments Change whether CLI arguments should use kebab case by enabling `cli_kebab_case`. By default, `cli_kebab_case=True` will ignore enum fields, and is equivalent to `cli_kebab_case='no_enums'`. To apply kebab case to everything, including enums, use `cli_kebab_case='all'`. ```py import sys from pydantic import Field from pydantic_settings import BaseSettings class Settings(BaseSettings, cli_parse_args=True, cli_kebab_case=True): my_option: str = Field(description='will show as kebab case on CLI') try: sys.argv = ['example.py', '--help'] Settings() except SystemExit as e: print(e) #> 0 """ usage: example.py [-h] [--my-option str] options: -h, --help show this help message and exit --my-option str will show as kebab case on CLI (required) """ ``` #### Change Whether CLI Should Exit on Error Change whether the CLI internal parser will exit on error or raise a `SettingsError` exception by using `cli_exit_on_error`. By default, the CLI internal parser will exit on error. ```py import sys from pydantic_settings import BaseSettings, SettingsError class Settings(BaseSettings, cli_parse_args=True, cli_exit_on_error=False): ... try: sys.argv = ['example.py', '--bad-arg'] Settings() except SettingsError as e: print(e) #> error parsing CLI: unrecognized arguments: --bad-arg ``` #### Enforce Required Arguments at CLI Pydantic settings is designed to pull values in from various sources when instantating a model. This means a field that is required is not strictly required from any single source (e.g. the CLI). Instead, all that matters is that one of the sources provides the required value. However, if your use case [aligns more with #2](#command-line-support), using Pydantic models to define CLIs, you will likely want required fields to be _strictly required at the CLI_. We can enable this behavior by using `cli_enforce_required`. !!! note A required `CliPositionalArg` field is always strictly required (enforced) at the CLI. ```py import os import sys from pydantic import Field from pydantic_settings import BaseSettings, SettingsError class Settings( BaseSettings, cli_parse_args=True, cli_enforce_required=True, cli_exit_on_error=False, ): my_required_field: str = Field(description='a top level required field') os.environ['MY_REQUIRED_FIELD'] = 'hello from environment' try: sys.argv = ['example.py'] Settings() except SettingsError as e: print(e) #> error parsing CLI: the following arguments are required: --my_required_field ``` #### Change the None Type Parse String Change the CLI string value that will be parsed (e.g. "null", "void", "None", etc.) into `None` by setting `cli_parse_none_str`. By default it will use the `env_parse_none_str` value if set. Otherwise, it will default to "null" if `cli_avoid_json` is `False`, and "None" if `cli_avoid_json` is `True`. ```py import sys from typing import Optional from pydantic import Field from pydantic_settings import BaseSettings class Settings(BaseSettings, cli_parse_args=True, cli_parse_none_str='void'): v1: Optional[int] = Field(description='the top level v0 option') sys.argv = ['example.py', '--v1', 'void'] print(Settings().model_dump()) #> {'v1': None} ``` #### Hide None Type Values Hide `None` values from the CLI help text by enabling `cli_hide_none_type`. ```py import sys from typing import Optional from pydantic import Field from pydantic_settings import BaseSettings class Settings(BaseSettings, cli_parse_args=True, cli_hide_none_type=True): v0: Optional[str] = Field(description='the top level v0 option') try: sys.argv = ['example.py', '--help'] Settings() except SystemExit as e: print(e) #> 0 """ usage: example.py [-h] [--v0 str] options: -h, --help show this help message and exit --v0 str the top level v0 option (required) """ ``` #### Avoid Adding JSON CLI Options Avoid adding complex fields that result in JSON strings at the CLI by enabling `cli_avoid_json`. ```py import sys from pydantic import BaseModel, Field from pydantic_settings import BaseSettings class SubModel(BaseModel): v1: int = Field(description='the sub model v1 option') class Settings(BaseSettings, cli_parse_args=True, cli_avoid_json=True): sub_model: SubModel = Field( description='The help summary for SubModel related options' ) try: sys.argv = ['example.py', '--help'] Settings() except SystemExit as e: print(e) #> 0 """ usage: example.py [-h] [--sub_model.v1 int] options: -h, --help show this help message and exit sub_model options: The help summary for SubModel related options --sub_model.v1 int the sub model v1 option (required) """ ``` #### Use Class Docstring for Group Help Text By default, when populating the group help text for nested models it will pull from the field descriptions. Alternatively, we can also configure CLI settings to pull from the class docstring instead. !!! note If the field is a union of nested models the group help text will always be pulled from the field description; even if `cli_use_class_docs_for_groups` is set to `True`. ```py import sys from pydantic import BaseModel, Field from pydantic_settings import BaseSettings class SubModel(BaseModel): """The help text from the class docstring.""" v1: int = Field(description='the sub model v1 option') class Settings(BaseSettings, cli_parse_args=True, cli_use_class_docs_for_groups=True): """My application help text.""" sub_model: SubModel = Field(description='The help text from the field description') try: sys.argv = ['example.py', '--help'] Settings() except SystemExit as e: print(e) #> 0 """ usage: example.py [-h] [--sub_model JSON] [--sub_model.v1 int] My application help text. options: -h, --help show this help message and exit sub_model options: The help text from the class docstring. --sub_model JSON set sub_model from JSON string --sub_model.v1 int the sub model v1 option (required) """ ``` #### Change the CLI Flag Prefix Character Change The CLI flag prefix character used in CLI optional arguments by settings `cli_flag_prefix_char`. ```py import sys from pydantic import AliasChoices, Field from pydantic_settings import BaseSettings class Settings(BaseSettings, cli_parse_args=True, cli_flag_prefix_char='+'): my_arg: str = Field(validation_alias=AliasChoices('m', 'my-arg')) sys.argv = ['example.py', '++my-arg', 'hi'] print(Settings().model_dump()) #> {'my_arg': 'hi'} sys.argv = ['example.py', '+m', 'hi'] print(Settings().model_dump()) #> {'my_arg': 'hi'} ``` #### Suppressing Fields from CLI Help Text To suppress a field from the CLI help text, the `CliSuppress` annotation can be used for field types, or the `CLI_SUPPRESS` string constant can be used for field descriptions. ```py import sys from pydantic import Field from pydantic_settings import CLI_SUPPRESS, BaseSettings, CliSuppress class Settings(BaseSettings, cli_parse_args=True): """Suppress fields from CLI help text.""" field_a: CliSuppress[int] = 0 field_b: str = Field(default=1, description=CLI_SUPPRESS) try: sys.argv = ['example.py', '--help'] Settings() except SystemExit as e: print(e) #> 0 """ usage: example.py [-h] Suppress fields from CLI help text. options: -h, --help show this help message and exit """ ``` #### CLI Shortcuts for Arguments Add alternative CLI argument names (shortcuts) for fields using the `cli_shortcuts` option in `SettingsConfigDict`. This allows you to define additional names for CLI arguments, which can be especially useful for providing more user-friendly or shorter aliases for deeply nested or verbose field names. The `cli_shortcuts` option takes a dictionary mapping the target field name (using dot notation for nested fields) to one or more shortcut names. If multiple fields share the same shortcut, the first matching field will take precedence. **Flat Example:** ```py from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): option: str = Field(default='foo') list_option: str = Field(default='fizz') model_config = SettingsConfigDict( cli_shortcuts={'option': 'option2', 'list_option': ['list_option2']} ) # Now you can use the shortcuts on the CLI: # --option2 sets 'option', --list_option2 sets 'list_option' ``` **Nested Example:** ```py from pydantic import BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict class TwiceNested(BaseModel): option: str = Field(default='foo') class Nested(BaseModel): twice_nested_option: TwiceNested = TwiceNested() option: str = Field(default='foo') class Settings(BaseSettings): nested: Nested = Nested() model_config = SettingsConfigDict( cli_shortcuts={ 'nested.option': 'option2', 'nested.twice_nested_option.option': 'twice_nested_option', } ) # Now you can use --option2 to set nested.option and --twice_nested_option to set nested.twice_nested_option.option ``` If a shortcut collides (is mapped to multiple fields), it will apply to the first matching field in the model. ### Integrating with Existing Parsers A CLI settings source can be integrated with existing parsers by overriding the default CLI settings source with a user defined one that specifies the `root_parser` object. ```py import sys from argparse import ArgumentParser from pydantic_settings import BaseSettings, CliApp, CliSettingsSource parser = ArgumentParser() parser.add_argument('--food', choices=['pear', 'kiwi', 'lime']) class Settings(BaseSettings): name: str = 'Bob' # Set existing `parser` as the `root_parser` object for the user defined settings source cli_settings = CliSettingsSource(Settings, root_parser=parser) # Parse and load CLI settings from the command line into the settings source. sys.argv = ['example.py', '--food', 'kiwi', '--name', 'waldo'] s = CliApp.run(Settings, cli_settings_source=cli_settings) print(s.model_dump()) #> {'name': 'waldo'} # Load CLI settings from pre-parsed arguments. i.e., the parsing occurs elsewhere and we # just need to load the pre-parsed args into the settings source. parsed_args = parser.parse_args(['--food', 'kiwi', '--name', 'ralph']) s = CliApp.run(Settings, cli_args=parsed_args, cli_settings_source=cli_settings) print(s.model_dump()) #> {'name': 'ralph'} ``` A `CliSettingsSource` connects with a `root_parser` object by using parser methods to add `settings_cls` fields as command line arguments. The `CliSettingsSource` internal parser representation is based on the `argparse` library, and therefore, requires parser methods that support the same attributes as their `argparse` counterparts. The available parser methods that can be customised, along with their argparse counterparts (the defaults), are listed below: * `parse_args_method` - (`argparse.ArgumentParser.parse_args`) * `add_argument_method` - (`argparse.ArgumentParser.add_argument`) * `add_argument_group_method` - (`argparse.ArgumentParser.add_argument_group`) * `add_parser_method` - (`argparse._SubParsersAction.add_parser`) * `add_subparsers_method` - (`argparse.ArgumentParser.add_subparsers`) * `format_help_method` - (`argparse.ArgumentParser.format_help`) * `formatter_class` - (`argparse.RawDescriptionHelpFormatter`) For a non-argparse parser the parser methods can be set to `None` if not supported. The CLI settings will only raise an error when connecting to the root parser if a parser method is necessary but set to `None`. !!! note The `formatter_class` is only applied to subcommands. The `CliSettingsSource` never touches or modifies any of the external parser settings to avoid breaking changes. Since subcommands reside on their own internal parser trees, we can safely apply the `formatter_class` settings without breaking the external parser logic. ## Secrets Placing secret values in files is a common pattern to provide sensitive configuration to an application. A secret file follows the same principal as a dotenv file except it only contains a single value and the file name is used as the key. A secret file will look like the following: ``` title="/var/run/database_password" super_secret_database_password ``` Once you have your secret files, *pydantic* supports loading it in two ways: 1. Setting the `secrets_dir` on `model_config` in a `BaseSettings` class to the directory where your secret files are stored. ````py hl_lines="4 5 6 7" from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(secrets_dir='/var/run') database_password: str ```` 2. Instantiating the `BaseSettings` derived class with the `_secrets_dir` keyword argument: ```` settings = Settings(_secrets_dir='/var/run') ```` In either case, the value of the passed argument can be any valid directory, either absolute or relative to the current working directory. **Note that a non existent directory will only generate a warning**. From there, *pydantic* will handle everything for you by loading in your variables and validating them. Even when using a secrets directory, *pydantic* will still read environment variables from a dotenv file or the environment, **a dotenv file and environment variables will always take priority over values loaded from the secrets directory**. Passing a file path via the `_secrets_dir` keyword argument on instantiation (method 2) will override the value (if any) set on the `model_config` class. If you need to load settings from multiple secrets directories, you can pass multiple paths as a tuple or list. Just like for `env_file`, values from subsequent paths override previous ones. ````python from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): # files in '/run/secrets' take priority over '/var/run' model_config = SettingsConfigDict(secrets_dir=('/var/run', '/run/secrets')) database_password: str ```` If any of `secrets_dir` is missing, it is ignored, and warning is shown. If any of `secrets_dir` is a file, error is raised. ### Use Case: Docker Secrets Docker Secrets can be used to provide sensitive configuration to an application running in a Docker container. To use these secrets in a *pydantic* application the process is simple. More information regarding creating, managing and using secrets in Docker see the official [Docker documentation](https://docs.docker.com/engine/reference/commandline/secret/). First, define your `Settings` class with a `SettingsConfigDict` that specifies the secrets directory. ```py hl_lines="4 5 6 7" from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(secrets_dir='/run/secrets') my_secret_data: str ``` !!! note By default [Docker uses `/run/secrets`](https://docs.docker.com/engine/swarm/secrets/#how-docker-manages-secrets) as the target mount point. If you want to use a different location, change `Config.secrets_dir` accordingly. Then, create your secret via the Docker CLI ```bash printf "This is a secret" | docker secret create my_secret_data - ``` Last, run your application inside a Docker container and supply your newly created secret ```bash docker service create --name pydantic-with-secrets --secret my_secret_data pydantic-app:latest ``` ## Nested Secrets The default secrets implementation, `SecretsSettingsSource`, has behaviour that is not always desired or sufficient. For example, the default implementation does not support secret fields in nested submodels. `NestedSecretsSettingsSource` can be used as a drop-in replacement to `SecretsSettingsSource` to adjust the default behaviour. All differences are summarized in the table below. | `SecretsSettingsSource` | `NestedSecretsSettingsSourcee` | |-----------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| | Secret fields must belong to a top level model. | Secrets can be fields of nested models. | | Secret files can be placed in `secrets_dir`s only. | Secret files can be placed in subdirectories for nested models. | | Secret files discovery is based on the same configuration options that are used by `EnvSettingsSource`: `case_sensitive`, `env_nested_delimiter`, `env_prefix`. | Default options are respected, but can be overridden with `secrets_case_sensitive`, `secrets_nested_delimiter`, `secrets_prefix`. | | When `secrets_dir` is missing on the file system, a warning is generated. | Use `secrets_dir_missing` options to choose whether to issue warning, raise error, or silently ignore. | ### Use Case: Plain Directory Layout ```text 📂 secrets ├── 📄 app_key └── 📄 db_passwd ``` In the example below, secrets nested delimiter `'_'` is different from env nested delimiter `'__'`. Value for `Settings.db.user` can be passed in env variable `MY_DB__USER`. ```py from pydantic import BaseModel, SecretStr from pydantic_settings import ( BaseSettings, NestedSecretsSettingsSource, SettingsConfigDict, ) class AppSettings(BaseModel): key: SecretStr class DbSettings(BaseModel): user: str passwd: SecretStr class Settings(BaseSettings): app: AppSettings db: DbSettings model_config = SettingsConfigDict( env_prefix='MY_', env_nested_delimiter='__', secrets_dir='secrets', secrets_nested_delimiter='_', ) @classmethod def settings_customise_sources( cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings, ): return ( init_settings, env_settings, dotenv_settings, NestedSecretsSettingsSource(file_secret_settings), ) ``` ### Use Case: Nested Directory Layout ```text 📂 secrets ├── 📂 app │ └── 📄 key └── 📂 db └── 📄 passwd ``` ```py from pydantic import BaseModel, SecretStr from pydantic_settings import ( BaseSettings, NestedSecretsSettingsSource, SettingsConfigDict, ) class AppSettings(BaseModel): key: SecretStr class DbSettings(BaseModel): user: str passwd: SecretStr class Settings(BaseSettings): app: AppSettings db: DbSettings model_config = SettingsConfigDict( env_prefix='MY_', env_nested_delimiter='__', secrets_dir='secrets', secrets_nested_subdir=True, ) @classmethod def settings_customise_sources( cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings, ): return ( init_settings, env_settings, dotenv_settings, NestedSecretsSettingsSource(file_secret_settings), ) ``` ### Use Case: Multiple Nested Directories ```text 📂 secrets ├── 📂 default │ ├── 📂 app │ │ └── 📄 key │ └── 📂 db │ └── 📄 passwd └── 📂 override ├── 📂 app │ └── 📄 key └── 📂 db └── 📄 passwd ``` ```py from pydantic import BaseModel, SecretStr from pydantic_settings import ( BaseSettings, NestedSecretsSettingsSource, SettingsConfigDict, ) class AppSettings(BaseModel): key: SecretStr class DbSettings(BaseModel): user: str passwd: SecretStr class Settings(BaseSettings): app: AppSettings db: DbSettings model_config = SettingsConfigDict( env_prefix='MY_', env_nested_delimiter='__', secrets_dir=['secrets/default', 'secrets/override'], secrets_nested_subdir=True, ) @classmethod def settings_customise_sources( cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings, ): return ( init_settings, env_settings, dotenv_settings, NestedSecretsSettingsSource(file_secret_settings), ) ``` ### Configuration Options #### secrets_dir Path to secrets directory, same as `SecretsSettingsSource.secrets_dir`. If `list`, the last match wins. If `secrets_dir` is passed in both source constructor and model config, values are not merged (constructor wins). #### secrets_dir_missing If `secrets_dir` does not exist, original `SecretsSettingsSource` issues a warning. However, this may be undesirable, for example if we don't mount Docker Secrets in e.g. dev environment. Use `secrets_dir_missing` to choose: * `'ok'` — do nothing if `secrets_dir` does not exist * `'warn'` (default) — print warning, same as `SecretsSettingsSource` * `'error'` — raise `SettingsError` If multiple `secrets_dir` passed, the same `secrets_dir_missing` action applies to each of them. #### secrets_dir_max_size Limit the size of `secrets_dir` for security reasons, defaults to `SECRETS_DIR_MAX_SIZE` equal to 16 MiB. `NestedSecretsSettingsSource` is a thin wrapper around `EnvSettingsSource`, which loads all potential secrets on initialization. This could lead to `MemoryError` if we mount a large file under `secrets_dir`. If multiple `secrets_dir` passed, the limit applies to each directory independently. #### secrets_case_sensitive Same as `case_sensitive`, but works for secrets only. If not specified, defaults to `case_sensitive`. #### secrets_nested_delimiter Same as `env_nested_delimiter`, but works for secrets only. If not specified, defaults to `env_nested_delimiter`. This option is used to implement _nested secrets directory_ layout and allows to do even nasty things like `/run/secrets/model/delim/nested1/delim/nested2`. #### secrets_nested_subdir Boolean flag to turn on _nested secrets directory_ mode, `False` by default. If `True`, sets `secrets_nested_delimiter` to `os.sep`. Raises `SettingsError` if `secrets_nested_delimiter` is already specified. #### secrets_prefix Secret path prefix, similar to `env_prefix`, but works for secrets only. Defaults to `env_prefix` if not specified. Works in both plain and nested directory modes, like `'/run/secrets/prefix_model__nested'` and `'/run/secrets/prefix_model/nested'`. ## AWS Secrets Manager You must set one parameter: - `secret_id`: The AWS secret id You must have the same naming convention in the key value in secret as in the field name. For example, if the key in secret is named `SqlServerPassword`, the field name must be the same. You can use an alias too. In AWS Secrets Manager, nested models are supported with the `--` separator in the key name. For example, `SqlServer--Password`. Arrays (e.g. `MySecret--0`, `MySecret--1`) are not supported. ```py import os from pydantic import BaseModel from pydantic_settings import ( AWSSecretsManagerSettingsSource, BaseSettings, PydanticBaseSettingsSource, ) class SubModel(BaseModel): a: str class AWSSecretsManagerSettings(BaseSettings): foo: str bar: int sub: SubModel @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: aws_secrets_manager_settings = AWSSecretsManagerSettingsSource( settings_cls, os.environ['AWS_SECRETS_MANAGER_SECRET_ID'], ) return ( init_settings, env_settings, dotenv_settings, file_secret_settings, aws_secrets_manager_settings, ) ``` ## Azure Key Vault You must set two parameters: - `url`: For example, `https://my-resource.vault.azure.net/`. - `credential`: If you use `DefaultAzureCredential`, in local you can execute `az login` to get your identity credentials. The identity must have a role assignment (the recommended one is `Key Vault Secrets User`), so you can access the secrets. You must have the same naming convention in the field name as in the Key Vault secret name. For example, if the secret is named `SqlServerPassword`, the field name must be the same. You can use an alias too. In Key Vault, nested models are supported with the `--` separator. For example, `SqlServer--Password`. Key Vault arrays (e.g. `MySecret--0`, `MySecret--1`) are not supported. ```py import os from azure.identity import DefaultAzureCredential from pydantic import BaseModel from pydantic_settings import ( AzureKeyVaultSettingsSource, BaseSettings, PydanticBaseSettingsSource, ) class SubModel(BaseModel): a: str class AzureKeyVaultSettings(BaseSettings): foo: str bar: int sub: SubModel @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: az_key_vault_settings = AzureKeyVaultSettingsSource( settings_cls, os.environ['AZURE_KEY_VAULT_URL'], DefaultAzureCredential(), ) return ( init_settings, env_settings, dotenv_settings, file_secret_settings, az_key_vault_settings, ) ``` ### Snake case conversion The Azure Key Vault source accepts a `snake_case_conversion` option, disabled by default, to convert Key Vault secret names by mapping them to Python's snake_case field names, without the need to use aliases. ```py import os from azure.identity import DefaultAzureCredential from pydantic_settings import ( AzureKeyVaultSettingsSource, BaseSettings, PydanticBaseSettingsSource, ) class AzureKeyVaultSettings(BaseSettings): my_setting: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: az_key_vault_settings = AzureKeyVaultSettingsSource( settings_cls, os.environ['AZURE_KEY_VAULT_URL'], DefaultAzureCredential(), snake_case_conversion=True, ) return (az_key_vault_settings,) ``` This setup will load Azure Key Vault secrets (e.g., `MySetting`, `mySetting`, `my-secret` or `MY-SECRET`), mapping them to the snake case version (`my_setting` in this case). ### Dash to underscore mapping The Azure Key Vault source accepts a `dash_to_underscore` option, disabled by default, to support Key Vault kebab-case secret names by mapping them to Python's snake_case field names. When enabled, dashes (`-`) in secret names are mapped to underscores (`_`) in field names during validation. This mapping applies only to *field names*, not to aliases. Consider snake case conversion if you need aliases or nested fields. ```py import os from azure.identity import DefaultAzureCredential from pydantic import Field from pydantic_settings import ( AzureKeyVaultSettingsSource, BaseSettings, PydanticBaseSettingsSource, ) class AzureKeyVaultSettings(BaseSettings): field_with_underscore: str field_with_alias: str = Field(..., alias='Alias-With-Dashes') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: az_key_vault_settings = AzureKeyVaultSettingsSource( settings_cls, os.environ['AZURE_KEY_VAULT_URL'], DefaultAzureCredential(), dash_to_underscore=True, ) return (az_key_vault_settings,) ``` This setup will load Azure Key Vault secrets named `field-with-underscore` and `Alias-With-Dashes`, mapping them to the `field_with_underscore` and `field_with_alias` fields, respectively. !!! tip Alternatively, you can configure an [alias_generator](alias.md#using-alias-generators) to map PascalCase secrets. ## Google Cloud Secret Manager Google Cloud Secret Manager allows you to store, manage, and access sensitive information as secrets in Google Cloud Platform. This integration lets you retrieve secrets directly from GCP Secret Manager for use in your Pydantic settings. ### Installation The Google Cloud Secret Manager integration requires additional dependencies: ```bash pip install "pydantic-settings[gcp-secret-manager]" ``` ### Basic Usage To use Google Cloud Secret Manager, you need to: 1. Create a `GoogleSecretManagerSettingsSource`. (See [GCP Authentication](#gcp-authentication) for authentication options.) 2. Add this source to your settings customization pipeline ```py from pydantic import BaseModel from pydantic_settings import ( BaseSettings, GoogleSecretManagerSettingsSource, PydanticBaseSettingsSource, SettingsConfigDict, ) class Database(BaseModel): password: str user: str class Settings(BaseSettings): database: Database model_config = SettingsConfigDict(env_nested_delimiter='__') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: # Create the GCP Secret Manager settings source gcp_settings = GoogleSecretManagerSettingsSource( settings_cls, # If not provided, will use google.auth.default() # to get credentials from the environemnt # credentials=your_credentials, # If not provided, will use google.auth.default() # to get project_id from the environemnt project_id='your-gcp-project-id', ) return ( init_settings, env_settings, dotenv_settings, file_secret_settings, gcp_settings, ) ``` ### GCP Authentication The `GoogleSecretManagerSettingsSource` supports several authentication methods: 1. **Default credentials** - If you don't provide credentials or project ID, it will use [`google.auth.default()`](https://google-auth.readthedocs.io/en/master/reference/google.auth.html#google.auth.default) to obtain them. This works with: - Service account credentials from `GOOGLE_APPLICATION_CREDENTIALS` environment variable - User credentials from `gcloud auth application-default login` - Compute Engine, GKE, Cloud Run, or Cloud Functions default service accounts 2. **Explicit credentials** - You can also provide `credentials` directly. e.g. `sa_credentials = google.oauth2.service_account.Credentials.from_service_account_file('path/to/service-account.json')` and then `GoogleSecretManagerSettingsSource(credentials=sa_credentials)` ### Nested Models For nested models, Secret Manager supports the `env_nested_delimiter` setting as long as it complies with the [naming rules](https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets#create-a-secret). In the example above, you would create secrets named `database__password` and `database__user` in Secret Manager. ### Secret Versions By default, `GoogleSecretManagerSettingsSource` uses the "latest" version of secrets. You can specify a different version using the `SecretVersion` annotation. ```py from typing import Annotated from pydantic import Field from pydantic_settings import ( BaseSettings, GoogleSecretManagerSettingsSource, PydanticBaseSettingsSource, ) from pydantic_settings.sources.types import SecretVersion class Settings(BaseSettings): # This will use the "latest" version my_secret: str = Field(alias='my-secret') # This will use version "1" my_secret_v1: Annotated[str, Field(alias='my-secret'), SecretVersion('1')] @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( GoogleSecretManagerSettingsSource(settings_cls, project_id='my-project'), init_settings, env_settings, dotenv_settings, file_secret_settings, ) ``` !!! note If you have multiple fields pointing to the same secret (alias) but with different versions, you MUST enable `populate_by_name=True` in `SettingsConfigDict`. ### Important Notes 1. **Case Sensitivity**: By default, secret names are case-sensitive. * If you set `case_sensitive=False`, `pydantic-settings` will attempt to resolve secrets in a case-insensitive manner. It prioritizes exact matches over case-insensitive matches. For some examples of this, imagine `case_sensitive=False` and the model attribute is named `my_secret`: * If Google Secret Manager has both `MY_SECRET` and `my_secret` defined - the value of `my_secret` will be returned. * If Google Secret Manager has `MY_SECRET`, `My_Secret`, and `my_Secret` defined - a warning will be raised and the value of `my_Secret` will be returned - as the secret names are first sorted in ASCII sort order (where lowercased letters are greater than upper case letters) and the last one is chosen (which would be `my_Secret` in this case). 2. **Secret Naming**: Create secrets in Google Secret Manager with names that match your field names (including any prefix). According to the [Secret Manager documentation](https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets#create-a-secret), a secret name can contain uppercase and lowercase letters, numerals, hyphens, and underscores. The maximum allowed length for a name is 255 characters. For more details on creating and managing secrets in Google Cloud Secret Manager, see the [official Google Cloud documentation](https://cloud.google.com/secret-manager/docs). ## Other settings source Other settings sources are available for common configuration files: - `JsonConfigSettingsSource` using `json_file` and `json_file_encoding` arguments - `PyprojectTomlConfigSettingsSource` using *(optional)* `pyproject_toml_depth` and *(optional)* `pyproject_toml_table_header` arguments - `TomlConfigSettingsSource` using `toml_file` argument - `YamlConfigSettingsSource` using `yaml_file` and yaml_file_encoding arguments To use them, you can use the same mechanism described [here](#customise-settings-sources). ```py from pydantic import BaseModel from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource, ) class Nested(BaseModel): nested_field: str class Settings(BaseSettings): foobar: str nested: Nested model_config = SettingsConfigDict(toml_file='config.toml') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (TomlConfigSettingsSource(settings_cls),) ``` This will be able to read the following "config.toml" file, located in your working directory: ```toml foobar = "Hello" [nested] nested_field = "world!" ``` You can also provide multiple files by providing a list of paths. ```py from pydantic import BaseModel from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource, ) class Nested(BaseModel): foo: int bar: int = 0 class Settings(BaseSettings): hello: str nested: Nested model_config = SettingsConfigDict( toml_file=['config.default.toml', 'config.custom.toml'] ) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (TomlConfigSettingsSource(settings_cls),) ``` The following two configuration files ```toml # config.default.toml hello = "World" [nested] foo = 1 bar = 2 ``` ```toml # config.custom.toml [nested] foo = 3 ``` are equivalent to ```toml hello = "world" [nested] foo = 3 ``` The files are merged shallowly in increasing order of priority. To enable deep merging, set `deep_merge=True` on the source directly. !!! warning The `deep_merge` option is **not available** through the `SettingsConfigDict`. ```py from pydantic import BaseModel from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource, ) class Nested(BaseModel): foo: int bar: int = 0 class Settings(BaseSettings): hello: str nested: Nested model_config = SettingsConfigDict( toml_file=['config.default.toml', 'config.custom.toml'] ) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (TomlConfigSettingsSource(settings_cls, deep_merge=True),) ``` With deep merge enabled, the following two configuration files ```toml # config.default.toml hello = "World" [nested] foo = 1 bar = 2 ``` ```toml # config.custom.toml [nested] foo = 3 ``` are equivalent to ```toml hello = "world" [nested] foo = 3 bar = 2 ``` ### pyproject.toml "pyproject.toml" is a standardized file for providing configuration values in Python projects. [PEP 518](https://peps.python.org/pep-0518/#tool-table) defines a `[tool]` table that can be used to provide arbitrary tool configuration. While encouraged to use the `[tool]` table, `PyprojectTomlConfigSettingsSource` can be used to load variables from any location with in "pyproject.toml" file. This is controlled by providing `SettingsConfigDict(pyproject_toml_table_header=tuple[str, ...])` where the value is a tuple of header parts. By default, `pyproject_toml_table_header=('tool', 'pydantic-settings')` which will load variables from the `[tool.pydantic-settings]` table. ```python from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, PyprojectTomlConfigSettingsSource, SettingsConfigDict, ) class Settings(BaseSettings): """Example loading values from the table used by default.""" field: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) class SomeTableSettings(Settings): """Example loading values from a user defined table.""" model_config = SettingsConfigDict( pyproject_toml_table_header=('tool', 'some-table') ) class RootSettings(Settings): """Example loading values from the root of a pyproject.toml file.""" model_config = SettingsConfigDict(extra='ignore', pyproject_toml_table_header=()) ``` This will be able to read the following "pyproject.toml" file, located in your working directory, resulting in `Settings(field='default-table')`, `SomeTableSettings(field='some-table')`, & `RootSettings(field='root')`: ```toml field = "root" [tool.pydantic-settings] field = "default-table" [tool.some-table] field = "some-table" ``` By default, `PyprojectTomlConfigSettingsSource` will only look for a "pyproject.toml" in the your current working directory. However, there are two options to change this behavior. * `SettingsConfigDict(pyproject_toml_depth=)` can be provided to check `` number of directories **up** in the directory tree for a "pyproject.toml" if one is not found in the current working directory. By default, no parent directories are checked. * An explicit file path can be provided to the source when it is instantiated (e.g. `PyprojectTomlConfigSettingsSource(settings_cls, Path('~/.config').resolve() / 'pyproject.toml')`). If a file path is provided this way, it will be treated as absolute (no other locations are checked). ```python from pathlib import Path from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, PyprojectTomlConfigSettingsSource, SettingsConfigDict, ) class DiscoverSettings(BaseSettings): """Example of discovering a pyproject.toml in parent directories in not in `Path.cwd()`.""" model_config = SettingsConfigDict(pyproject_toml_depth=2) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) class ExplicitFilePathSettings(BaseSettings): """Example of explicitly providing the path to the file to load.""" field: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( PyprojectTomlConfigSettingsSource( settings_cls, Path('~/.config').resolve() / 'pyproject.toml' ), ) ``` ## Field value priority In the case where a value is specified for the same `Settings` field in multiple ways, the selected value is determined as follows (in descending order of priority): 1. If `cli_parse_args` is enabled, arguments passed in at the CLI. 2. Arguments passed to the `Settings` class initialiser. 3. Environment variables, e.g. `my_prefix_special_function` as described above. 4. Variables loaded from a dotenv (`.env`) file. 5. Variables loaded from the secrets directory. 6. The default field values for the `Settings` model. ## Customise settings sources If the default order of priority doesn't match your needs, it's possible to change it by overriding the `settings_customise_sources` method of your `Settings` . `settings_customise_sources` takes four callables as arguments and returns any number of callables as a tuple. In turn these callables are called to build the inputs to the fields of the settings class. Each callable should take an instance of the settings class as its sole argument and return a `dict`. ### Changing Priority The order of the returned callables decides the priority of inputs; first item is the highest priority. ```py from pydantic import PostgresDsn from pydantic_settings import BaseSettings, PydanticBaseSettingsSource class Settings(BaseSettings): database_dsn: PostgresDsn @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return env_settings, init_settings, file_secret_settings print(Settings(database_dsn='postgres://postgres@localhost:5432/kwargs_db')) #> database_dsn=PostgresDsn('postgres://postgres@localhost:5432/kwargs_db') ``` By flipping `env_settings` and `init_settings`, environment variables now have precedence over `__init__` kwargs. ### Adding sources As explained earlier, *pydantic* ships with multiples built-in settings sources. However, you may occasionally need to add your own custom sources, `settings_customise_sources` makes this very easy: ```py import json from pathlib import Path from typing import Any from pydantic.fields import FieldInfo from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, ) class JsonConfigSettingsSource(PydanticBaseSettingsSource): """ A simple settings source class that loads variables from a JSON file at the project's root. Here we happen to choose to use the `env_file_encoding` from Config when reading `config.json` """ def get_field_value( self, field: FieldInfo, field_name: str ) -> tuple[Any, str, bool]: encoding = self.config.get('env_file_encoding') file_content_json = json.loads( Path('tests/example_test_config.json').read_text(encoding) ) field_value = file_content_json.get(field_name) return field_value, field_name, False def prepare_field_value( self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool ) -> Any: return value def __call__(self) -> dict[str, Any]: d: dict[str, Any] = {} for field_name, field in self.settings_cls.model_fields.items(): field_value, field_key, value_is_complex = self.get_field_value( field, field_name ) field_value = self.prepare_field_value( field_name, field, field_value, value_is_complex ) if field_value is not None: d[field_key] = field_value return d class Settings(BaseSettings): model_config = SettingsConfigDict(env_file_encoding='utf-8') foobar: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( init_settings, JsonConfigSettingsSource(settings_cls), env_settings, file_secret_settings, ) print(Settings()) #> foobar='test' ``` #### Accessing the result of previous sources Each source of settings can access the output of the previous ones. ```python from typing import Any from pydantic.fields import FieldInfo from pydantic_settings import PydanticBaseSettingsSource class MyCustomSource(PydanticBaseSettingsSource): def get_field_value( self, field: FieldInfo, field_name: str ) -> tuple[Any, str, bool]: ... def __call__(self) -> dict[str, Any]: # Retrieve the aggregated settings from previous sources current_state = self.current_state current_state.get('some_setting') # Retrive settings from all sources individually # self.settings_sources_data["SettingsSourceName"]: dict[str, Any] settings_sources_data = self.settings_sources_data settings_sources_data['SomeSettingsSource'].get('some_setting') # Your code here... ``` ### Removing sources You might also want to disable a source: ```py from pydantic import ValidationError from pydantic_settings import BaseSettings, PydanticBaseSettingsSource class Settings(BaseSettings): my_api_key: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: # here we choose to ignore arguments from init_settings return env_settings, file_secret_settings try: Settings(my_api_key='this is ignored') except ValidationError as exc_info: print(exc_info) """ 1 validation error for Settings my_api_key Field required [type=missing, input_value={}, input_type=dict] For further information visit https://errors.pydantic.dev/2/v/missing """ ``` ## In-place reloading In case you want to reload in-place an existing setting, you can do it by using its `__init__` method : ```py import os from pydantic import Field from pydantic_settings import BaseSettings class Settings(BaseSettings): foo: str = Field('foo') mutable_settings = Settings() print(mutable_settings.foo) #> foo os.environ['foo'] = 'bar' print(mutable_settings.foo) #> foo mutable_settings.__init__() print(mutable_settings.foo) #> bar os.environ.pop('foo') mutable_settings.__init__() print(mutable_settings.foo) #> foo ``` pydantic-pydantic-settings-198e71c/docs/logo-white.svg000066400000000000000000000011321514433345000230650ustar00rootroot00000000000000 pydantic-pydantic-settings-198e71c/docs/theme/000077500000000000000000000000001514433345000213735ustar00rootroot00000000000000pydantic-pydantic-settings-198e71c/docs/theme/main.html000066400000000000000000000004131514433345000232030ustar00rootroot00000000000000{% extends "base.html" %} {% block announce %} {% include 'announce.html' ignore missing %} {% endblock %} {% block content %} {{ super() }} {% endblock %} pydantic-pydantic-settings-198e71c/mkdocs.yml000066400000000000000000000025121514433345000213440ustar00rootroot00000000000000site_name: pydantic site_description: Data validation using Python type hints strict: true site_url: https://docs.pydantic.dev/ theme: name: 'material' custom_dir: 'docs/theme' palette: - media: "(prefers-color-scheme: light)" scheme: default primary: pink accent: pink toggle: icon: material/lightbulb-outline name: "Switch to dark mode" - media: "(prefers-color-scheme: dark)" scheme: slate primary: pink accent: pink toggle: icon: material/lightbulb name: "Switch to light mode" features: - content.tabs.link - content.code.annotate - announce.dismiss - navigation.tabs logo: 'logo-white.svg' favicon: 'favicon.png' repo_name: pydantic/pydantic repo_url: https://github.com/pydantic/pydantic edit_uri: edit/main/docs/ extra_css: - 'extra/terminal.css' - 'extra/tweaks.css' nav: - index.md markdown_extensions: - tables - toc: permalink: true title: Page contents - admonition - pymdownx.highlight - pymdownx.extra - pymdownx.emoji: emoji_index: !!python/name:materialx.emoji.twemoji emoji_generator: !!python/name:materialx.emoji.to_svg - pymdownx.tabbed: alternate_style: true extra: version: provider: mike plugins: - mike: alias_type: symlink canonical_version: latest - search - exclude: glob: - __pycache__/* pydantic-pydantic-settings-198e71c/pydantic_settings/000077500000000000000000000000001514433345000230745ustar00rootroot00000000000000pydantic-pydantic-settings-198e71c/pydantic_settings/__init__.py000066400000000000000000000032531514433345000252100ustar00rootroot00000000000000from .exceptions import SettingsError from .main import BaseSettings, CliApp, SettingsConfigDict from .sources import ( CLI_SUPPRESS, AWSSecretsManagerSettingsSource, AzureKeyVaultSettingsSource, CliDualFlag, CliExplicitFlag, CliImplicitFlag, CliMutuallyExclusiveGroup, CliPositionalArg, CliSettingsSource, CliSubCommand, CliSuppress, CliToggleFlag, CliUnknownArgs, DotEnvSettingsSource, EnvSettingsSource, ForceDecode, GoogleSecretManagerSettingsSource, InitSettingsSource, JsonConfigSettingsSource, NestedSecretsSettingsSource, NoDecode, PydanticBaseSettingsSource, PyprojectTomlConfigSettingsSource, SecretsSettingsSource, TomlConfigSettingsSource, YamlConfigSettingsSource, get_subcommand, ) from .version import VERSION __all__ = ( 'CLI_SUPPRESS', 'AWSSecretsManagerSettingsSource', 'AzureKeyVaultSettingsSource', 'BaseSettings', 'CliApp', 'CliExplicitFlag', 'CliImplicitFlag', 'CliToggleFlag', 'CliDualFlag', 'CliMutuallyExclusiveGroup', 'CliPositionalArg', 'CliSettingsSource', 'CliSubCommand', 'CliSuppress', 'CliUnknownArgs', 'DotEnvSettingsSource', 'EnvSettingsSource', 'ForceDecode', 'GoogleSecretManagerSettingsSource', 'InitSettingsSource', 'JsonConfigSettingsSource', 'NestedSecretsSettingsSource', 'NoDecode', 'PydanticBaseSettingsSource', 'PyprojectTomlConfigSettingsSource', 'SecretsSettingsSource', 'SettingsConfigDict', 'SettingsError', 'TomlConfigSettingsSource', 'YamlConfigSettingsSource', '__version__', 'get_subcommand', ) __version__ = VERSION pydantic-pydantic-settings-198e71c/pydantic_settings/exceptions.py000066400000000000000000000001411514433345000256230ustar00rootroot00000000000000class SettingsError(ValueError): """Base exception for settings-related errors.""" pass pydantic-pydantic-settings-198e71c/pydantic_settings/main.py000066400000000000000000001244601514433345000244010ustar00rootroot00000000000000from __future__ import annotations as _annotations import asyncio import inspect import re import threading import warnings from argparse import Namespace from collections.abc import Mapping from types import SimpleNamespace from typing import Any, ClassVar, Literal, TextIO, TypeVar, cast from pydantic import ConfigDict from pydantic._internal._config import config_keys from pydantic._internal._signature import _field_name_for_signature from pydantic._internal._utils import deep_update, is_model_class from pydantic.dataclasses import is_pydantic_dataclass from pydantic.main import BaseModel from .exceptions import SettingsError from .sources import ( ENV_FILE_SENTINEL, CliSettingsSource, DefaultSettingsSource, DotEnvSettingsSource, DotenvType, EnvPrefixTarget, EnvSettingsSource, InitSettingsSource, JsonConfigSettingsSource, PathType, PydanticBaseSettingsSource, PydanticModel, PyprojectTomlConfigSettingsSource, SecretsSettingsSource, TomlConfigSettingsSource, YamlConfigSettingsSource, get_subcommand, ) from .sources.utils import _get_alias_names T = TypeVar('T') class SettingsConfigDict(ConfigDict, total=False): case_sensitive: bool nested_model_default_partial_update: bool | None env_prefix: str env_prefix_target: EnvPrefixTarget env_file: DotenvType | None env_file_encoding: str | None env_ignore_empty: bool env_nested_delimiter: str | None env_nested_max_split: int | None env_parse_none_str: str | None env_parse_enums: bool | None cli_prog_name: str | None cli_parse_args: bool | list[str] | tuple[str, ...] | None cli_parse_none_str: str | None cli_hide_none_type: bool cli_avoid_json: bool cli_enforce_required: bool cli_use_class_docs_for_groups: bool cli_exit_on_error: bool cli_prefix: str cli_flag_prefix_char: str cli_implicit_flags: bool | Literal['dual', 'toggle'] | None cli_ignore_unknown_args: bool | None cli_kebab_case: bool | Literal['all', 'no_enums'] | None cli_shortcuts: Mapping[str, str | list[str]] | None secrets_dir: PathType | None json_file: PathType | None json_file_encoding: str | None yaml_file: PathType | None yaml_file_encoding: str | None yaml_config_section: str | None """ Specifies the section in a YAML file from which to load the settings. Supports dot-notation for nested paths (e.g., 'config.app.settings'). If provided, the settings will be loaded from the specified section. This is useful when the YAML file contains multiple configuration sections and you only want to load a specific subset into your settings model. """ pyproject_toml_depth: int """ Number of levels **up** from the current working directory to attempt to find a pyproject.toml file. This is only used when a pyproject.toml file is not found in the current working directory. """ pyproject_toml_table_header: tuple[str, ...] """ Header of the TOML table within a pyproject.toml file to use when filling variables. This is supplied as a `tuple[str, ...]` instead of a `str` to accommodate for headers containing a `.`. For example, `toml_table_header = ("tool", "my.tool", "foo")` can be used to fill variable values from a table with header `[tool."my.tool".foo]`. To use the root table, exclude this config setting or provide an empty tuple. """ toml_file: PathType | None enable_decoding: bool # Extend `config_keys` by pydantic settings config keys to # support setting config through class kwargs. # Pydantic uses `config_keys` in `pydantic._internal._config.ConfigWrapper.for_model` # to extract config keys from model kwargs, So, by adding pydantic settings keys to # `config_keys`, they will be considered as valid config keys and will be collected # by Pydantic. config_keys |= set(SettingsConfigDict.__annotations__.keys()) class BaseSettings(BaseModel): """ Base class for settings, allowing values to be overridden by environment variables. This is useful in production for secrets you do not wish to save in code, it plays nicely with docker(-compose), Heroku and any 12 factor app design. All the below attributes can be set via `model_config`. Args: _case_sensitive: Whether environment and CLI variable names should be read with case-sensitivity. Defaults to `None`. _nested_model_default_partial_update: Whether to allow partial updates on nested model default object fields. Defaults to `False`. _env_prefix: Prefix for all environment variables. Defaults to `None`. _env_prefix_target: Targets to which `_env_prefix` is applied. Default: `variable`. _env_file: The env file(s) to load settings values from. Defaults to `Path('')`, which means that the value from `model_config['env_file']` should be used. You can also pass `None` to indicate that environment variables should not be loaded from an env file. _env_file_encoding: The env file encoding, e.g. `'latin-1'`. Defaults to `None`. _env_ignore_empty: Ignore environment variables where the value is an empty string. Default to `False`. _env_nested_delimiter: The nested env values delimiter. Defaults to `None`. _env_nested_max_split: The nested env values maximum nesting. Defaults to `None`, which means no limit. _env_parse_none_str: The env string value that should be parsed (e.g. "null", "void", "None", etc.) into `None` type(None). Defaults to `None` type(None), which means no parsing should occur. _env_parse_enums: Parse enum field names to values. Defaults to `None.`, which means no parsing should occur. _cli_prog_name: The CLI program name to display in help text. Defaults to `None` if _cli_parse_args is `None`. Otherwise, defaults to sys.argv[0]. _cli_parse_args: The list of CLI arguments to parse. Defaults to None. If set to `True`, defaults to sys.argv[1:]. _cli_settings_source: Override the default CLI settings source with a user defined instance. Defaults to None. _cli_parse_none_str: The CLI string value that should be parsed (e.g. "null", "void", "None", etc.) into `None` type(None). Defaults to _env_parse_none_str value if set. Otherwise, defaults to "null" if _cli_avoid_json is `False`, and "None" if _cli_avoid_json is `True`. _cli_hide_none_type: Hide `None` values in CLI help text. Defaults to `False`. _cli_avoid_json: Avoid complex JSON objects in CLI help text. Defaults to `False`. _cli_enforce_required: Enforce required fields at the CLI. Defaults to `False`. _cli_use_class_docs_for_groups: Use class docstrings in CLI group help text instead of field descriptions. Defaults to `False`. _cli_exit_on_error: Determines whether or not the internal parser exits with error info when an error occurs. Defaults to `True`. _cli_prefix: The root parser command line arguments prefix. Defaults to "". _cli_flag_prefix_char: The flag prefix character to use for CLI optional arguments. Defaults to '-'. _cli_implicit_flags: Controls how `bool` fields are exposed as CLI flags. - False (default): no implicit flags are generated; booleans must be set explicitly (e.g. --flag=true). - True / 'dual': optional boolean fields generate both positive and negative forms (--flag and --no-flag). - 'toggle': required boolean fields remain in 'dual' mode, while optional boolean fields generate a single flag aligned with the default value (if default=False, expose --flag; if default=True, expose --no-flag). _cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`. _cli_kebab_case: CLI args use kebab case. Defaults to `False`. _cli_shortcuts: Mapping of target field name to alias names. Defaults to `None`. _secrets_dir: The secret files directory or a sequence of directories. Defaults to `None`. _build_sources: Pre-initialized sources and init kwargs to use for building instantiation values. Defaults to `None`. """ def __init__( __pydantic_self__, _case_sensitive: bool | None = None, _nested_model_default_partial_update: bool | None = None, _env_prefix: str | None = None, _env_prefix_target: EnvPrefixTarget | None = None, _env_file: DotenvType | None = ENV_FILE_SENTINEL, _env_file_encoding: str | None = None, _env_ignore_empty: bool | None = None, _env_nested_delimiter: str | None = None, _env_nested_max_split: int | None = None, _env_parse_none_str: str | None = None, _env_parse_enums: bool | None = None, _cli_prog_name: str | None = None, _cli_parse_args: bool | list[str] | tuple[str, ...] | None = None, _cli_settings_source: CliSettingsSource[Any] | None = None, _cli_parse_none_str: str | None = None, _cli_hide_none_type: bool | None = None, _cli_avoid_json: bool | None = None, _cli_enforce_required: bool | None = None, _cli_use_class_docs_for_groups: bool | None = None, _cli_exit_on_error: bool | None = None, _cli_prefix: str | None = None, _cli_flag_prefix_char: str | None = None, _cli_implicit_flags: bool | Literal['dual', 'toggle'] | None = None, _cli_ignore_unknown_args: bool | None = None, _cli_kebab_case: bool | Literal['all', 'no_enums'] | None = None, _cli_shortcuts: Mapping[str, str | list[str]] | None = None, _secrets_dir: PathType | None = None, _build_sources: tuple[tuple[PydanticBaseSettingsSource, ...], dict[str, Any]] | None = None, **values: Any, ) -> None: sources, init_kwargs = ( _build_sources if _build_sources is not None else __pydantic_self__.__class__._settings_init_sources( _case_sensitive=_case_sensitive, _nested_model_default_partial_update=_nested_model_default_partial_update, _env_prefix=_env_prefix, _env_prefix_target=_env_prefix_target, _env_file=_env_file, _env_file_encoding=_env_file_encoding, _env_ignore_empty=_env_ignore_empty, _env_nested_delimiter=_env_nested_delimiter, _env_nested_max_split=_env_nested_max_split, _env_parse_none_str=_env_parse_none_str, _env_parse_enums=_env_parse_enums, _cli_prog_name=_cli_prog_name, _cli_parse_args=_cli_parse_args, _cli_settings_source=_cli_settings_source, _cli_parse_none_str=_cli_parse_none_str, _cli_hide_none_type=_cli_hide_none_type, _cli_avoid_json=_cli_avoid_json, _cli_enforce_required=_cli_enforce_required, _cli_use_class_docs_for_groups=_cli_use_class_docs_for_groups, _cli_exit_on_error=_cli_exit_on_error, _cli_prefix=_cli_prefix, _cli_flag_prefix_char=_cli_flag_prefix_char, _cli_implicit_flags=_cli_implicit_flags, _cli_ignore_unknown_args=_cli_ignore_unknown_args, _cli_kebab_case=_cli_kebab_case, _cli_shortcuts=_cli_shortcuts, _secrets_dir=_secrets_dir, **values, ) ) super().__init__(**__pydantic_self__.__class__._settings_build_values(sources, init_kwargs)) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: """ Define the sources and their order for loading the settings values. Args: settings_cls: The Settings class. init_settings: The `InitSettingsSource` instance. env_settings: The `EnvSettingsSource` instance. dotenv_settings: The `DotEnvSettingsSource` instance. file_secret_settings: The `SecretsSettingsSource` instance. Returns: A tuple containing the sources and their order for loading the settings values. """ return init_settings, env_settings, dotenv_settings, file_secret_settings @classmethod def _settings_init_sources( cls, _case_sensitive: bool | None = None, _nested_model_default_partial_update: bool | None = None, _env_prefix: str | None = None, _env_prefix_target: EnvPrefixTarget | None = None, _env_file: DotenvType | None = None, _env_file_encoding: str | None = None, _env_ignore_empty: bool | None = None, _env_nested_delimiter: str | None = None, _env_nested_max_split: int | None = None, _env_parse_none_str: str | None = None, _env_parse_enums: bool | None = None, _cli_prog_name: str | None = None, _cli_parse_args: bool | list[str] | tuple[str, ...] | None = None, _cli_settings_source: CliSettingsSource[Any] | None = None, _cli_parse_none_str: str | None = None, _cli_hide_none_type: bool | None = None, _cli_avoid_json: bool | None = None, _cli_enforce_required: bool | None = None, _cli_use_class_docs_for_groups: bool | None = None, _cli_exit_on_error: bool | None = None, _cli_prefix: str | None = None, _cli_flag_prefix_char: str | None = None, _cli_implicit_flags: bool | Literal['dual', 'toggle'] | None = None, _cli_ignore_unknown_args: bool | None = None, _cli_kebab_case: bool | Literal['all', 'no_enums'] | None = None, _cli_shortcuts: Mapping[str, str | list[str]] | None = None, _secrets_dir: PathType | None = None, **init_kwargs: dict[str, Any], ) -> tuple[tuple[PydanticBaseSettingsSource, ...], dict[str, Any]]: # Determine settings config values case_sensitive = _case_sensitive if _case_sensitive is not None else cls.model_config.get('case_sensitive') env_prefix = _env_prefix if _env_prefix is not None else cls.model_config.get('env_prefix') env_prefix_target = ( _env_prefix_target if _env_prefix_target is not None else cls.model_config.get('env_prefix_target') ) nested_model_default_partial_update = ( _nested_model_default_partial_update if _nested_model_default_partial_update is not None else cls.model_config.get('nested_model_default_partial_update') ) env_file = _env_file if _env_file != ENV_FILE_SENTINEL else cls.model_config.get('env_file') env_file_encoding = ( _env_file_encoding if _env_file_encoding is not None else cls.model_config.get('env_file_encoding') ) env_ignore_empty = ( _env_ignore_empty if _env_ignore_empty is not None else cls.model_config.get('env_ignore_empty') ) env_nested_delimiter = ( _env_nested_delimiter if _env_nested_delimiter is not None else cls.model_config.get('env_nested_delimiter') ) env_nested_max_split = ( _env_nested_max_split if _env_nested_max_split is not None else cls.model_config.get('env_nested_max_split') ) env_parse_none_str = ( _env_parse_none_str if _env_parse_none_str is not None else cls.model_config.get('env_parse_none_str') ) env_parse_enums = _env_parse_enums if _env_parse_enums is not None else cls.model_config.get('env_parse_enums') cli_prog_name = _cli_prog_name if _cli_prog_name is not None else cls.model_config.get('cli_prog_name') cli_parse_args = _cli_parse_args if _cli_parse_args is not None else cls.model_config.get('cli_parse_args') cli_settings_source = ( _cli_settings_source if _cli_settings_source is not None else cls.model_config.get('cli_settings_source') ) cli_parse_none_str = ( _cli_parse_none_str if _cli_parse_none_str is not None else cls.model_config.get('cli_parse_none_str') ) cli_parse_none_str = cli_parse_none_str if not env_parse_none_str else env_parse_none_str cli_hide_none_type = ( _cli_hide_none_type if _cli_hide_none_type is not None else cls.model_config.get('cli_hide_none_type') ) cli_avoid_json = _cli_avoid_json if _cli_avoid_json is not None else cls.model_config.get('cli_avoid_json') cli_enforce_required = ( _cli_enforce_required if _cli_enforce_required is not None else cls.model_config.get('cli_enforce_required') ) cli_use_class_docs_for_groups = ( _cli_use_class_docs_for_groups if _cli_use_class_docs_for_groups is not None else cls.model_config.get('cli_use_class_docs_for_groups') ) cli_exit_on_error = ( _cli_exit_on_error if _cli_exit_on_error is not None else cls.model_config.get('cli_exit_on_error') ) cli_prefix = _cli_prefix if _cli_prefix is not None else cls.model_config.get('cli_prefix') cli_flag_prefix_char = ( _cli_flag_prefix_char if _cli_flag_prefix_char is not None else cls.model_config.get('cli_flag_prefix_char') ) cli_implicit_flags = ( _cli_implicit_flags if _cli_implicit_flags is not None else cls.model_config.get('cli_implicit_flags') ) cli_ignore_unknown_args = ( _cli_ignore_unknown_args if _cli_ignore_unknown_args is not None else cls.model_config.get('cli_ignore_unknown_args') ) cli_kebab_case = _cli_kebab_case if _cli_kebab_case is not None else cls.model_config.get('cli_kebab_case') cli_shortcuts = _cli_shortcuts if _cli_shortcuts is not None else cls.model_config.get('cli_shortcuts') secrets_dir = _secrets_dir if _secrets_dir is not None else cls.model_config.get('secrets_dir') # Configure built-in sources default_settings = DefaultSettingsSource( cls, nested_model_default_partial_update=nested_model_default_partial_update ) init_settings = InitSettingsSource( cls, init_kwargs=init_kwargs, nested_model_default_partial_update=nested_model_default_partial_update, ) env_settings = EnvSettingsSource( cls, case_sensitive=case_sensitive, env_prefix=env_prefix, env_prefix_target=env_prefix_target, env_nested_delimiter=env_nested_delimiter, env_nested_max_split=env_nested_max_split, env_ignore_empty=env_ignore_empty, env_parse_none_str=env_parse_none_str, env_parse_enums=env_parse_enums, ) dotenv_settings = DotEnvSettingsSource( cls, env_file=env_file, env_file_encoding=env_file_encoding, case_sensitive=case_sensitive, env_prefix=env_prefix, env_prefix_target=env_prefix_target, env_nested_delimiter=env_nested_delimiter, env_nested_max_split=env_nested_max_split, env_ignore_empty=env_ignore_empty, env_parse_none_str=env_parse_none_str, env_parse_enums=env_parse_enums, ) file_secret_settings = SecretsSettingsSource( cls, secrets_dir=secrets_dir, case_sensitive=case_sensitive, env_prefix=env_prefix, env_prefix_target=env_prefix_target, ) # Provide a hook to set built-in sources priority and add / remove sources sources = cls.settings_customise_sources( cls, init_settings=init_settings, env_settings=env_settings, dotenv_settings=dotenv_settings, file_secret_settings=file_secret_settings, ) + (default_settings,) custom_cli_sources = [source for source in sources if isinstance(source, CliSettingsSource)] if not any(custom_cli_sources): if isinstance(cli_settings_source, CliSettingsSource): sources = (cli_settings_source,) + sources elif cli_parse_args is not None: cli_settings = CliSettingsSource[Any]( cls, cli_prog_name=cli_prog_name, cli_parse_args=cli_parse_args, cli_parse_none_str=cli_parse_none_str, cli_hide_none_type=cli_hide_none_type, cli_avoid_json=cli_avoid_json, cli_enforce_required=cli_enforce_required, cli_use_class_docs_for_groups=cli_use_class_docs_for_groups, cli_exit_on_error=cli_exit_on_error, cli_prefix=cli_prefix, cli_flag_prefix_char=cli_flag_prefix_char, cli_implicit_flags=cli_implicit_flags, cli_ignore_unknown_args=cli_ignore_unknown_args, cli_kebab_case=cli_kebab_case, cli_shortcuts=cli_shortcuts, case_sensitive=case_sensitive, ) sources = (cli_settings,) + sources # We ensure that if command line arguments haven't been parsed yet, we do so. elif cli_parse_args not in (None, False) and not custom_cli_sources[0].env_vars: custom_cli_sources[0](args=cli_parse_args) # type: ignore cls._settings_warn_unused_config_keys(sources, cls.model_config) return sources, init_kwargs @classmethod def _settings_build_values( cls, sources: tuple[PydanticBaseSettingsSource, ...], init_kwargs: dict[str, Any] ) -> dict[str, Any]: if sources: state: dict[str, Any] = {} defaults: dict[str, Any] = {} states: dict[str, dict[str, Any]] = {} for source in sources: if isinstance(source, PydanticBaseSettingsSource): source._set_current_state(state) source._set_settings_sources_data(states) source_name = source.__name__ if hasattr(source, '__name__') else type(source).__name__ source_state = source() if isinstance(source, DefaultSettingsSource): defaults = source_state states[source_name] = source_state state = deep_update(source_state, state) # Strip any default values not explicity set before returning final state state = {key: val for key, val in state.items() if key not in defaults or defaults[key] != val} cls._settings_restore_init_kwarg_names(cls, init_kwargs, state) return state else: # no one should mean to do this, but I think returning an empty dict is marginally preferable # to an informative error and much better than a confusing error return {} @staticmethod def _settings_restore_init_kwarg_names( settings_cls: type[BaseSettings], init_kwargs: dict[str, Any], state: dict[str, Any] ) -> None: """ Restore the init_kwarg key names to the final merged state dictionary. This function renames keys in state to match the original init_kwargs key names, preserving the merged values from the source priority order. """ if init_kwargs and state: state_kwarg_names = set(state.keys()) init_kwarg_names = set(init_kwargs.keys()) for field_name, field_info in settings_cls.model_fields.items(): alias_names, *_ = _get_alias_names(field_name, field_info) matchable_names = set(alias_names) include_name = settings_cls.model_config.get( 'populate_by_name', False ) or settings_cls.model_config.get('validate_by_name', False) if include_name: matchable_names.add(field_name) init_kwarg_name = init_kwarg_names & matchable_names state_kwarg_name = state_kwarg_names & matchable_names if init_kwarg_name and state_kwarg_name: # Use deterministic selection for both keys. # Target key: the key from init_kwargs that should be used in the final state. target_key = next(iter(init_kwarg_name)) # Source key: prefer the alias (first in alias_names) if present in state, # as InitSettingsSource normalizes to the preferred alias. # This ensures we get the highest-priority value for this field. source_key = None for alias in alias_names: if alias in state_kwarg_name: source_key = alias break if source_key is None: # Fall back to field_name if no alias found in state source_key = field_name if field_name in state_kwarg_name else next(iter(state_kwarg_name)) # Get the value from the source key and remove all matching keys value = state.pop(source_key) for key in state_kwarg_name - {source_key}: state.pop(key, None) state[target_key] = value @staticmethod def _settings_warn_unused_config_keys(sources: tuple[object, ...], model_config: SettingsConfigDict) -> None: """ Warns if any values in model_config were set but the corresponding settings source has not been initialised. The list alternative sources and their config keys can be found here: https://docs.pydantic.dev/latest/concepts/pydantic_settings/#other-settings-source Args: sources: The tuple of configured sources model_config: The model config to check for unused config keys """ def warn_if_not_used(source_type: type[PydanticBaseSettingsSource], keys: tuple[str, ...]) -> None: if not any(isinstance(source, source_type) for source in sources): for key in keys: if model_config.get(key) is not None: warnings.warn( f'Config key `{key}` is set in model_config but will be ignored because no ' f'{source_type.__name__} source is configured. To use this config key, add a ' f'{source_type.__name__} source to the settings sources via the ' 'settings_customise_sources hook.', UserWarning, stacklevel=3, ) warn_if_not_used(JsonConfigSettingsSource, ('json_file', 'json_file_encoding')) warn_if_not_used(PyprojectTomlConfigSettingsSource, ('pyproject_toml_depth', 'pyproject_toml_table_header')) warn_if_not_used(TomlConfigSettingsSource, ('toml_file',)) warn_if_not_used(YamlConfigSettingsSource, ('yaml_file', 'yaml_file_encoding', 'yaml_config_section')) model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict( extra='forbid', arbitrary_types_allowed=True, validate_default=True, case_sensitive=False, env_prefix='', env_prefix_target='variable', nested_model_default_partial_update=False, env_file=None, env_file_encoding=None, env_ignore_empty=False, env_nested_delimiter=None, env_nested_max_split=None, env_parse_none_str=None, env_parse_enums=None, cli_prog_name=None, cli_parse_args=None, cli_parse_none_str=None, cli_hide_none_type=False, cli_avoid_json=False, cli_enforce_required=False, cli_use_class_docs_for_groups=False, cli_exit_on_error=True, cli_prefix='', cli_flag_prefix_char='-', cli_implicit_flags=False, cli_ignore_unknown_args=False, cli_kebab_case=False, cli_shortcuts=None, json_file=None, json_file_encoding=None, yaml_file=None, yaml_file_encoding=None, yaml_config_section=None, toml_file=None, secrets_dir=None, protected_namespaces=('model_validate', 'model_dump', 'settings_customise_sources'), enable_decoding=True, ) class CliApp: """ A utility class for running Pydantic `BaseSettings`, `BaseModel`, or `pydantic.dataclasses.dataclass` as CLI applications. """ _subcommand_stack: ClassVar[dict[int, tuple[CliSettingsSource[Any], Any, str]]] = {} _ansi_color: ClassVar[re.Pattern[str]] = re.compile(r'\x1b\[[0-9;]*m') @staticmethod def _get_base_settings_cls(model_cls: type[Any]) -> type[BaseSettings]: if issubclass(model_cls, BaseSettings): return model_cls class CliAppBaseSettings(BaseSettings, model_cls): # type: ignore __doc__ = model_cls.__doc__ model_config = SettingsConfigDict( nested_model_default_partial_update=True, case_sensitive=True, cli_hide_none_type=True, cli_avoid_json=True, cli_enforce_required=True, cli_implicit_flags=True, cli_kebab_case=True, ) return CliAppBaseSettings @staticmethod def _run_cli_cmd(model: Any, cli_cmd_method_name: str, is_required: bool) -> Any: command = getattr(type(model), cli_cmd_method_name, None) if command is None: if is_required: raise SettingsError(f'Error: {type(model).__name__} class is missing {cli_cmd_method_name} entrypoint') return model # If the method is asynchronous, we handle its execution based on the current event loop status. if inspect.iscoroutinefunction(command): # For asynchronous methods, we have two execution scenarios: # 1. If no event loop is running in the current thread, run the coroutine directly with asyncio.run(). # 2. If an event loop is already running in the current thread, run the coroutine in a separate thread to avoid conflicts. try: # Check if an event loop is currently running in this thread. loop = asyncio.get_running_loop() except RuntimeError: loop = None if loop and loop.is_running(): # We're in a context with an active event loop (e.g., Jupyter Notebook). # Running asyncio.run() here would cause conflicts, so we use a separate thread. exception_container = [] def run_coro() -> None: try: # Execute the coroutine in a new event loop in this separate thread. asyncio.run(command(model)) except Exception as e: exception_container.append(e) thread = threading.Thread(target=run_coro) thread.start() thread.join() if exception_container: # Propagate exceptions from the separate thread. raise exception_container[0] else: # No event loop is running; safe to run the coroutine directly. asyncio.run(command(model)) else: # For synchronous methods, call them directly. command(model) return model @staticmethod def run( model_cls: type[T], cli_args: list[str] | Namespace | SimpleNamespace | dict[str, Any] | None = None, cli_settings_source: CliSettingsSource[Any] | None = None, cli_exit_on_error: bool | None = None, cli_cmd_method_name: str = 'cli_cmd', **model_init_data: Any, ) -> T: """ Runs a Pydantic `BaseSettings`, `BaseModel`, or `pydantic.dataclasses.dataclass` as a CLI application. Running a model as a CLI application requires the `cli_cmd` method to be defined in the model class. Args: model_cls: The model class to run as a CLI application. cli_args: The list of CLI arguments to parse. If `cli_settings_source` is specified, this may also be a namespace or dictionary of pre-parsed CLI arguments. Defaults to `sys.argv[1:]`. cli_settings_source: Override the default CLI settings source with a user defined instance. Defaults to `None`. cli_exit_on_error: Determines whether this function exits on error. If model is subclass of `BaseSettings`, defaults to BaseSettings `cli_exit_on_error` value. Otherwise, defaults to `True`. cli_cmd_method_name: The CLI command method name to run. Defaults to "cli_cmd". model_init_data: The model init data. Returns: The ran instance of model. Raises: SettingsError: If model_cls is not subclass of `BaseModel` or `pydantic.dataclasses.dataclass`. SettingsError: If model_cls does not have a `cli_cmd` entrypoint defined. """ if not (is_pydantic_dataclass(model_cls) or is_model_class(model_cls)): raise SettingsError( f'Error: {model_cls.__name__} is not subclass of BaseModel or pydantic.dataclasses.dataclass' ) cli_settings = None cli_parse_args = True if cli_args is None else cli_args if cli_settings_source is not None: if isinstance(cli_parse_args, (Namespace, SimpleNamespace, dict)): cli_settings = cli_settings_source(parsed_args=cli_parse_args) else: cli_settings = cli_settings_source(args=cli_parse_args) elif isinstance(cli_parse_args, (Namespace, SimpleNamespace, dict)): raise SettingsError('Error: `cli_args` must be list[str] or None when `cli_settings_source` is not used') model_init_data['_cli_parse_args'] = cli_parse_args model_init_data['_cli_exit_on_error'] = cli_exit_on_error model_init_data['_cli_settings_source'] = cli_settings if not issubclass(model_cls, BaseSettings): base_settings_cls = CliApp._get_base_settings_cls(model_cls) sources, init_kwargs = base_settings_cls._settings_init_sources(**model_init_data) model = base_settings_cls(**base_settings_cls._settings_build_values(sources, init_kwargs)) model_init_data = {} for field_name, field_info in base_settings_cls.model_fields.items(): model_init_data[_field_name_for_signature(field_name, field_info)] = getattr(model, field_name) command = model_cls(**model_init_data) else: sources, init_kwargs = model_cls._settings_init_sources(**model_init_data) command = model_cls(_build_sources=(sources, init_kwargs)) subcommand_dest = ':subcommand' cli_settings_source = [source for source in sources if isinstance(source, CliSettingsSource)][0] CliApp._subcommand_stack[id(command)] = (cli_settings_source, cli_settings_source.root_parser, subcommand_dest) try: data_model = CliApp._run_cli_cmd(command, cli_cmd_method_name, is_required=False) finally: del CliApp._subcommand_stack[id(command)] return data_model @staticmethod def run_subcommand( model: PydanticModel, cli_exit_on_error: bool | None = None, cli_cmd_method_name: str = 'cli_cmd' ) -> PydanticModel: """ Runs the model subcommand. Running a model subcommand requires the `cli_cmd` method to be defined in the nested model subcommand class. Args: model: The model to run the subcommand from. cli_exit_on_error: Determines whether this function exits with error if no subcommand is found. Defaults to model_config `cli_exit_on_error` value if set. Otherwise, defaults to `True`. cli_cmd_method_name: The CLI command method name to run. Defaults to "cli_cmd". Returns: The ran subcommand model. Raises: SystemExit: When no subcommand is found and cli_exit_on_error=`True` (the default). SettingsError: When no subcommand is found and cli_exit_on_error=`False`. """ if id(model) in CliApp._subcommand_stack: cli_settings_source, parser, subcommand_dest = CliApp._subcommand_stack[id(model)] else: cli_settings_source = CliSettingsSource[Any](CliApp._get_base_settings_cls(type(model))) parser = cli_settings_source.root_parser subcommand_dest = ':subcommand' cli_exit_on_error = cli_settings_source.cli_exit_on_error if cli_exit_on_error is None else cli_exit_on_error errors: list[SettingsError | SystemExit] = [] subcommand = get_subcommand( model, is_required=True, cli_exit_on_error=cli_exit_on_error, _suppress_errors=errors ) if errors: err = errors[0] if err.__context__ is None and err.__cause__ is None and cli_settings_source._format_help is not None: error_message = f'{err}\n{cli_settings_source._format_help(parser)}' raise type(err)(error_message) from None else: raise err subcommand_cls = cast(type[BaseModel], type(subcommand)) subcommand_arg = cli_settings_source._parser_map[subcommand_dest][subcommand_cls] subcommand_alias = subcommand_arg.subcommand_alias(subcommand_cls) subcommand_dest = f'{subcommand_dest.split(":")[0]}{subcommand_alias}.:subcommand' subcommand_parser = subcommand_arg.parser CliApp._subcommand_stack[id(subcommand)] = (cli_settings_source, subcommand_parser, subcommand_dest) try: data_model = CliApp._run_cli_cmd(subcommand, cli_cmd_method_name, is_required=True) finally: del CliApp._subcommand_stack[id(subcommand)] return data_model @staticmethod def serialize( model: PydanticModel, list_style: Literal['json', 'argparse', 'lazy'] = 'json', dict_style: Literal['json', 'env'] = 'json', positionals_first: bool = False, ) -> list[str]: """ Serializes the CLI arguments for a Pydantic data model. Args: model: The data model to serialize. list_style: Controls how list-valued fields are serialized on the command line. - 'json' (default): Lists are encoded as a single JSON array. Example: `--tags '["a","b","c"]'` - 'argparse': Each list element becomes its own repeated flag, following typical `argparse` conventions. Example: `--tags a --tags b --tags c` - 'lazy': Lists are emitted as a single comma-separated string without JSON quoting or escaping. Example: `--tags a,b,c` dict_style: Controls how dictionary-valued fields are serialized. - 'json' (default): The entire dictionary is emitted as a single JSON object. Example: `--config '{"host": "localhost", "port": 5432}'` - 'env': The dictionary is flattened into multiple CLI flags using environment-variable-style assignement. Example: `--config host=localhost --config port=5432` positionals_first: Controls whether positional arguments should be serialized first compared to optional arguments. Defaults to `False`. Returns: The serialized CLI arguments for the data model. """ base_settings_cls = CliApp._get_base_settings_cls(type(model)) serialized_args = CliSettingsSource[Any](base_settings_cls)._serialized_args( model, list_style=list_style, dict_style=dict_style, positionals_first=positionals_first, ) return CliSettingsSource._flatten_serialized_args(serialized_args, positionals_first) @staticmethod def format_help( model: PydanticModel | type[T], cli_settings_source: CliSettingsSource[Any] | None = None, strip_ansi_color: bool = False, ) -> str: """ Return a string containing a help message for a Pydantic model. Args: model: The model or model class. cli_settings_source: Override the default CLI settings source with a user defined instance. Defaults to `None`. strip_ansi_color: Strips ANSI color codes from the help message when set to `True`. Returns: The help message string for the model. """ model_cls = model if isinstance(model, type) else type(model) if cli_settings_source is None: if not isinstance(model, type) and id(model) in CliApp._subcommand_stack: cli_settings_source, *_ = CliApp._subcommand_stack[id(model)] else: cli_settings_source = CliSettingsSource(CliApp._get_base_settings_cls(model_cls)) help_message = cli_settings_source._format_help(cli_settings_source.root_parser) return help_message if not strip_ansi_color else CliApp._ansi_color.sub('', help_message) @staticmethod def print_help( model: PydanticModel | type[T], cli_settings_source: CliSettingsSource[Any] | None = None, file: TextIO | None = None, strip_ansi_color: bool = False, ) -> None: """ Print a help message for a Pydantic model. Args: model: The model or model class. cli_settings_source: Override the default CLI settings source with a user defined instance. Defaults to `None`. file: A text stream to which the help message is written. If `None`, the output is sent to sys.stdout. strip_ansi_color: Strips ANSI color codes from the help message when set to `True`. """ print( CliApp.format_help( model, cli_settings_source=cli_settings_source, strip_ansi_color=strip_ansi_color, ), file=file, ) pydantic-pydantic-settings-198e71c/pydantic_settings/py.typed000066400000000000000000000000001514433345000245610ustar00rootroot00000000000000pydantic-pydantic-settings-198e71c/pydantic_settings/sources/000077500000000000000000000000001514433345000245575ustar00rootroot00000000000000pydantic-pydantic-settings-198e71c/pydantic_settings/sources/__init__.py000066400000000000000000000044021514433345000266700ustar00rootroot00000000000000"""Package for handling configuration sources in pydantic-settings.""" from .base import ( ConfigFileSourceMixin, DefaultSettingsSource, InitSettingsSource, PydanticBaseEnvSettingsSource, PydanticBaseSettingsSource, get_subcommand, ) from .providers.aws import AWSSecretsManagerSettingsSource from .providers.azure import AzureKeyVaultSettingsSource from .providers.cli import ( CLI_SUPPRESS, CliDualFlag, CliExplicitFlag, CliImplicitFlag, CliMutuallyExclusiveGroup, CliPositionalArg, CliSettingsSource, CliSubCommand, CliSuppress, CliToggleFlag, CliUnknownArgs, ) from .providers.dotenv import DotEnvSettingsSource, read_env_file from .providers.env import EnvSettingsSource from .providers.gcp import GoogleSecretManagerSettingsSource from .providers.json import JsonConfigSettingsSource from .providers.nested_secrets import NestedSecretsSettingsSource from .providers.pyproject import PyprojectTomlConfigSettingsSource from .providers.secrets import SecretsSettingsSource from .providers.toml import TomlConfigSettingsSource from .providers.yaml import YamlConfigSettingsSource from .types import ( DEFAULT_PATH, ENV_FILE_SENTINEL, DotenvType, EnvPrefixTarget, ForceDecode, NoDecode, PathType, PydanticModel, ) __all__ = [ 'CLI_SUPPRESS', 'ENV_FILE_SENTINEL', 'DEFAULT_PATH', 'AWSSecretsManagerSettingsSource', 'AzureKeyVaultSettingsSource', 'CliExplicitFlag', 'CliImplicitFlag', 'CliToggleFlag', 'CliDualFlag', 'CliMutuallyExclusiveGroup', 'CliPositionalArg', 'CliSettingsSource', 'CliSubCommand', 'CliSuppress', 'CliUnknownArgs', 'DefaultSettingsSource', 'DotEnvSettingsSource', 'DotenvType', 'EnvPrefixTarget', 'EnvSettingsSource', 'ForceDecode', 'GoogleSecretManagerSettingsSource', 'InitSettingsSource', 'JsonConfigSettingsSource', 'NestedSecretsSettingsSource', 'NoDecode', 'PathType', 'PydanticBaseEnvSettingsSource', 'PydanticBaseSettingsSource', 'ConfigFileSourceMixin', 'PydanticModel', 'PyprojectTomlConfigSettingsSource', 'SecretsSettingsSource', 'TomlConfigSettingsSource', 'YamlConfigSettingsSource', 'get_subcommand', 'read_env_file', ] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/base.py000066400000000000000000000561521514433345000260540ustar00rootroot00000000000000"""Base classes and core functionality for pydantic-settings sources.""" from __future__ import annotations as _annotations import json from abc import ABC, abstractmethod from collections.abc import Sequence from dataclasses import asdict, is_dataclass from pathlib import Path from typing import TYPE_CHECKING, Any, cast, get_args from pydantic import AliasChoices, AliasPath, BaseModel, TypeAdapter from pydantic._internal._typing_extra import ( # type: ignore[attr-defined] get_origin, ) from pydantic._internal._utils import deep_update, is_model_class from pydantic.fields import FieldInfo from typing_inspection import typing_objects from typing_inspection.introspection import is_union_origin from ..exceptions import SettingsError from ..utils import _lenient_issubclass from .types import EnvNoneType, EnvPrefixTarget, ForceDecode, NoDecode, PathType, PydanticModel, _CliSubCommand from .utils import ( _annotation_is_complex, _get_alias_names, _get_field_metadata, _get_model_fields, _strip_annotated, _union_is_complex, ) if TYPE_CHECKING: from pydantic_settings.main import BaseSettings def get_subcommand( model: PydanticModel, is_required: bool = True, cli_exit_on_error: bool | None = None, _suppress_errors: list[SettingsError | SystemExit] | None = None, ) -> PydanticModel | None: """ Get the subcommand from a model. Args: model: The model to get the subcommand from. is_required: Determines whether a model must have subcommand set and raises error if not found. Defaults to `True`. cli_exit_on_error: Determines whether this function exits with error if no subcommand is found. Defaults to model_config `cli_exit_on_error` value if set. Otherwise, defaults to `True`. Returns: The subcommand model if found, otherwise `None`. Raises: SystemExit: When no subcommand is found and is_required=`True` and cli_exit_on_error=`True` (the default). SettingsError: When no subcommand is found and is_required=`True` and cli_exit_on_error=`False`. """ model_cls = type(model) if cli_exit_on_error is None and is_model_class(model_cls): model_default = model_cls.model_config.get('cli_exit_on_error') if isinstance(model_default, bool): cli_exit_on_error = model_default if cli_exit_on_error is None: cli_exit_on_error = True subcommands: list[str] = [] for field_name, field_info in _get_model_fields(model_cls).items(): if _CliSubCommand in field_info.metadata: if getattr(model, field_name) is not None: return getattr(model, field_name) subcommands.append(field_name) if is_required: error_message = ( f'Error: CLI subcommand is required {{{", ".join(subcommands)}}}' if subcommands else 'Error: CLI subcommand is required but no subcommands were found.' ) err = SystemExit(error_message) if cli_exit_on_error else SettingsError(error_message) if _suppress_errors is None: raise err _suppress_errors.append(err) return None class PydanticBaseSettingsSource(ABC): """ Abstract base class for settings sources, every settings source classes should inherit from it. """ def __init__(self, settings_cls: type[BaseSettings]): self.settings_cls = settings_cls self.config = settings_cls.model_config self._current_state: dict[str, Any] = {} self._settings_sources_data: dict[str, dict[str, Any]] = {} def _set_current_state(self, state: dict[str, Any]) -> None: """ Record the state of settings from the previous settings sources. This should be called right before __call__. """ self._current_state = state def _set_settings_sources_data(self, states: dict[str, dict[str, Any]]) -> None: """ Record the state of settings from all previous settings sources. This should be called right before __call__. """ self._settings_sources_data = states @property def current_state(self) -> dict[str, Any]: """ The current state of the settings, populated by the previous settings sources. """ return self._current_state @property def settings_sources_data(self) -> dict[str, dict[str, Any]]: """ The state of all previous settings sources. """ return self._settings_sources_data @abstractmethod def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: """ Gets the value, the key for model creation, and a flag to determine whether value is complex. This is an abstract method that should be overridden in every settings source classes. Args: field: The field. field_name: The field name. Returns: A tuple that contains the value, key and a flag to determine whether value is complex. """ pass def field_is_complex(self, field: FieldInfo) -> bool: """ Checks whether a field is complex, in which case it will attempt to be parsed as JSON. Args: field: The field. Returns: Whether the field is complex. """ return _annotation_is_complex(field.annotation, field.metadata) def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: """ Prepares the value of a field. Args: field_name: The field name. field: The field. value: The value of the field that has to be prepared. value_is_complex: A flag to determine whether value is complex. Returns: The prepared value. """ if value is not None and (self.field_is_complex(field) or value_is_complex): return self.decode_complex_value(field_name, field, value) return value def decode_complex_value(self, field_name: str, field: FieldInfo, value: Any) -> Any: """ Decode the value for a complex field Args: field_name: The field name. field: The field. value: The value of the field that has to be prepared. Returns: The decoded value for further preparation """ if field and ( NoDecode in _get_field_metadata(field) or (self.config.get('enable_decoding') is False and ForceDecode not in field.metadata) ): return value return json.loads(value) @abstractmethod def __call__(self) -> dict[str, Any]: pass class ConfigFileSourceMixin(ABC): def _read_files(self, files: PathType | None, deep_merge: bool = False) -> dict[str, Any]: if files is None: return {} if not isinstance(files, Sequence) or isinstance(files, str): files = [files] vars: dict[str, Any] = {} for file in files: if isinstance(file, str): file_path = Path(file) else: file_path = file if isinstance(file_path, Path): file_path = file_path.expanduser() if not file_path.is_file(): continue updating_vars = self._read_file(file_path) if deep_merge: vars = deep_update(vars, updating_vars) else: vars.update(updating_vars) return vars @abstractmethod def _read_file(self, path: Path) -> dict[str, Any]: pass class DefaultSettingsSource(PydanticBaseSettingsSource): """ Source class for loading default object values. Args: settings_cls: The Settings class. nested_model_default_partial_update: Whether to allow partial updates on nested model default object fields. Defaults to `False`. """ def __init__(self, settings_cls: type[BaseSettings], nested_model_default_partial_update: bool | None = None): super().__init__(settings_cls) self.defaults: dict[str, Any] = {} self.nested_model_default_partial_update = ( nested_model_default_partial_update if nested_model_default_partial_update is not None else self.config.get('nested_model_default_partial_update', False) ) if self.nested_model_default_partial_update: for field_name, field_info in settings_cls.model_fields.items(): alias_names, *_ = _get_alias_names(field_name, field_info) preferred_alias = alias_names[0] if is_dataclass(type(field_info.default)): self.defaults[preferred_alias] = asdict(field_info.default) elif is_model_class(type(field_info.default)): self.defaults[preferred_alias] = field_info.default.model_dump() def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: # Nothing to do here. Only implement the return statement to make mypy happy return None, '', False def __call__(self) -> dict[str, Any]: return self.defaults def __repr__(self) -> str: return ( f'{self.__class__.__name__}(nested_model_default_partial_update={self.nested_model_default_partial_update})' ) class InitSettingsSource(PydanticBaseSettingsSource): """ Source class for loading values provided during settings class initialization. """ def __init__( self, settings_cls: type[BaseSettings], init_kwargs: dict[str, Any], nested_model_default_partial_update: bool | None = None, ): self.init_kwargs = {} init_kwarg_names = set(init_kwargs.keys()) for field_name, field_info in settings_cls.model_fields.items(): alias_names, *_ = _get_alias_names(field_name, field_info) # When populate_by_name is True, allow using the field name as an input key, # but normalize to the preferred alias to keep keys consistent across sources. matchable_names = set(alias_names) include_name = settings_cls.model_config.get('populate_by_name', False) or settings_cls.model_config.get( 'validate_by_name', False ) if include_name: matchable_names.add(field_name) init_kwarg_name = init_kwarg_names & matchable_names if init_kwarg_name: preferred_alias = alias_names[0] if alias_names else field_name # Choose provided key deterministically: prefer the first alias in alias_names order; # fall back to field_name if allowed and provided. provided_key = next((alias for alias in alias_names if alias in init_kwarg_names), None) if provided_key is None and include_name and field_name in init_kwarg_names: provided_key = field_name # provided_key should not be None here because init_kwarg_name is non-empty assert provided_key is not None init_kwarg_names -= init_kwarg_name self.init_kwargs[preferred_alias] = init_kwargs[provided_key] # Include any remaining init kwargs (e.g., extras) unchanged # Note: If populate_by_name is True and the provided key is the field name, but # no alias exists, we keep it as-is so it can be processed as extra if allowed. self.init_kwargs.update({key: val for key, val in init_kwargs.items() if key in init_kwarg_names}) super().__init__(settings_cls) self.nested_model_default_partial_update = ( nested_model_default_partial_update if nested_model_default_partial_update is not None else self.config.get('nested_model_default_partial_update', False) ) def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: # Nothing to do here. Only implement the return statement to make mypy happy return None, '', False def __call__(self) -> dict[str, Any]: return ( TypeAdapter(dict[str, Any]).dump_python(self.init_kwargs) if self.nested_model_default_partial_update else self.init_kwargs ) def __repr__(self) -> str: return f'{self.__class__.__name__}(init_kwargs={self.init_kwargs!r})' class PydanticBaseEnvSettingsSource(PydanticBaseSettingsSource): def __init__( self, settings_cls: type[BaseSettings], case_sensitive: bool | None = None, env_prefix: str | None = None, env_prefix_target: EnvPrefixTarget | None = None, env_ignore_empty: bool | None = None, env_parse_none_str: str | None = None, env_parse_enums: bool | None = None, ) -> None: super().__init__(settings_cls) self.case_sensitive = case_sensitive if case_sensitive is not None else self.config.get('case_sensitive', False) self.env_prefix = env_prefix if env_prefix is not None else self.config.get('env_prefix', '') self.env_prefix_target = ( env_prefix_target if env_prefix_target is not None else self.config.get('env_prefix_target', 'variable') ) self.env_ignore_empty = ( env_ignore_empty if env_ignore_empty is not None else self.config.get('env_ignore_empty', False) ) self.env_parse_none_str = ( env_parse_none_str if env_parse_none_str is not None else self.config.get('env_parse_none_str') ) self.env_parse_enums = env_parse_enums if env_parse_enums is not None else self.config.get('env_parse_enums') def _apply_case_sensitive(self, value: str) -> str: return value.lower() if not self.case_sensitive else value def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[str, str, bool]]: """ Extracts field info. This info is used to get the value of field from environment variables. It returns a list of tuples, each tuple contains: * field_key: The key of field that has to be used in model creation. * env_name: The environment variable name of the field. * value_is_complex: A flag to determine whether the value from environment variable is complex and has to be parsed. Args: field (FieldInfo): The field. field_name (str): The field name. Returns: list[tuple[str, str, bool]]: List of tuples, each tuple contains field_key, env_name, and value_is_complex. """ field_info: list[tuple[str, str, bool]] = [] if isinstance(field.validation_alias, (AliasChoices, AliasPath)): v_alias: str | list[str | int] | list[list[str | int]] | None = field.validation_alias.convert_to_aliases() else: v_alias = field.validation_alias if v_alias: env_prefix = self.env_prefix if self.env_prefix_target in ('alias', 'all') else '' if isinstance(v_alias, list): # AliasChoices, AliasPath for alias in v_alias: if isinstance(alias, str): # AliasPath field_info.append( (alias, self._apply_case_sensitive(env_prefix + alias), True if len(alias) > 1 else False) ) elif isinstance(alias, list): # AliasChoices first_arg = cast(str, alias[0]) # first item of an AliasChoices must be a str field_info.append( ( first_arg, self._apply_case_sensitive(env_prefix + first_arg), True if len(alias) > 1 else False, ) ) else: # string validation alias field_info.append((v_alias, self._apply_case_sensitive(env_prefix + v_alias), False)) if not v_alias or self.config.get('populate_by_name', False) or self.config.get('validate_by_name', False): annotation = field.annotation env_prefix = self.env_prefix if self.env_prefix_target in ('variable', 'all') else '' if typing_objects.is_typealiastype(annotation) or typing_objects.is_typealiastype(get_origin(annotation)): annotation = _strip_annotated(annotation.__value__) # type: ignore[union-attr] if is_union_origin(get_origin(annotation)) and _union_is_complex(annotation, field.metadata): field_info.append((field_name, self._apply_case_sensitive(env_prefix + field_name), True)) else: field_info.append((field_name, self._apply_case_sensitive(env_prefix + field_name), False)) return field_info def _replace_field_names_case_insensitively(self, field: FieldInfo, field_values: dict[str, Any]) -> dict[str, Any]: """ Replace field names in values dict by looking in models fields insensitively. By having the following models: ```py class SubSubSub(BaseModel): VaL3: str class SubSub(BaseModel): Val2: str SUB_sub_SuB: SubSubSub class Sub(BaseModel): VAL1: str SUB_sub: SubSub class Settings(BaseSettings): nested: Sub model_config = SettingsConfigDict(env_nested_delimiter='__') ``` Then: _replace_field_names_case_insensitively( field, {"val1": "v1", "sub_SUB": {"VAL2": "v2", "sub_SUB_sUb": {"vAl3": "v3"}}} ) Returns {'VAL1': 'v1', 'SUB_sub': {'Val2': 'v2', 'SUB_sub_SuB': {'VaL3': 'v3'}}} """ values: dict[str, Any] = {} for name, value in field_values.items(): sub_model_field: FieldInfo | None = None annotation = field.annotation # If field is Optional, we need to find the actual type if is_union_origin(get_origin(field.annotation)): args = get_args(annotation) if len(args) == 2 and type(None) in args: for arg in args: if arg is not None: annotation = arg break # This is here to make mypy happy # Item "None" of "Optional[Type[Any]]" has no attribute "model_fields" if not annotation or not hasattr(annotation, 'model_fields'): values[name] = value continue else: model_fields: dict[str, FieldInfo] = annotation.model_fields # Find field in sub model by looking in fields case insensitively field_key: str | None = None for sub_model_field_name, sub_model_field in model_fields.items(): aliases, _ = _get_alias_names(sub_model_field_name, sub_model_field) _search = (alias for alias in aliases if alias.lower() == name.lower()) if field_key := next(_search, None): break if not field_key: values[name] = value continue if ( sub_model_field is not None and _lenient_issubclass(sub_model_field.annotation, BaseModel) and isinstance(value, dict) ): values[field_key] = self._replace_field_names_case_insensitively(sub_model_field, value) else: values[field_key] = value return values def _replace_env_none_type_values(self, field_value: dict[str, Any]) -> dict[str, Any]: """ Recursively parse values that are of "None" type(EnvNoneType) to `None` type(None). """ values: dict[str, Any] = {} for key, value in field_value.items(): if not isinstance(value, EnvNoneType): values[key] = value if not isinstance(value, dict) else self._replace_env_none_type_values(value) else: values[key] = None return values def _get_resolved_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: """ Gets the value, the preferred alias key for model creation, and a flag to determine whether value is complex. Note: In V3, this method should either be made public, or, this method should be removed and the abstract method get_field_value should be updated to include a "use_preferred_alias" flag. Args: field: The field. field_name: The field name. Returns: A tuple that contains the value, preferred key and a flag to determine whether value is complex. """ field_value, field_key, value_is_complex = self.get_field_value(field, field_name) # Only use preferred_key when no value was found; otherwise preserve the key that matched if field_value is None and not ( value_is_complex or ( (self.config.get('populate_by_name', False) or self.config.get('validate_by_name', False)) and (field_key == field_name) ) ): field_infos = self._extract_field_info(field, field_name) preferred_key, *_ = field_infos[0] return field_value, preferred_key, value_is_complex return field_value, field_key, value_is_complex def __call__(self) -> dict[str, Any]: data: dict[str, Any] = {} for field_name, field in self.settings_cls.model_fields.items(): try: field_value, field_key, value_is_complex = self._get_resolved_field_value(field, field_name) except Exception as e: raise SettingsError( f'error getting value for field "{field_name}" from source "{self.__class__.__name__}"' ) from e try: field_value = self.prepare_field_value(field_name, field, field_value, value_is_complex) except ValueError as e: raise SettingsError( f'error parsing value for field "{field_name}" from source "{self.__class__.__name__}"' ) from e if field_value is not None: if self.env_parse_none_str is not None: if isinstance(field_value, dict): field_value = self._replace_env_none_type_values(field_value) elif isinstance(field_value, EnvNoneType): field_value = None if ( not self.case_sensitive # and _lenient_issubclass(field.annotation, BaseModel) and isinstance(field_value, dict) ): data[field_key] = self._replace_field_names_case_insensitively(field, field_value) else: data[field_key] = field_value return data __all__ = [ 'ConfigFileSourceMixin', 'DefaultSettingsSource', 'InitSettingsSource', 'PydanticBaseEnvSettingsSource', 'PydanticBaseSettingsSource', 'SettingsError', ] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/000077500000000000000000000000001514433345000265745ustar00rootroot00000000000000pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/__init__.py000066400000000000000000000024011514433345000307020ustar00rootroot00000000000000"""Package containing individual source implementations.""" from .aws import AWSSecretsManagerSettingsSource from .azure import AzureKeyVaultSettingsSource from .cli import ( CliDualFlag, CliExplicitFlag, CliImplicitFlag, CliMutuallyExclusiveGroup, CliPositionalArg, CliSettingsSource, CliSubCommand, CliSuppress, CliToggleFlag, ) from .dotenv import DotEnvSettingsSource from .env import EnvSettingsSource from .gcp import GoogleSecretManagerSettingsSource from .json import JsonConfigSettingsSource from .pyproject import PyprojectTomlConfigSettingsSource from .secrets import SecretsSettingsSource from .toml import TomlConfigSettingsSource from .yaml import YamlConfigSettingsSource __all__ = [ 'AWSSecretsManagerSettingsSource', 'AzureKeyVaultSettingsSource', 'CliExplicitFlag', 'CliImplicitFlag', 'CliToggleFlag', 'CliDualFlag', 'CliMutuallyExclusiveGroup', 'CliPositionalArg', 'CliSettingsSource', 'CliSubCommand', 'CliSuppress', 'DotEnvSettingsSource', 'EnvSettingsSource', 'GoogleSecretManagerSettingsSource', 'JsonConfigSettingsSource', 'PyprojectTomlConfigSettingsSource', 'SecretsSettingsSource', 'TomlConfigSettingsSource', 'YamlConfigSettingsSource', ] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/aws.py000066400000000000000000000052511514433345000277430ustar00rootroot00000000000000from __future__ import annotations as _annotations # important for BaseSettings import to work import json from collections.abc import Mapping from typing import TYPE_CHECKING from ..utils import parse_env_vars from .env import EnvSettingsSource if TYPE_CHECKING: from pydantic_settings.main import BaseSettings boto3_client = None SecretsManagerClient = None def import_aws_secrets_manager() -> None: global boto3_client global SecretsManagerClient try: from boto3 import client as boto3_client from mypy_boto3_secretsmanager.client import SecretsManagerClient except ImportError as e: # pragma: no cover raise ImportError( 'AWS Secrets Manager dependencies are not installed, run `pip install pydantic-settings[aws-secrets-manager]`' ) from e class AWSSecretsManagerSettingsSource(EnvSettingsSource): _secret_id: str _secretsmanager_client: SecretsManagerClient # type: ignore def __init__( self, settings_cls: type[BaseSettings], secret_id: str, region_name: str | None = None, endpoint_url: str | None = None, case_sensitive: bool | None = True, env_prefix: str | None = None, env_nested_delimiter: str | None = '--', env_parse_none_str: str | None = None, env_parse_enums: bool | None = None, version_id: str | None = None, ) -> None: import_aws_secrets_manager() self._secretsmanager_client = boto3_client('secretsmanager', region_name=region_name, endpoint_url=endpoint_url) # type: ignore self._secret_id = secret_id self._version_id = version_id super().__init__( settings_cls, case_sensitive=case_sensitive, env_prefix=env_prefix, env_nested_delimiter=env_nested_delimiter, env_ignore_empty=False, env_parse_none_str=env_parse_none_str, env_parse_enums=env_parse_enums, ) def _load_env_vars(self) -> Mapping[str, str | None]: request = {'SecretId': self._secret_id} if self._version_id: request['VersionId'] = self._version_id response = self._secretsmanager_client.get_secret_value(**request) # type: ignore return parse_env_vars( json.loads(response['SecretString']), self.case_sensitive, self.env_ignore_empty, self.env_parse_none_str, ) def __repr__(self) -> str: return ( f'{self.__class__.__name__}(secret_id={self._secret_id!r}, ' f'env_nested_delimiter={self.env_nested_delimiter!r})' ) __all__ = [ 'AWSSecretsManagerSettingsSource', ] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/azure.py000066400000000000000000000127201514433345000302760ustar00rootroot00000000000000"""Azure Key Vault settings source.""" from __future__ import annotations as _annotations from collections.abc import Iterator, Mapping from typing import TYPE_CHECKING from pydantic.alias_generators import to_snake from pydantic.fields import FieldInfo from .env import EnvSettingsSource if TYPE_CHECKING: from azure.core.credentials import TokenCredential from azure.core.exceptions import ResourceNotFoundError from azure.keyvault.secrets import SecretClient from pydantic_settings.main import BaseSettings else: TokenCredential = None ResourceNotFoundError = None SecretClient = None def import_azure_key_vault() -> None: global TokenCredential global SecretClient global ResourceNotFoundError try: from azure.core.credentials import TokenCredential from azure.core.exceptions import ResourceNotFoundError from azure.keyvault.secrets import SecretClient except ImportError as e: # pragma: no cover raise ImportError( 'Azure Key Vault dependencies are not installed, run `pip install pydantic-settings[azure-key-vault]`' ) from e class AzureKeyVaultMapping(Mapping[str, str | None]): _loaded_secrets: dict[str, str | None] _secret_client: SecretClient _secret_names: list[str] def __init__( self, secret_client: SecretClient, case_sensitive: bool, snake_case_conversion: bool, env_prefix: str | None, ) -> None: self._loaded_secrets = {} self._secret_client = secret_client self._case_sensitive = case_sensitive self._snake_case_conversion = snake_case_conversion self._env_prefix = env_prefix if env_prefix else '' self._secret_map: dict[str, str] = self._load_remote() def _load_remote(self) -> dict[str, str]: secret_names: Iterator[str] = ( secret.name for secret in self._secret_client.list_properties_of_secrets() if secret.name and secret.enabled ) if self._snake_case_conversion: name_map: dict[str, str] = {} for name in secret_names: if name.startswith(self._env_prefix): name_map[f'{self._env_prefix}{to_snake(name[len(self._env_prefix) :])}'] = name else: name_map[to_snake(name)] = name return name_map if self._case_sensitive: return {name: name for name in secret_names} return {name.lower(): name for name in secret_names} def __getitem__(self, key: str) -> str | None: new_key = key if self._snake_case_conversion: if key.startswith(self._env_prefix): new_key = f'{self._env_prefix}{to_snake(key[len(self._env_prefix) :])}' else: new_key = to_snake(key) elif not self._case_sensitive: new_key = key.lower() if new_key not in self._loaded_secrets: if new_key in self._secret_map: self._loaded_secrets[new_key] = self._secret_client.get_secret(self._secret_map[new_key]).value else: raise KeyError(key) return self._loaded_secrets[new_key] def __len__(self) -> int: return len(self._secret_map) def __iter__(self) -> Iterator[str]: return iter(self._secret_map.keys()) class AzureKeyVaultSettingsSource(EnvSettingsSource): _url: str _credential: TokenCredential def __init__( self, settings_cls: type[BaseSettings], url: str, credential: TokenCredential, dash_to_underscore: bool = False, case_sensitive: bool | None = None, snake_case_conversion: bool = False, env_prefix: str | None = None, env_parse_none_str: str | None = None, env_parse_enums: bool | None = None, ) -> None: import_azure_key_vault() self._url = url self._credential = credential self._dash_to_underscore = dash_to_underscore self._snake_case_conversion = snake_case_conversion super().__init__( settings_cls, case_sensitive=True if snake_case_conversion else case_sensitive, env_prefix=env_prefix, env_nested_delimiter='__' if snake_case_conversion else '--', env_ignore_empty=False, env_parse_none_str=env_parse_none_str, env_parse_enums=env_parse_enums, ) def _load_env_vars(self) -> Mapping[str, str | None]: secret_client = SecretClient(vault_url=self._url, credential=self._credential) return AzureKeyVaultMapping( secret_client=secret_client, case_sensitive=self.case_sensitive, snake_case_conversion=self._snake_case_conversion, env_prefix=self.env_prefix, ) def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[str, str, bool]]: if self._snake_case_conversion: field_info = list((x[0], x[1], x[2]) for x in super()._extract_field_info(field, field_name)) return field_info if self._dash_to_underscore: return list((x[0], x[1].replace('_', '-'), x[2]) for x in super()._extract_field_info(field, field_name)) return super()._extract_field_info(field, field_name) def __repr__(self) -> str: return f'{self.__class__.__name__}(url={self._url!r}, env_nested_delimiter={self.env_nested_delimiter!r})' __all__ = ['AzureKeyVaultMapping', 'AzureKeyVaultSettingsSource'] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/cli.py000066400000000000000000002120231514433345000277150ustar00rootroot00000000000000"""Command-line interface settings source.""" from __future__ import annotations as _annotations import copy import json import re import shlex import sys import typing from argparse import ( SUPPRESS, ArgumentParser, BooleanOptionalAction, Namespace, RawDescriptionHelpFormatter, _SubParsersAction, ) from collections import defaultdict from collections.abc import Callable, Mapping, Sequence from enum import Enum from functools import cached_property from itertools import chain from textwrap import dedent from types import SimpleNamespace from typing import ( TYPE_CHECKING, Annotated, Any, Generic, Literal, NoReturn, TypeVar, cast, get_args, get_origin, overload, ) from pydantic import AliasChoices, AliasPath, BaseModel, Field, PrivateAttr, TypeAdapter, ValidationError from pydantic._internal._repr import Representation from pydantic._internal._utils import is_model_class from pydantic.dataclasses import is_pydantic_dataclass from pydantic.fields import FieldInfo from pydantic_core import PydanticUndefined from typing_inspection import typing_objects from typing_inspection.introspection import is_union_origin from ...exceptions import SettingsError from ...utils import _lenient_issubclass, _typing_base, _WithArgsTypes from ..types import ( ForceDecode, NoDecode, PydanticModel, _CliDualFlag, _CliExplicitFlag, _CliImplicitFlag, _CliPositionalArg, _CliSubCommand, _CliToggleFlag, _CliUnknownArgs, ) from ..utils import ( _annotation_contains_types, _annotation_enum_val_to_name, _get_alias_names, _get_model_fields, _is_function, _strip_annotated, parse_env_vars, ) from .env import EnvSettingsSource if TYPE_CHECKING: from pydantic_settings.main import BaseSettings class _CliInternalArgParser(ArgumentParser): def __init__(self, cli_exit_on_error: bool = True, **kwargs: Any) -> None: super().__init__(**kwargs) self._cli_exit_on_error = cli_exit_on_error def error(self, message: str) -> NoReturn: if not self._cli_exit_on_error: raise SettingsError(f'error parsing CLI: {message}') super().error(message) class CliMutuallyExclusiveGroup(BaseModel): pass class _CliArg(BaseModel): model: Any parser: Any field_name: str arg_prefix: str case_sensitive: bool hide_none_type: bool kebab_case: bool | Literal['all', 'no_enums'] | None enable_decoding: bool | None env_prefix_len: int args: list[str] = [] kwargs: dict[str, Any] = {} _alias_names: tuple[str, ...] = PrivateAttr(()) _alias_paths: dict[str, int | None] = PrivateAttr({}) _is_alias_path_only: bool = PrivateAttr(False) _field_info: FieldInfo = PrivateAttr() def __init__( self, field_info: FieldInfo, parser_map: defaultdict[str | FieldInfo, dict[int | None | str | type[BaseModel], _CliArg]], **values: Any, ) -> None: super().__init__(**values) self._field_info = field_info self._alias_names, self._is_alias_path_only = _get_alias_names( self.field_name, self.field_info, alias_path_args=self._alias_paths, case_sensitive=self.case_sensitive ) alias_path_dests = {f'{self.arg_prefix}{name}': index for name, index in self._alias_paths.items()} if self.subcommand_dest: for sub_model in self.sub_models: subcommand_alias = self.subcommand_alias(sub_model) parser_map[self.subcommand_dest][subcommand_alias] = self.model_copy(update={'args': [], 'kwargs': {}}) parser_map[self.subcommand_dest][sub_model] = parser_map[self.subcommand_dest][subcommand_alias] parser_map[self.field_info][subcommand_alias] = parser_map[self.subcommand_dest][subcommand_alias] elif self.dest not in alias_path_dests: parser_map[self.dest][None] = self parser_map[self.field_info][None] = parser_map[self.dest][None] for alias_path_dest, index in alias_path_dests.items(): parser_map[alias_path_dest][index] = self.model_copy(update={'args': [], 'kwargs': {}}) parser_map[self.field_info][index] = parser_map[alias_path_dest][index] @classmethod def get_kebab_case(cls, name: str, kebab_case: bool | Literal['all', 'no_enums'] | None) -> str: return name.replace('_', '-') if kebab_case not in (None, False) else name @classmethod def get_enum_names( cls, annotation: type[Any], kebab_case: bool | Literal['all', 'no_enums'] | None ) -> tuple[str, ...]: enum_names: tuple[str, ...] = () annotation = _strip_annotated(annotation) for type_ in get_args(annotation): enum_names += cls.get_enum_names(type_, kebab_case) if annotation and _lenient_issubclass(annotation, Enum): enum_names += tuple(cls.get_kebab_case(name, kebab_case == 'all') for name in annotation.__members__.keys()) return enum_names def subcommand_alias(self, sub_model: type[BaseModel]) -> str: return self.get_kebab_case( sub_model.__name__ if len(self.sub_models) > 1 else self.preferred_alias, self.kebab_case ) @cached_property def field_info(self) -> FieldInfo: return self._field_info @cached_property def subcommand_dest(self) -> str | None: return f'{self.arg_prefix}:subcommand' if _CliSubCommand in self.field_info.metadata else None @cached_property def dest(self) -> str: if ( not self.subcommand_dest and self.arg_prefix and self.field_info.validation_alias is not None and not self.is_parser_submodel ): # Strip prefix if validation alias is set and value is not complex. # Related https://github.com/pydantic/pydantic-settings/pull/25 return f'{self.arg_prefix}{self.preferred_alias}'[self.env_prefix_len :] return f'{self.arg_prefix}{self.preferred_alias}' @cached_property def preferred_arg_name(self) -> str: return self.args[0].replace('_', '-') if self.kebab_case else self.args[0] @cached_property def sub_models(self) -> list[type[BaseModel]]: field_types: tuple[Any, ...] = ( (self.field_info.annotation,) if not get_args(self.field_info.annotation) else get_args(self.field_info.annotation) ) if self.hide_none_type: field_types = tuple([type_ for type_ in field_types if type_ is not type(None)]) sub_models: list[type[BaseModel]] = [] for type_ in field_types: if _annotation_contains_types(type_, (_CliSubCommand,), is_include_origin=False): raise SettingsError( f'CliSubCommand is not outermost annotation for {self.model.__name__}.{self.field_name}' ) elif _annotation_contains_types(type_, (_CliPositionalArg,), is_include_origin=False): raise SettingsError( f'CliPositionalArg is not outermost annotation for {self.model.__name__}.{self.field_name}' ) if is_model_class(_strip_annotated(type_)) or is_pydantic_dataclass(_strip_annotated(type_)): sub_models.append(_strip_annotated(type_)) return sub_models @cached_property def alias_names(self) -> tuple[str, ...]: return self._alias_names @cached_property def alias_paths(self) -> dict[str, int | None]: return self._alias_paths @cached_property def preferred_alias(self) -> str: return self._alias_names[0] @cached_property def is_alias_path_only(self) -> bool: return self._is_alias_path_only @cached_property def is_append_action(self) -> bool: return not self.subcommand_dest and _annotation_contains_types( self.field_info.annotation, (list, set, dict, Sequence, Mapping), is_strip_annotated=True ) @cached_property def is_parser_submodel(self) -> bool: return not self.subcommand_dest and bool(self.sub_models) and not self.is_append_action @cached_property def is_no_decode(self) -> bool: return self.field_info is not None and ( NoDecode in self.field_info.metadata or (self.enable_decoding is False and ForceDecode not in self.field_info.metadata) ) T = TypeVar('T') CliSubCommand = Annotated[T | None, _CliSubCommand] CliPositionalArg = Annotated[T, _CliPositionalArg] _CliBoolFlag = TypeVar('_CliBoolFlag', bound=bool) CliImplicitFlag = Annotated[_CliBoolFlag, _CliImplicitFlag] CliExplicitFlag = Annotated[_CliBoolFlag, _CliExplicitFlag] CliToggleFlag = Annotated[_CliBoolFlag, _CliToggleFlag] CliDualFlag = Annotated[_CliBoolFlag, _CliDualFlag] CLI_SUPPRESS = SUPPRESS CliSuppress = Annotated[T, CLI_SUPPRESS] CliUnknownArgs = Annotated[list[str], Field(default=[]), _CliUnknownArgs, NoDecode] class CliSettingsSource(EnvSettingsSource, Generic[T]): """ Source class for loading settings values from CLI. Note: A `CliSettingsSource` connects with a `root_parser` object by using the parser methods to add `settings_cls` fields as command line arguments. The `CliSettingsSource` internal parser representation is based upon the `argparse` parsing library, and therefore, requires the parser methods to support the same attributes as their `argparse` library counterparts. Args: cli_prog_name: The CLI program name to display in help text. Defaults to `None` if cli_parse_args is `None`. Otherwise, defaults to sys.argv[0]. cli_parse_args: The list of CLI arguments to parse. Defaults to None. If set to `True`, defaults to sys.argv[1:]. cli_parse_none_str: The CLI string value that should be parsed (e.g. "null", "void", "None", etc.) into `None` type(None). Defaults to "null" if cli_avoid_json is `False`, and "None" if cli_avoid_json is `True`. cli_hide_none_type: Hide `None` values in CLI help text. Defaults to `False`. cli_avoid_json: Avoid complex JSON objects in CLI help text. Defaults to `False`. cli_enforce_required: Enforce required fields at the CLI. Defaults to `False`. cli_use_class_docs_for_groups: Use class docstrings in CLI group help text instead of field descriptions. Defaults to `False`. cli_exit_on_error: Determines whether or not the internal parser exits with error info when an error occurs. Defaults to `True`. cli_prefix: Prefix for command line arguments added under the root parser. Defaults to "". cli_flag_prefix_char: The flag prefix character to use for CLI optional arguments. Defaults to '-'. cli_implicit_flags: Controls how `bool` fields are exposed as CLI flags. - False (default): no implicit flags are generated; booleans must be set explicitly (e.g. --flag=true). - True / 'dual': optional boolean fields generate both positive and negative forms (--flag and --no-flag). - 'toggle': required boolean fields remain in 'dual' mode, while optional boolean fields generate a single flag aligned with the default value (if default=False, expose --flag; if default=True, expose --no-flag). cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`. cli_kebab_case: CLI args use kebab case. Defaults to `False`. cli_shortcuts: Mapping of target field name to alias names. Defaults to `None`. case_sensitive: Whether CLI "--arg" names should be read with case-sensitivity. Defaults to `True`. Note: Case-insensitive matching is only supported on the internal root parser and does not apply to CLI subcommands. root_parser: The root parser object. parse_args_method: The root parser parse args method. Defaults to `argparse.ArgumentParser.parse_args`. add_argument_method: The root parser add argument method. Defaults to `argparse.ArgumentParser.add_argument`. add_argument_group_method: The root parser add argument group method. Defaults to `argparse.ArgumentParser.add_argument_group`. add_parser_method: The root parser add new parser (sub-command) method. Defaults to `argparse._SubParsersAction.add_parser`. add_subparsers_method: The root parser add subparsers (sub-commands) method. Defaults to `argparse.ArgumentParser.add_subparsers`. format_help_method: The root parser format help method. Defaults to `argparse.ArgumentParser.format_help`. formatter_class: A class for customizing the root parser help text. Defaults to `argparse.RawDescriptionHelpFormatter`. """ def __init__( self, settings_cls: type[BaseSettings], cli_prog_name: str | None = None, cli_parse_args: bool | list[str] | tuple[str, ...] | None = None, cli_parse_none_str: str | None = None, cli_hide_none_type: bool | None = None, cli_avoid_json: bool | None = None, cli_enforce_required: bool | None = None, cli_use_class_docs_for_groups: bool | None = None, cli_exit_on_error: bool | None = None, cli_prefix: str | None = None, cli_flag_prefix_char: str | None = None, cli_implicit_flags: bool | Literal['dual', 'toggle'] | None = None, cli_ignore_unknown_args: bool | None = None, cli_kebab_case: bool | Literal['all', 'no_enums'] | None = None, cli_shortcuts: Mapping[str, str | list[str]] | None = None, case_sensitive: bool | None = True, root_parser: Any = None, parse_args_method: Callable[..., Any] | None = None, add_argument_method: Callable[..., Any] | None = ArgumentParser.add_argument, add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group, add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser, add_subparsers_method: Callable[..., Any] | None = ArgumentParser.add_subparsers, format_help_method: Callable[..., Any] | None = ArgumentParser.format_help, formatter_class: Any = RawDescriptionHelpFormatter, ) -> None: self.cli_prog_name = ( cli_prog_name if cli_prog_name is not None else settings_cls.model_config.get('cli_prog_name', sys.argv[0]) ) self.cli_hide_none_type = ( cli_hide_none_type if cli_hide_none_type is not None else settings_cls.model_config.get('cli_hide_none_type', False) ) self.cli_avoid_json = ( cli_avoid_json if cli_avoid_json is not None else settings_cls.model_config.get('cli_avoid_json', False) ) if not cli_parse_none_str: cli_parse_none_str = 'None' if self.cli_avoid_json is True else 'null' self.cli_parse_none_str = cli_parse_none_str self.cli_enforce_required = ( cli_enforce_required if cli_enforce_required is not None else settings_cls.model_config.get('cli_enforce_required', False) ) self.cli_use_class_docs_for_groups = ( cli_use_class_docs_for_groups if cli_use_class_docs_for_groups is not None else settings_cls.model_config.get('cli_use_class_docs_for_groups', False) ) self.cli_exit_on_error = ( cli_exit_on_error if cli_exit_on_error is not None else settings_cls.model_config.get('cli_exit_on_error', True) ) self.cli_prefix = cli_prefix if cli_prefix is not None else settings_cls.model_config.get('cli_prefix', '') self.cli_flag_prefix_char = ( cli_flag_prefix_char if cli_flag_prefix_char is not None else settings_cls.model_config.get('cli_flag_prefix_char', '-') ) self._cli_flag_prefix = self.cli_flag_prefix_char * 2 if self.cli_prefix: if cli_prefix.startswith('.') or cli_prefix.endswith('.') or not cli_prefix.replace('.', '').isidentifier(): # type: ignore raise SettingsError(f'CLI settings source prefix is invalid: {cli_prefix}') self.cli_prefix += '.' self.cli_implicit_flags = ( cli_implicit_flags if cli_implicit_flags is not None else settings_cls.model_config.get('cli_implicit_flags', False) ) self.cli_ignore_unknown_args = ( cli_ignore_unknown_args if cli_ignore_unknown_args is not None else settings_cls.model_config.get('cli_ignore_unknown_args', False) ) self.cli_kebab_case = ( cli_kebab_case if cli_kebab_case is not None else settings_cls.model_config.get('cli_kebab_case', False) ) self.cli_shortcuts = ( cli_shortcuts if cli_shortcuts is not None else settings_cls.model_config.get('cli_shortcuts', None) ) case_sensitive = case_sensitive if case_sensitive is not None else True if not case_sensitive and root_parser is not None: raise SettingsError('Case-insensitive matching is only supported on the internal root parser') super().__init__( settings_cls, env_nested_delimiter='.', env_parse_none_str=self.cli_parse_none_str, env_parse_enums=True, env_prefix=self.cli_prefix, case_sensitive=case_sensitive, ) root_parser = ( _CliInternalArgParser( cli_exit_on_error=self.cli_exit_on_error, prog=self.cli_prog_name, description=None if settings_cls.__doc__ is None else dedent(settings_cls.__doc__), formatter_class=formatter_class, prefix_chars=self.cli_flag_prefix_char, allow_abbrev=False, add_help=False, ) if root_parser is None else root_parser ) self._connect_root_parser( root_parser=root_parser, parse_args_method=parse_args_method, add_argument_method=add_argument_method, add_argument_group_method=add_argument_group_method, add_parser_method=add_parser_method, add_subparsers_method=add_subparsers_method, format_help_method=format_help_method, formatter_class=formatter_class, ) if cli_parse_args not in (None, False): if cli_parse_args is True: cli_parse_args = sys.argv[1:] elif not isinstance(cli_parse_args, (list, tuple)): raise SettingsError( f'cli_parse_args must be a list or tuple of strings, received {type(cli_parse_args)}' ) self._load_env_vars(parsed_args=self._parse_args(self.root_parser, cli_parse_args)) @overload def __call__(self) -> dict[str, Any]: ... @overload def __call__(self, *, args: list[str] | tuple[str, ...] | bool) -> CliSettingsSource[T]: """ Parse and load the command line arguments list into the CLI settings source. Args: args: The command line arguments to parse and load. Defaults to `None`, which means do not parse command line arguments. If set to `True`, defaults to sys.argv[1:]. If set to `False`, does not parse command line arguments. Returns: CliSettingsSource: The object instance itself. """ ... @overload def __call__(self, *, parsed_args: Namespace | SimpleNamespace | dict[str, Any]) -> CliSettingsSource[T]: """ Loads parsed command line arguments into the CLI settings source. Note: The parsed args must be in `argparse.Namespace`, `SimpleNamespace`, or vars dictionary (e.g., vars(argparse.Namespace)) format. Args: parsed_args: The parsed args to load. Returns: CliSettingsSource: The object instance itself. """ ... def __call__( self, *, args: list[str] | tuple[str, ...] | bool | None = None, parsed_args: Namespace | SimpleNamespace | dict[str, list[str] | str] | None = None, ) -> dict[str, Any] | CliSettingsSource[T]: if args is not None and parsed_args is not None: raise SettingsError('`args` and `parsed_args` are mutually exclusive') elif args is not None: if args is False: return self._load_env_vars(parsed_args={}) if args is True: args = sys.argv[1:] return self._load_env_vars(parsed_args=self._parse_args(self.root_parser, args)) elif parsed_args is not None: return self._load_env_vars(parsed_args=copy.copy(parsed_args)) else: return super().__call__() @overload def _load_env_vars(self) -> Mapping[str, str | None]: ... @overload def _load_env_vars(self, *, parsed_args: Namespace | SimpleNamespace | dict[str, Any]) -> CliSettingsSource[T]: """ Loads the parsed command line arguments into the CLI environment settings variables. Note: The parsed args must be in `argparse.Namespace`, `SimpleNamespace`, or vars dictionary (e.g., vars(argparse.Namespace)) format. Args: parsed_args: The parsed args to load. Returns: CliSettingsSource: The object instance itself. """ ... def _load_env_vars( self, *, parsed_args: Namespace | SimpleNamespace | dict[str, list[str] | str] | None = None ) -> Mapping[str, str | None] | CliSettingsSource[T]: if parsed_args is None: return {} if isinstance(parsed_args, (Namespace, SimpleNamespace)): parsed_args = vars(parsed_args) selected_subcommands = self._resolve_parsed_args(parsed_args) for arg_dest, arg_map in self._parser_map.items(): if isinstance(arg_dest, str) and arg_dest.endswith(':subcommand'): for subcommand_dest in [arg.dest for arg in arg_map.values()]: if subcommand_dest not in selected_subcommands: parsed_args[subcommand_dest] = self.cli_parse_none_str parsed_args = { key: val for key, val in parsed_args.items() if not key.endswith(':subcommand') and val is not PydanticUndefined } if selected_subcommands: last_selected_subcommand = max(selected_subcommands, key=len) if not any(field_name for field_name in parsed_args.keys() if f'{last_selected_subcommand}.' in field_name): parsed_args[last_selected_subcommand] = '{}' parsed_args.update(self._cli_unknown_args) self.env_vars = parse_env_vars( cast(Mapping[str, str], parsed_args), self.case_sensitive, self.env_ignore_empty, self.cli_parse_none_str, ) return self def _resolve_parsed_args(self, parsed_args: dict[str, list[str] | str]) -> list[str]: selected_subcommands: list[str] = [] for field_name, val in list(parsed_args.items()): if isinstance(val, list): if self._is_nested_alias_path_only_workaround(parsed_args, field_name, val): # Workaround for nested alias path environment variables not being handled. # See https://github.com/pydantic/pydantic-settings/issues/670 continue cli_arg = self._parser_map.get(field_name, {}).get(None) if cli_arg and cli_arg.is_no_decode: parsed_args[field_name] = ','.join(val) continue parsed_args[field_name] = self._merge_parsed_list(val, field_name) elif field_name.endswith(':subcommand') and val is not None: selected_subcommands.append(self._parser_map[field_name][val].dest) elif self.cli_kebab_case == 'all' and isinstance(val, str): snake_val = val.replace('-', '_') cli_arg = self._parser_map.get(field_name, {}).get(None) if ( cli_arg and cli_arg.field_info.annotation and (snake_val in cli_arg.get_enum_names(cli_arg.field_info.annotation, False)) ): if '_' in val: raise ValueError(f'Input should be kebab-case "{val.replace("_", "-")}", not "{val}"') parsed_args[field_name] = snake_val return selected_subcommands def _is_nested_alias_path_only_workaround( self, parsed_args: dict[str, list[str] | str], field_name: str, val: list[str] ) -> bool: """ Workaround for nested alias path environment variables not being handled. See https://github.com/pydantic/pydantic-settings/issues/670 """ known_arg = self._parser_map.get(field_name, {}).values() if not known_arg: return False arg = next(iter(known_arg)) if arg.is_alias_path_only and arg.arg_prefix.endswith('.'): del parsed_args[field_name] nested_dest = arg.arg_prefix[:-1] nested_val = f'"{arg.preferred_alias}": {self._merge_parsed_list(val, field_name)}' parsed_args[nested_dest] = ( f'{{{nested_val}}}' if nested_dest not in parsed_args else f'{parsed_args[nested_dest][:-1]}, {nested_val}}}' ) return True return False def _get_merge_parsed_list_types(self, parsed_list: list[str], field_name: str) -> tuple[type | None, type | None]: merge_type = self._cli_dict_args.get(field_name, list) if ( merge_type is list or not is_union_origin(get_origin(merge_type)) or not any( type_ for type_ in get_args(merge_type) if type_ is not type(None) and get_origin(type_) not in (dict, Mapping) ) ): inferred_type = merge_type else: inferred_type = list if parsed_list and (len(parsed_list) > 1 or parsed_list[0].startswith('[')) else str return merge_type, inferred_type def _merged_list_to_str(self, merged_list: list[str], field_name: str) -> str: decode_list: list[str] = [] is_use_decode: bool | None = None cli_arg_map = self._parser_map.get(field_name, {}) try: list_adapter: Any = TypeAdapter(next(iter(cli_arg_map.values())).field_info.annotation) is_num_type_str = type(list_adapter.validate_python(['1'])[0]) is str except (StopIteration, ValidationError): is_num_type_str = None for index, item in enumerate(merged_list): cli_arg = cli_arg_map.get(index) is_decode = cli_arg is None or not cli_arg.is_no_decode if is_use_decode is None: is_use_decode = is_decode elif is_use_decode != is_decode: raise SettingsError('Mixing Decode and NoDecode across different AliasPath fields is not allowed') if is_use_decode: item = item.replace('\\', '\\\\') try: unquoted_item = item[1:-1] if item.startswith('"') and item.endswith('"') else item float(unquoted_item) item = f'"{unquoted_item}"' if is_num_type_str else unquoted_item except ValueError: pass elif item.startswith('"') and item.endswith('"'): item = item[1:-1] decode_list.append(item) merged_list_str = ','.join(decode_list) return f'[{merged_list_str}]' if is_use_decode else merged_list_str def _merge_parsed_list(self, parsed_list: list[str], field_name: str) -> str: try: merged_list: list[str] = [] is_last_consumed_a_value = False merge_type, inferred_type = self._get_merge_parsed_list_types(parsed_list, field_name) for val in parsed_list: if not isinstance(val, str): # If val is not a string, it's from an external parser and we can ignore parsing the rest of the # list. break val = val.strip() if val.startswith('[') and val.endswith(']'): val = val[1:-1].strip() while val: val = val.strip() if val.startswith(','): val = self._consume_comma(val, merged_list, is_last_consumed_a_value) is_last_consumed_a_value = False else: if val.startswith('{') or val.startswith('['): val = self._consume_object_or_array(val, merged_list) else: try: val = self._consume_string_or_number(val, merged_list, merge_type) except ValueError as e: if merge_type is inferred_type: raise e merge_type = inferred_type val = self._consume_string_or_number(val, merged_list, merge_type) is_last_consumed_a_value = True if not is_last_consumed_a_value: val = self._consume_comma(val, merged_list, is_last_consumed_a_value) if merge_type is str: return merged_list[0] elif merge_type is list: return self._merged_list_to_str(merged_list, field_name) else: merged_dict: dict[str, str] = {} for item in merged_list: merged_dict.update(json.loads(item)) return json.dumps(merged_dict) except Exception as e: raise SettingsError(f'Parsing error encountered for {field_name}: {e}') def _consume_comma(self, item: str, merged_list: list[str], is_last_consumed_a_value: bool) -> str: if not is_last_consumed_a_value: merged_list.append('""') return item[1:] def _consume_object_or_array(self, item: str, merged_list: list[str]) -> str: count = 1 close_delim = '}' if item.startswith('{') else ']' in_str = False for consumed in range(1, len(item)): if item[consumed] == '"' and item[consumed - 1] != '\\': in_str = not in_str elif in_str: continue elif item[consumed] in ('{', '['): count += 1 elif item[consumed] in ('}', ']'): count -= 1 if item[consumed] == close_delim and count == 0: merged_list.append(item[: consumed + 1]) return item[consumed + 1 :] raise SettingsError(f'Missing end delimiter "{close_delim}"') def _consume_string_or_number(self, item: str, merged_list: list[str], merge_type: type[Any] | None) -> str: consumed = 0 if merge_type is not str else len(item) is_find_end_quote = False while consumed < len(item): if item[consumed] == '"' and (consumed == 0 or item[consumed - 1] != '\\'): is_find_end_quote = not is_find_end_quote if not is_find_end_quote and item[consumed] == ',': break consumed += 1 if is_find_end_quote: raise SettingsError('Mismatched quotes') val_string = item[:consumed].strip() if merge_type in (list, str): try: float(val_string) except ValueError: if val_string == self.cli_parse_none_str: val_string = 'null' if val_string not in ('true', 'false', 'null') and not val_string.startswith('"'): val_string = f'"{val_string}"' merged_list.append(val_string) else: key, val = (kv for kv in val_string.split('=', 1)) if key.startswith('"') and not key.endswith('"') and not val.startswith('"') and val.endswith('"'): raise ValueError(f'Dictionary key=val parameter is a quoted string: {val_string}') key, val = key.strip('"'), val.strip('"') merged_list.append(json.dumps({key: val})) return item[consumed:] def _verify_cli_flag_annotations(self, model: type[BaseModel], field_name: str, field_info: FieldInfo) -> None: if _CliImplicitFlag in field_info.metadata: cli_flag_name = 'CliImplicitFlag' elif _CliExplicitFlag in field_info.metadata: cli_flag_name = 'CliExplicitFlag' elif _CliToggleFlag in field_info.metadata: cli_flag_name = 'CliToggleFlag' if not isinstance(field_info.default, bool): raise SettingsError( f'{cli_flag_name} argument {model.__name__}.{field_name} must have a default bool value' ) elif _CliDualFlag in field_info.metadata: cli_flag_name = 'CliDualFlag' else: return if field_info.annotation is not bool: raise SettingsError(f'{cli_flag_name} argument {model.__name__}.{field_name} is not of type bool') def _sort_arg_fields(self, model: type[BaseModel]) -> list[tuple[str, FieldInfo]]: positional_variadic_arg = [] positional_args, subcommand_args, optional_args = [], [], [] for field_name, field_info in _get_model_fields(model).items(): if _CliSubCommand in field_info.metadata: if not field_info.is_required(): raise SettingsError(f'subcommand argument {model.__name__}.{field_name} has a default value') else: alias_names, *_ = _get_alias_names(field_name, field_info) if len(alias_names) > 1: raise SettingsError(f'subcommand argument {model.__name__}.{field_name} has multiple aliases') field_types = [type_ for type_ in get_args(field_info.annotation) if type_ is not type(None)] for field_type in field_types: if not (is_model_class(field_type) or is_pydantic_dataclass(field_type)): raise SettingsError( f'subcommand argument {model.__name__}.{field_name} has type not derived from BaseModel' ) subcommand_args.append((field_name, field_info)) elif _CliPositionalArg in field_info.metadata: alias_names, *_ = _get_alias_names(field_name, field_info) if len(alias_names) > 1: raise SettingsError(f'positional argument {model.__name__}.{field_name} has multiple aliases') is_append_action = _annotation_contains_types( field_info.annotation, (list, set, dict, Sequence, Mapping), is_strip_annotated=True ) if not is_append_action: positional_args.append((field_name, field_info)) else: positional_variadic_arg.append((field_name, field_info)) else: self._verify_cli_flag_annotations(model, field_name, field_info) optional_args.append((field_name, field_info)) if positional_variadic_arg: if len(positional_variadic_arg) > 1: field_names = ', '.join([name for name, info in positional_variadic_arg]) raise SettingsError(f'{model.__name__} has multiple variadic positional arguments: {field_names}') elif subcommand_args: field_names = ', '.join([name for name, info in positional_variadic_arg + subcommand_args]) raise SettingsError( f'{model.__name__} has variadic positional arguments and subcommand arguments: {field_names}' ) return positional_args + positional_variadic_arg + subcommand_args + optional_args @property def root_parser(self) -> T: """The connected root parser instance.""" return self._root_parser def _connect_parser_method( self, parser_method: Callable[..., Any] | None, method_name: str, *args: Any, **kwargs: Any ) -> Callable[..., Any]: if ( parser_method is not None and self.case_sensitive is False and method_name == 'parse_args_method' and isinstance(self._root_parser, _CliInternalArgParser) ): def parse_args_insensitive_method( root_parser: _CliInternalArgParser, args: list[str] | tuple[str, ...] | None = None, namespace: Namespace | None = None, ) -> Any: insensitive_args = [] for arg in shlex.split(shlex.join(args)) if args else []: flag_prefix = rf'\{self.cli_flag_prefix_char}{{1,2}}' matched = re.match(rf'^({flag_prefix}[^\s=]+)(.*)', arg) if matched: arg = matched.group(1).lower() + matched.group(2) insensitive_args.append(arg) return parser_method(root_parser, insensitive_args, namespace) return parse_args_insensitive_method elif parser_method is None: def none_parser_method(*args: Any, **kwargs: Any) -> Any: raise SettingsError( f'cannot connect CLI settings source root parser: {method_name} is set to `None` but is needed for connecting' ) return none_parser_method else: return parser_method def _connect_group_method(self, add_argument_group_method: Callable[..., Any] | None) -> Callable[..., Any]: add_argument_group = self._connect_parser_method(add_argument_group_method, 'add_argument_group_method') def add_group_method(parser: Any, **kwargs: Any) -> Any: if not kwargs.pop('_is_cli_mutually_exclusive_group'): kwargs.pop('required') return add_argument_group(parser, **kwargs) else: main_group_kwargs = {arg: kwargs.pop(arg) for arg in ['title', 'description'] if arg in kwargs} main_group_kwargs['title'] += ' (mutually exclusive)' group = add_argument_group(parser, **main_group_kwargs) if not hasattr(group, 'add_mutually_exclusive_group'): raise SettingsError( 'cannot connect CLI settings source root parser: ' 'group object is missing add_mutually_exclusive_group but is needed for connecting' ) return group.add_mutually_exclusive_group(**kwargs) return add_group_method def _connect_root_parser( self, root_parser: T, parse_args_method: Callable[..., Any] | None, add_argument_method: Callable[..., Any] | None = ArgumentParser.add_argument, add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group, add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser, add_subparsers_method: Callable[..., Any] | None = ArgumentParser.add_subparsers, format_help_method: Callable[..., Any] | None = ArgumentParser.format_help, formatter_class: Any = RawDescriptionHelpFormatter, ) -> None: self._cli_unknown_args: dict[str, list[str]] = {} def _parse_known_args(*args: Any, **kwargs: Any) -> Namespace: args, unknown_args = ArgumentParser.parse_known_args(*args, **kwargs) for dest in self._cli_unknown_args: self._cli_unknown_args[dest] = unknown_args return cast(Namespace, args) self._root_parser = root_parser if parse_args_method is None: parse_args_method = _parse_known_args if self.cli_ignore_unknown_args else ArgumentParser.parse_args self._parse_args = self._connect_parser_method(parse_args_method, 'parse_args_method') self._add_argument = self._connect_parser_method(add_argument_method, 'add_argument_method') self._add_group = self._connect_group_method(add_argument_group_method) self._add_parser = self._connect_parser_method(add_parser_method, 'add_parser_method') self._add_subparsers = self._connect_parser_method(add_subparsers_method, 'add_subparsers_method') self._format_help = self._connect_parser_method(format_help_method, 'format_help_method') self._formatter_class = formatter_class self._cli_dict_args: dict[str, type[Any] | None] = {} self._parser_map: defaultdict[str | FieldInfo, dict[int | None | str | type[BaseModel], _CliArg]] = defaultdict( dict ) self._add_default_help() self._add_parser_args( parser=self.root_parser, model=self.settings_cls, added_args=[], arg_prefix=self.env_prefix, subcommand_prefix=self.env_prefix, group=None, alias_prefixes=[], model_default=PydanticUndefined, ) def _add_default_help(self) -> None: if isinstance(self._root_parser, _CliInternalArgParser): if not self.cli_prefix: for field_name, field_info in _get_model_fields(self.settings_cls).items(): alias_names, *_ = _get_alias_names(field_name, field_info, case_sensitive=self.case_sensitive) if 'help' in alias_names: return self._add_argument( self.root_parser, f'{self._cli_flag_prefix[:1]}h', f'{self._cli_flag_prefix[:2]}help', action='help', default=SUPPRESS, help='show this help message and exit', ) def _add_parser_args( self, parser: Any, model: type[BaseModel], added_args: list[str], arg_prefix: str, subcommand_prefix: str, group: Any, alias_prefixes: list[str], model_default: Any, is_model_suppressed: bool = False, discriminator_vals: dict[str, set[Any]] = {}, is_last_discriminator: bool = True, ) -> ArgumentParser: subparsers: Any = None alias_path_args: dict[str, int | None] = {} # Ignore model default if the default is a model and not a subclass of the current model. model_default = ( None if ( (is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default))) and not issubclass(type(model_default), model) ) else model_default ) for field_name, field_info in self._sort_arg_fields(model): arg = _CliArg( parser=parser, field_info=field_info, parser_map=self._parser_map, model=model, field_name=field_name, arg_prefix=arg_prefix, case_sensitive=self.case_sensitive, hide_none_type=self.cli_hide_none_type, kebab_case=self.cli_kebab_case, enable_decoding=self.config.get('enable_decoding'), env_prefix_len=self.env_prefix_len, ) alias_path_args.update(arg.alias_paths) if arg.subcommand_dest: for sub_model in arg.sub_models: subcommand_alias = arg.subcommand_alias(sub_model) subcommand_arg = self._parser_map[arg.subcommand_dest][subcommand_alias] subcommand_arg.args = [subcommand_alias] subcommand_arg.kwargs['allow_abbrev'] = False subcommand_arg.kwargs['formatter_class'] = self._formatter_class subcommand_arg.kwargs['description'] = ( None if sub_model.__doc__ is None else dedent(sub_model.__doc__) ) subcommand_arg.kwargs['help'] = None if len(arg.sub_models) > 1 else field_info.description if self.cli_use_class_docs_for_groups: subcommand_arg.kwargs['help'] = None if sub_model.__doc__ is None else dedent(sub_model.__doc__) subparsers = ( self._add_subparsers( parser, title='subcommands', dest=f'{arg_prefix}:subcommand', description=field_info.description if len(arg.sub_models) > 1 else None, ) if subparsers is None else subparsers ) if hasattr(subparsers, 'metavar'): subparsers.metavar = ( f'{subparsers.metavar[:-1]},{subcommand_alias}}}' if subparsers.metavar else f'{{{subcommand_alias}}}' ) subcommand_arg.parser = self._add_parser(subparsers, *subcommand_arg.args, **subcommand_arg.kwargs) self._add_parser_args( parser=subcommand_arg.parser, model=sub_model, added_args=[], arg_prefix=f'{arg.dest}.', subcommand_prefix=f'{subcommand_prefix}{arg.preferred_alias}.', group=None, alias_prefixes=[], model_default=PydanticUndefined, ) else: flag_prefix: str = self._cli_flag_prefix arg.kwargs['dest'] = arg.dest arg.kwargs['default'] = CLI_SUPPRESS arg.kwargs['help'] = self._help_format(field_name, field_info, model_default, is_model_suppressed) arg.kwargs['metavar'] = self._metavar_format(field_info.annotation) arg.kwargs['required'] = ( self.cli_enforce_required and field_info.is_required() and model_default is PydanticUndefined ) arg_names = self._get_arg_names( arg, subcommand_prefix, alias_prefixes, added_args, discriminator_vals, is_last_discriminator, ) if not arg_names or (arg.kwargs['dest'] in added_args): continue self._convert_append_action(arg.kwargs, field_info, arg.is_append_action) if _CliPositionalArg in field_info.metadata: arg_names, flag_prefix = self._convert_positional_arg( arg.kwargs, field_info, arg.preferred_alias, model_default ) self._convert_bool_flag(arg.kwargs, field_info, model_default) if arg.is_parser_submodel and not getattr(field_info.annotation, '__pydantic_root_model__', False): self._add_parser_submodels( parser, model, arg.sub_models, added_args, arg_prefix, subcommand_prefix, flag_prefix, arg_names, arg.kwargs, field_name, field_info, arg.alias_names, model_default=model_default, is_model_suppressed=is_model_suppressed, ) elif _CliUnknownArgs in field_info.metadata: self._cli_unknown_args[arg.kwargs['dest']] = [] elif not arg.is_alias_path_only: if isinstance(group, dict): group = self._add_group(parser, **group) context = parser if group is None else group if arg.kwargs.get('action') == 'store_false': flag_prefix += 'no-' arg.args = [f'{flag_prefix[: 1 if len(name) == 1 else None]}{name}' for name in arg_names] self._add_argument(context, *arg.args, **arg.kwargs) added_args += list(arg_names) self._add_parser_alias_paths(parser, alias_path_args, added_args, arg_prefix, subcommand_prefix, group) return parser def _convert_append_action(self, kwargs: dict[str, Any], field_info: FieldInfo, is_append_action: bool) -> None: if is_append_action: kwargs['action'] = 'append' if _annotation_contains_types(field_info.annotation, (dict, Mapping), is_strip_annotated=True): self._cli_dict_args[kwargs['dest']] = field_info.annotation def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, model_default: Any) -> None: if kwargs['metavar'] == 'bool': meta_bool_flags = [ meta for meta in field_info.metadata if issubclass(meta, _CliImplicitFlag | _CliExplicitFlag) ] if not meta_bool_flags and self.cli_implicit_flags: meta_bool_flags = [_CliImplicitFlag] if meta_bool_flags: bool_flag = meta_bool_flags.pop() if bool_flag is _CliImplicitFlag: bool_flag = ( _CliToggleFlag if self.cli_implicit_flags == 'toggle' and isinstance(field_info.default, bool) else _CliDualFlag ) if bool_flag is _CliDualFlag: del kwargs['metavar'] kwargs['action'] = BooleanOptionalAction elif bool_flag is _CliToggleFlag: del kwargs['metavar'] kwargs['action'] = 'store_false' if field_info.default else 'store_true' def _convert_positional_arg( self, kwargs: dict[str, Any], field_info: FieldInfo, preferred_alias: str, model_default: Any ) -> tuple[list[str], str]: flag_prefix = '' arg_names = [kwargs['dest']] kwargs['default'] = PydanticUndefined kwargs['metavar'] = _CliArg.get_kebab_case(preferred_alias.upper(), self.cli_kebab_case) # Note: CLI positional args are always strictly required at the CLI. Therefore, use field_info.is_required in # conjunction with model_default instead of the derived kwargs['required']. is_required = field_info.is_required() and model_default is PydanticUndefined if kwargs.get('action') == 'append': del kwargs['action'] kwargs['nargs'] = '+' if is_required else '*' elif not is_required: kwargs['nargs'] = '?' del kwargs['dest'] del kwargs['required'] return arg_names, flag_prefix def _get_arg_names( self, arg: _CliArg, subcommand_prefix: str, alias_prefixes: list[str], added_args: list[str], discriminator_vals: dict[str, set[Any]], is_last_discriminator: bool, ) -> list[str]: arg_names: list[str] = [] for prefix in [arg.arg_prefix] + alias_prefixes: for name in arg.alias_names: arg_name = _CliArg.get_kebab_case( f'{prefix}{name}' if subcommand_prefix == self.env_prefix else f'{prefix.replace(subcommand_prefix, "", 1)}{name}', self.cli_kebab_case, ) if arg_name not in added_args: arg_names.append(arg_name) if self.cli_shortcuts: for target, aliases in self.cli_shortcuts.items(): if target in arg_names: alias_list = [aliases] if isinstance(aliases, str) else aliases arg_names.extend(alias for alias in alias_list if alias not in added_args) tags: set[Any] = set() discriminators = discriminator_vals.get(arg.dest) if discriminators is not None: _annotation_contains_types( arg.field_info.annotation, (Literal,), is_include_origin=True, collect=tags, ) discriminators.update(chain.from_iterable(get_args(tag) for tag in tags)) if not is_last_discriminator: return [] arg.kwargs['metavar'] = self._metavar_format(Literal[tuple(sorted(discriminators))]) return arg_names def _add_parser_submodels( self, parser: Any, model: type[BaseModel], sub_models: list[type[BaseModel]], added_args: list[str], arg_prefix: str, subcommand_prefix: str, flag_prefix: str, arg_names: list[str], kwargs: dict[str, Any], field_name: str, field_info: FieldInfo, alias_names: tuple[str, ...], model_default: Any, is_model_suppressed: bool, ) -> None: if issubclass(model, CliMutuallyExclusiveGroup): # Argparse has deprecated "calling add_argument_group() or add_mutually_exclusive_group() on a # mutually exclusive group" (https://docs.python.org/3/library/argparse.html#mutual-exclusion). # Since nested models result in a group add, raise an exception for nested models in a mutually # exclusive group. raise SettingsError('cannot have nested models in a CliMutuallyExclusiveGroup') model_group_kwargs: dict[str, Any] = {} model_group_kwargs['title'] = f'{arg_names[0]} options' model_group_kwargs['description'] = field_info.description model_group_kwargs['required'] = kwargs['required'] model_group_kwargs['_is_cli_mutually_exclusive_group'] = any( issubclass(model, CliMutuallyExclusiveGroup) for model in sub_models ) if model_group_kwargs['_is_cli_mutually_exclusive_group'] and len(sub_models) > 1: raise SettingsError('cannot use union with CliMutuallyExclusiveGroup') if self.cli_use_class_docs_for_groups and len(sub_models) == 1: model_group_kwargs['description'] = None if sub_models[0].__doc__ is None else dedent(sub_models[0].__doc__) if model_default is not PydanticUndefined: if is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default)): model_default = getattr(model_default, field_name) else: if field_info.default is not PydanticUndefined: model_default = field_info.default elif field_info.default_factory is not None: model_default = field_info.default_factory if model_default is None: desc_header = f'default: {self.cli_parse_none_str} (undefined)' if model_group_kwargs['description'] is not None: model_group_kwargs['description'] = dedent(f'{desc_header}\n{model_group_kwargs["description"]}') else: model_group_kwargs['description'] = desc_header preferred_alias = alias_names[0] is_model_suppressed = self._is_field_suppressed(field_info) or is_model_suppressed if is_model_suppressed: model_group_kwargs['description'] = CLI_SUPPRESS added_args.append(arg_names[0]) kwargs['required'] = False kwargs['nargs'] = '?' kwargs['const'] = '{}' kwargs['help'] = ( CLI_SUPPRESS if is_model_suppressed or self.cli_avoid_json else f'set {arg_names[0]} from JSON string (default: {{}})' ) model_group = self._add_group(parser, **model_group_kwargs) self._add_argument(model_group, *(f'{flag_prefix}{name}' for name in arg_names), **kwargs) discriminator_vals: dict[str, set[Any]] = ( {f'{arg_prefix}{preferred_alias}.{field_info.discriminator}': set()} if field_info.discriminator else {} ) for model in sub_models: self._add_parser_args( parser=parser, model=model, added_args=added_args, arg_prefix=f'{arg_prefix}{preferred_alias}.', subcommand_prefix=subcommand_prefix, group=model_group, alias_prefixes=[f'{arg_prefix}{name}.' for name in alias_names[1:]], model_default=model_default, is_model_suppressed=is_model_suppressed, discriminator_vals=discriminator_vals, is_last_discriminator=model is sub_models[-1], ) def _add_parser_alias_paths( self, parser: Any, alias_path_args: dict[str, int | None], added_args: list[str], arg_prefix: str, subcommand_prefix: str, group: Any, ) -> None: if alias_path_args: context = parser if group is not None: context = self._add_group(parser, **group) if isinstance(group, dict) else group for name, index in alias_path_args.items(): arg_name = ( f'{arg_prefix}{name}' if subcommand_prefix == self.env_prefix else f'{arg_prefix.replace(subcommand_prefix, "", 1)}{name}' ) kwargs: dict[str, Any] = {} kwargs['default'] = CLI_SUPPRESS kwargs['help'] = 'pydantic alias path' kwargs['action'] = 'append' kwargs['metavar'] = 'list' if index is None: kwargs['metavar'] = 'dict' self._cli_dict_args[arg_name] = dict args = [f'{self._cli_flag_prefix}{arg_name}'] for key, arg in self._parser_map[arg_name].items(): arg.args, arg.kwargs = args, kwargs self._add_argument(context, *args, **kwargs) added_args.append(arg_name) def _get_modified_args(self, obj: Any) -> tuple[str, ...]: if not self.cli_hide_none_type: return get_args(obj) else: return tuple([type_ for type_ in get_args(obj) if type_ is not type(None)]) def _metavar_format_choices(self, args: list[str], obj_qualname: str | None = None) -> str: if 'JSON' in args: args = args[: args.index('JSON') + 1] + [arg for arg in args[args.index('JSON') + 1 :] if arg != 'JSON'] metavar = ','.join(args) if obj_qualname: return f'{obj_qualname}[{metavar}]' else: return metavar if len(args) == 1 else f'{{{metavar}}}' def _metavar_format_recurse(self, obj: Any) -> str: """Pretty metavar representation of a type. Adapts logic from `pydantic._repr.display_as_type`.""" obj = _strip_annotated(obj) if _is_function(obj): # If function is locally defined use __name__ instead of __qualname__ return obj.__name__ if '' in obj.__qualname__ else obj.__qualname__ elif obj is ...: return '...' elif isinstance(obj, Representation): return repr(obj) elif isinstance(obj, typing.ForwardRef) or typing_objects.is_typealiastype(obj): return str(obj) if not isinstance(obj, (_typing_base, _WithArgsTypes, type)): obj = obj.__class__ origin = get_origin(obj) if is_union_origin(origin): return self._metavar_format_choices(list(map(self._metavar_format_recurse, self._get_modified_args(obj)))) elif typing_objects.is_literal(origin): return self._metavar_format_choices(list(map(str, self._get_modified_args(obj)))) elif _lenient_issubclass(obj, Enum): return self._metavar_format_choices( [_CliArg.get_kebab_case(name, self.cli_kebab_case == 'all') for name in obj.__members__.keys()] ) elif isinstance(obj, _WithArgsTypes): return self._metavar_format_choices( list(map(self._metavar_format_recurse, self._get_modified_args(obj))), obj_qualname=obj.__qualname__ if hasattr(obj, '__qualname__') else str(obj), ) elif obj is type(None): return self.cli_parse_none_str elif is_model_class(obj) or is_pydantic_dataclass(obj): return ( self._metavar_format_recurse(_get_model_fields(obj)['root'].annotation) if getattr(obj, '__pydantic_root_model__', False) else 'JSON' ) elif isinstance(obj, type): return obj.__qualname__ else: return repr(obj).replace('typing.', '').replace('typing_extensions.', '') def _metavar_format(self, obj: Any) -> str: return self._metavar_format_recurse(obj).replace(', ', ',') def _help_format( self, field_name: str, field_info: FieldInfo, model_default: Any, is_model_suppressed: bool ) -> str: _help = field_info.description if field_info.description else '' if is_model_suppressed or self._is_field_suppressed(field_info): return CLI_SUPPRESS if field_info.is_required() and model_default in (PydanticUndefined, None): if _CliPositionalArg not in field_info.metadata: ifdef = 'ifdef: ' if model_default is None else '' _help += f' ({ifdef}required)' if _help else f'({ifdef}required)' else: default = f'(default: {self.cli_parse_none_str})' if is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default)): default = f'(default: {getattr(model_default, field_name)})' elif model_default not in (PydanticUndefined, None) and _is_function(model_default): default = f'(default factory: {self._metavar_format(model_default)})' elif field_info.default not in (PydanticUndefined, None): enum_name = _annotation_enum_val_to_name(field_info.annotation, field_info.default) default = f'(default: {field_info.default if enum_name is None else enum_name})' elif field_info.default_factory is not None: default = f'(default factory: {self._metavar_format(field_info.default_factory)})' if _CliToggleFlag not in field_info.metadata: _help += f' {default}' if _help else default return _help.replace('%', '%%') if issubclass(type(self._root_parser), ArgumentParser) else _help def _is_field_suppressed(self, field_info: FieldInfo) -> bool: _help = field_info.description if field_info.description else '' return _help == CLI_SUPPRESS or CLI_SUPPRESS in field_info.metadata def _update_alias_path_only_default( self, arg_name: str, value: Any, field_info: FieldInfo, alias_path_only_defaults: dict[str, Any] ) -> list[Any] | dict[str, Any]: alias_path: AliasPath = [ alias if isinstance(alias, AliasPath) else cast(AliasPath, alias.choices[0]) for alias in (field_info.alias, field_info.validation_alias) if isinstance(alias, (AliasPath, AliasChoices)) ][0] alias_nested_paths: list[str] = alias_path.path[1:-1] # type: ignore if not alias_nested_paths: alias_path_only_defaults.setdefault(arg_name, []) alias_default = alias_path_only_defaults[arg_name] else: alias_path_only_defaults.setdefault(arg_name, {}) current_path = alias_path_only_defaults[arg_name] for nested_path in alias_nested_paths[:-1]: current_path.setdefault(nested_path, {}) current_path = current_path[nested_path] current_path.setdefault(alias_nested_paths[-1], []) alias_default = current_path[alias_nested_paths[-1]] alias_path_index = cast(int, alias_path.path[-1]) alias_default.extend([''] * max(alias_path_index + 1 - len(alias_default), 0)) alias_default[alias_path_index] = value return alias_path_only_defaults[arg_name] def _coerce_value_styles( self, model_default: Any, value: str | list[Any] | dict[str, Any], list_style: Literal['json', 'argparse', 'lazy'] = 'json', dict_style: Literal['json', 'env'] = 'json', ) -> list[str | list[Any] | dict[str, Any]]: values = [value] if isinstance(value, str): if isinstance(model_default, list): if list_style == 'lazy': values = [','.join(f'{v}' for v in json.loads(value))] elif list_style == 'argparse': values = [f'{v}' for v in json.loads(value)] elif isinstance(model_default, dict): if dict_style == 'env': values = [f'{k}={v}' for k, v in json.loads(value).items()] return values @staticmethod def _flatten_serialized_args( serialized_args: dict[str, list[str]], positionals_first: bool, ) -> list[str]: return ( serialized_args['optional'] + serialized_args['positional'] if not positionals_first else serialized_args['positional'] + serialized_args['optional'] ) + serialized_args['subcommand'] def _serialized_args( self, model: PydanticModel, list_style: Literal['json', 'argparse', 'lazy'] = 'json', dict_style: Literal['json', 'env'] = 'json', positionals_first: bool = False, _is_submodel: bool = False, ) -> dict[str, list[str]]: alias_path_only_defaults: dict[str, Any] = {} optional_args: list[str | list[Any] | dict[str, Any]] = [] positional_args: list[str | list[Any] | dict[str, Any]] = [] subcommand_args: list[str] = [] for field_name, field_info in _get_model_fields(type(model) if _is_submodel else self.settings_cls).items(): model_default = getattr(model, field_name) if field_info.default == model_default: continue if _CliSubCommand in field_info.metadata and model_default is None: continue arg = next(iter(self._parser_map[field_info].values())) if arg.subcommand_dest: subcommand_args.append(arg.subcommand_alias(type(model_default))) sub_args = self._serialized_args( model_default, list_style=list_style, dict_style=dict_style, positionals_first=positionals_first, _is_submodel=True, ) subcommand_args += self._flatten_serialized_args(sub_args, positionals_first) continue if is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default)): sub_args = self._serialized_args( model_default, list_style=list_style, dict_style=dict_style, positionals_first=positionals_first, _is_submodel=True, ) optional_args += sub_args['optional'] positional_args += sub_args['positional'] subcommand_args += sub_args['subcommand'] continue matched = re.match(r'(-*)(.+)', arg.preferred_arg_name) flag_chars, arg_name = matched.groups() if matched else ('', '') value: str | list[Any] | dict[str, Any] = ( json.dumps(model_default) if isinstance(model_default, (dict, list, set)) else str(model_default) ) if arg.is_alias_path_only: # For alias path only, we wont know the complete value until we've finished parsing the entire class. In # this case, insert value as a non-string reference pointing to the relevant alias_path_only_defaults # entry and convert into completed string value later. value = self._update_alias_path_only_default(arg_name, value, field_info, alias_path_only_defaults) if _CliPositionalArg in field_info.metadata: for value in model_default if isinstance(model_default, list) else [model_default]: value = json.dumps(value) if isinstance(value, (dict, list, set)) else str(value) positional_args.append(value) continue # Note: prepend 'no-' for boolean optional action flag if model_default value is False and flag is not a short option if arg.kwargs.get('action') == BooleanOptionalAction and model_default is False and flag_chars == '--': flag_chars += 'no-' for value in self._coerce_value_styles(model_default, value, list_style=list_style, dict_style=dict_style): optional_args.append(f'{flag_chars}{arg_name}') # If implicit bool flag, do not add a value if arg.kwargs.get('action') not in (BooleanOptionalAction, 'store_true', 'store_false'): optional_args.append(value) return { 'optional': [json.dumps(value) if not isinstance(value, str) else value for value in optional_args], 'positional': [json.dumps(value) if not isinstance(value, str) else value for value in positional_args], 'subcommand': subcommand_args, } pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/dotenv.py000066400000000000000000000136561514433345000304600ustar00rootroot00000000000000"""Dotenv file settings source.""" from __future__ import annotations as _annotations import os import warnings from collections.abc import Mapping from pathlib import Path from typing import TYPE_CHECKING, Any from dotenv import dotenv_values from pydantic._internal._typing_extra import ( # type: ignore[attr-defined] get_origin, ) from typing_inspection.introspection import is_union_origin from ..types import ENV_FILE_SENTINEL, DotenvType, EnvPrefixTarget from ..utils import ( _annotation_is_complex, _union_is_complex, parse_env_vars, ) from .env import EnvSettingsSource if TYPE_CHECKING: from pydantic_settings.main import BaseSettings class DotEnvSettingsSource(EnvSettingsSource): """ Source class for loading settings values from env files. """ def __init__( self, settings_cls: type[BaseSettings], env_file: DotenvType | None = ENV_FILE_SENTINEL, env_file_encoding: str | None = None, case_sensitive: bool | None = None, env_prefix: str | None = None, env_prefix_target: EnvPrefixTarget | None = None, env_nested_delimiter: str | None = None, env_nested_max_split: int | None = None, env_ignore_empty: bool | None = None, env_parse_none_str: str | None = None, env_parse_enums: bool | None = None, ) -> None: self.env_file = env_file if env_file != ENV_FILE_SENTINEL else settings_cls.model_config.get('env_file') self.env_file_encoding = ( env_file_encoding if env_file_encoding is not None else settings_cls.model_config.get('env_file_encoding') ) super().__init__( settings_cls, case_sensitive, env_prefix, env_prefix_target, env_nested_delimiter, env_nested_max_split, env_ignore_empty, env_parse_none_str, env_parse_enums, ) def _load_env_vars(self) -> Mapping[str, str | None]: return self._read_env_files() @staticmethod def _static_read_env_file( file_path: Path, *, encoding: str | None = None, case_sensitive: bool = False, ignore_empty: bool = False, parse_none_str: str | None = None, ) -> Mapping[str, str | None]: file_vars: dict[str, str | None] = dotenv_values(file_path, encoding=encoding or 'utf8') return parse_env_vars(file_vars, case_sensitive, ignore_empty, parse_none_str) def _read_env_file( self, file_path: Path, ) -> Mapping[str, str | None]: return self._static_read_env_file( file_path, encoding=self.env_file_encoding, case_sensitive=self.case_sensitive, ignore_empty=self.env_ignore_empty, parse_none_str=self.env_parse_none_str, ) def _read_env_files(self) -> Mapping[str, str | None]: env_files = self.env_file if env_files is None: return {} if isinstance(env_files, (str, os.PathLike)): env_files = [env_files] dotenv_vars: dict[str, str | None] = {} for env_file in env_files: env_path = Path(env_file).expanduser() if env_path.is_file(): dotenv_vars.update(self._read_env_file(env_path)) return dotenv_vars def __call__(self) -> dict[str, Any]: data: dict[str, Any] = super().__call__() is_extra_allowed = self.config.get('extra') != 'forbid' # As `extra` config is allowed in dotenv settings source, We have to # update data with extra env variables from dotenv file. for env_name, env_value in self.env_vars.items(): if not env_value or env_name in data or (self.env_prefix and env_name in self.settings_cls.model_fields): continue env_used = False for field_name, field in self.settings_cls.model_fields.items(): for _, field_env_name, _ in self._extract_field_info(field, field_name): if env_name == field_env_name or ( ( _annotation_is_complex(field.annotation, field.metadata) or ( is_union_origin(get_origin(field.annotation)) and _union_is_complex(field.annotation, field.metadata) ) ) and env_name.startswith(field_env_name) ): env_used = True break if env_used: break if not env_used: if is_extra_allowed and env_name.startswith(self.env_prefix): # env_prefix should be respected and removed from the env_name normalized_env_name = env_name[len(self.env_prefix) :] data[normalized_env_name] = env_value else: data[env_name] = env_value return data def __repr__(self) -> str: return ( f'{self.__class__.__name__}(env_file={self.env_file!r}, env_file_encoding={self.env_file_encoding!r}, ' f'env_nested_delimiter={self.env_nested_delimiter!r}, env_prefix_len={self.env_prefix_len!r})' ) def read_env_file( file_path: Path, *, encoding: str | None = None, case_sensitive: bool = False, ignore_empty: bool = False, parse_none_str: str | None = None, ) -> Mapping[str, str | None]: warnings.warn( 'read_env_file will be removed in the next version, use DotEnvSettingsSource._static_read_env_file if you must', DeprecationWarning, ) return DotEnvSettingsSource._static_read_env_file( file_path, encoding=encoding, case_sensitive=case_sensitive, ignore_empty=ignore_empty, parse_none_str=parse_none_str, ) __all__ = ['DotEnvSettingsSource', 'read_env_file'] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/env.py000066400000000000000000000274551514433345000277530ustar00rootroot00000000000000from __future__ import annotations as _annotations import os from collections.abc import Mapping from typing import ( TYPE_CHECKING, Any, get_args, get_origin, ) from pydantic import Json, TypeAdapter, ValidationError from pydantic._internal._utils import deep_update, is_model_class from pydantic.dataclasses import is_pydantic_dataclass from pydantic.fields import FieldInfo from typing_inspection.introspection import is_union_origin from ...utils import _lenient_issubclass from ..base import PydanticBaseEnvSettingsSource from ..types import EnvNoneType, EnvPrefixTarget from ..utils import ( _annotation_contains_types, _annotation_enum_name_to_val, _get_model_fields, _union_is_complex, parse_env_vars, ) if TYPE_CHECKING: from pydantic_settings.main import BaseSettings class EnvSettingsSource(PydanticBaseEnvSettingsSource): """ Source class for loading settings values from environment variables. """ def __init__( self, settings_cls: type[BaseSettings], case_sensitive: bool | None = None, env_prefix: str | None = None, env_prefix_target: EnvPrefixTarget | None = None, env_nested_delimiter: str | None = None, env_nested_max_split: int | None = None, env_ignore_empty: bool | None = None, env_parse_none_str: str | None = None, env_parse_enums: bool | None = None, ) -> None: super().__init__( settings_cls, case_sensitive, env_prefix, env_prefix_target, env_ignore_empty, env_parse_none_str, env_parse_enums, ) self.env_nested_delimiter = ( env_nested_delimiter if env_nested_delimiter is not None else self.config.get('env_nested_delimiter') ) self.env_nested_max_split = ( env_nested_max_split if env_nested_max_split is not None else self.config.get('env_nested_max_split') ) self.maxsplit = (self.env_nested_max_split or 0) - 1 self.env_prefix_len = len(self.env_prefix) self.env_vars = self._load_env_vars() def _load_env_vars(self) -> Mapping[str, str | None]: return parse_env_vars(os.environ, self.case_sensitive, self.env_ignore_empty, self.env_parse_none_str) def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: """ Gets the value for field from environment variables and a flag to determine whether value is complex. Args: field: The field. field_name: The field name. Returns: A tuple that contains the value (`None` if not found), key, and a flag to determine whether value is complex. """ env_val: str | None = None for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name): env_val = self.env_vars.get(env_name) if env_val is not None: break return env_val, field_key, value_is_complex def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: """ Prepare value for the field. * Extract value for nested field. * Deserialize value to python object for complex field. Args: field: The field. field_name: The field name. Returns: A tuple contains prepared value for the field. Raises: ValuesError: When There is an error in deserializing value for complex field. """ is_complex, allow_parse_failure = self._field_is_complex(field) if self.env_parse_enums: enum_val = _annotation_enum_name_to_val(field.annotation, value) value = value if enum_val is None else enum_val if is_complex or value_is_complex: if isinstance(value, EnvNoneType): return value elif value is None: # field is complex but no value found so far, try explode_env_vars env_val_built = self.explode_env_vars(field_name, field, self.env_vars) if env_val_built: return env_val_built else: # field is complex and there's a value, decode that as JSON, then add explode_env_vars try: value = self.decode_complex_value(field_name, field, value) except ValueError as e: if not allow_parse_failure: raise e if isinstance(value, dict): return deep_update(value, self.explode_env_vars(field_name, field, self.env_vars)) else: return value elif value is not None: # simplest case, field is not complex, we only need to add the value if it was found return self._coerce_env_val_strict(field, value) def _field_is_complex(self, field: FieldInfo) -> tuple[bool, bool]: """ Find out if a field is complex, and if so whether JSON errors should be ignored """ if self.field_is_complex(field): allow_parse_failure = False elif is_union_origin(get_origin(field.annotation)) and _union_is_complex(field.annotation, field.metadata): allow_parse_failure = True else: return False, False return True, allow_parse_failure # Default value of `case_sensitive` is `None`, because we don't want to break existing behavior. # We have to change the method to a non-static method and use # `self.case_sensitive` instead in V3. def next_field( self, field: FieldInfo | Any | None, key: str, case_sensitive: bool | None = None ) -> FieldInfo | None: """ Find the field in a sub model by key(env name) By having the following models: ```py class SubSubModel(BaseSettings): dvals: Dict class SubModel(BaseSettings): vals: list[str] sub_sub_model: SubSubModel class Cfg(BaseSettings): sub_model: SubModel ``` Then: next_field(sub_model, 'vals') Returns the `vals` field of `SubModel` class next_field(sub_model, 'sub_sub_model') Returns `sub_sub_model` field of `SubModel` class Args: field: The field. key: The key (env name). case_sensitive: Whether to search for key case sensitively. Returns: Field if it finds the next field otherwise `None`. """ if not field: return None annotation = field.annotation if isinstance(field, FieldInfo) else field for type_ in get_args(annotation): type_has_key = self.next_field(type_, key, case_sensitive) if type_has_key: return type_has_key if _lenient_issubclass(get_origin(annotation), dict): # get value type if it's a dict return get_args(annotation)[-1] elif is_model_class(annotation) or is_pydantic_dataclass(annotation): # type: ignore[arg-type] fields = _get_model_fields(annotation) # `case_sensitive is None` is here to be compatible with the old behavior. # Has to be removed in V3. for field_name, f in fields.items(): for _, env_name, _ in self._extract_field_info(f, field_name): if case_sensitive is None or case_sensitive: if field_name == key or env_name == key: return f elif field_name.lower() == key.lower() or env_name.lower() == key.lower(): return f return None def explode_env_vars(self, field_name: str, field: FieldInfo, env_vars: Mapping[str, str | None]) -> dict[str, Any]: """ Process env_vars and extract the values of keys containing env_nested_delimiter into nested dictionaries. This is applied to a single field, hence filtering by env_var prefix. Args: field_name: The field name. field: The field. env_vars: Environment variables. Returns: A dictionary contains extracted values from nested env values. """ if not self.env_nested_delimiter: return {} ann = field.annotation is_dict = ann is dict or _lenient_issubclass(get_origin(ann), dict) prefixes = [ f'{env_name}{self.env_nested_delimiter}' for _, env_name, _ in self._extract_field_info(field, field_name) ] result: dict[str, Any] = {} for env_name, env_val in env_vars.items(): try: prefix = next(prefix for prefix in prefixes if env_name.startswith(prefix)) except StopIteration: continue # we remove the prefix before splitting in case the prefix has characters in common with the delimiter env_name_without_prefix = env_name[len(prefix) :] *keys, last_key = env_name_without_prefix.split(self.env_nested_delimiter, self.maxsplit) env_var = result target_field: FieldInfo | None = field for key in keys: target_field = self.next_field(target_field, key, self.case_sensitive) if isinstance(env_var, dict): env_var = env_var.setdefault(key, {}) # get proper field with last_key target_field = self.next_field(target_field, last_key, self.case_sensitive) # check if env_val maps to a complex field and if so, parse the env_val if (target_field or is_dict) and env_val: if target_field: is_complex, allow_json_failure = self._field_is_complex(target_field) if self.env_parse_enums: enum_val = _annotation_enum_name_to_val(target_field.annotation, env_val) env_val = env_val if enum_val is None else enum_val else: # nested field type is dict is_complex, allow_json_failure = True, True if is_complex: try: env_val = self.decode_complex_value(last_key, target_field, env_val) # type: ignore except ValueError as e: if not allow_json_failure: raise e if isinstance(env_var, dict): if last_key not in env_var or not isinstance(env_val, EnvNoneType) or env_var[last_key] == {}: env_var[last_key] = self._coerce_env_val_strict(target_field, env_val) return result def _coerce_env_val_strict(self, field: FieldInfo | None, value: Any) -> Any: """ Coerce environment string values based on field annotation if model config is `strict=True`. Args: field: The field. value: The value to coerce. Returns: The coerced value if successful, otherwise the original value. """ try: if self.config.get('strict') and isinstance(value, str) and field is not None: if value == self.env_parse_none_str: return value if not _annotation_contains_types(field.annotation, (Json,), is_instance=True): return TypeAdapter(field.annotation).validate_python(value) except ValidationError: # Allow validation error to be raised at time of instatiation pass return value def __repr__(self) -> str: return ( f'{self.__class__.__name__}(env_nested_delimiter={self.env_nested_delimiter!r}, ' f'env_prefix_len={self.env_prefix_len!r})' ) __all__ = ['EnvSettingsSource'] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/gcp.py000066400000000000000000000233521514433345000277240ustar00rootroot00000000000000from __future__ import annotations as _annotations import warnings from collections.abc import Iterator, Mapping from functools import cached_property from typing import TYPE_CHECKING, Any from pydantic.fields import FieldInfo from ..types import SecretVersion from .env import EnvSettingsSource if TYPE_CHECKING: from google.auth import default as google_auth_default from google.auth.credentials import Credentials from google.cloud.secretmanager import SecretManagerServiceClient from pydantic_settings.main import BaseSettings else: Credentials = None SecretManagerServiceClient = None google_auth_default = None def import_gcp_secret_manager() -> None: global Credentials global SecretManagerServiceClient global google_auth_default try: from google.auth import default as google_auth_default from google.auth.credentials import Credentials with warnings.catch_warnings(): warnings.filterwarnings('ignore', category=FutureWarning) from google.cloud.secretmanager import SecretManagerServiceClient except ImportError as e: # pragma: no cover raise ImportError( 'GCP Secret Manager dependencies are not installed, run `pip install pydantic-settings[gcp-secret-manager]`' ) from e class GoogleSecretManagerMapping(Mapping[str, str | None]): _loaded_secrets: dict[str, str | None] _secret_client: SecretManagerServiceClient def __init__(self, secret_client: SecretManagerServiceClient, project_id: str, case_sensitive: bool) -> None: self._loaded_secrets = {} self._secret_client = secret_client self._project_id = project_id self._case_sensitive = case_sensitive @property def _gcp_project_path(self) -> str: return self._secret_client.common_project_path(self._project_id) def _select_case_insensitive_secret(self, lower_name: str, candidates: list[str]) -> str: if len(candidates) == 1: return candidates[0] # Sort to ensure deterministic selection (prefer lowercase / ASCII last) candidates.sort() winner = candidates[-1] warnings.warn( f"Secret collision: Found multiple secrets {candidates} normalizing to '{lower_name}'. " f"Using '{winner}' for case-insensitive lookup.", UserWarning, stacklevel=2, ) return winner @cached_property def _secret_name_map(self) -> dict[str, str]: mapping: dict[str, str] = {} # Group secrets by normalized name to detect collisions normalized_groups: dict[str, list[str]] = {} secrets = self._secret_client.list_secrets(parent=self._gcp_project_path) for secret in secrets: name = self._secret_client.parse_secret_path(secret.name).get('secret', '') mapping[name] = name if not self._case_sensitive: lower_name = name.lower() if lower_name not in normalized_groups: normalized_groups[lower_name] = [] normalized_groups[lower_name].append(name) if not self._case_sensitive: for lower_name, candidates in normalized_groups.items(): mapping[lower_name] = self._select_case_insensitive_secret(lower_name, candidates) return mapping @property def _secret_names(self) -> list[str]: return list(self._secret_name_map.keys()) def _secret_version_path(self, key: str, version: str = 'latest') -> str: return self._secret_client.secret_version_path(self._project_id, key, version) def _get_secret_value(self, gcp_secret_name: str, version: str = 'latest') -> str | None: try: return self._secret_client.access_secret_version( name=self._secret_version_path(gcp_secret_name, version) ).payload.data.decode('UTF-8') except Exception: return None def __getitem__(self, key: str) -> str | None: if key in self._loaded_secrets: return self._loaded_secrets[key] gcp_secret_name = self._secret_name_map.get(key) if gcp_secret_name is None and not self._case_sensitive: gcp_secret_name = self._secret_name_map.get(key.lower()) if gcp_secret_name: self._loaded_secrets[key] = self._get_secret_value(gcp_secret_name) else: raise KeyError(key) return self._loaded_secrets[key] def __len__(self) -> int: return len(self._secret_names) def __iter__(self) -> Iterator[str]: return iter(self._secret_names) class GoogleSecretManagerSettingsSource(EnvSettingsSource): _credentials: Credentials _secret_client: SecretManagerServiceClient _project_id: str def __init__( self, settings_cls: type[BaseSettings], credentials: Credentials | None = None, project_id: str | None = None, env_prefix: str | None = None, env_parse_none_str: str | None = None, env_parse_enums: bool | None = None, secret_client: SecretManagerServiceClient | None = None, case_sensitive: bool | None = True, ) -> None: # Import Google Packages if they haven't already been imported if SecretManagerServiceClient is None or Credentials is None or google_auth_default is None: import_gcp_secret_manager() # If credentials or project_id are not passed, then # try to get them from the default function if not credentials or not project_id: _creds, _project_id = google_auth_default() # Set the credentials and/or project id if they weren't specified if credentials is None: credentials = _creds if project_id is None: if isinstance(_project_id, str): project_id = _project_id else: raise AttributeError( 'project_id is required to be specified either as an argument or from the google.auth.default. See https://google-auth.readthedocs.io/en/master/reference/google.auth.html#google.auth.default' ) self._credentials: Credentials = credentials self._project_id: str = project_id if secret_client: self._secret_client = secret_client else: self._secret_client = SecretManagerServiceClient(credentials=self._credentials) super().__init__( settings_cls, case_sensitive=case_sensitive, env_prefix=env_prefix, env_ignore_empty=False, env_parse_none_str=env_parse_none_str, env_parse_enums=env_parse_enums, ) def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: """Override get_field_value to get the secret value from GCP Secret Manager. Look for a SecretVersion metadata field to specify a particular SecretVersion. Args: field: The field to get the value for field_name: The declared name of the field Returns: A tuple of (value, key, value_is_complex), where `key` is the identifier used to populate the model (either the field name or an alias, depending on configuration). """ secret_version = next((m.version for m in field.metadata if isinstance(m, SecretVersion)), None) # If a secret version is specified, try to get that specific version of the secret from # GCP Secret Manager via the GoogleSecretManagerMapping. This allows different versions # of the same secret name to be retrieved independently and cached in the GoogleSecretManagerMapping if secret_version and isinstance(self.env_vars, GoogleSecretManagerMapping): for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name): gcp_secret_name = self.env_vars._secret_name_map.get(env_name) if gcp_secret_name is None and not self.case_sensitive: gcp_secret_name = self.env_vars._secret_name_map.get(env_name.lower()) if gcp_secret_name: env_val = self.env_vars._get_secret_value(gcp_secret_name, secret_version) if env_val is not None: # If populate_by_name is enabled, return field_name to allow multiple fields # with the same alias but different versions to be distinguished if self.settings_cls.model_config.get('populate_by_name'): return env_val, field_name, value_is_complex return env_val, field_key, value_is_complex # If a secret version is specified but not found, we should not fall back to "latest" (default behavior) # as that would be incorrect. We return None to indicate the value was not found. return None, field_name, False val, key, is_complex = super().get_field_value(field, field_name) # If populate_by_name is enabled, we need to return the field_name as the key # without this being enabled, you cannot load two secrets with the same name but different versions if self.settings_cls.model_config.get('populate_by_name') and val is not None: return val, field_name, is_complex return val, key, is_complex def _load_env_vars(self) -> Mapping[str, str | None]: return GoogleSecretManagerMapping( self._secret_client, project_id=self._project_id, case_sensitive=self.case_sensitive ) def __repr__(self) -> str: return f'{self.__class__.__name__}(project_id={self._project_id!r}, env_nested_delimiter={self.env_nested_delimiter!r})' __all__ = ['GoogleSecretManagerSettingsSource', 'GoogleSecretManagerMapping'] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/json.py000066400000000000000000000027241514433345000301240ustar00rootroot00000000000000"""JSON file settings source.""" from __future__ import annotations as _annotations import json from pathlib import Path from typing import ( TYPE_CHECKING, Any, ) from ..base import ConfigFileSourceMixin, InitSettingsSource from ..types import DEFAULT_PATH, PathType if TYPE_CHECKING: from pydantic_settings.main import BaseSettings class JsonConfigSettingsSource(InitSettingsSource, ConfigFileSourceMixin): """ A source class that loads variables from a JSON file """ def __init__( self, settings_cls: type[BaseSettings], json_file: PathType | None = DEFAULT_PATH, json_file_encoding: str | None = None, deep_merge: bool = False, ): self.json_file_path = json_file if json_file != DEFAULT_PATH else settings_cls.model_config.get('json_file') self.json_file_encoding = ( json_file_encoding if json_file_encoding is not None else settings_cls.model_config.get('json_file_encoding') ) self.json_data = self._read_files(self.json_file_path, deep_merge=deep_merge) super().__init__(settings_cls, self.json_data) def _read_file(self, file_path: Path) -> dict[str, Any]: with file_path.open(encoding=self.json_file_encoding) as json_file: return json.load(json_file) def __repr__(self) -> str: return f'{self.__class__.__name__}(json_file={self.json_file_path})' __all__ = ['JsonConfigSettingsSource'] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/nested_secrets.py000066400000000000000000000147361514433345000321730ustar00rootroot00000000000000import os import warnings from functools import reduce from glob import iglob from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, Optional from ...exceptions import SettingsError from ...utils import path_type_label from ..base import PydanticBaseSettingsSource from ..utils import parse_env_vars from .env import EnvSettingsSource from .secrets import SecretsSettingsSource if TYPE_CHECKING: from ...main import BaseSettings from ...sources import PathType SECRETS_DIR_MAX_SIZE = 16 * 2**20 # 16 MiB seems to be a reasonable default class NestedSecretsSettingsSource(EnvSettingsSource): def __init__( self, file_secret_settings: PydanticBaseSettingsSource | SecretsSettingsSource, secrets_dir: Optional['PathType'] = None, secrets_dir_missing: Literal['ok', 'warn', 'error'] | None = None, secrets_dir_max_size: int | None = None, secrets_case_sensitive: bool | None = None, secrets_prefix: str | None = None, secrets_nested_delimiter: str | None = None, secrets_nested_subdir: bool | None = None, # args for compatibility with SecretsSettingsSource, don't use directly case_sensitive: bool | None = None, env_prefix: str | None = None, ) -> None: # We allow the first argument to be settings_cls like original # SecretsSettingsSource. However, it is recommended to pass # SecretsSettingsSource instance instead (as it is shown in usage examples), # otherwise `_secrets_dir` arg passed to Settings() constructor will be ignored. settings_cls: type[BaseSettings] = getattr( file_secret_settings, 'settings_cls', file_secret_settings, # type: ignore[arg-type] ) # config options conf = settings_cls.model_config self.secrets_dir: PathType | None = first_not_none( getattr(file_secret_settings, 'secrets_dir', None), secrets_dir, conf.get('secrets_dir'), ) self.secrets_dir_missing: Literal['ok', 'warn', 'error'] = first_not_none( secrets_dir_missing, conf.get('secrets_dir_missing'), 'warn', ) if self.secrets_dir_missing not in ('ok', 'warn', 'error'): raise SettingsError(f'invalid secrets_dir_missing value: {self.secrets_dir_missing}') self.secrets_dir_max_size: int = first_not_none( secrets_dir_max_size, conf.get('secrets_dir_max_size'), SECRETS_DIR_MAX_SIZE, ) self.case_sensitive: bool = first_not_none( secrets_case_sensitive, conf.get('secrets_case_sensitive'), case_sensitive, conf.get('case_sensitive'), False, ) self.secrets_prefix: str = first_not_none( secrets_prefix, conf.get('secrets_prefix'), env_prefix, conf.get('env_prefix'), '', ) # nested options self.secrets_nested_delimiter: str | None = first_not_none( secrets_nested_delimiter, conf.get('secrets_nested_delimiter'), conf.get('env_nested_delimiter'), ) self.secrets_nested_subdir: bool = first_not_none( secrets_nested_subdir, conf.get('secrets_nested_subdir'), False, ) if self.secrets_nested_subdir: if secrets_nested_delimiter or conf.get('secrets_nested_delimiter'): raise SettingsError('Options secrets_nested_delimiter and secrets_nested_subdir are mutually exclusive') else: self.secrets_nested_delimiter = os.sep # ensure valid secrets_path if self.secrets_dir is None: paths = [] elif isinstance(self.secrets_dir, (Path, str)): paths = [self.secrets_dir] else: paths = list(self.secrets_dir) self.secrets_paths: list[Path] = [Path(p).expanduser().resolve() for p in paths] for path in self.secrets_paths: self.validate_secrets_path(path) # construct parent super().__init__( settings_cls, case_sensitive=self.case_sensitive, env_prefix=self.secrets_prefix, env_nested_delimiter=self.secrets_nested_delimiter, env_ignore_empty=False, # match SecretsSettingsSource behaviour env_parse_enums=True, # we can pass everything here, it will still behave as "True" env_parse_none_str=None, # match SecretsSettingsSource behaviour ) self.env_parse_none_str = None # update manually because of None # update parent members if not len(self.secrets_paths): self.env_vars = {} else: secrets = reduce( lambda d1, d2: dict((*d1.items(), *d2.items())), (self.load_secrets(p) for p in self.secrets_paths), ) self.env_vars = parse_env_vars( secrets, self.case_sensitive, self.env_ignore_empty, self.env_parse_none_str, ) def validate_secrets_path(self, path: Path) -> None: if not path.exists(): if self.secrets_dir_missing == 'ok': pass elif self.secrets_dir_missing == 'warn': warnings.warn(f'directory "{path}" does not exist', stacklevel=2) elif self.secrets_dir_missing == 'error': raise SettingsError(f'directory "{path}" does not exist') else: raise ValueError # unreachable, checked before else: if not path.is_dir(): raise SettingsError(f'secrets_dir must reference a directory, not a {path_type_label(path)}') secrets_dir_size = sum(f.stat().st_size for f in path.glob('**/*') if f.is_file()) if secrets_dir_size > self.secrets_dir_max_size: raise SettingsError(f'secrets_dir size is above {self.secrets_dir_max_size} bytes') @staticmethod def load_secrets(path: Path) -> dict[str, str]: return { str(p.relative_to(path)): p.read_text().strip() for p in map(Path, iglob(f'{path}/**/*', recursive=True)) if p.is_file() } def __repr__(self) -> str: return f'NestedSecretsSettingsSource(secrets_dir={self.secrets_dir!r})' def first_not_none(*objs: Any) -> Any: return next(filter(lambda o: o is not None, objs), None) pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/pyproject.py000066400000000000000000000040041514433345000311630ustar00rootroot00000000000000"""Pyproject TOML file settings source.""" from __future__ import annotations as _annotations from pathlib import Path from typing import ( TYPE_CHECKING, ) from .toml import TomlConfigSettingsSource if TYPE_CHECKING: from pydantic_settings.main import BaseSettings class PyprojectTomlConfigSettingsSource(TomlConfigSettingsSource): """ A source class that loads variables from a `pyproject.toml` file. """ def __init__( self, settings_cls: type[BaseSettings], toml_file: Path | None = None, ) -> None: self.toml_file_path = self._pick_pyproject_toml_file( toml_file, settings_cls.model_config.get('pyproject_toml_depth', 0) ) self.toml_table_header: tuple[str, ...] = settings_cls.model_config.get( 'pyproject_toml_table_header', ('tool', 'pydantic-settings') ) self.toml_data = self._read_files(self.toml_file_path) for key in self.toml_table_header: self.toml_data = self.toml_data.get(key, {}) super(TomlConfigSettingsSource, self).__init__(settings_cls, self.toml_data) @staticmethod def _pick_pyproject_toml_file(provided: Path | None, depth: int) -> Path: """Pick a `pyproject.toml` file path to use. Args: provided: Explicit path provided when instantiating this class. depth: Number of directories up the tree to check of a pyproject.toml. """ if provided: return provided.resolve() rv = Path.cwd() / 'pyproject.toml' count = 0 if not rv.is_file(): child = rv.parent.parent / 'pyproject.toml' while count < depth: if child.is_file(): return child if str(child.parent) == rv.root: break # end discovery after checking system root once child = child.parent.parent / 'pyproject.toml' count += 1 return rv __all__ = ['PyprojectTomlConfigSettingsSource'] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/secrets.py000066400000000000000000000105661514433345000306260ustar00rootroot00000000000000"""Secrets file settings source.""" from __future__ import annotations as _annotations import os import warnings from pathlib import Path from typing import ( TYPE_CHECKING, Any, ) from pydantic.fields import FieldInfo from pydantic_settings.utils import path_type_label from ...exceptions import SettingsError from ..base import PydanticBaseEnvSettingsSource from ..types import EnvPrefixTarget, PathType if TYPE_CHECKING: from pydantic_settings.main import BaseSettings class SecretsSettingsSource(PydanticBaseEnvSettingsSource): """ Source class for loading settings values from secret files. """ def __init__( self, settings_cls: type[BaseSettings], secrets_dir: PathType | None = None, case_sensitive: bool | None = None, env_prefix: str | None = None, env_prefix_target: EnvPrefixTarget | None = None, env_ignore_empty: bool | None = None, env_parse_none_str: str | None = None, env_parse_enums: bool | None = None, ) -> None: super().__init__( settings_cls, case_sensitive, env_prefix, env_prefix_target, env_ignore_empty, env_parse_none_str, env_parse_enums, ) self.secrets_dir = secrets_dir if secrets_dir is not None else self.config.get('secrets_dir') def __call__(self) -> dict[str, Any]: """ Build fields from "secrets" files. """ secrets: dict[str, str | None] = {} if self.secrets_dir is None: return secrets secrets_dirs = [self.secrets_dir] if isinstance(self.secrets_dir, (str, os.PathLike)) else self.secrets_dir secrets_paths = [Path(p).expanduser() for p in secrets_dirs] self.secrets_paths = [] for path in secrets_paths: if not path.exists(): warnings.warn(f'directory "{path}" does not exist') else: self.secrets_paths.append(path) if not len(self.secrets_paths): return secrets for path in self.secrets_paths: if not path.is_dir(): raise SettingsError(f'secrets_dir must reference a directory, not a {path_type_label(path)}') return super().__call__() @classmethod def find_case_path(cls, dir_path: Path, file_name: str, case_sensitive: bool) -> Path | None: """ Find a file within path's directory matching filename, optionally ignoring case. Args: dir_path: Directory path. file_name: File name. case_sensitive: Whether to search for file name case sensitively. Returns: Whether file path or `None` if file does not exist in directory. """ for f in dir_path.iterdir(): if f.name == file_name: return f elif not case_sensitive and f.name.lower() == file_name.lower(): return f return None def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: """ Gets the value for field from secret file and a flag to determine whether value is complex. Args: field: The field. field_name: The field name. Returns: A tuple that contains the value (`None` if the file does not exist), key, and a flag to determine whether value is complex. """ for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name): # paths reversed to match the last-wins behaviour of `env_file` for secrets_path in reversed(self.secrets_paths): path = self.find_case_path(secrets_path, env_name, self.case_sensitive) if not path: # path does not exist, we currently don't return a warning for this continue if path.is_file(): return path.read_text().strip(), field_key, value_is_complex else: warnings.warn( f'attempted to load secret file "{path}" but found a {path_type_label(path)} instead.', stacklevel=4, ) return None, field_key, value_is_complex def __repr__(self) -> str: return f'{self.__class__.__name__}(secrets_dir={self.secrets_dir!r})' pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/toml.py000066400000000000000000000035331514433345000301250ustar00rootroot00000000000000"""TOML file settings source.""" from __future__ import annotations as _annotations import sys from pathlib import Path from typing import ( TYPE_CHECKING, Any, ) from ..base import ConfigFileSourceMixin, InitSettingsSource from ..types import DEFAULT_PATH, PathType if TYPE_CHECKING: from pydantic_settings.main import BaseSettings if sys.version_info >= (3, 11): import tomllib else: tomllib = None import tomli else: tomllib = None tomli = None def import_toml() -> None: global tomli global tomllib if sys.version_info < (3, 11): if tomli is not None: return try: import tomli except ImportError as e: # pragma: no cover raise ImportError('tomli is not installed, run `pip install pydantic-settings[toml]`') from e else: if tomllib is not None: return import tomllib class TomlConfigSettingsSource(InitSettingsSource, ConfigFileSourceMixin): """ A source class that loads variables from a TOML file """ def __init__( self, settings_cls: type[BaseSettings], toml_file: PathType | None = DEFAULT_PATH, deep_merge: bool = False, ): self.toml_file_path = toml_file if toml_file != DEFAULT_PATH else settings_cls.model_config.get('toml_file') self.toml_data = self._read_files(self.toml_file_path, deep_merge=deep_merge) super().__init__(settings_cls, self.toml_data) def _read_file(self, file_path: Path) -> dict[str, Any]: import_toml() with file_path.open(mode='rb') as toml_file: if sys.version_info < (3, 11): return tomli.load(toml_file) return tomllib.load(toml_file) def __repr__(self) -> str: return f'{self.__class__.__name__}(toml_file={self.toml_file_path})' pydantic-pydantic-settings-198e71c/pydantic_settings/sources/providers/yaml.py000066400000000000000000000112511514433345000301100ustar00rootroot00000000000000"""YAML file settings source.""" from __future__ import annotations as _annotations from pathlib import Path from typing import ( TYPE_CHECKING, Any, ) from ..base import ConfigFileSourceMixin, InitSettingsSource from ..types import DEFAULT_PATH, PathType if TYPE_CHECKING: import yaml from pydantic_settings.main import BaseSettings else: yaml = None def import_yaml() -> None: global yaml if yaml is not None: return try: import yaml except ImportError as e: raise ImportError('PyYAML is not installed, run `pip install pydantic-settings[yaml]`') from e class YamlConfigSettingsSource(InitSettingsSource, ConfigFileSourceMixin): """ A source class that loads variables from a yaml file """ def __init__( self, settings_cls: type[BaseSettings], yaml_file: PathType | None = DEFAULT_PATH, yaml_file_encoding: str | None = None, yaml_config_section: str | None = None, deep_merge: bool = False, ): self.yaml_file_path = yaml_file if yaml_file != DEFAULT_PATH else settings_cls.model_config.get('yaml_file') self.yaml_file_encoding = ( yaml_file_encoding if yaml_file_encoding is not None else settings_cls.model_config.get('yaml_file_encoding') ) self.yaml_config_section = ( yaml_config_section if yaml_config_section is not None else settings_cls.model_config.get('yaml_config_section') ) self.yaml_data = self._read_files(self.yaml_file_path, deep_merge=deep_merge) if self.yaml_config_section is not None: self.yaml_data = self._traverse_nested_section( self.yaml_data, self.yaml_config_section, self.yaml_config_section ) super().__init__(settings_cls, self.yaml_data) def _read_file(self, file_path: Path) -> dict[str, Any]: import_yaml() with file_path.open(encoding=self.yaml_file_encoding) as yaml_file: return yaml.safe_load(yaml_file) or {} def _traverse_nested_section( self, data: dict[str, Any], section_path: str, original_path: str | None = None ) -> dict[str, Any]: """ Traverse nested YAML sections using dot-notation path. This method tries to match the longest possible key first before splitting on dots, allowing access to YAML keys that contain literal dot characters. For example, with section_path="a.b.c", it will try: 1. "a.b.c" as a literal key 2. "a.b" as a key, then traverse to "c" 3. "a" as a key, then traverse to "b.c" 4. "a" as a key, then "b" as a key, then "c" as a key """ # Track the original path for error messages if original_path is None: original_path = section_path # Only reject truly empty paths if not section_path: raise ValueError('yaml_config_section cannot be empty') # Try the full path as a literal key first (even with leading/trailing/consecutive dots) try: return data[section_path] except KeyError: pass # Not a literal key, try splitting except TypeError: raise TypeError( f'yaml_config_section path "{original_path}" cannot be traversed in {self.yaml_file_path}. ' f'An intermediate value is not a dictionary.' ) # If path contains no dots, we already tried it as a literal key above if '.' not in section_path: raise KeyError(f'yaml_config_section key "{original_path}" not found in {self.yaml_file_path}') # Try progressively shorter prefixes (greedy left-to-right approach) parts = section_path.split('.') for i in range(len(parts) - 1, 0, -1): prefix = '.'.join(parts[:i]) suffix = '.'.join(parts[i:]) if prefix in data: # Found the prefix as a literal key, now recursively traverse the suffix try: return self._traverse_nested_section(data[prefix], suffix, original_path) except TypeError: raise TypeError( f'yaml_config_section path "{original_path}" cannot be traversed in {self.yaml_file_path}. ' f'An intermediate value is not a dictionary.' ) # If we get here, no match was found raise KeyError(f'yaml_config_section key "{original_path}" not found in {self.yaml_file_path}') def __repr__(self) -> str: return f'{self.__class__.__name__}(yaml_file={self.yaml_file_path})' __all__ = ['YamlConfigSettingsSource'] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/types.py000066400000000000000000000036011514433345000262750ustar00rootroot00000000000000"""Type definitions for pydantic-settings sources.""" from __future__ import annotations as _annotations from collections.abc import Sequence from pathlib import Path from typing import TYPE_CHECKING, Any, Literal if TYPE_CHECKING: from pydantic._internal._dataclasses import PydanticDataclass from pydantic.main import BaseModel PydanticModel = PydanticDataclass | BaseModel else: PydanticModel = Any class EnvNoneType(str): pass class NoDecode: """Annotation to prevent decoding of a field value.""" pass class ForceDecode: """Annotation to force decoding of a field value.""" pass EnvPrefixTarget = Literal['variable', 'alias', 'all'] DotenvType = Path | str | Sequence[Path | str] PathType = Path | str | Sequence[Path | str] DEFAULT_PATH: PathType = Path('') # This is used as default value for `_env_file` in the `BaseSettings` class and # `env_file` in `DotEnvSettingsSource` so the default can be distinguished from `None`. # See the docstring of `BaseSettings` for more details. ENV_FILE_SENTINEL: DotenvType = Path('') class _CliSubCommand: pass class _CliPositionalArg: pass class _CliImplicitFlag: pass class _CliToggleFlag(_CliImplicitFlag): pass class _CliDualFlag(_CliImplicitFlag): pass class _CliExplicitFlag: pass class _CliUnknownArgs: pass class SecretVersion: def __init__(self, version: str) -> None: self.version = version def __repr__(self) -> str: return f'{self.__class__.__name__}({self.version!r})' __all__ = [ 'DEFAULT_PATH', 'ENV_FILE_SENTINEL', 'EnvPrefixTarget', 'DotenvType', 'EnvNoneType', 'ForceDecode', 'NoDecode', 'PathType', 'PydanticModel', 'SecretVersion', '_CliExplicitFlag', '_CliImplicitFlag', '_CliToggleFlag', '_CliDualFlag', '_CliPositionalArg', '_CliSubCommand', '_CliUnknownArgs', ] pydantic-pydantic-settings-198e71c/pydantic_settings/sources/utils.py000066400000000000000000000212431514433345000262730ustar00rootroot00000000000000"""Utility functions for pydantic-settings sources.""" from __future__ import annotations as _annotations from collections import deque from collections.abc import Mapping, Sequence from dataclasses import is_dataclass from enum import Enum from typing import Any, cast, get_args, get_origin from pydantic import BaseModel, Json, RootModel, Secret from pydantic._internal._utils import is_model_class from pydantic.dataclasses import is_pydantic_dataclass from pydantic.fields import FieldInfo from typing_inspection import typing_objects from ..exceptions import SettingsError from ..utils import _lenient_issubclass from .types import EnvNoneType def _get_env_var_key(key: str, case_sensitive: bool = False) -> str: return key if case_sensitive else key.lower() def _parse_env_none_str(value: str | None, parse_none_str: str | None = None) -> str | None | EnvNoneType: return value if not (value == parse_none_str and parse_none_str is not None) else EnvNoneType(value) def parse_env_vars( env_vars: Mapping[str, str | None], case_sensitive: bool = False, ignore_empty: bool = False, parse_none_str: str | None = None, ) -> Mapping[str, str | None]: return { _get_env_var_key(k, case_sensitive): _parse_env_none_str(v, parse_none_str) for k, v in env_vars.items() if not (ignore_empty and v == '') } def _annotation_is_complex(annotation: Any, metadata: list[Any]) -> bool: # If the model is a root model, the root annotation should be used to # evaluate the complexity. if typing_objects.is_typealiastype(annotation) or typing_objects.is_typealiastype(get_origin(annotation)): annotation = annotation.__value__ if annotation is not None and _lenient_issubclass(annotation, RootModel) and annotation is not RootModel: annotation = cast('type[RootModel[Any]]', annotation) root_annotation = annotation.model_fields['root'].annotation if root_annotation is not None: # pragma: no branch annotation = root_annotation if any(isinstance(md, Json) for md in metadata): # type: ignore[misc] return False origin = get_origin(annotation) # Check if annotation is of the form Annotated[type, metadata]. if typing_objects.is_annotated(origin): # Return result of recursive call on inner type. inner, *meta = get_args(annotation) return _annotation_is_complex(inner, meta) if origin is Secret: return False return ( _annotation_is_complex_inner(annotation) or _annotation_is_complex_inner(origin) or hasattr(origin, '__pydantic_core_schema__') or hasattr(origin, '__get_pydantic_core_schema__') ) def _get_field_metadata(field: FieldInfo) -> list[Any]: annotation = field.annotation metadata = field.metadata if typing_objects.is_typealiastype(annotation) or typing_objects.is_typealiastype(get_origin(annotation)): annotation = annotation.__value__ # type: ignore[union-attr] origin = get_origin(annotation) if typing_objects.is_annotated(origin): _, *meta = get_args(annotation) metadata += meta return metadata def _annotation_is_complex_inner(annotation: type[Any] | None) -> bool: if _lenient_issubclass(annotation, (str, bytes)): return False return _lenient_issubclass( annotation, (BaseModel, Mapping, Sequence, tuple, set, frozenset, deque) ) or is_dataclass(annotation) def _union_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool: """Check if a union type contains any complex types.""" return any(_annotation_is_complex(arg, metadata) for arg in get_args(annotation)) def _annotation_contains_types( annotation: type[Any] | None, types: tuple[Any, ...], is_include_origin: bool = True, is_strip_annotated: bool = False, is_instance: bool = False, collect: set[Any] | None = None, ) -> bool: """Check if a type annotation contains any of the specified types.""" if is_strip_annotated: annotation = _strip_annotated(annotation) if is_include_origin is True: origin = get_origin(annotation) if origin in types: if collect is None: return True collect.add(annotation) if is_instance and any(isinstance(origin, type_) for type_ in types): if collect is None: return True collect.add(annotation) for type_ in get_args(annotation): if ( _annotation_contains_types( type_, types, is_include_origin=True, is_strip_annotated=is_strip_annotated, is_instance=is_instance, collect=collect, ) and collect is None ): return True if is_instance and any(isinstance(annotation, type_) for type_ in types): if collect is None: return True collect.add(annotation) if annotation in types: if collect is not None: collect.add(annotation) return True return False def _strip_annotated(annotation: Any) -> Any: if typing_objects.is_annotated(get_origin(annotation)): return annotation.__origin__ else: return annotation def _annotation_enum_val_to_name(annotation: type[Any] | None, value: Any) -> str | None: for type_ in (annotation, get_origin(annotation), *get_args(annotation)): if _lenient_issubclass(type_, Enum): if value in type_.__members__.values(): return type_(value).name return None def _annotation_enum_name_to_val(annotation: type[Any] | None, name: Any) -> Any: for type_ in (annotation, get_origin(annotation), *get_args(annotation)): if _lenient_issubclass(type_, Enum): if name in type_.__members__.keys(): return type_[name] return None def _get_model_fields(model_cls: type[Any]) -> dict[str, Any]: """Get fields from a pydantic model or dataclass.""" if is_pydantic_dataclass(model_cls) and hasattr(model_cls, '__pydantic_fields__'): return model_cls.__pydantic_fields__ if is_model_class(model_cls): return model_cls.model_fields raise SettingsError(f'Error: {model_cls.__name__} is not subclass of BaseModel or pydantic.dataclasses.dataclass') def _get_alias_names( field_name: str, field_info: Any, alias_path_args: dict[str, int | None] | None = None, case_sensitive: bool = True, ) -> tuple[tuple[str, ...], bool]: """Get alias names for a field, handling alias paths and case sensitivity.""" from pydantic import AliasChoices, AliasPath alias_names: list[str] = [] is_alias_path_only: bool = True if not any((field_info.alias, field_info.validation_alias)): alias_names += [field_name] is_alias_path_only = False else: new_alias_paths: list[AliasPath] = [] for alias in (field_info.alias, field_info.validation_alias): if alias is None: continue elif isinstance(alias, str): alias_names.append(alias) is_alias_path_only = False elif isinstance(alias, AliasChoices): for name in alias.choices: if isinstance(name, str): alias_names.append(name) is_alias_path_only = False else: new_alias_paths.append(name) else: new_alias_paths.append(alias) for alias_path in new_alias_paths: name = cast(str, alias_path.path[0]) name = name.lower() if not case_sensitive else name if alias_path_args is not None: alias_path_args[name] = ( alias_path.path[1] if len(alias_path.path) > 1 and isinstance(alias_path.path[1], int) else None ) if not alias_names and is_alias_path_only: alias_names.append(name) if not case_sensitive: alias_names = [alias_name.lower() for alias_name in alias_names] return tuple(dict.fromkeys(alias_names)), is_alias_path_only def _is_function(obj: Any) -> bool: """Check if an object is a function.""" from types import BuiltinFunctionType, FunctionType return isinstance(obj, (FunctionType, BuiltinFunctionType)) __all__ = [ '_annotation_contains_types', '_annotation_enum_name_to_val', '_annotation_enum_val_to_name', '_annotation_is_complex', '_annotation_is_complex_inner', '_get_alias_names', '_get_env_var_key', '_get_model_fields', '_is_function', '_parse_env_none_str', '_strip_annotated', '_union_is_complex', 'parse_env_vars', ] pydantic-pydantic-settings-198e71c/pydantic_settings/utils.py000066400000000000000000000025111514433345000246050ustar00rootroot00000000000000import types from pathlib import Path from typing import Any, _Final, _GenericAlias, get_origin # type: ignore [attr-defined] _PATH_TYPE_LABELS = { Path.is_dir: 'directory', Path.is_file: 'file', Path.is_mount: 'mount point', Path.is_symlink: 'symlink', Path.is_block_device: 'block device', Path.is_char_device: 'char device', Path.is_fifo: 'FIFO', Path.is_socket: 'socket', } def path_type_label(p: Path) -> str: """ Find out what sort of thing a path is. """ assert p.exists(), 'path does not exist' for method, name in _PATH_TYPE_LABELS.items(): if method(p): return name return 'unknown' # pragma: no cover # TODO remove and replace usage by `isinstance(cls, type) and issubclass(cls, class_or_tuple)` # once we drop support for Python 3.10. def _lenient_issubclass(cls: Any, class_or_tuple: Any) -> bool: # pragma: no cover try: return isinstance(cls, type) and issubclass(cls, class_or_tuple) except TypeError: if get_origin(cls) is not None: # Up until Python 3.10, isinstance(, type) is True # (e.g. list[int]) return False raise _WithArgsTypes = (_GenericAlias, types.GenericAlias, types.UnionType) _typing_base: Any = _Final # pyright: ignore[reportAttributeAccessIssue] pydantic-pydantic-settings-198e71c/pydantic_settings/version.py000066400000000000000000000000231514433345000251260ustar00rootroot00000000000000VERSION = '2.13.0' pydantic-pydantic-settings-198e71c/pyproject.toml000066400000000000000000000114471514433345000222640ustar00rootroot00000000000000[build-system] requires = ['hatchling'] build-backend = 'hatchling.build' [tool.hatch.version] path = 'pydantic_settings/version.py' [project] name = 'pydantic-settings' description = 'Settings management using Pydantic' authors = [ {name = 'Samuel Colvin', email = 's@muelcolvin.com'}, {name = 'Eric Jolibois', email = 'em.jolibois@gmail.com'}, {name = 'Hasan Ramezani', email = 'hasan.r67@gmail.com'}, ] license = 'MIT' readme = 'README.md' classifiers = [ 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: MIT License', 'Framework :: Pydantic', 'Framework :: Pydantic :: 2', 'Operating System :: Unix', 'Operating System :: POSIX :: Linux', 'Environment :: Console', 'Environment :: MacOS X', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Internet', ] requires-python = '>=3.10' dependencies = [ 'pydantic>=2.7.0', 'python-dotenv>=0.21.0', 'typing-inspection>=0.4.0', ] dynamic = ['version'] [project.optional-dependencies] yaml = ["pyyaml>=6.0.1"] toml = ["tomli>=2.0.1"] azure-key-vault = ["azure-keyvault-secrets>=4.8.0", "azure-identity>=1.16.0"] aws-secrets-manager = ["boto3>=1.35.0", "boto3-stubs[secretsmanager]"] gcp-secret-manager = [ "google-cloud-secret-manager>=2.23.1", ] [project.urls] Homepage = 'https://github.com/pydantic/pydantic-settings' Funding = 'https://github.com/sponsors/samuelcolvin' Source = 'https://github.com/pydantic/pydantic-settings' Changelog = 'https://github.com/pydantic/pydantic-settings/releases' Documentation = 'https://docs.pydantic.dev/dev-v2/concepts/pydantic_settings/' [dependency-groups] linting = [ "black", "mypy", "pre-commit", "pyyaml", "ruff", "types-pyyaml", "boto3-stubs[secretsmanager]", ] testing = [ "coverage[toml]", "pytest", "pytest-examples", "pytest-mock", "pytest-pretty", "moto[secretsmanager]", "diff-cover>=9.2.0", ] [tool.pytest.ini_options] testpaths = 'tests' filterwarnings = [ 'error', 'ignore:This is a placeholder until pydantic-settings.*:UserWarning', 'ignore::DeprecationWarning:botocore.*:', ] # https://coverage.readthedocs.io/en/latest/config.html#run [tool.coverage.run] include = [ "pydantic_settings/**/*.py", "tests/**/*.py", ] branch = true # https://coverage.readthedocs.io/en/latest/config.html#report [tool.coverage.report] skip_covered = true show_missing = true ignore_errors = true precision = 2 exclude_lines = [ 'pragma: no cover', 'raise NotImplementedError', 'if TYPE_CHECKING:', 'if typing.TYPE_CHECKING:', '@overload', '@deprecated', '@typing.overload', '@abstractmethod', '\(Protocol\):$', 'typing.assert_never', '$\s*assert_never\(', 'if __name__ == .__main__.:', 'except ImportError as _import_error:', '$\s*pass$', ] [tool.coverage.paths] source = [ 'pydantic_settings/', ] [tool.ruff] line-length = 120 target-version = 'py310' [tool.ruff.lint.pyupgrade] keep-runtime-typing = true [tool.ruff.lint] extend-select = ['Q', 'RUF100', 'C90', 'UP', 'I'] flake8-quotes = {inline-quotes = 'single', multiline-quotes = 'double'} isort = { known-first-party = ['pydantic_settings', 'tests'] } mccabe = { max-complexity = 14 } pydocstyle = { convention = 'google' } [tool.ruff.format] quote-style = 'single' [tool.mypy] python_version = '3.10' show_error_codes = true follow_imports = 'silent' strict_optional = true warn_redundant_casts = true warn_unused_ignores = true disallow_any_generics = true check_untyped_defs = true no_implicit_reexport = true warn_unused_configs = true disallow_subclassing_any = true disallow_incomplete_defs = true disallow_untyped_decorators = true disallow_untyped_calls = true # for strict mypy: (this is the tricky one :-)) disallow_untyped_defs = true # remaining arguments from `mypy --strict` which cause errors # no_implicit_optional = true # warn_return_any = true # ansi2html and devtools are required to avoid the need to install these packages when running linting, # they're used in the docs build script [[tool.mypy.overrides]] module = [ 'dotenv.*', ] ignore_missing_imports = true # configuring https://github.com/pydantic/hooky [tool.hooky] assignees = ['samuelcolvin', 'dmontagu', 'hramezani'] reviewers = ['samuelcolvin', 'dmontagu', 'hramezani'] require_change_file = false pydantic-pydantic-settings-198e71c/tests/000077500000000000000000000000001514433345000205035ustar00rootroot00000000000000pydantic-pydantic-settings-198e71c/tests/conftest.py000066400000000000000000000052751514433345000227130ustar00rootroot00000000000000from __future__ import annotations import os from pathlib import Path from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from collections.abc import Iterator class SetEnv: def __init__(self): self.envars = set() def set(self, name, value): self.envars.add(name) os.environ[name] = value def pop(self, name): self.envars.remove(name) os.environ.pop(name) def clear(self): for n in self.envars: os.environ.pop(n) class Dir: def __init__(self, basedir: Path) -> None: self.basedir = basedir def write(self, files: dict[str, str]) -> None: for path, content in files.items(): file_path = self.basedir / path file_path.parent.mkdir(parents=True, exist_ok=True) file_path.write_text(content) @pytest.fixture def tmp_files(tmp_path): yield Dir(tmp_path) @pytest.fixture def cd_tmp_path(tmp_path: Path) -> Iterator[Path]: """Change directory into the value of the ``tmp_path`` fixture. .. rubric:: Example .. code-block:: python from typing import TYPE_CHECKING if TYPE_CHECKING: from pathlib import Path def test_something(cd_tmp_path: Path) -> None: ... Returns: Value of the :fixture:`tmp_path` fixture (a :class:`~pathlib.Path` object). """ prev_dir = Path.cwd() os.chdir(tmp_path) try: yield tmp_path finally: os.chdir(prev_dir) @pytest.fixture def env(): setenv = SetEnv() yield setenv setenv.clear() @pytest.fixture def docs_test_env(): setenv = SetEnv() # envs for basic usage example setenv.set('my_auth_key', 'xxx') setenv.set('my_api_key', 'xxx') # envs for parsing environment variable values example setenv.set('V0', '0') setenv.set('SUB_MODEL', '{"v1": "json-1", "v2": "json-2"}') setenv.set('SUB_MODEL__V2', 'nested-2') setenv.set('SUB_MODEL__V3', '3') setenv.set('SUB_MODEL__DEEP__V4', 'v4') # envs for parsing environment variable values example with env_nested_max_split=1 setenv.set('GENERATION_LLM_PROVIDER', 'anthropic') setenv.set('GENERATION_LLM_API_KEY', 'your-api-key') setenv.set('GENERATION_LLM_API_VERSION', '2024-03-15') # Variables from index.md that need to be cleaned up setenv.set('FooAlias', '') setenv.set('BAR', '') setenv.set('TARGET_BAR', '') setenv.set('TARGET_ALL_FooAlias', '') setenv.set('TARGET_ALIAS_FooAliase', '') yield setenv setenv.clear() @pytest.fixture def cli_test_env(): setenv = SetEnv() # envs for reproducible cli tests setenv.set('COLUMNS', '80') yield setenv setenv.clear() pydantic-pydantic-settings-198e71c/tests/example_test_config.json000066400000000000000000000000231514433345000254100ustar00rootroot00000000000000{"foobar": "test"} pydantic-pydantic-settings-198e71c/tests/test_docs.py000066400000000000000000000077111514433345000230520ustar00rootroot00000000000000from __future__ import annotations as _annotations import platform import re import sys from pathlib import Path import pytest from pytest_examples import CodeExample, EvalExample, find_examples from pytest_examples.config import ExamplesConfig from pytest_examples.lint import black_format DOCS_ROOT = Path(__file__).parent.parent / 'docs' def skip_docs_tests(): if sys.platform not in {'linux', 'darwin'}: return 'not in linux or macos' if platform.python_implementation() != 'CPython': return 'not cpython' class GroupModuleGlobals: def __init__(self) -> None: self.name = None self.module_dict: dict[str, str] = {} def get(self, name: str | None): if name is not None and name == self.name: return self.module_dict def set(self, name: str | None, module_dict: dict[str, str]): self.name = name if self.name is None: self.module_dict = None else: self.module_dict = module_dict group_globals = GroupModuleGlobals() skip_reason = skip_docs_tests() def print_callback(print_statement: str) -> str: # make error display uniform s = re.sub(r'(https://errors.pydantic.dev)/.+?/', r'\1/2/', print_statement) # hack until https://github.com/pydantic/pytest-examples/issues/11 is fixed if '' in s: # avoid function repr breaking black formatting s = re.sub('', 'math.cos', s) return black_format(s, ExamplesConfig()).rstrip('\n') return s @pytest.mark.filterwarnings('ignore:(parse_obj_as|schema_json_of|schema_of) is deprecated.*:DeprecationWarning') @pytest.mark.skipif(bool(skip_reason), reason=skip_reason or 'not skipping') @pytest.mark.parametrize('example', find_examples(str(DOCS_ROOT), skip=sys.platform == 'win32'), ids=str) def test_docs_examples( # noqa C901 example: CodeExample, eval_example: EvalExample, tmp_path: Path, mocker, docs_test_env ): eval_example.print_callback = print_callback prefix_settings = example.prefix_settings() test_settings = prefix_settings.get('test') lint_settings = prefix_settings.get('lint') if test_settings == 'skip' and lint_settings == 'skip': pytest.skip('both test and lint skipped') requires_settings = prefix_settings.get('requires') if requires_settings: major, minor = map(int, requires_settings.split('.')) if sys.version_info < (major, minor): pytest.skip(f'requires python {requires_settings}') group_name = prefix_settings.get('group') if '# ignore-above' in example.source: eval_example.set_config(ruff_ignore=['E402']) if group_name: eval_example.set_config(ruff_ignore=['F821']) # eval_example.set_config(line_length=120) if lint_settings != 'skip': if eval_example.update_examples: eval_example.format(example) else: eval_example.lint(example) if test_settings == 'skip': return group_name = prefix_settings.get('group') d = group_globals.get(group_name) xfail = None if test_settings and test_settings.startswith('xfail'): xfail = test_settings[5:].lstrip(' -') rewrite_assertions = prefix_settings.get('rewrite_assert', 'true') == 'true' try: if test_settings == 'no-print-intercept': d2 = eval_example.run(example, module_globals=d, rewrite_assertions=rewrite_assertions) elif eval_example.update_examples: d2 = eval_example.run_print_update(example, module_globals=d, rewrite_assertions=rewrite_assertions) else: d2 = eval_example.run_print_check(example, module_globals=d, rewrite_assertions=rewrite_assertions) except BaseException as e: # run_print_check raises a BaseException if xfail: pytest.xfail(f'{xfail}, {type(e).__name__}: {e}') raise else: if xfail: pytest.fail('expected xfail') group_globals.set(group_name, d2) pydantic-pydantic-settings-198e71c/tests/test_precedence_and_merging.py000066400000000000000000000110711514433345000265430ustar00rootroot00000000000000from __future__ import annotations as _annotations from pathlib import Path from typing import Literal from pydantic import AnyHttpUrl, Field from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, ) def test_init_kwargs_override_env_for_alias_with_populate_by_name(env): class Settings(BaseSettings): abc: AnyHttpUrl = Field(validation_alias='my_abc') model_config = SettingsConfigDict(populate_by_name=True, extra='allow') env.set('MY_ABC', 'http://localhost.com') # Passing by field name should be accepted (populate_by_name=True) and should # override env-derived value. Also ensures init > env precedence with validation_alias. assert str(Settings(abc='http://prod.localhost.com/').abc) == 'http://prod.localhost.com/' def test_precedence_init_over_env(tmp_path: Path, env): class Settings(BaseSettings): foo: str env.set('FOO', 'from-env') s = Settings(foo='from-init') assert s.foo == 'from-init' def test_precedence_env_over_dotenv(tmp_path: Path, env): env_file = tmp_path / '.env' env_file.write_text('FOO=from-dotenv\n') class Settings(BaseSettings): foo: str model_config = SettingsConfigDict(env_file=env_file) env.set('FOO', 'from-env') s = Settings() assert s.foo == 'from-env' def test_precedence_dotenv_over_secrets(tmp_path: Path): # create dotenv env_file = tmp_path / '.env' env_file.write_text('FOO=from-dotenv\n') # create secrets directory with same key secrets_dir = tmp_path / 'secrets' secrets_dir.mkdir() (secrets_dir / 'FOO').write_text('from-secrets\n') class Settings(BaseSettings): foo: str model_config = SettingsConfigDict(env_file=env_file, secrets_dir=secrets_dir) # No env set, dotenv should override secrets s = Settings() assert s.foo == 'from-dotenv' def test_precedence_secrets_over_defaults(tmp_path: Path): secrets_dir = tmp_path / 'secrets' secrets_dir.mkdir() (secrets_dir / 'FOO').write_text('from-secrets\n') class Settings(BaseSettings): foo: str = 'from-default' model_config = SettingsConfigDict(secrets_dir=secrets_dir) s = Settings() assert s.foo == 'from-secrets' def test_merging_preserves_earlier_values(tmp_path: Path, env): # Prove that merging preserves earlier source values: init -> env -> dotenv -> secrets -> defaults # We'll populate nested from dotenv and env parts, then set a default for a, and init for b env_file = tmp_path / '.env' env_file.write_text('NESTED={"x":1}\n') secrets_dir = tmp_path / 'secrets' secrets_dir.mkdir() (secrets_dir / 'NESTED').write_text('{"y": 2}') class Settings(BaseSettings): a: int = 10 b: int = 0 nested: dict model_config = SettingsConfigDict(env_file=env_file, secrets_dir=secrets_dir, env_nested_delimiter='__') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ): # normal order; we want to assert deep merging return init_settings, env_settings, dotenv_settings, file_secret_settings # env contributes nested.y and overrides dotenv nested.x=1 if set; we'll set only y to prove merge env.set('NESTED__y', '3') # init contributes b, defaults contribute a s = Settings(b=20) assert s.a == 10 # defaults preserved assert s.b == 20 # init wins # nested: dotenv provides x=1; env provides y=3; deep merged => {x:1, y:3} assert s.nested == {'x': 1, 'y': 3} def test_init_kwargs_override_env_with_alias_and_extra_forbid(env): # Reproduction for https://github.com/pydantic/pydantic-settings/issues/744 class Settings(BaseSettings): env_kind: Literal['dev', 'hosted'] = Field(default='dev', alias='ENV_KIND2') model_config = SettingsConfigDict(populate_by_name=True, extra='forbid') env.set('ENV_KIND', 'dev') # This should work: init kwargs should override env vars # We saw intermittent failures due to non-deterministic set.pop(), it failed with: # pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings # env_kind # Extra inputs are not permitted [type=extra_forbidden, input_value='dev', input_type=str] s = Settings(env_kind='hosted') assert s.env_kind == 'hosted' pydantic-pydantic-settings-198e71c/tests/test_settings.py000066400000000000000000003006071514433345000237620ustar00rootroot00000000000000import dataclasses import json import os import pathlib import sys import uuid from collections.abc import Callable, Hashable from datetime import date, datetime, timezone from enum import IntEnum from pathlib import Path from typing import Annotated, Any, Generic, Literal, TypeVar from unittest import mock import pytest from annotated_types import MinLen from pydantic import ( AliasChoices, AliasGenerator, AliasPath, BaseModel, Discriminator, Field, HttpUrl, Json, PostgresDsn, RootModel, Secret, SecretStr, Tag, ValidationError, field_validator, model_validator, ) from pydantic import ( dataclasses as pydantic_dataclasses, ) from pydantic.fields import FieldInfo from typing_extensions import TypeAliasType, override from pydantic_settings import ( BaseSettings, DotEnvSettingsSource, EnvSettingsSource, ForceDecode, InitSettingsSource, NoDecode, PydanticBaseSettingsSource, SecretsSettingsSource, SettingsConfigDict, SettingsError, ) from pydantic_settings.sources import DefaultSettingsSource try: import dotenv except ImportError: dotenv = None class FruitsEnum(IntEnum): pear = 0 kiwi = 1 lime = 2 class SimpleSettings(BaseSettings): apple: str class SettingWithIgnoreEmpty(BaseSettings): apple: str = 'default' model_config = SettingsConfigDict(env_ignore_empty=True) class SettingWithPopulateByName(BaseSettings): apple: str = Field('default', alias='pomo') model_config = SettingsConfigDict(populate_by_name=True) @pytest.fixture(autouse=True) def clean_env(): with mock.patch.dict(os.environ, clear=True): yield def test_sub_env(env): env.set('apple', 'hello') s = SimpleSettings() assert s.apple == 'hello' def test_sub_env_override(env): env.set('apple', 'hello') s = SimpleSettings(apple='goodbye') assert s.apple == 'goodbye' def test_sub_env_missing(): with pytest.raises(ValidationError) as exc_info: SimpleSettings() assert exc_info.value.errors(include_url=False) == [ {'type': 'missing', 'loc': ('apple',), 'msg': 'Field required', 'input': {}} ] def test_other_setting(): with pytest.raises(ValidationError): SimpleSettings(apple='a', foobar=42) def test_ignore_empty_when_empty_uses_default(env): env.set('apple', '') s = SettingWithIgnoreEmpty() assert s.apple == 'default' def test_ignore_empty_when_not_empty_uses_value(env): env.set('apple', 'a') s = SettingWithIgnoreEmpty() assert s.apple == 'a' def test_ignore_empty_with_dotenv_when_empty_uses_default(tmp_path): p = tmp_path / '.env' p.write_text('a=') class Settings(BaseSettings): a: str = 'default' model_config = SettingsConfigDict(env_file=p, env_ignore_empty=True) s = Settings() assert s.a == 'default' def test_ignore_empty_with_dotenv_when_not_empty_uses_value(tmp_path): p = tmp_path / '.env' p.write_text('a=b') class Settings(BaseSettings): a: str = 'default' model_config = SettingsConfigDict(env_file=p, env_ignore_empty=True) s = Settings() assert s.a == 'b' def test_populate_by_name_when_using_alias(env): env.set('pomo', 'bongusta') s = SettingWithPopulateByName() assert s.apple == 'bongusta' def test_populate_by_name_when_using_name(env): env.set('apple', 'honeycrisp') s = SettingWithPopulateByName() assert s.apple == 'honeycrisp' def test_populate_by_name_when_using_both(env): env.set('apple', 'honeycrisp') env.set('pomo', 'bongusta') s = SettingWithPopulateByName() assert s.apple == 'bongusta', 'Expected alias value to be prioritized.' def test_populate_by_name_with_alias_path_when_using_alias(env): env.set('fruits', '["empire", "honeycrisp"]') class Settings(BaseSettings): apple: str = Field('default', validation_alias=AliasPath('fruits', 0)) model_config = SettingsConfigDict(populate_by_name=True) s = Settings() assert s.apple == 'empire' def test_populate_by_name_with_alias_path_when_using_name(env): env.set('apple', 'jonathan gold') class Settings(BaseSettings): apple: str = Field('default', validation_alias=AliasPath('fruits', 0)) model_config = SettingsConfigDict(populate_by_name=True) s = Settings() assert s.apple == 'jonathan gold' @pytest.mark.parametrize( 'env_vars, expected_value', [ pytest.param({'pomo': 'pomo-chosen'}, 'pomo-chosen', id='pomo'), pytest.param({'pomme': 'pomme-chosen'}, 'pomme-chosen', id='pomme'), pytest.param({'manzano': 'manzano-chosen'}, 'manzano-chosen', id='manzano'), pytest.param( {'pomo': 'pomo-chosen', 'pomme': 'pomme-chosen', 'manzano': 'manzano-chosen'}, 'pomo-chosen', id='pomo-priority', ), pytest.param({'pomme': 'pomme-chosen', 'manzano': 'manzano-chosen'}, 'pomme-chosen', id='pomme-priority'), ], ) def test_populate_by_name_with_alias_choices_when_using_alias(env, env_vars: dict[str, str], expected_value: str): for k, v in env_vars.items(): env.set(k, v) class Settings(BaseSettings): apple: str = Field('default', validation_alias=AliasChoices('pomo', 'pomme', 'manzano')) model_config = SettingsConfigDict(populate_by_name=True) s = Settings() assert s.apple == expected_value def test_populate_by_name_with_dotenv_when_using_alias(tmp_path): p = tmp_path / '.env' p.write_text('pomo=bongusta') class Settings(BaseSettings): apple: str = Field('default', alias='pomo') model_config = SettingsConfigDict(env_file=p, populate_by_name=True) s = Settings() assert s.apple == 'bongusta' def test_populate_by_name_with_dotenv_when_using_name(tmp_path): p = tmp_path / '.env' p.write_text('apple=honeycrisp') class Settings(BaseSettings): apple: str = Field('default', alias='pomo') model_config = SettingsConfigDict(env_file=p, populate_by_name=True) s = Settings() assert s.apple == 'honeycrisp' def test_populate_by_name_with_dotenv_when_using_both(tmp_path): p = tmp_path / '.env' p.write_text('apple=honeycrisp') p.write_text('pomo=bongusta') class Settings(BaseSettings): apple: str = Field('default', alias='pomo') model_config = SettingsConfigDict(env_file=p, populate_by_name=True) s = Settings() assert s.apple == 'bongusta', 'Expected alias value to be prioritized.' def test_with_prefix(env): class Settings(BaseSettings): apple: str model_config = SettingsConfigDict(env_prefix='foobar_') with pytest.raises(ValidationError): Settings() env.set('foobar_apple', 'has_prefix') s = Settings() assert s.apple == 'has_prefix' def test_nested_env_with_basemodel(env): class TopValue(BaseModel): apple: str banana: str class Settings(BaseSettings): top: TopValue with pytest.raises(ValidationError): Settings() env.set('top', '{"banana": "secret_value"}') s = Settings(top={'apple': 'value'}) assert s.top.apple == 'value' assert s.top.banana == 'secret_value' def test_merge_dict(env): class Settings(BaseSettings): top: dict[str, str] with pytest.raises(ValidationError): Settings() env.set('top', '{"banana": "secret_value"}') s = Settings(top={'apple': 'value'}) assert s.top == {'apple': 'value', 'banana': 'secret_value'} def test_nested_env_delimiter(env): class SubSubValue(BaseSettings): v6: str class SubValue(BaseSettings): v4: str v5: int sub_sub: SubSubValue class TopValue(BaseSettings): v1: str v2: str v3: str sub: SubValue class Cfg(BaseSettings): v0: str v0_union: SubValue | int top: TopValue model_config = SettingsConfigDict(env_nested_delimiter='__') env.set('top', '{"v1": "json-1", "v2": "json-2", "sub": {"v5": "xx"}}') env.set('top__sub__v5', '5') env.set('v0', '0') env.set('top__v2', '2') env.set('top__v3', '3') env.set('v0_union', '0') env.set('top__sub__sub_sub__v6', '6') env.set('top__sub__v4', '4') cfg = Cfg() assert cfg.model_dump() == { 'v0': '0', 'v0_union': 0, 'top': { 'v1': 'json-1', 'v2': '2', 'v3': '3', 'sub': {'v4': '4', 'v5': 5, 'sub_sub': {'v6': '6'}}, }, } def test_nested_env_optional_json(env): class Child(BaseModel): num_list: list[int] | None = None class Cfg(BaseSettings, env_nested_delimiter='__'): child: Child | None = None env.set('CHILD__NUM_LIST', '[1,2,3]') cfg = Cfg() assert cfg.model_dump() == { 'child': { 'num_list': [1, 2, 3], }, } def test_nested_env_delimiter_with_prefix(env): class Subsettings(BaseSettings): banana: str class Settings(BaseSettings): subsettings: Subsettings model_config = SettingsConfigDict(env_nested_delimiter='_', env_prefix='myprefix_') env.set('myprefix_subsettings_banana', 'banana') s = Settings() assert s.subsettings.banana == 'banana' class Settings(BaseSettings): subsettings: Subsettings model_config = SettingsConfigDict(env_nested_delimiter='_', env_prefix='myprefix__') env.set('myprefix__subsettings_banana', 'banana') s = Settings() assert s.subsettings.banana == 'banana' def test_nested_env_delimiter_complex_required(env): class Cfg(BaseSettings): v: str = 'default' model_config = SettingsConfigDict(env_nested_delimiter='__') env.set('v__x', 'x') env.set('v__y', 'y') cfg = Cfg() assert cfg.model_dump() == {'v': 'default'} def test_nested_env_delimiter_aliases(env): class SubModel(BaseModel): v1: str v2: str class Cfg(BaseSettings): sub_model: SubModel = Field(validation_alias=AliasChoices('foo', 'bar')) model_config = SettingsConfigDict(env_nested_delimiter='__') env.set('foo__v1', '-1-') env.set('bar__v2', '-2-') assert Cfg().model_dump() == {'sub_model': {'v1': '-1-', 'v2': '-2-'}} @pytest.mark.parametrize('env_prefix', [None, 'prefix_', 'prefix__']) def test_nested_env_max_split(env, env_prefix): class Person(BaseModel): sex: Literal['M', 'F'] first_name: str date_of_birth: date class Cfg(BaseSettings): caregiver: Person significant_other: Person | None = None next_of_kin: Person | None = None model_config = SettingsConfigDict(env_nested_delimiter='_', env_nested_max_split=1) if env_prefix is not None: model_config['env_prefix'] = env_prefix env_prefix = env_prefix or '' env.set(env_prefix + 'caregiver_sex', 'M') env.set(env_prefix + 'caregiver_first_name', 'Joe') env.set(env_prefix + 'caregiver_date_of_birth', '1975-09-12') env.set(env_prefix + 'significant_other_sex', 'F') env.set(env_prefix + 'significant_other_first_name', 'Jill') env.set(env_prefix + 'significant_other_date_of_birth', '1998-04-19') env.set(env_prefix + 'next_of_kin_sex', 'M') env.set(env_prefix + 'next_of_kin_first_name', 'Jack') env.set(env_prefix + 'next_of_kin_date_of_birth', '1999-04-19') assert Cfg().model_dump() == { 'caregiver': {'sex': 'M', 'first_name': 'Joe', 'date_of_birth': date(1975, 9, 12)}, 'significant_other': {'sex': 'F', 'first_name': 'Jill', 'date_of_birth': date(1998, 4, 19)}, 'next_of_kin': {'sex': 'M', 'first_name': 'Jack', 'date_of_birth': date(1999, 4, 19)}, } class DateModel(BaseModel): pips: bool = False class ComplexSettings(BaseSettings): apples: list[str] = [] bananas: set[int] = set() carrots: dict = {} date: DateModel = DateModel() def test_list(env): env.set('apples', '["russet", "granny smith"]') s = ComplexSettings() assert s.apples == ['russet', 'granny smith'] assert s.date.pips is False def test_annotated_list(env): class AnnotatedComplexSettings(BaseSettings): apples: Annotated[list[str], MinLen(2)] = [] env.set('apples', '["russet", "granny smith"]') s = AnnotatedComplexSettings() assert s.apples == ['russet', 'granny smith'] env.set('apples', '["russet"]') with pytest.raises(ValidationError) as exc_info: AnnotatedComplexSettings() assert exc_info.value.errors(include_url=False) == [ { 'ctx': {'actual_length': 1, 'field_type': 'List', 'min_length': 2}, 'input': ['russet'], 'loc': ('apples',), 'msg': 'List should have at least 2 items after validation, not 1', 'type': 'too_short', } ] def test_annotated_with_type(env): """https://github.com/pydantic/pydantic-settings/issues/536. PEP 695 type aliases need to be analyzed when determining if an annotation is complex. """ MinLenList = TypeAliasType('MinLenList', Annotated[list[str] | list[int], MinLen(2)]) class AnnotatedComplexSettings(BaseSettings): apples: MinLenList env.set('apples', '["russet", "granny smith"]') s = AnnotatedComplexSettings() assert s.apples == ['russet', 'granny smith'] T = TypeVar('T') MinLenList = TypeAliasType('MinLenList', Annotated[list[T] | tuple[T], MinLen(2)], type_params=(T,)) class AnnotatedComplexSettings(BaseSettings): apples: MinLenList[str] s = AnnotatedComplexSettings() assert s.apples == ['russet', 'granny smith'] def test_annotated_with_type_no_decode(env): A = TypeAliasType('A', Annotated[list[str], NoDecode]) class Settings(BaseSettings): a: A # decode the value here. the field value won't be decoded because of NoDecode @field_validator('a', mode='before') @classmethod def decode_a(cls, v: str) -> list[str]: return json.loads(v) env.set('a', '["one", "two"]') s = Settings() assert s.model_dump() == {'a': ['one', 'two']} def test_set_dict_model(env): env.set('bananas', '[1, 2, 3, 3]') env.set('CARROTS', '{"a": null, "b": 4}') env.set('daTE', '{"pips": true}') s = ComplexSettings() assert s.bananas == {1, 2, 3} assert s.carrots == {'a': None, 'b': 4} assert s.date.pips is True def test_invalid_json(env): env.set('apples', '["russet", "granny smith",]') with pytest.raises(SettingsError, match='error parsing value for field "apples" from source "EnvSettingsSource"'): ComplexSettings() def test_required_sub_model(env): class Settings(BaseSettings): foobar: DateModel with pytest.raises(ValidationError): Settings() env.set('FOOBAR', '{"pips": "TRUE"}') s = Settings() assert s.foobar.pips is True def test_non_class(env): class Settings(BaseSettings): foobar: str | None env.set('FOOBAR', 'xxx') s = Settings() assert s.foobar == 'xxx' @pytest.mark.parametrize('dataclass_decorator', (pydantic_dataclasses.dataclass, dataclasses.dataclass)) def test_generic_dataclass(env, dataclass_decorator): T = TypeVar('T') @dataclass_decorator class GenericDataclass(Generic[T]): x: T class ComplexSettings(BaseSettings): field: GenericDataclass[int] env.set('field', '{"x": 1}') s = ComplexSettings() assert s.field.x == 1 env.set('field', '{"x": "a"}') with pytest.raises(ValidationError) as exc_info: ComplexSettings() assert exc_info.value.errors(include_url=False) == [ { 'input': 'a', 'loc': ('field', 'x'), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'type': 'int_parsing', } ] def test_generic_basemodel(env): T = TypeVar('T') class GenericModel(BaseModel, Generic[T]): x: T class ComplexSettings(BaseSettings): field: GenericModel[int] env.set('field', '{"x": 1}') s = ComplexSettings() assert s.field.x == 1 env.set('field', '{"x": "a"}') with pytest.raises(ValidationError) as exc_info: ComplexSettings() assert exc_info.value.errors(include_url=False) == [ { 'input': 'a', 'loc': ('field', 'x'), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'type': 'int_parsing', } ] def test_annotated(env): T = TypeVar('T') class GenericModel(BaseModel, Generic[T]): x: T class ComplexSettings(BaseSettings): field: GenericModel[int] env.set('field', '{"x": 1}') s = ComplexSettings() assert s.field.x == 1 env.set('field', '{"x": "a"}') with pytest.raises(ValidationError) as exc_info: ComplexSettings() assert exc_info.value.errors(include_url=False) == [ { 'input': 'a', 'loc': ('field', 'x'), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'type': 'int_parsing', } ] def test_class_nested_model_default_partial_update(env): class NestedA(BaseModel): v0: bool v1: bool @pydantic_dataclasses.dataclass class NestedB: v0: bool v1: bool @dataclasses.dataclass class NestedC: v0: bool v1: bool class NestedD(BaseModel): v0: bool = False v1: bool = True class SettingsDefaultsA(BaseSettings, env_nested_delimiter='__', nested_model_default_partial_update=True): nested_a: NestedA = NestedA(v0=False, v1=True) nested_b: NestedB = NestedB(v0=False, v1=True) nested_d: NestedC = NestedC(v0=False, v1=True) nested_c: NestedD = NestedD() assert SettingsDefaultsA().model_dump() == { 'nested_a': {'v0': False, 'v1': True}, 'nested_b': {'v0': False, 'v1': True}, 'nested_c': {'v0': False, 'v1': True}, 'nested_d': {'v0': False, 'v1': True}, } assert SettingsDefaultsA().model_dump(exclude_unset=True) == {} env.set('NESTED_A__V0', 'True') env.set('NESTED_B__V0', 'True') assert SettingsDefaultsA().model_dump() == { 'nested_a': {'v0': True, 'v1': True}, 'nested_b': {'v0': True, 'v1': True}, 'nested_c': {'v0': False, 'v1': True}, 'nested_d': {'v0': False, 'v1': True}, } assert SettingsDefaultsA().model_dump(exclude_unset=True) == { 'nested_a': {'v0': True, 'v1': True}, 'nested_b': {'v0': True, 'v1': True}, } env.set('NESTED_C__V0', 'True') env.set('NESTED_D__V0', 'True') assert SettingsDefaultsA().model_dump() == { 'nested_a': {'v0': True, 'v1': True}, 'nested_b': {'v0': True, 'v1': True}, 'nested_c': {'v0': True, 'v1': True}, 'nested_d': {'v0': True, 'v1': True}, } assert SettingsDefaultsA().model_dump(exclude_unset=True) == { 'nested_a': {'v0': True, 'v1': True}, 'nested_b': {'v0': True, 'v1': True}, 'nested_c': {'v0': True, 'v1': True}, 'nested_d': {'v0': True, 'v1': True}, } def test_init_kwargs_nested_model_default_partial_update(env): class DeepSubModel(BaseModel): v4: str class SubModel(BaseModel): v1: str v2: bytes v3: int deep: DeepSubModel class Settings(BaseSettings, env_nested_delimiter='__', nested_model_default_partial_update=True): v0: str sub_model: SubModel @classmethod def settings_customise_sources( cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings ): return env_settings, dotenv_settings, init_settings, file_secret_settings env.set('SUB_MODEL__DEEP__V4', 'override-v4') s_final = {'v0': '0', 'sub_model': {'v1': 'init-v1', 'v2': b'init-v2', 'v3': 3, 'deep': {'v4': 'override-v4'}}} s = Settings(v0='0', sub_model={'v1': 'init-v1', 'v2': b'init-v2', 'v3': 3, 'deep': {'v4': 'init-v4'}}) assert s.model_dump() == s_final s = Settings(v0='0', sub_model=SubModel(v1='init-v1', v2=b'init-v2', v3=3, deep=DeepSubModel(v4='init-v4'))) assert s.model_dump() == s_final s = Settings(v0='0', sub_model=SubModel(v1='init-v1', v2=b'init-v2', v3=3, deep={'v4': 'init-v4'})) assert s.model_dump() == s_final s = Settings(v0='0', sub_model={'v1': 'init-v1', 'v2': b'init-v2', 'v3': 3, 'deep': DeepSubModel(v4='init-v4')}) assert s.model_dump() == s_final def test_alias_resolution_init_source(env): class Example(BaseSettings): model_config = SettingsConfigDict(env_prefix='PREFIX') name: str last_name: str = Field(validation_alias=AliasChoices('PREFIX_LAST_NAME', 'PREFIX_SURNAME')) env.set('PREFIX_SURNAME', 'smith') assert Example(name='john', PREFIX_SURNAME='doe').model_dump() == {'name': 'john', 'last_name': 'doe'} class Settings(BaseSettings): NAME: str = Field( default='', validation_alias=AliasChoices('NAME', 'OLD_NAME'), ) @model_validator(mode='before') def check_for_deprecated_attributes(cls, data: Any) -> Any: if isinstance(data, dict): old_keys = {k for k in data.keys() if k.startswith('OLD_')} assert not old_keys return data s = Settings(NAME='foo') s.model_dump() == {'NAME': 'foo'} with pytest.raises(ValidationError, match="Assertion failed, assert not {'OLD_NAME'}"): Settings(OLD_NAME='foo') def test_init_kwargs_alias_resolution_deterministic(): class Example(BaseSettings): name: str last_name: str = Field(validation_alias=AliasChoices('surname', 'last_name')) result = Example(name='john', surname='doe', last_name='smith').model_dump() assert result == {'name': 'john', 'last_name': 'doe'} def test_alias_nested_model_default_partial_update(): class SubModel(BaseModel): v1: str = 'default' v2: bytes = b'hello' v3: int class Settings(BaseSettings): model_config = SettingsConfigDict( nested_model_default_partial_update=True, alias_generator=AliasGenerator(lambda s: s.replace('_', '-')) ) v0: str = 'ok' sub_model: SubModel = SubModel(v1='top default', v3=33) assert Settings(**{'sub-model': {'v1': 'cli'}}).model_dump() == { 'v0': 'ok', 'sub_model': {'v1': 'cli', 'v2': b'hello', 'v3': 33}, } def test_env_str(env): class Settings(BaseSettings): apple: str = Field(None, validation_alias='BOOM') env.set('BOOM', 'hello') assert Settings().apple == 'hello' def test_env_list(env): class Settings(BaseSettings): foobar: str = Field(validation_alias=AliasChoices('different1', 'different2')) env.set('different1', 'value 1') env.set('different2', 'value 2') s = Settings() assert s.foobar == 'value 1' def test_env_list_field(env): class Settings(BaseSettings): foobar: str = Field(validation_alias='foobar_env_name') env.set('FOOBAR_ENV_NAME', 'env value') s = Settings() assert s.foobar == 'env value' def test_env_list_last(env): class Settings(BaseSettings): foobar: str = Field(validation_alias=AliasChoices('different2')) env.set('different1', 'value 1') env.set('different2', 'value 2') s = Settings() assert s.foobar == 'value 2' def test_env_inheritance_field(env): class SettingsParent(BaseSettings): foobar: str = Field('parent default', validation_alias='foobar_env') class SettingsChild(SettingsParent): foobar: str = 'child default' assert SettingsParent().foobar == 'parent default' assert SettingsChild().foobar == 'child default' assert SettingsChild(foobar='abc').foobar == 'abc' env.set('foobar_env', 'env value') assert SettingsParent().foobar == 'env value' assert SettingsChild().foobar == 'child default' assert SettingsChild(foobar='abc').foobar == 'abc' def test_env_inheritance_config(env): env.set('foobar', 'foobar') env.set('prefix_foobar', 'prefix_foobar') env.set('foobar_parent_from_field', 'foobar_parent_from_field') env.set('prefix_foobar_parent_from_field', 'prefix_foobar_parent_from_field') env.set('foobar_parent_from_config', 'foobar_parent_from_config') env.set('foobar_child_from_config', 'foobar_child_from_config') env.set('foobar_child_from_field', 'foobar_child_from_field') # a. Child class config overrides prefix class Parent(BaseSettings): foobar: str = Field(None, validation_alias='foobar_parent_from_field') model_config = SettingsConfigDict(env_prefix='p_') class Child(Parent): model_config = SettingsConfigDict(env_prefix='prefix_') assert Child().foobar == 'foobar_parent_from_field' # b. Child class overrides field class Parent(BaseSettings): foobar: str = Field(None, validation_alias='foobar_parent_from_config') class Child(Parent): foobar: str = Field(None, validation_alias='foobar_child_from_config') assert Child().foobar == 'foobar_child_from_config' # . Child class overrides parent prefix and field class Parent(BaseSettings): foobar: str | None model_config = SettingsConfigDict(env_prefix='p_') class Child(Parent): foobar: str = Field(None, validation_alias='foobar_child_from_field') model_config = SettingsConfigDict(env_prefix='prefix_') assert Child().foobar == 'foobar_child_from_field' def test_invalid_validation_alias(env): with pytest.raises( TypeError, match='Invalid `validation_alias` type. it should be `str`, `AliasChoices`, or `AliasPath`' ): class Settings(BaseSettings): foobar: str = Field(validation_alias=123) def test_validation_aliases(env): class Settings(BaseSettings): foobar: str = Field('default value', validation_alias='foobar_alias') assert Settings().foobar == 'default value' assert Settings(foobar_alias='42').foobar == '42' env.set('foobar_alias', 'xxx') assert Settings().foobar == 'xxx' assert Settings(foobar_alias='42').foobar == '42' def test_validation_aliases_alias_path(env): class Settings(BaseSettings): foobar: str = Field(validation_alias=AliasPath('foo', 'bar', 1)) env.set('foo', '{"bar": ["val0", "val1"]}') assert Settings().foobar == 'val1' def test_validation_aliases_alias_choices(env): class Settings(BaseSettings): foobar: str = Field(validation_alias=AliasChoices('foo', AliasPath('foo1', 'bar', 1), AliasPath('bar', 2))) env.set('foo', 'val1') assert Settings().foobar == 'val1' env.pop('foo') env.set('foo1', '{"bar": ["val0", "val2"]}') assert Settings().foobar == 'val2' env.pop('foo1') env.set('bar', '["val1", "val2", "val3"]') assert Settings().foobar == 'val3' def test_validation_alias_alias_choices_with_alias_path_first(env): """Test that AliasPath in AliasChoices doesn't interfere with env var lookup. Regression test for https://github.com/pydantic/pydantic-settings/issues/766 When AliasChoices has AliasPath as first choice, the env source should not use the AliasPath's first element as the key when a string alias matches. """ class Settings(BaseSettings): my_field: str = Field( default='default-value', validation_alias=AliasChoices(AliasPath('nested', 'key'), 'MY_FIELD'), ) # The env var MY_FIELD should be used, not 'nested' from the AliasPath env.set('MY_FIELD', 'env-value') assert Settings().my_field == 'env-value' def test_validation_alias_with_env_prefix(env): class Settings(BaseSettings): foobar: str = Field(validation_alias='foo') model_config = SettingsConfigDict(env_prefix='p_') env.set('p_foo', 'bar') with pytest.raises(ValidationError) as exc_info: Settings() assert exc_info.value.errors(include_url=False) == [ {'type': 'missing', 'loc': ('foo',), 'msg': 'Field required', 'input': {}} ] env.set('foo', 'bar') assert Settings().foobar == 'bar' @pytest.mark.parametrize('env_prefix_target', ['all', 'alias']) def test_validation_alias_with_env_prefix_and_env_prefix_target(env, env_prefix_target): class Settings(BaseSettings): foobar: str = Field(validation_alias='foo') model_config = SettingsConfigDict(env_prefix='p_', env_prefix_target=env_prefix_target) env.set('foo', 'bar') with pytest.raises(ValidationError) as exc_info: Settings() assert exc_info.value.errors(include_url=False) == [ {'type': 'missing', 'loc': ('foo',), 'msg': 'Field required', 'input': {}} ] env.set('p_foo', 'bar') assert Settings().foobar == 'bar' def test_case_sensitive(monkeypatch): class Settings(BaseSettings): foo: str model_config = SettingsConfigDict(case_sensitive=True) # Need to patch os.environ to get build to work on Windows, where os.environ is case insensitive monkeypatch.setattr(os, 'environ', value={'Foo': 'foo'}) with pytest.raises(ValidationError) as exc_info: Settings() assert exc_info.value.errors(include_url=False) == [ {'type': 'missing', 'loc': ('foo',), 'msg': 'Field required', 'input': {}} ] @pytest.mark.parametrize('env_nested_delimiter', [None, '']) def test_case_sensitive_no_nested_delimiter(monkeypatch, env_nested_delimiter): class Subsettings(BaseSettings): foo: str class Settings(BaseSettings): subsettings: Subsettings model_config = SettingsConfigDict(case_sensitive=True, env_nested_delimiter=env_nested_delimiter) # Need to patch os.environ to get build to work on Windows, where os.environ is case insensitive monkeypatch.setattr(os, 'environ', value={'subsettingsNonefoo': '1'}) with pytest.raises(ValidationError) as exc_info: Settings() assert exc_info.value.errors(include_url=False) == [ {'type': 'missing', 'loc': ('subsettings',), 'msg': 'Field required', 'input': {}} ] def test_nested_dataclass(env): @pydantic_dataclasses.dataclass class DeepNestedDataclass: boo: int rar: str @pydantic_dataclasses.dataclass class MyDataclass: foo: int bar: str deep: DeepNestedDataclass class Settings(BaseSettings, env_nested_delimiter='__'): n: MyDataclass env.set('N', '{"foo": 123, "bar": "bar value"}') env.set('N__DEEP', '{"boo": 1, "rar": "eek"}') s = Settings() assert isinstance(s.n, MyDataclass) assert s.n.foo == 123 assert s.n.bar == 'bar value' def test_nested_vanilla_dataclass(env): @dataclasses.dataclass class MyDataclass: value: str class NestedSettings(BaseSettings, MyDataclass): pass class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__') sub: NestedSettings env.set('SUB__VALUE', 'something') s = Settings() assert s.sub.value == 'something' def test_env_takes_precedence(env): class Settings(BaseSettings): foo: int bar: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return env_settings, init_settings env.set('BAR', 'env setting') s = Settings(foo='123', bar='argument') assert s.foo == 123 assert s.bar == 'env setting' def test_config_file_settings_nornir(env): """ See https://github.com/pydantic/pydantic/pull/341#issuecomment-450378771 """ def nornir_settings_source() -> dict[str, Any]: return {'param_a': 'config a', 'param_b': 'config b', 'param_c': 'config c'} class Settings(BaseSettings): param_a: str param_b: str param_c: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return env_settings, init_settings, nornir_settings_source env.set('PARAM_C', 'env setting c') s = Settings(param_b='argument b', param_c='argument c') assert s.param_a == 'config a' assert s.param_b == 'argument b' assert s.param_c == 'env setting c' def test_env_union_with_complex_subfields_parses_json(env): class A(BaseModel): a: str class B(BaseModel): b: int class Settings(BaseSettings): content: A | B | int env.set('content', '{"a": "test"}') s = Settings() assert s.content == A(a='test') def test_env_union_with_complex_subfields_parses_plain_if_json_fails(env): class A(BaseModel): a: str class B(BaseModel): b: int class Settings(BaseSettings): content: A | B | datetime env.set('content', '{"a": "test"}') s = Settings() assert s.content == A(a='test') env.set('content', '2020-07-05T00:00:00Z') s = Settings() assert s.content == datetime(2020, 7, 5, 0, 0, tzinfo=timezone.utc) def test_env_union_without_complex_subfields_does_not_parse_json(env): class Settings(BaseSettings): content: datetime | str env.set('content', '2020-07-05T00:00:00Z') s = Settings() assert s.content == '2020-07-05T00:00:00Z' test_env_file = """\ # this is a comment A=good string # another one, followed by whitespace b='better string' c="best string" """ def test_env_file_config(env, tmp_path): p = tmp_path / '.env' p.write_text(test_env_file) class Settings(BaseSettings): a: str b: str c: str model_config = SettingsConfigDict(env_file=p) env.set('A', 'overridden var') s = Settings() assert s.a == 'overridden var' assert s.b == 'better string' assert s.c == 'best string' prefix_test_env_file = """\ # this is a comment prefix_A=good string # another one, followed by whitespace prefix_b='better string' prefix_c="best string" """ def test_env_file_with_env_prefix(env, tmp_path): p = tmp_path / '.env' p.write_text(prefix_test_env_file) class Settings(BaseSettings): a: str b: str c: str model_config = SettingsConfigDict(env_file=p, env_prefix='prefix_') env.set('prefix_A', 'overridden var') s = Settings() assert s.a == 'overridden var' assert s.b == 'better string' assert s.c == 'best string' prefix_test_env_invalid_file = """\ # this is a comment prefix_A=good string # another one, followed by whitespace prefix_b='better string' prefix_c="best string" f="random value" """ def test_env_file_with_env_prefix_invalid(tmp_path): p = tmp_path / '.env' p.write_text(prefix_test_env_invalid_file) class Settings(BaseSettings): a: str b: str c: str model_config = SettingsConfigDict(env_file=p, env_prefix='prefix_') with pytest.raises(ValidationError) as exc_info: Settings() assert exc_info.value.errors(include_url=False) == [ {'type': 'extra_forbidden', 'loc': ('f',), 'msg': 'Extra inputs are not permitted', 'input': 'random value'} ] def test_ignore_env_file_with_env_prefix_invalid(tmp_path): p = tmp_path / '.env' p.write_text(prefix_test_env_invalid_file) class Settings(BaseSettings): a: str b: str c: str model_config = SettingsConfigDict(env_file=p, env_prefix='prefix_', extra='ignore') s = Settings() assert s.a == 'good string' assert s.b == 'better string' assert s.c == 'best string' def test_env_file_config_case_sensitive(tmp_path): p = tmp_path / '.env' p.write_text(test_env_file) class Settings(BaseSettings): a: str b: str c: str model_config = SettingsConfigDict(env_file=p, case_sensitive=True, extra='ignore') with pytest.raises(ValidationError) as exc_info: Settings() assert exc_info.value.errors(include_url=False) == [ { 'type': 'missing', 'loc': ('a',), 'msg': 'Field required', 'input': {'b': 'better string', 'c': 'best string', 'A': 'good string'}, } ] def test_env_file_export(env, tmp_path): p = tmp_path / '.env' p.write_text( """\ export A='good string' export B=better-string export C="best string" """ ) class Settings(BaseSettings): a: str b: str c: str model_config = SettingsConfigDict(env_file=p) env.set('A', 'overridden var') s = Settings() assert s.a == 'overridden var' assert s.b == 'better-string' assert s.c == 'best string' def test_env_file_export_validation_alias(env, tmp_path): p = tmp_path / '.env' p.write_text("""export a='{"b": ["1", "2"]}'""") class Settings(BaseSettings): a: str = Field(validation_alias=AliasChoices(AliasPath('a', 'b', 1))) model_config = SettingsConfigDict(env_file=p) s = Settings() assert s.a == '2' def test_env_file_config_custom_encoding(tmp_path): p = tmp_path / '.env' p.write_text('pika=p!±@', encoding='latin-1') class Settings(BaseSettings): pika: str model_config = SettingsConfigDict(env_file=p, env_file_encoding='latin-1') s = Settings() assert s.pika == 'p!±@' @pytest.fixture def home_tmp(tmp_path, env): env.set('HOME', str(tmp_path)) env.set('USERPROFILE', str(tmp_path)) env.set('HOMEPATH', str(tmp_path)) tmp_filename = f'{uuid.uuid4()}.env' home_tmp_path = tmp_path / tmp_filename yield home_tmp_path, tmp_filename home_tmp_path.unlink() def test_env_file_home_directory(home_tmp): home_tmp_path, tmp_filename = home_tmp home_tmp_path.write_text('pika=baz') class Settings(BaseSettings): pika: str model_config = SettingsConfigDict(env_file=f'~/{tmp_filename}') assert Settings().pika == 'baz' def test_env_file_none(tmp_path): p = tmp_path / '.env' p.write_text('a') class Settings(BaseSettings): a: str = 'xxx' s = Settings(_env_file=p) assert s.a == 'xxx' def test_env_file_override_file(tmp_path): p1 = tmp_path / '.env' p1.write_text(test_env_file) p2 = tmp_path / '.env.prod' p2.write_text('A="new string"') class Settings(BaseSettings): a: str model_config = SettingsConfigDict(env_file=str(p1)) s = Settings(_env_file=p2) assert s.a == 'new string' def test_env_file_override_none(tmp_path): p = tmp_path / '.env' p.write_text(test_env_file) class Settings(BaseSettings): a: str | None = None model_config = SettingsConfigDict(env_file=p) s = Settings(_env_file=None) assert s.a is None def test_env_file_not_a_file(env): class Settings(BaseSettings): a: str = None env.set('A', 'ignore non-file') s = Settings(_env_file='tests/') assert s.a == 'ignore non-file' def test_read_env_file_case_sensitive(tmp_path): p = tmp_path / '.env' p.write_text('a="test"\nB=123') assert DotEnvSettingsSource._static_read_env_file(p) == {'a': 'test', 'b': '123'} assert DotEnvSettingsSource._static_read_env_file(p, case_sensitive=True) == {'a': 'test', 'B': '123'} def test_read_env_file_syntax_wrong(tmp_path): p = tmp_path / '.env' p.write_text('NOT_AN_ASSIGNMENT') assert DotEnvSettingsSource._static_read_env_file(p, case_sensitive=True) == {'NOT_AN_ASSIGNMENT': None} def test_env_file_example(tmp_path): p = tmp_path / '.env' p.write_text( """\ # ignore comment ENVIRONMENT="production" REDIS_ADDRESS=localhost:6379 MEANING_OF_LIFE=42 MY_VAR='Hello world' """ ) class Settings(BaseSettings): environment: str redis_address: str meaning_of_life: int my_var: str s = Settings(_env_file=str(p)) assert s.model_dump() == { 'environment': 'production', 'redis_address': 'localhost:6379', 'meaning_of_life': 42, 'my_var': 'Hello world', } def test_env_file_custom_encoding(tmp_path): p = tmp_path / '.env' p.write_text('pika=p!±@', encoding='latin-1') class Settings(BaseSettings): pika: str with pytest.raises(UnicodeDecodeError): Settings(_env_file=str(p)) s = Settings(_env_file=str(p), _env_file_encoding='latin-1') assert s.model_dump() == {'pika': 'p!±@'} test_default_env_file = """\ debug_mode=true host=localhost Port=8000 """ test_prod_env_file = """\ debug_mode=false host=https://example.com/services """ def test_multiple_env_file(tmp_path): base_env = tmp_path / '.env' base_env.write_text(test_default_env_file) prod_env = tmp_path / '.env.prod' prod_env.write_text(test_prod_env_file) class Settings(BaseSettings): debug_mode: bool host: str port: int model_config = SettingsConfigDict(env_file=[base_env, prod_env]) s = Settings() assert s.debug_mode is False assert s.host == 'https://example.com/services' assert s.port == 8000 def test_model_env_file_override_model_config(tmp_path): base_env = tmp_path / '.env' base_env.write_text(test_default_env_file) prod_env = tmp_path / '.env.prod' prod_env.write_text(test_prod_env_file) class Settings(BaseSettings): debug_mode: bool host: str port: int model_config = SettingsConfigDict(env_file=prod_env) s = Settings(_env_file=base_env) assert s.debug_mode is True assert s.host == 'localhost' assert s.port == 8000 def test_multiple_env_file_encoding(tmp_path): base_env = tmp_path / '.env' base_env.write_text('pika=p!±@', encoding='latin-1') prod_env = tmp_path / '.env.prod' prod_env.write_text('pika=chu!±@', encoding='latin-1') class Settings(BaseSettings): pika: str s = Settings(_env_file=[base_env, prod_env], _env_file_encoding='latin-1') assert s.pika == 'chu!±@' def test_read_dotenv_vars(tmp_path): base_env = tmp_path / '.env' base_env.write_text(test_default_env_file) prod_env = tmp_path / '.env.prod' prod_env.write_text(test_prod_env_file) source = DotEnvSettingsSource( BaseSettings(), env_file=[base_env, prod_env], env_file_encoding='utf8', case_sensitive=False ) assert source._read_env_files() == { 'debug_mode': 'false', 'host': 'https://example.com/services', 'port': '8000', } source = DotEnvSettingsSource( BaseSettings(), env_file=[base_env, prod_env], env_file_encoding='utf8', case_sensitive=True ) assert source._read_env_files() == { 'debug_mode': 'false', 'host': 'https://example.com/services', 'Port': '8000', } def test_read_dotenv_vars_when_env_file_is_none(): assert ( DotEnvSettingsSource( BaseSettings(), env_file=None, env_file_encoding=None, case_sensitive=False )._read_env_files() == {} ) def test_dotenvsource_override(env): class StdinDotEnvSettingsSource(DotEnvSettingsSource): @override def _read_env_file(self, file_path: Path) -> dict[str, str]: assert str(file_path) == '-' return {'foo': 'stdin_foo', 'bar': 'stdin_bar'} @override def _read_env_files(self) -> dict[str, str]: return self._read_env_file(Path('-')) source = StdinDotEnvSettingsSource(BaseSettings()) assert source._read_env_files() == {'foo': 'stdin_foo', 'bar': 'stdin_bar'} # test that calling read_env_file issues a DeprecationWarning # TODO: remove this test once read_env_file is removed def test_read_env_file_deprecation(tmp_path): from pydantic_settings.sources import read_env_file base_env = tmp_path / '.env' base_env.write_text(test_default_env_file) with pytest.deprecated_call(): assert read_env_file(base_env) == { 'debug_mode': 'true', 'host': 'localhost', 'port': '8000', } def test_alias_set(env): class Settings(BaseSettings): foo: str = Field('default foo', validation_alias='foo_env') bar: str = 'bar default' assert Settings.model_fields['bar'].alias is None assert Settings.model_fields['bar'].validation_alias is None assert Settings.model_fields['foo'].alias is None assert Settings.model_fields['foo'].validation_alias == 'foo_env' class SubSettings(Settings): spam: str = 'spam default' assert SubSettings.model_fields['bar'].alias is None assert SubSettings.model_fields['bar'].validation_alias is None assert SubSettings.model_fields['foo'].alias is None assert SubSettings.model_fields['foo'].validation_alias == 'foo_env' assert SubSettings().model_dump() == {'foo': 'default foo', 'bar': 'bar default', 'spam': 'spam default'} env.set('foo_env', 'fff') assert SubSettings().model_dump() == {'foo': 'fff', 'bar': 'bar default', 'spam': 'spam default'} env.set('bar', 'bbb') assert SubSettings().model_dump() == {'foo': 'fff', 'bar': 'bbb', 'spam': 'spam default'} env.set('spam', 'sss') assert SubSettings().model_dump() == {'foo': 'fff', 'bar': 'bbb', 'spam': 'sss'} def test_prefix_on_parent(env): class MyBaseSettings(BaseSettings): var: str = 'old' class MySubSettings(MyBaseSettings): model_config = SettingsConfigDict(env_prefix='PREFIX_') assert MyBaseSettings().model_dump() == {'var': 'old'} assert MySubSettings().model_dump() == {'var': 'old'} env.set('PREFIX_VAR', 'new') assert MyBaseSettings().model_dump() == {'var': 'old'} assert MySubSettings().model_dump() == {'var': 'new'} def test_secrets_path(tmp_path): p = tmp_path / 'foo' p.write_text('foo_secret_value_str') class Settings(BaseSettings): foo: str model_config = SettingsConfigDict(secrets_dir=tmp_path) assert Settings().model_dump() == {'foo': 'foo_secret_value_str'} def test_secrets_path_multiple(tmp_path): d1 = tmp_path / 'dir1' d2 = tmp_path / 'dir2' d1.mkdir() d2.mkdir() (d1 / 'foo1').write_text('foo1_dir1_secret_value_str') (d1 / 'foo2').write_text('foo2_dir1_secret_value_str') (d2 / 'foo2').write_text('foo2_dir2_secret_value_str') (d2 / 'foo3').write_text('foo3_dir2_secret_value_str') class Settings(BaseSettings): foo1: str foo2: str foo3: str assert Settings(_secrets_dir=(d1, d2)).model_dump() == { 'foo1': 'foo1_dir1_secret_value_str', 'foo2': 'foo2_dir2_secret_value_str', # dir2 takes priority 'foo3': 'foo3_dir2_secret_value_str', } assert Settings(_secrets_dir=(d2, d1)).model_dump() == { 'foo1': 'foo1_dir1_secret_value_str', 'foo2': 'foo2_dir1_secret_value_str', # dir1 takes priority 'foo3': 'foo3_dir2_secret_value_str', } def test_secrets_path_with_validation_alias(tmp_path): p = tmp_path / 'foo' p.write_text('{"bar": ["test"]}') class Settings(BaseSettings): foo: str = Field(validation_alias=AliasChoices(AliasPath('foo', 'bar', 0))) model_config = SettingsConfigDict(secrets_dir=tmp_path) assert Settings().model_dump() == {'foo': 'test'} def test_secrets_case_sensitive(tmp_path): (tmp_path / 'SECRET_VAR').write_text('foo_env_value_str') class Settings(BaseSettings): secret_var: str | None = None model_config = SettingsConfigDict(secrets_dir=tmp_path, case_sensitive=True) assert Settings().model_dump() == {'secret_var': None} def test_secrets_case_insensitive(tmp_path): (tmp_path / 'SECRET_VAR').write_text('foo_env_value_str') class Settings(BaseSettings): secret_var: str | None model_config = SettingsConfigDict(secrets_dir=tmp_path, case_sensitive=False) settings = Settings().model_dump() assert settings == {'secret_var': 'foo_env_value_str'} def test_secrets_path_url(tmp_path): (tmp_path / 'foo').write_text('http://www.example.com') (tmp_path / 'bar').write_text('snap') class Settings(BaseSettings): foo: HttpUrl bar: SecretStr model_config = SettingsConfigDict(secrets_dir=tmp_path) settings = Settings() assert str(settings.foo) == 'http://www.example.com/' assert settings.bar == SecretStr('snap') def test_secrets_path_json(tmp_path): p = tmp_path / 'foo' p.write_text('{"a": "b"}') class Settings(BaseSettings): foo: dict[str, str] model_config = SettingsConfigDict(secrets_dir=tmp_path) assert Settings().model_dump() == {'foo': {'a': 'b'}} def test_secrets_nested_optional_json(tmp_path): p = tmp_path / 'foo' p.write_text('{"a": 10}') class Foo(BaseModel): a: int class Settings(BaseSettings): foo: Foo | None = None model_config = SettingsConfigDict(secrets_dir=tmp_path) assert Settings().model_dump() == {'foo': {'a': 10}} def test_secrets_path_invalid_json(tmp_path): p = tmp_path / 'foo' p.write_text('{"a": "b"') class Settings(BaseSettings): foo: dict[str, str] model_config = SettingsConfigDict(secrets_dir=tmp_path) with pytest.raises(SettingsError, match='error parsing value for field "foo" from source "SecretsSettingsSource"'): Settings() def test_secrets_missing(tmp_path): class Settings(BaseSettings): foo: str bar: list[str] model_config = SettingsConfigDict(secrets_dir=tmp_path) with pytest.raises(ValidationError) as exc_info: Settings() assert exc_info.value.errors(include_url=False) == [ {'type': 'missing', 'loc': ('foo',), 'msg': 'Field required', 'input': {}}, {'input': {}, 'loc': ('bar',), 'msg': 'Field required', 'type': 'missing'}, ] def test_secrets_invalid_secrets_dir(tmp_path): p1 = tmp_path / 'foo' p1.write_text('foo_secret_value_str') class Settings(BaseSettings): foo: str model_config = SettingsConfigDict(secrets_dir=p1) with pytest.raises(SettingsError, match='secrets_dir must reference a directory, not a file'): Settings() def test_secrets_invalid_secrets_dir_multiple_all(tmp_path): class Settings(BaseSettings): foo: str (d1 := tmp_path / 'dir1').write_text('') (d2 := tmp_path / 'dir2').write_text('') with pytest.raises(SettingsError, match='secrets_dir must reference a directory, not a file'): Settings(_secrets_dir=[d1, d2]) def test_secrets_invalid_secrets_dir_multiple_one(tmp_path): class Settings(BaseSettings): foo: str (d1 := tmp_path / 'dir1').mkdir() (d2 := tmp_path / 'dir2').write_text('') with pytest.raises(SettingsError, match='secrets_dir must reference a directory, not a file'): Settings(_secrets_dir=[d1, d2]) @pytest.mark.skipif(sys.platform.startswith('win'), reason='windows paths break regex') def test_secrets_missing_location(tmp_path): class Settings(BaseSettings): model_config = SettingsConfigDict(secrets_dir=tmp_path / 'does_not_exist') with pytest.warns(UserWarning, match=f'directory "{tmp_path}/does_not_exist" does not exist'): Settings() @pytest.mark.skipif(sys.platform.startswith('win'), reason='windows paths break regex') def test_secrets_missing_location_multiple_all(tmp_path): class Settings(BaseSettings): foo: str | None = None with pytest.warns() as record: Settings(_secrets_dir=[tmp_path / 'dir1', tmp_path / 'dir2']) assert len(record) == 2 assert record[0].category is UserWarning and record[1].category is UserWarning assert str(record[0].message) == f'directory "{tmp_path}/dir1" does not exist' assert str(record[1].message) == f'directory "{tmp_path}/dir2" does not exist' @pytest.mark.skipif(sys.platform.startswith('win'), reason='windows paths break regex') def test_secrets_missing_location_multiple_one(tmp_path): class Settings(BaseSettings): foo: str | None = None (d1 := tmp_path / 'dir1').mkdir() (d1 / 'foo').write_text('secret_value') with pytest.warns(UserWarning, match=f'directory "{tmp_path}/dir2" does not exist'): conf = Settings(_secrets_dir=[d1, tmp_path / 'dir2']) assert conf.foo == 'secret_value' # value obtained from first directory @pytest.mark.skipif(sys.platform.startswith('win'), reason='windows paths break regex') def test_secrets_file_is_a_directory(tmp_path): p1 = tmp_path / 'foo' p1.mkdir() class Settings(BaseSettings): foo: str | None = None model_config = SettingsConfigDict(secrets_dir=tmp_path) with pytest.warns( UserWarning, match=f'attempted to load secret file "{tmp_path}/foo" but found a directory instead' ): Settings() @pytest.mark.skipif(sys.platform.startswith('win'), reason='windows paths break regex') def test_secrets_file_is_a_directory_multiple_all(tmp_path): class Settings(BaseSettings): foo: str | None = None (d1 := tmp_path / 'dir1').mkdir() (d2 := tmp_path / 'dir2').mkdir() (d1 / 'foo').mkdir() (d2 / 'foo').mkdir() with pytest.warns() as record: Settings(_secrets_dir=[d1, d2]) assert len(record) == 2 assert record[0].category is UserWarning and record[1].category is UserWarning # warnings are emitted in reverse order assert str(record[0].message) == f'attempted to load secret file "{d2}/foo" but found a directory instead.' assert str(record[1].message) == f'attempted to load secret file "{d1}/foo" but found a directory instead.' @pytest.mark.skipif(sys.platform.startswith('win'), reason='windows paths break regex') def test_secrets_file_is_a_directory_multiple_one(tmp_path): class Settings(BaseSettings): foo: str | None = None (d1 := tmp_path / 'dir1').mkdir() (d2 := tmp_path / 'dir2').mkdir() (d1 / 'foo').write_text('secret_value') (d2 / 'foo').mkdir() with pytest.warns(UserWarning, match=f'attempted to load secret file "{d2}/foo" but found a directory instead.'): conf = Settings(_secrets_dir=[d1, d2]) assert conf.foo == 'secret_value' # value obtained from first directory def test_secrets_dotenv_precedence(tmp_path): s = tmp_path / 'foo' s.write_text('foo_secret_value_str') e = tmp_path / '.env' e.write_text('foo=foo_env_value_str') class Settings(BaseSettings): foo: str model_config = SettingsConfigDict(secrets_dir=tmp_path) assert Settings(_env_file=e).model_dump() == {'foo': 'foo_env_value_str'} def test_external_settings_sources_precedence(env): def external_source_0() -> dict[str, str]: return {'apple': 'value 0', 'banana': 'value 2'} def external_source_1() -> dict[str, str]: return {'apple': 'value 1', 'raspberry': 'value 3'} class Settings(BaseSettings): apple: str banana: str raspberry: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( init_settings, env_settings, dotenv_settings, file_secret_settings, external_source_0, external_source_1, ) env.set('banana', 'value 1') assert Settings().model_dump() == {'apple': 'value 0', 'banana': 'value 1', 'raspberry': 'value 3'} def test_external_settings_sources_filter_env_vars(): vault_storage = {'user:password': {'apple': 'value 0', 'banana': 'value 2'}} class VaultSettingsSource(PydanticBaseSettingsSource): def __init__(self, settings_cls: type[BaseSettings], user: str, password: str): self.user = user self.password = password super().__init__(settings_cls) def get_field_value(self, field: FieldInfo, field_name: str) -> Any: pass def __call__(self) -> dict[str, str]: vault_vars = vault_storage[f'{self.user}:{self.password}'] return { field_name: vault_vars[field_name] for field_name in self.settings_cls.model_fields.keys() if field_name in vault_vars } class Settings(BaseSettings): apple: str banana: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( init_settings, env_settings, dotenv_settings, file_secret_settings, VaultSettingsSource(settings_cls, user='user', password='password'), ) assert Settings().model_dump() == {'apple': 'value 0', 'banana': 'value 2'} def test_customise_sources_empty(): class Settings(BaseSettings): apple: str = 'default' banana: str = 'default' @classmethod def settings_customise_sources(cls, *args, **kwargs): return () assert Settings().model_dump() == {'apple': 'default', 'banana': 'default'} assert Settings(apple='xxx').model_dump() == {'apple': 'default', 'banana': 'default'} def test_builtins_settings_source_repr(): assert ( repr(DefaultSettingsSource(BaseSettings, nested_model_default_partial_update=True)) == 'DefaultSettingsSource(nested_model_default_partial_update=True)' ) assert ( repr(InitSettingsSource(BaseSettings, init_kwargs={'apple': 'value 0', 'banana': 'value 1'})) == "InitSettingsSource(init_kwargs={'apple': 'value 0', 'banana': 'value 1'})" ) assert ( repr(EnvSettingsSource(BaseSettings, env_nested_delimiter='__')) == "EnvSettingsSource(env_nested_delimiter='__', env_prefix_len=0)" ) assert repr(DotEnvSettingsSource(BaseSettings, env_file='.env', env_file_encoding='utf-8')) == ( "DotEnvSettingsSource(env_file='.env', env_file_encoding='utf-8', env_nested_delimiter=None, env_prefix_len=0)" ) assert ( repr(SecretsSettingsSource(BaseSettings, secrets_dir='/secrets')) == "SecretsSettingsSource(secrets_dir='/secrets')" ) def _parse_custom_dict(value: str) -> Callable[[str], dict[int, str]]: """A custom parsing function passed into env parsing test.""" res = {} for part in value.split(','): k, v = part.split('=') res[int(k)] = v return res class CustomEnvSettingsSource(EnvSettingsSource): def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: if not value: return None return _parse_custom_dict(value) def test_env_setting_source_custom_env_parse(env): class Settings(BaseSettings): top: dict[int, str] @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (CustomEnvSettingsSource(settings_cls),) with pytest.raises(ValidationError): Settings() env.set('top', '1=apple,2=banana') s = Settings() assert s.top == {1: 'apple', 2: 'banana'} class BadCustomEnvSettingsSource(EnvSettingsSource): def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: """A custom parsing function passed into env parsing test.""" return int(value) def test_env_settings_source_custom_env_parse_is_bad(env): class Settings(BaseSettings): top: dict[int, str] @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (BadCustomEnvSettingsSource(settings_cls),) env.set('top', '1=apple,2=banana') with pytest.raises( SettingsError, match='error parsing value for field "top" from source "BadCustomEnvSettingsSource"' ): Settings() class CustomSecretsSettingsSource(SecretsSettingsSource): def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: if not value: return None return _parse_custom_dict(value) def test_secret_settings_source_custom_env_parse(tmp_path): p = tmp_path / 'top' p.write_text('1=apple,2=banana') class Settings(BaseSettings): top: dict[int, str] model_config = SettingsConfigDict(secrets_dir=tmp_path) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (CustomSecretsSettingsSource(settings_cls, tmp_path),) s = Settings() assert s.top == {1: 'apple', 2: 'banana'} class BadCustomSettingsSource(EnvSettingsSource): def get_field_value(self, field: FieldInfo, field_name: str) -> Any: raise ValueError('Error') def test_custom_source_get_field_value_error(env): class Settings(BaseSettings): top: dict[int, str] @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (BadCustomSettingsSource(settings_cls),) with pytest.raises( SettingsError, match='error getting value for field "top" from source "BadCustomSettingsSource"' ): Settings() def test_nested_env_complex_values(env): class SubSubModel(BaseSettings): dvals: dict class SubModel(BaseSettings): vals: list[str] sub_sub_model: SubSubModel class Cfg(BaseSettings): sub_model: SubModel model_config = SettingsConfigDict(env_prefix='cfg_', env_nested_delimiter='__') env.set('cfg_sub_model__vals', '["one", "two"]') env.set('cfg_sub_model__sub_sub_model__dvals', '{"three": 4}') assert Cfg().model_dump() == {'sub_model': {'vals': ['one', 'two'], 'sub_sub_model': {'dvals': {'three': 4}}}} env.set('cfg_sub_model__vals', 'invalid') with pytest.raises( SettingsError, match='error parsing value for field "sub_model" from source "EnvSettingsSource"' ): Cfg() def test_nested_env_nonexisting_field(env): class SubModel(BaseSettings): vals: list[str] class Cfg(BaseSettings): sub_model: SubModel model_config = SettingsConfigDict(env_prefix='cfg_', env_nested_delimiter='__') env.set('cfg_sub_model__foo_vals', '[]') with pytest.raises(ValidationError): Cfg() def test_nested_env_nonexisting_field_deep(env): class SubModel(BaseSettings): vals: list[str] class Cfg(BaseSettings): sub_model: SubModel model_config = SettingsConfigDict(env_prefix='cfg_', env_nested_delimiter='__') env.set('cfg_sub_model__vals__foo__bar__vals', '[]') with pytest.raises(ValidationError): Cfg() def test_nested_env_union_complex_values(env): class SubModel(BaseSettings): vals: list[str] | dict[str, str] class Cfg(BaseSettings): sub_model: SubModel model_config = SettingsConfigDict(env_prefix='cfg_', env_nested_delimiter='__') env.set('cfg_sub_model__vals', '["one", "two"]') assert Cfg().model_dump() == {'sub_model': {'vals': ['one', 'two']}} env.set('cfg_sub_model__vals', '{"three": "four"}') assert Cfg().model_dump() == {'sub_model': {'vals': {'three': 'four'}}} env.set('cfg_sub_model__vals', 'stringval') with pytest.raises(ValidationError): Cfg() env.set('cfg_sub_model__vals', '{"invalid": dict}') with pytest.raises(ValidationError): Cfg() def test_discriminated_union_with_callable_discriminator(env): class A(BaseModel): x: Literal['a'] = 'a' y: str class B(BaseModel): x: Literal['b'] = 'b' z: str def get_discriminator_value(v: Any) -> Hashable: if isinstance(v, dict): v0 = v.get('x') else: v0 = getattr(v, 'x', None) if v0 == 'a': return 'a' elif v0 == 'b': return 'b' else: return None class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__') # Discriminated union using a callable discriminator. a_or_b: Annotated[Annotated[A, Tag('a')] | Annotated[B, Tag('b')], Discriminator(get_discriminator_value)] # Set up environment so that the discriminator is 'a'. env.set('a_or_b__x', 'a') env.set('a_or_b__y', 'foo') s = Settings() assert s.a_or_b.x == 'a' assert s.a_or_b.y == 'foo' def test_json_field_with_discriminated_union(env): class A(BaseModel): x: Literal['a'] = 'a' class B(BaseModel): x: Literal['b'] = 'b' A_OR_B = Annotated[A | B, Field(discriminator='x')] class Settings(BaseSettings): a_or_b: Json[A_OR_B] | None = None # Set up environment so that the discriminator is 'a'. env.set('a_or_b', '{"x": "a"}') s = Settings() assert s.a_or_b.x == 'a' def test_nested_model_case_insensitive(env): class SubSubSub(BaseModel): VaL3: str val4: str = Field(validation_alias='VAL4') class SubSub(BaseModel): Val2: str SUB_sub_SuB: SubSubSub class Sub(BaseModel): VAL1: str SUB_sub: SubSub class Settings(BaseSettings): nested: Sub model_config = SettingsConfigDict(env_nested_delimiter='__') env.set('nested', '{"val1": "v1", "sub_SUB": {"VAL2": "v2", "sub_SUB_sUb": {"vAl3": "v3", "VAL4": "v4"}}}') s = Settings() assert s.nested.VAL1 == 'v1' assert s.nested.SUB_sub.Val2 == 'v2' assert s.nested.SUB_sub.SUB_sub_SuB.VaL3 == 'v3' assert s.nested.SUB_sub.SUB_sub_SuB.val4 == 'v4' def test_dotenv_extra_allow(tmp_path): p = tmp_path / '.env' p.write_text('a=b\nx=y') class Settings(BaseSettings): a: str model_config = SettingsConfigDict(env_file=p, extra='allow') s = Settings() assert s.a == 'b' assert s.x == 'y' def test_dotenv_extra_forbid(tmp_path): p = tmp_path / '.env' p.write_text('a=b\nx=y') class Settings(BaseSettings): a: str model_config = SettingsConfigDict(env_file=p, extra='forbid') with pytest.raises(ValidationError) as exc_info: Settings() assert exc_info.value.errors(include_url=False) == [ {'type': 'extra_forbidden', 'loc': ('x',), 'msg': 'Extra inputs are not permitted', 'input': 'y'} ] def test_dotenv_extra_case_insensitive(tmp_path): p = tmp_path / '.env' p.write_text('a=b') class Settings(BaseSettings): A: str model_config = SettingsConfigDict(env_file=p, extra='forbid') s = Settings() assert s.A == 'b' def test_dotenv_extra_sub_model_case_insensitive(tmp_path): p = tmp_path / '.env' p.write_text('a=b\nSUB_model={"v": "v1"}') class SubModel(BaseModel): v: str class Settings(BaseSettings): A: str sub_MODEL: SubModel model_config = SettingsConfigDict(env_file=p, extra='forbid') s = Settings() assert s.A == 'b' assert s.sub_MODEL.v == 'v1' def test_nested_bytes_field(env): class SubModel(BaseModel): v1: str v2: bytes class Settings(BaseSettings): v0: str sub_model: SubModel model_config = SettingsConfigDict(env_nested_delimiter='__', env_prefix='TEST_') env.set('TEST_V0', 'v0') env.set('TEST_SUB_MODEL__V1', 'v1') env.set('TEST_SUB_MODEL__V2', 'v2') s = Settings() assert s.v0 == 'v0' assert s.sub_model.v1 == 'v1' assert s.sub_model.v2 == b'v2' def test_protected_namespace_defaults(): # pydantic default with pytest.warns( UserWarning, match=( 'Field "model_dump_prefixed_field" in Model has conflict with protected namespace "model_dump"|' r"Field 'model_dump_prefixed_field' in 'Model' conflicts with protected namespace 'model_dump'\..*" ), ): class Model(BaseSettings): model_dump_prefixed_field: str # pydantic-settings default with pytest.warns( UserWarning, match=( 'Field "settings_customise_sources_prefixed_field" in Model1 has conflict with protected namespace "settings_customise_sources"|' r"Field 'settings_customise_sources_prefixed_field' in 'Model1' conflicts with protected namespace 'settings_customise_sources'\..*" ), ): class Model1(BaseSettings): settings_customise_sources_prefixed_field: str with pytest.raises( (NameError, ValueError), match=( r'Field (["\'])settings_customise_sources\1 conflicts with member > " r'of protected namespace \1settings_customise_sources\1\.' ), ): class Model2(BaseSettings): settings_customise_sources: str def test_case_sensitive_from_args(monkeypatch): class Settings(BaseSettings): foo: str # Need to patch os.environ to get build to work on Windows, where os.environ is case insensitive monkeypatch.setattr(os, 'environ', value={'Foo': 'foo'}) with pytest.raises(ValidationError) as exc_info: Settings(_case_sensitive=True) assert exc_info.value.errors(include_url=False) == [ {'type': 'missing', 'loc': ('foo',), 'msg': 'Field required', 'input': {}} ] def test_env_prefix_from_args(env): class Settings(BaseSettings): apple: str env.set('foobar_apple', 'has_prefix') s = Settings(_env_prefix='foobar_') assert s.apple == 'has_prefix' def test_env_json_field(env): class Settings(BaseSettings): x: Json env.set('x', '{"foo": "bar"}') s = Settings() assert s.x == {'foo': 'bar'} env.set('x', 'test') with pytest.raises(ValidationError) as exc_info: Settings() assert exc_info.value.errors(include_url=False) == [ { 'type': 'json_invalid', 'loc': ('x',), 'msg': 'Invalid JSON: expected ident at line 1 column 2', 'input': 'test', 'ctx': {'error': 'expected ident at line 1 column 2'}, } ] def test_env_parse_enums(env): class NestedEnum(BaseModel): fruit: FruitsEnum class Settings(BaseSettings, env_nested_delimiter='__'): fruit: FruitsEnum union_fruit: int | FruitsEnum | None = None nested: NestedEnum with pytest.raises(ValidationError) as exc_info: env.set('FRUIT', 'kiwi') env.set('UNION_FRUIT', 'kiwi') env.set('NESTED__FRUIT', 'kiwi') s = Settings() assert exc_info.value.errors(include_url=False) == [ { 'type': 'enum', 'loc': ('fruit',), 'msg': 'Input should be 0, 1 or 2', 'input': 'kiwi', 'ctx': {'expected': '0, 1 or 2'}, }, { 'input': 'kiwi', 'loc': ( 'union_fruit', 'int', ), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'type': 'int_parsing', }, { 'ctx': { 'expected': '0, 1 or 2', }, 'input': 'kiwi', 'loc': ( 'union_fruit', 'int-enum[FruitsEnum]', ), 'msg': 'Input should be 0, 1 or 2', 'type': 'enum', }, { 'ctx': { 'expected': '0, 1 or 2', }, 'input': 'kiwi', 'loc': ( 'nested', 'fruit', ), 'msg': 'Input should be 0, 1 or 2', 'type': 'enum', }, ] env.set('FRUIT', str(FruitsEnum.lime.value)) env.set('UNION_FRUIT', str(FruitsEnum.lime.value)) env.set('NESTED__FRUIT', str(FruitsEnum.lime.value)) s = Settings() assert s.fruit == FruitsEnum.lime assert s.union_fruit == FruitsEnum.lime assert s.nested.fruit == FruitsEnum.lime env.set('FRUIT', 'kiwi') env.set('UNION_FRUIT', 'kiwi') env.set('NESTED__FRUIT', 'kiwi') s = Settings(_env_parse_enums=True) assert s.fruit == FruitsEnum.kiwi assert s.union_fruit == FruitsEnum.kiwi assert s.nested.fruit == FruitsEnum.kiwi env.set('FRUIT', str(FruitsEnum.lime.value)) env.set('UNION_FRUIT', str(FruitsEnum.lime.value)) env.set('NESTED__FRUIT', str(FruitsEnum.lime.value)) s = Settings(_env_parse_enums=True) assert s.fruit == FruitsEnum.lime assert s.union_fruit == FruitsEnum.lime assert s.nested.fruit == FruitsEnum.lime def test_env_parse_none_str(env): env.set('x', 'null') env.set('y', 'y_override') class Settings(BaseSettings): x: str | None = 'x_default' y: str | None = 'y_default' s = Settings() assert s.x == 'null' assert s.y == 'y_override' s = Settings(_env_parse_none_str='null') assert s.x is None assert s.y == 'y_override' env.set('nested__x', 'None') env.set('nested__y', 'y_override') env.set('nested__deep__z', 'None') class NestedBaseModel(BaseModel): x: str | None = 'x_default' y: str | None = 'y_default' deep: dict | None = {'z': 'z_default'} keep: dict | None = {'z': 'None'} class NestedSettings(BaseSettings, env_nested_delimiter='__'): nested: NestedBaseModel | None = NestedBaseModel() s = NestedSettings() assert s.nested.x == 'None' assert s.nested.y == 'y_override' assert s.nested.deep['z'] == 'None' assert s.nested.keep['z'] == 'None' s = NestedSettings(_env_parse_none_str='None') assert s.nested.x is None assert s.nested.y == 'y_override' assert s.nested.deep['z'] is None assert s.nested.keep['z'] == 'None' env.set('nested__deep', 'None') with pytest.raises(ValidationError): s = NestedSettings() s = NestedSettings(_env_parse_none_str='None') assert s.nested.x is None assert s.nested.y == 'y_override' assert s.nested.deep['z'] is None assert s.nested.keep['z'] == 'None' env.pop('nested__deep__z') with pytest.raises(ValidationError): s = NestedSettings() s = NestedSettings(_env_parse_none_str='None') assert s.nested.x is None assert s.nested.y == 'y_override' assert s.nested.deep is None assert s.nested.keep['z'] == 'None' def test_env_json_field_dict(env): class Settings(BaseSettings): x: Json[dict[str, int]] env.set('x', '{"foo": 1}') s = Settings() assert s.x == {'foo': 1} env.set('x', '{"foo": "bar"}') with pytest.raises(ValidationError) as exc_info: Settings() assert exc_info.value.errors(include_url=False) == [ { 'type': 'int_parsing', 'loc': ('x', 'foo'), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'bar', } ] def test_custom_env_source_default_values_from_config(): class CustomEnvSettingsSource(EnvSettingsSource): pass class Settings(BaseSettings): foo: str = 'test' model_config = SettingsConfigDict(env_prefix='prefix_', case_sensitive=True) s = Settings() assert s.model_config['env_prefix'] == 'prefix_' assert s.model_config['case_sensitive'] is True c = CustomEnvSettingsSource(Settings) assert c.env_prefix == 'prefix_' assert c.case_sensitive is True def test_model_config_through_class_kwargs(env): class Settings(BaseSettings, env_prefix='foobar_', title='Test Settings Model'): apple: str assert Settings.model_config['title'] == 'Test Settings Model' # pydantic config assert Settings.model_config['env_prefix'] == 'foobar_' # pydantic-settings config assert Settings.model_json_schema()['title'] == 'Test Settings Model' env.set('foobar_apple', 'has_prefix') s = Settings() assert s.apple == 'has_prefix' def test_root_model_as_field(env): class Foo(BaseModel): x: int y: dict[str, int] FooRoot = RootModel[list[Foo]] class Settings(BaseSettings): z: FooRoot env.set('z', '[{"x": 1, "y": {"foo": 1}}, {"x": 2, "y": {"foo": 2}}]') s = Settings() assert s.model_dump() == {'z': [{'x': 1, 'y': {'foo': 1}}, {'x': 2, 'y': {'foo': 2}}]} def test_str_based_root_model(env): """Testing to pass string directly to root model.""" class Foo(RootModel[str]): root: str class Settings(BaseSettings): foo: Foo plain: str TEST_STR = 'hello world' env.set('foo', TEST_STR) env.set('plain', TEST_STR) s = Settings() assert s.model_dump() == {'foo': TEST_STR, 'plain': TEST_STR} def test_path_based_root_model(env): """Testing to pass path directly to root model.""" class Foo(RootModel[pathlib.PurePosixPath]): root: pathlib.PurePosixPath class Settings(BaseSettings): foo: Foo plain: pathlib.PurePosixPath TEST_PATH: str = '/hello/world' env.set('foo', TEST_PATH) env.set('plain', TEST_PATH) s = Settings() assert s.model_dump() == { 'foo': pathlib.PurePosixPath(TEST_PATH), 'plain': pathlib.PurePosixPath(TEST_PATH), } def test_optional_field_from_env(env): class Settings(BaseSettings): x: str | None = None env.set('x', '123') s = Settings() assert s.x == '123' def test_dotenv_optional_json_field(tmp_path): p = tmp_path / '.env' p.write_text("""DATA='{"foo":"bar"}'""") class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=p) data: Json[dict[str, str]] | None = Field(default=None) s = Settings() assert s.data == {'foo': 'bar'} def test_dotenv_with_alias_and_env_prefix(tmp_path): p = tmp_path / '.env' p.write_text('xxx__foo=1\nxxx__bar=2') class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=p, env_prefix='xxx__') foo: str = '' bar_alias: str = Field('', validation_alias='xxx__bar') s = Settings() assert s.model_dump() == {'foo': '1', 'bar_alias': '2'} class Settings1(BaseSettings): model_config = SettingsConfigDict(env_file=p, env_prefix='xxx__') foo: str = '' bar_alias: str = Field('', alias='bar') with pytest.raises(ValidationError) as exc_info: Settings1() assert exc_info.value.errors(include_url=False) == [ {'type': 'extra_forbidden', 'loc': ('xxx__bar',), 'msg': 'Extra inputs are not permitted', 'input': '2'} ] def test_dotenv_with_alias_and_env_prefix_nested(tmp_path): p = tmp_path / '.env' p.write_text('xxx__bar=0\nxxx__nested__a=1\nxxx__nested__b=2') class NestedSettings(BaseModel): a: str = 'a' b: str = 'b' class Settings(BaseSettings): model_config = SettingsConfigDict(env_prefix='xxx__', env_nested_delimiter='__', env_file=p) foo: str = '' bar_alias: str = Field('', alias='xxx__bar') nested_alias: NestedSettings = Field(default_factory=NestedSettings, alias='xxx__nested') s = Settings() assert s.model_dump() == {'foo': '', 'bar_alias': '0', 'nested_alias': {'a': '1', 'b': '2'}} def test_dotenv_with_extra_and_env_prefix(tmp_path): p = tmp_path / '.env' p.write_text('xxx__foo=1\nxxx__extra_var=extra_value') class Settings(BaseSettings): model_config = SettingsConfigDict(extra='allow', env_file=p, env_prefix='xxx__') foo: str = '' s = Settings() assert s.model_dump() == {'foo': '1', 'extra_var': 'extra_value'} def test_nested_field_with_alias_init_source(): class NestedSettings(BaseModel): foo: str = Field(alias='fooAlias') class Settings(BaseSettings): nested_foo: NestedSettings s = Settings(nested_foo=NestedSettings(fooAlias='EXAMPLE')) assert s.model_dump() == {'nested_foo': {'foo': 'EXAMPLE'}} def test_nested_models_as_dict_value(env): class NestedSettings(BaseModel): foo: dict[str, int] class Settings(BaseSettings): nested: NestedSettings sub_dict: dict[str, NestedSettings] model_config = SettingsConfigDict(env_nested_delimiter='__') env.set('nested__foo', '{"a": 1}') env.set('sub_dict__bar__foo', '{"b": 2}') s = Settings() assert s.model_dump() == {'nested': {'foo': {'a': 1}}, 'sub_dict': {'bar': {'foo': {'b': 2}}}} def test_env_nested_dict_value(env): class Settings(BaseSettings): nested: dict[str, dict[str, dict[str, str]]] model_config = SettingsConfigDict(env_nested_delimiter='__') env.set('nested__foo__a__b', 'bar') s = Settings() assert s.model_dump() == {'nested': {'foo': {'a': {'b': 'bar'}}}} def test_nested_models_leaf_vs_deeper_env_dict_assumed(env): class NestedSettings(BaseModel): foo: str class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__') nested: NestedSettings env.set('nested__foo', 'string') env.set( 'nested__foo__bar', 'this should not be evaluated, since foo is a string by annotation and not a dict', ) env.set( 'nested__foo__bar__baz', 'one more', ) s = Settings() assert s.model_dump() == {'nested': {'foo': 'string'}} def test_case_insensitive_nested_optional(env): class NestedSettings(BaseModel): FOO: str BaR: int class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__', case_sensitive=False) nested: NestedSettings | None env.set('nested__FoO', 'string') env.set('nested__bar', '123') s = Settings() assert s.model_dump() == {'nested': {'BaR': 123, 'FOO': 'string'}} def test_case_insensitive_nested_alias(env): """Ensure case-insensitive environment lookup works with nested aliases.""" class NestedSettings(BaseModel): FOO: str = Field(..., alias='Foo') BaR: int class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__', case_sensitive=False) nEstEd: NestedSettings = Field(..., alias='NesTed') env.set('nested__FoO', 'string') env.set('nested__bar', '123') s = Settings() assert s.model_dump() == {'nEstEd': {'BaR': 123, 'FOO': 'string'}} def test_case_insensitive_nested_list(env): class NestedSettings(BaseModel): FOO: list[str] class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__', case_sensitive=False) nested: NestedSettings | None env.set('nested__FOO', '["string1", "string2"]') s = Settings() assert s.model_dump() == {'nested': {'FOO': ['string1', 'string2']}} def test_settings_source_current_state(env): class SettingsSource(PydanticBaseSettingsSource): def get_field_value(self, field: FieldInfo, field_name: str) -> Any: pass def __call__(self) -> dict[str, Any]: current_state = self.current_state if current_state.get('one') == '1': return {'two': '1'} return {} class Settings(BaseSettings): one: bool = False two: bool = False @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (env_settings, SettingsSource(settings_cls)) env.set('one', '1') s = Settings() assert s.two is True def test_settings_source_settings_sources_data(env): class SettingsSource(PydanticBaseSettingsSource): def get_field_value(self, field: FieldInfo, field_name: str) -> Any: pass def __call__(self) -> dict[str, Any]: settings_sources_data = self.settings_sources_data if settings_sources_data == { 'InitSettingsSource': {'one': True, 'two': True}, 'EnvSettingsSource': {'one': '1'}, 'function_settings_source': {'three': 'false'}, }: return {'four': '1'} return {} def function_settings_source(): return {'three': 'false'} class Settings(BaseSettings): one: bool = False two: bool = False three: bool = False four: bool = False @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (env_settings, init_settings, function_settings_source, SettingsSource(settings_cls)) env.set('one', '1') s = Settings(one=True, two=True) assert s.four is True def test_dotenv_extra_allow_similar_fields(tmp_path): p = tmp_path / '.env' p.write_text('POSTGRES_USER=postgres\nPOSTGRES_USER_2=postgres2\nPOSTGRES_NAME=name') class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=p, extra='allow') POSTGRES_USER: str s = Settings() assert s.POSTGRES_USER == 'postgres' assert s.model_dump() == {'POSTGRES_USER': 'postgres', 'postgres_name': 'name', 'postgres_user_2': 'postgres2'} def test_annotation_is_complex_root_model_check(): """Test for https://github.com/pydantic/pydantic-settings/issues/390""" class Settings(BaseSettings): foo: list[str] = [] Settings() def test_nested_model_field_with_alias(env): class NestedSettings(BaseModel): foo: list[str] = Field(alias='fooalias') class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__') nested: NestedSettings env.set('nested__fooalias', '["one", "two"]') s = Settings() assert s.model_dump() == {'nested': {'foo': ['one', 'two']}} def test_nested_model_field_with_alias_case_sensitive(monkeypatch): class NestedSettings(BaseModel): foo: list[str] = Field(alias='fooAlias') class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__', case_sensitive=True) nested: NestedSettings # Need to patch os.environ to get build to work on Windows, where os.environ is case insensitive monkeypatch.setattr(os, 'environ', value={'nested__fooalias': '["one", "two"]'}) with pytest.raises(ValidationError) as exc_info: Settings() assert exc_info.value.errors(include_url=False) == [ { 'type': 'missing', 'loc': ('nested', 'fooAlias'), 'msg': 'Field required', 'input': {'fooalias': '["one", "two"]'}, } ] monkeypatch.setattr(os, 'environ', value={'nested__fooAlias': '["one", "two"]'}) s = Settings() assert s.model_dump() == {'nested': {'foo': ['one', 'two']}} def test_nested_model_field_with_alias_choices(env): class NestedSettings(BaseModel): foo: list[str] = Field(alias=AliasChoices('fooalias', 'foo-alias')) class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__') nested: NestedSettings env.set('nested__fooalias', '["one", "two"]') s = Settings() assert s.model_dump() == {'nested': {'foo': ['one', 'two']}} def test_dotenv_optional_nested(tmp_path): p = tmp_path / '.env' p.write_text('not_nested=works\nNESTED__A=fails\nNESTED__b=2') class NestedSettings(BaseModel): A: str b: int class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=p, env_nested_delimiter='__', extra='forbid', ) not_nested: str NESTED: NestedSettings | None s = Settings() assert s.model_dump() == {'not_nested': 'works', 'NESTED': {'A': 'fails', 'b': 2}} def test_dotenv_env_prefix_env_without_prefix(tmp_path): p = tmp_path / '.env' p.write_text('test_foo=test-foo\nfoo=foo') class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=p, env_prefix='TEST_', extra='ignore', ) foo: str s = Settings() assert s.model_dump() == {'foo': 'test-foo'} def test_dotenv_env_prefix_env_without_prefix_ignored(tmp_path): p = tmp_path / '.env' p.write_text('foo=foo') class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=p, env_prefix='TEST_', extra='ignore', ) foo: str = '' s = Settings() assert s.model_dump() == {'foo': ''} def test_nested_model_dotenv_env_prefix_env_without_prefix_ignored(tmp_path): p = tmp_path / '.env' p.write_text('foo__val=1') class Foo(BaseModel): val: int = 0 class Settings(BaseSettings): model_config = SettingsConfigDict( env_nested_delimiter='__', env_file=p, env_prefix='TEST_', extra='ignore', ) foo: Foo = Foo() s = Settings() assert s.model_dump() == {'foo': {'val': 0}} def test_dotenv_env_prefix_env_with_alias_without_prefix(tmp_path): p = tmp_path / '.env' p.write_text('FooAlias=foo') class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=p, env_prefix='TEST_', extra='ignore', ) foo: str = Field('xxx', alias='FooAlias') s = Settings() assert s.model_dump() == {'foo': 'foo'} @pytest.mark.parametrize('env_prefix_target', ['all', 'alias']) def test_dotenv_env_prefix_env_with_alias_with_prefix(tmp_path, env_prefix_target): p = tmp_path / '.env' p.write_text('TEST_FooAlias=foo') class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=p, env_prefix_target=env_prefix_target, env_prefix='TEST_', extra='ignore', ) foo: str = Field('xxx', alias='FooAlias') s = Settings() assert s.model_dump() == {'foo': 'foo'} def test_parsing_secret_field(env): class Settings(BaseSettings): foo: Secret[int] bar: Secret[PostgresDsn] env.set('foo', '123') env.set('bar', 'postgres://user:password@localhost/dbname') s = Settings() assert s.foo.get_secret_value() == 123 assert s.bar.get_secret_value() == PostgresDsn('postgres://user:password@localhost/dbname') def test_field_annotated_no_decode(env): class Settings(BaseSettings): a: list[str] # this field will be decoded because of default `enable_decoding=True` b: Annotated[list[str], NoDecode] # decode the value here. the field value won't be decoded because of NoDecode @field_validator('b', mode='before') @classmethod def decode_b(cls, v: str) -> list[str]: return json.loads(v) env.set('a', '["one", "two"]') env.set('b', '["1", "2"]') s = Settings() assert s.model_dump() == {'a': ['one', 'two'], 'b': ['1', '2']} def test_field_annotated_no_decode_and_disable_decoding(env): class Settings(BaseSettings): model_config = SettingsConfigDict(enable_decoding=False) a: Annotated[list[str], NoDecode] # decode the value here. the field value won't be decoded because of NoDecode @field_validator('a', mode='before') @classmethod def decode_b(cls, v: str) -> list[str]: return json.loads(v) env.set('a', '["one", "two"]') s = Settings() assert s.model_dump() == {'a': ['one', 'two']} def test_field_annotated_disable_decoding(env): class Settings(BaseSettings): model_config = SettingsConfigDict(enable_decoding=False) a: list[str] # decode the value here. the field value won't be decoded because of `enable_decoding=False` @field_validator('a', mode='before') @classmethod def decode_b(cls, v: str) -> list[str]: return json.loads(v) env.set('a', '["one", "two"]') s = Settings() assert s.model_dump() == {'a': ['one', 'two']} def test_field_annotated_force_decode_disable_decoding(env): class Settings(BaseSettings): model_config = SettingsConfigDict(enable_decoding=False) a: Annotated[list[str], ForceDecode] env.set('a', '["one", "two"]') s = Settings() assert s.model_dump() == {'a': ['one', 'two']} def test_warns_if_config_keys_are_set_but_source_is_missing(): class Settings(BaseSettings): model_config = SettingsConfigDict( json_file='config.json', pyproject_toml_depth=2, toml_file='config.toml', yaml_file='config.yaml', yaml_config_section='myapp', ) with pytest.warns() as record: Settings() assert len(record) == 5 key_class_pairs = [ ('json_file', 'JsonConfigSettingsSource'), ('pyproject_toml_depth', 'PyprojectTomlConfigSettingsSource'), ('toml_file', 'TomlConfigSettingsSource'), ('yaml_file', 'YamlConfigSettingsSource'), ('yaml_config_section', 'YamlConfigSettingsSource'), ] for warning, key_class_pair in zip(record, key_class_pairs): assert warning.category is UserWarning expected_message = ( f'Config key `{key_class_pair[0]}` is set in model_config but will be ignored because no ' f'{key_class_pair[1]} source is configured. To use this config key, add a {key_class_pair[1]} ' f'source to the settings sources via the settings_customise_sources hook.' ) assert warning.message.args[0] == expected_message def test_env_strict_coercion(env): class SubModel(BaseModel): my_str: str my_int: int class Settings(BaseSettings, env_nested_delimiter='__'): my_str: str my_int: int sub_model: SubModel env.set('MY_STR', '0') env.set('MY_INT', '0') env.set('SUB_MODEL__MY_STR', '1') env.set('SUB_MODEL__MY_INT', '1') Settings().model_dump() == { 'my_str': '0', 'my_int': 0, 'sub_model': { 'my_str': '1', 'my_int': 1, }, } class StrictSettings(BaseSettings, env_nested_delimiter='__', strict=True): my_str: str my_int: int sub_model: SubModel StrictSettings().model_dump() == { 'my_str': '0', 'my_int': 0, 'sub_model': { 'my_str': '1', 'my_int': 1, }, } def test_env_source_when_load_multi_nested_config(env): class EmbeddingModel(BaseModel): model: str = 'text-embedding-3-small' keys: list[str] = Field(default_factory=list) class LLM(BaseModel): embeddings: dict[str, EmbeddingModel] = Field(default_factory=dict) class LLMSettings(BaseSettings): llm: LLM = Field(default_factory=lambda: LLM()) model_config = SettingsConfigDict(env_prefix='my_prefix_', env_nested_delimiter='__') env.set('my_prefix_llm__embeddings__openai__keys', '["sk-..."]') env.set('my_prefix_llm__embeddings__qwen__keys', '["sk-..."]') llm_setting = LLMSettings() assert llm_setting.llm.embeddings['openai'].keys == ['sk-...'] assert llm_setting.llm.embeddings['qwen'].keys == ['sk-...'] pydantic-pydantic-settings-198e71c/tests/test_source_aws_secrets_manager.py000066400000000000000000000125141514433345000275130ustar00rootroot00000000000000""" Test pydantic_settings.AWSSecretsManagerSettingsSource. """ import json import os import pytest try: import yaml from moto import mock_aws except ImportError: yaml = None mock_aws = None from pydantic import BaseModel, Field from pydantic_settings import ( AWSSecretsManagerSettingsSource, BaseSettings, PydanticBaseSettingsSource, ) from pydantic_settings.sources.providers.aws import import_aws_secrets_manager try: aws_secrets_manager = True import_aws_secrets_manager() import boto3 os.environ['AWS_DEFAULT_REGION'] = os.environ.get('AWS_DEFAULT_REGION', 'us-east-1') except ImportError: aws_secrets_manager = False MODULE = 'pydantic_settings.sources' if not yaml: pytest.skip('PyYAML is not installed', allow_module_level=True) @pytest.mark.skipif(not aws_secrets_manager, reason='pydantic-settings[aws-secrets-manager] is not installed') class TestAWSSecretsManagerSettingsSource: """Test AWSSecretsManagerSettingsSource.""" @mock_aws def test_repr(self) -> None: client = boto3.client('secretsmanager') client.create_secret(Name='test-secret', SecretString='{}') source = AWSSecretsManagerSettingsSource(BaseSettings, 'test-secret') assert repr(source) == "AWSSecretsManagerSettingsSource(secret_id='test-secret', env_nested_delimiter='--')" @mock_aws def test___init__(self) -> None: """Test __init__.""" class AWSSecretsManagerSettings(BaseSettings): """AWSSecretsManager settings.""" client = boto3.client('secretsmanager') client.create_secret(Name='test-secret', SecretString='{}') AWSSecretsManagerSettingsSource(AWSSecretsManagerSettings, 'test-secret') @mock_aws def test___call__(self) -> None: """Test __call__.""" class SqlServer(BaseModel): password: str = Field(..., alias='Password') class AWSSecretsManagerSettings(BaseSettings): """AWSSecretsManager settings.""" sql_server_user: str = Field(..., alias='SqlServerUser') sql_server: SqlServer = Field(..., alias='SqlServer') secret_data = {'SqlServerUser': 'test-user', 'SqlServer--Password': 'test-password'} client = boto3.client('secretsmanager') client.create_secret(Name='test-secret', SecretString=json.dumps(secret_data)) obj = AWSSecretsManagerSettingsSource(AWSSecretsManagerSettings, 'test-secret') settings = obj() assert settings['SqlServerUser'] == 'test-user' assert settings['SqlServer']['Password'] == 'test-password' @mock_aws def test_secret_manager_case_insensitive_success(self) -> None: """Test secret manager getitem case insensitive success.""" class SqlServer(BaseModel): password: str = Field(..., alias='Password') class AWSSecretsManagerSettings(BaseSettings): """AWSSecretsManager settings.""" sql_server_user: str sql_server: SqlServer @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (AWSSecretsManagerSettingsSource(settings_cls, 'test-secret', case_sensitive=False),) secret_data = { 'SQL_SERVER_USER': 'test-user', 'SQL_SERVER--PASSWORD': 'test-password', } client = boto3.client('secretsmanager') client.create_secret(Name='test-secret', SecretString=json.dumps(secret_data)) settings = AWSSecretsManagerSettings() # type: ignore assert settings.sql_server_user == 'test-user' assert settings.sql_server.password == 'test-password' @mock_aws def test_aws_secrets_manager_settings_source(self) -> None: """Test AWSSecretsManagerSettingsSource.""" class SqlServer(BaseModel): password: str = Field(..., alias='Password') class AWSSecretsManagerSettings(BaseSettings): """AWSSecretsManager settings.""" sql_server_user: str = Field(..., alias='SqlServerUser') sql_server: SqlServer = Field(..., alias='SqlServer') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (AWSSecretsManagerSettingsSource(settings_cls, 'test-secret'),) secret_data = {'SqlServerUser': 'test-user', 'SqlServer--Password': 'test-password'} client = boto3.client('secretsmanager') client.create_secret(Name='test-secret', SecretString=json.dumps(secret_data)) settings = AWSSecretsManagerSettings() # type: ignore assert settings.sql_server_user == 'test-user' assert settings.sql_server.password == 'test-password' pydantic-pydantic-settings-198e71c/tests/test_source_azure_key_vault.py000066400000000000000000000311221514433345000267040ustar00rootroot00000000000000""" Test pydantic_settings.AzureKeyVaultSettingsSource. """ import pytest from pydantic import BaseModel, Field, ValidationError from pytest_mock import MockerFixture from pydantic_settings import ( AzureKeyVaultSettingsSource, BaseSettings, PydanticBaseSettingsSource, ) from pydantic_settings.sources.providers.azure import import_azure_key_vault try: azure_key_vault = True import_azure_key_vault() from azure.core.exceptions import ResourceNotFoundError from azure.identity import DefaultAzureCredential from azure.keyvault.secrets import KeyVaultSecret, SecretClient, SecretProperties except ImportError: azure_key_vault = False @pytest.mark.skipif(not azure_key_vault, reason='pydantic-settings[azure-key-vault] is not installed') class TestAzureKeyVaultSettingsSource: """Test AzureKeyVaultSettingsSource.""" def test___init__(self, mocker: MockerFixture) -> None: """Test __init__.""" class AzureKeyVaultSettings(BaseSettings): """AzureKeyVault settings.""" mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.list_properties_of_secrets.__qualname__}', return_value=[], ) AzureKeyVaultSettingsSource( AzureKeyVaultSettings, 'https://my-resource.vault.azure.net/', DefaultAzureCredential() ) def test___call__(self, mocker: MockerFixture) -> None: """Test __call__.""" class SqlServer(BaseModel): password: str = Field(..., alias='Password') class AzureKeyVaultSettings(BaseSettings): """AzureKeyVault settings.""" SqlServerUser: str sql_server_user: str = Field(..., alias='SqlServerUser') sql_server: SqlServer = Field(..., alias='SqlServer') expected_secrets = [ type('', (), {'name': 'SqlServerUser', 'enabled': True}), type('', (), {'name': 'SqlServer--Password', 'enabled': True}), ] expected_secret_value = 'SecretValue' mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.list_properties_of_secrets.__qualname__}', return_value=expected_secrets, ) mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.get_secret.__qualname__}', side_effect=self._raise_resource_not_found_when_getting_parent_secret_name, ) obj = AzureKeyVaultSettingsSource( AzureKeyVaultSettings, 'https://my-resource.vault.azure.net/', DefaultAzureCredential() ) settings = obj() assert settings['SqlServerUser'] == expected_secret_value assert settings['SqlServer']['Password'] == expected_secret_value def test_do_not_load_disabled_secrets(self, mocker: MockerFixture) -> None: class AzureKeyVaultSettings(BaseSettings): """AzureKeyVault settings.""" SqlServerPassword: str DisabledSqlServerPassword: str disabled_secret_name = 'SqlServerPassword' expected_secrets = [ type('', (), {'name': disabled_secret_name, 'enabled': False}), ] mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.list_properties_of_secrets.__qualname__}', return_value=expected_secrets, ) mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.get_secret.__qualname__}', return_value=KeyVaultSecret(SecretProperties(), 'SecretValue'), ) obj = AzureKeyVaultSettingsSource( AzureKeyVaultSettings, 'https://my-resource.vault.azure.net/', DefaultAzureCredential() ) settings = obj() assert disabled_secret_name not in settings def test_azure_key_vault_settings_source(self, mocker: MockerFixture) -> None: """Test AzureKeyVaultSettingsSource.""" class SqlServer(BaseModel): password: str = Field(..., alias='Password') class AzureKeyVaultSettings(BaseSettings): """AzureKeyVault settings.""" SqlServerUser: str sql_server_user: str = Field(..., alias='SqlServerUser') sql_server: SqlServer = Field(..., alias='SqlServer') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( AzureKeyVaultSettingsSource( settings_cls, 'https://my-resource.vault.azure.net/', DefaultAzureCredential() ), ) expected_secrets = [ type('', (), {'name': 'SqlServerUser', 'enabled': True}), type('', (), {'name': 'SqlServer--Password', 'enabled': True}), ] expected_secret_value = 'SecretValue' mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.list_properties_of_secrets.__qualname__}', return_value=expected_secrets, ) mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.get_secret.__qualname__}', side_effect=self._raise_resource_not_found_when_getting_parent_secret_name, ) settings = AzureKeyVaultSettings() # type: ignore assert settings.SqlServerUser == expected_secret_value assert settings.sql_server_user == expected_secret_value assert settings.sql_server.password == expected_secret_value def _raise_resource_not_found_when_getting_parent_secret_name(self, secret_name: str): expected_secret_value = 'SecretValue' key_vault_secret = KeyVaultSecret(SecretProperties(), expected_secret_value) if secret_name == 'SqlServer': raise ResourceNotFoundError() return key_vault_secret def test_dash_to_underscore_translation(self, mocker: MockerFixture) -> None: """Test that dashes in secret names are mapped to underscores in field names.""" class AzureKeyVaultSettings(BaseSettings): my_field: str alias_field: str = Field(..., alias='Secret-Alias') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( AzureKeyVaultSettingsSource( settings_cls, 'https://my-resource.vault.azure.net/', DefaultAzureCredential(), dash_to_underscore=True, ), ) expected_secrets = [ type('', (), {'name': 'my-field', 'enabled': True}), type('', (), {'name': 'Secret-Alias', 'enabled': True}), ] expected_secret_value = 'SecretValue' mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.list_properties_of_secrets.__qualname__}', return_value=expected_secrets, ) mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.get_secret.__qualname__}', return_value=KeyVaultSecret(SecretProperties(), expected_secret_value), ) settings = AzureKeyVaultSettings() assert settings.my_field == expected_secret_value assert settings.alias_field == expected_secret_value @pytest.mark.parametrize( 'env_prefix', (None, 'singlewordprefix', 'prefix-kebab-case-', 'PrefixPascalCaseprefixCamelCaseSeparator-') ) def test_snake_case_conversion(self, mocker: MockerFixture, env_prefix: str | None) -> None: """Test that secret names are mapped to snake case in field names.""" class NestedModel(BaseModel): nested_field: str class AzureKeyVaultSettings(BaseSettings): my_field_from_kebab_case: str my_field_from_pascal_case: str my_field_from_camel_case: str alias_field: str = Field(alias='Secret-Alias') alias_field_2: str = Field(alias='another-SECRET-AliaS') nested_model: NestedModel @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( AzureKeyVaultSettingsSource( settings_cls, 'https://my-resource.vault.azure.net/', DefaultAzureCredential(), snake_case_conversion=True, env_prefix=env_prefix, ), ) expected_secrets = [ type( '', (), {'name': f'{"" if env_prefix is None else env_prefix}my-field-from-kebab-case', 'enabled': True} ), type('', (), {'name': f'{"" if env_prefix is None else env_prefix}MyFieldFromPascalCase', 'enabled': True}), type('', (), {'name': f'{"" if env_prefix is None else env_prefix}myFieldFromCamelCase', 'enabled': True}), type('', (), {'name': 'Secret-Alias', 'enabled': True}), type('', (), {'name': 'another-SECRET-AliaS', 'enabled': True}), type( '', (), {'name': f'{"" if env_prefix is None else env_prefix}NestedModel--NestedField', 'enabled': True} ), ] expected_secret_value = 'SecretValue' mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.list_properties_of_secrets.__qualname__}', return_value=expected_secrets, ) mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.get_secret.__qualname__}', return_value=KeyVaultSecret(SecretProperties(), expected_secret_value), ) settings = AzureKeyVaultSettings() assert settings.my_field_from_kebab_case == expected_secret_value assert settings.my_field_from_pascal_case == expected_secret_value assert settings.my_field_from_camel_case == expected_secret_value assert settings.alias_field == expected_secret_value assert settings.alias_field_2 == expected_secret_value assert settings.nested_model.nested_field == expected_secret_value def test_snake_case_conversion_missing_alias(self, mocker: MockerFixture) -> None: class AzureKeyVaultSettings(BaseSettings): my_field_from_kebab_case: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( AzureKeyVaultSettingsSource( settings_cls, 'https://my-resource.vault.azure.net/', DefaultAzureCredential(), snake_case_conversion=True, env_prefix='some-prefix', ), ) expected_secrets = [ type('', (), {'name': 'my-field-from-kebab-case', 'enabled': True}), ] expected_secret_value = 'SecretValue' mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.list_properties_of_secrets.__qualname__}', return_value=expected_secrets, ) mocker.patch( f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.get_secret.__qualname__}', return_value=KeyVaultSecret(SecretProperties(), expected_secret_value), ) with pytest.raises(ValidationError): AzureKeyVaultSettings() pydantic-pydantic-settings-198e71c/tests/test_source_cli.py000066400000000000000000003132171514433345000242520ustar00rootroot00000000000000import argparse import asyncio import re import sys import time import typing from enum import IntEnum from pathlib import Path, PureWindowsPath from typing import Annotated, Any, Dict, Generic, List, Literal, Tuple, TypeVar, Union # noqa: UP035 import pytest import typing_extensions from pydantic import ( AliasChoices, AliasGenerator, AliasPath, BaseModel, ConfigDict, DirectoryPath, Discriminator, Field, RootModel, Tag, ValidationError, field_validator, model_validator, ) from pydantic import ( dataclasses as pydantic_dataclasses, ) from pydantic._internal._repr import Representation from pydantic_settings import ( BaseSettings, CliApp, ForceDecode, NoDecode, PydanticBaseSettingsSource, SettingsConfigDict, SettingsError, ) from pydantic_settings.sources import ( CLI_SUPPRESS, CliDualFlag, CliExplicitFlag, CliImplicitFlag, CliMutuallyExclusiveGroup, CliPositionalArg, CliSettingsSource, CliSubCommand, CliSuppress, CliToggleFlag, CliUnknownArgs, get_subcommand, ) ARGPARSE_OPTIONS_TEXT = 'options' if sys.version_info >= (3, 10) else 'optional arguments' @pytest.fixture(autouse=True) def cli_test_env_autouse(cli_test_env): pass def foobar(a, b, c=4): pass class FruitsEnum(IntEnum): pear = 0 kiwi = 1 lime = 2 T = TypeVar('T') class LoggedVar(Generic[T]): def get(self) -> T: ... class SimpleSettings(BaseSettings): apple: str class SettingWithIgnoreEmpty(BaseSettings): apple: str = 'default' model_config = SettingsConfigDict(env_ignore_empty=True) class CliDummyArgGroup(BaseModel, arbitrary_types_allowed=True): group: argparse._ArgumentGroup def add_argument(self, *args: Any, **kwargs: Any) -> None: self.group.add_argument(*args, **kwargs) class CliDummySubParsers(BaseModel, arbitrary_types_allowed=True): sub_parser: argparse._SubParsersAction def add_parser(self, *args: Any, **kwargs: Any) -> 'CliDummyParser': return CliDummyParser(parser=self.sub_parser.add_parser(*args, **kwargs)) class CliDummyParser(BaseModel, arbitrary_types_allowed=True): parser: argparse.ArgumentParser = Field(default_factory=lambda: argparse.ArgumentParser()) def add_argument(self, *args: Any, **kwargs: Any) -> None: self.parser.add_argument(*args, **kwargs) def add_argument_group(self, *args: Any, **kwargs: Any) -> CliDummyArgGroup: return CliDummyArgGroup(group=self.parser.add_argument_group(*args, **kwargs)) def add_subparsers(self, *args: Any, **kwargs: Any) -> CliDummySubParsers: return CliDummySubParsers(sub_parser=self.parser.add_subparsers(*args, **kwargs)) def parse_args(self, *args: Any, **kwargs: Any) -> argparse.Namespace: return self.parser.parse_args(*args, **kwargs) def test_cli_validation_alias_with_cli_prefix(): class Settings(BaseSettings, cli_exit_on_error=False): foobar: str = Field(validation_alias='foo') model_config = SettingsConfigDict(cli_prefix='p') with pytest.raises(SettingsError, match='error parsing CLI: unrecognized arguments: --foo bar'): CliApp.run(Settings, cli_args=['--foo', 'bar']) assert CliApp.run(Settings, cli_args=['--p.foo', 'bar']).foobar == 'bar' @pytest.mark.parametrize( 'alias_generator', [ AliasGenerator(validation_alias=lambda s: AliasChoices(s, s.replace('_', '-'))), AliasGenerator(validation_alias=lambda s: AliasChoices(s.replace('_', '-'), s)), ], ) def test_cli_alias_resolution_consistency_with_env(env, alias_generator): class SubModel(BaseModel): v1: str = 'model default' class Settings(BaseSettings): model_config = SettingsConfigDict( env_nested_delimiter='__', nested_model_default_partial_update=True, alias_generator=alias_generator, ) sub_model: SubModel = SubModel(v1='top default') assert CliApp.run(Settings, cli_args=[]).model_dump() == {'sub_model': {'v1': 'top default'}} env.set('SUB_MODEL__V1', 'env default') assert CliApp.run(Settings, cli_args=[]).model_dump() == {'sub_model': {'v1': 'env default'}} assert CliApp.run(Settings, cli_args=['--sub-model.v1=cli default']).model_dump() == { 'sub_model': {'v1': 'cli default'} } def test_cli_nested_arg(): class SubSubValue(BaseModel): v6: str class SubValue(BaseModel): v4: str v5: int sub_sub: SubSubValue class TopValue(BaseModel): v1: str v2: str v3: str sub: SubValue class Cfg(BaseSettings): v0: str v0_union: SubValue | int top: TopValue args: list[str] = [] args += ['--top', '{"v1": "json-1", "v2": "json-2", "sub": {"v5": "xx"}}'] args += ['--top.sub.v5', '5'] args += ['--v0', '0'] args += ['--top.v2', '2'] args += ['--top.v3', '3'] args += ['--v0_union', '0'] args += ['--top.sub.sub_sub.v6', '6'] args += ['--top.sub.v4', '4'] cfg = CliApp.run(Cfg, cli_args=args) assert cfg.model_dump() == { 'v0': '0', 'v0_union': 0, 'top': { 'v1': 'json-1', 'v2': '2', 'v3': '3', 'sub': {'v4': '4', 'v5': 5, 'sub_sub': {'v6': '6'}}, }, } def test_cli_source_prioritization(env): class CfgDefault(BaseSettings): foo: str class CfgPrioritized(BaseSettings): foo: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return env_settings, CliSettingsSource(settings_cls, cli_parse_args=['--foo', 'FOO FROM CLI']) env.set('FOO', 'FOO FROM ENV') cfg = CliApp.run(CfgDefault, cli_args=['--foo', 'FOO FROM CLI']) assert cfg.model_dump() == {'foo': 'FOO FROM CLI'} cfg = CfgPrioritized() assert cfg.model_dump() == {'foo': 'FOO FROM ENV'} def test_cli_alias_subcommand_and_positional_args(capsys, monkeypatch): class SubCmd(BaseModel): pos_arg: CliPositionalArg[str] = Field(validation_alias='pos-arg') class Cfg(BaseSettings, cli_prog_name='example.py'): sub_cmd: CliSubCommand[SubCmd] = Field(validation_alias='sub-cmd') cfg = Cfg(**{'sub-cmd': {'pos-arg': 'howdy'}}) assert cfg.model_dump() == {'sub_cmd': {'pos_arg': 'howdy'}} cfg = CliApp.run(Cfg, cli_args=['sub-cmd', 'howdy']) assert cfg.model_dump() == {'sub_cmd': {'pos_arg': 'howdy'}} with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): CliApp.run(Cfg) assert ( capsys.readouterr().out == f"""usage: example.py [-h] {{sub-cmd}} ... {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit subcommands: {{sub-cmd}} sub-cmd """ ) m.setattr(sys, 'argv', ['example.py', 'sub-cmd', '--help']) with pytest.raises(SystemExit): CliApp.run(Cfg) assert ( capsys.readouterr().out == f"""usage: example.py sub-cmd [-h] POS-ARG positional arguments: POS-ARG {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit """ ) @pytest.mark.parametrize('avoid_json', [True, False]) def test_cli_alias_arg(capsys, monkeypatch, avoid_json): class Cfg(BaseSettings, cli_avoid_json=avoid_json): alias_choice_w_path: str = Field(validation_alias=AliasChoices('a', AliasPath('path0', 1))) alias_choice_w_only_path: str = Field(validation_alias=AliasChoices(AliasPath('path1', 1))) alias_choice_no_path: str = Field(validation_alias=AliasChoices('b', 'c')) alias_path: str = Field(validation_alias=AliasPath('path2', 'deep', 1)) alias_extra_deep: str = Field(validation_alias=AliasPath('path3', 'deep', 'extra', 'deep', 1)) alias_str: str = Field(validation_alias='str') cfg = CliApp.run( Cfg, cli_args=[ '-a', 'a', '-b', 'b', '--str', 'str', '--path0', 'a0,b0,c0', '--path1', 'a1,b1,c1', '--path2', '{"deep": ["a2","b2","c2"]}', '--path3', '{"deep": {"extra": {"deep": ["a3","b3","c3"]}}}', ], ) assert cfg.model_dump() == { 'alias_choice_w_path': 'a', 'alias_choice_w_only_path': 'b1', 'alias_choice_no_path': 'b', 'alias_path': 'b2', 'alias_extra_deep': 'b3', 'alias_str': 'str', } serialized_cli_args = CliApp.serialize(cfg) assert serialized_cli_args == [ '-a', 'a', '--path1', '["", "b1"]', '-b', 'b', '--path2', '{"deep": ["", "b2"]}', '--path3', '{"deep": {"extra": {"deep": ["", "b3"]}}}', '--str', 'str', ] assert CliApp.run(Cfg, cli_args=serialized_cli_args).model_dump() == cfg.model_dump() @pytest.mark.parametrize('avoid_json', [True, False]) def test_cli_alias_nested_arg(capsys, monkeypatch, avoid_json): class Nested(BaseModel): alias_choice_w_path: str = Field(validation_alias=AliasChoices('a', AliasPath('path0', 1))) alias_choice_w_only_path: str = Field(validation_alias=AliasChoices(AliasPath('path1', 1))) alias_choice_no_path: str = Field(validation_alias=AliasChoices('b', 'c')) alias_path: str = Field(validation_alias=AliasPath('path2', 'deep', 1)) alias_extra_deep: str = Field(validation_alias=AliasPath('path3', 'deep', 'extra', 'deep', 1)) alias_str: str = Field(validation_alias='str') class Cfg(BaseSettings, cli_avoid_json=avoid_json): nest: Nested cfg = CliApp.run( Cfg, cli_args=[ '--nest.a', 'a', '--nest.b', 'b', '--nest.str', 'str', '--nest.path0', '["a0","b0","c0"]', '--nest.path1', '["a1","b1","c1"]', '--nest.path2', '{"deep": ["a2","b2","c2"]}', '--nest.path3', '{"deep": {"extra": {"deep": ["a3","b3","c3"]}}}', ], ) assert cfg.model_dump() == { 'nest': { 'alias_choice_w_path': 'a', 'alias_choice_w_only_path': 'b1', 'alias_choice_no_path': 'b', 'alias_path': 'b2', 'alias_extra_deep': 'b3', 'alias_str': 'str', } } serialized_cli_args = CliApp.serialize(cfg) assert serialized_cli_args == [ '--nest.a', 'a', '--nest.path1', '["", "b1"]', '--nest.b', 'b', '--nest.path2', '{"deep": ["", "b2"]}', '--nest.path3', '{"deep": {"extra": {"deep": ["", "b3"]}}}', '--nest.str', 'str', ] assert CliApp.run(Cfg, cli_args=serialized_cli_args).model_dump() == cfg.model_dump() def test_cli_alias_exceptions(capsys, monkeypatch): with pytest.raises(SettingsError, match='subcommand argument BadCliSubCommand.foo has multiple aliases'): class SubCmd(BaseModel): v0: int class BadCliSubCommand(BaseSettings): foo: CliSubCommand[SubCmd] = Field(validation_alias=AliasChoices('bar', 'boo')) CliApp.run(BadCliSubCommand) with pytest.raises(SettingsError, match='positional argument BadCliPositionalArg.foo has multiple alias'): class BadCliPositionalArg(BaseSettings): foo: CliPositionalArg[int] = Field(validation_alias=AliasChoices('bar', 'boo')) CliApp.run(BadCliPositionalArg) def test_cli_case_insensitive_arg(): class Cfg(BaseSettings, cli_exit_on_error=False): foo: str = Field(validation_alias=AliasChoices('F', 'Foo')) bar: str = Field(validation_alias=AliasChoices('B', 'Bar')) cfg = CliApp.run( Cfg, cli_args=[ '--FOO=--VAL', '--BAR', '"--VAL"', ], ) assert cfg.model_dump() == {'foo': '--VAL', 'bar': '"--VAL"'} cfg = CliApp.run( Cfg, cli_args=[ '-f=-V', '-b', '"-V"', ], ) assert cfg.model_dump() == {'foo': '-V', 'bar': '"-V"'} cfg = Cfg(_cli_parse_args=['--Foo=--VAL', '--Bar', '"--VAL"'], _case_sensitive=True) assert cfg.model_dump() == {'foo': '--VAL', 'bar': '"--VAL"'} cfg = Cfg(_cli_parse_args=['-F=-V', '-B', '"-V"'], _case_sensitive=True) assert cfg.model_dump() == {'foo': '-V', 'bar': '"-V"'} with pytest.raises(SettingsError, match='error parsing CLI: unrecognized arguments: --FOO=--VAL --BAR "--VAL"'): Cfg(_cli_parse_args=['--FOO=--VAL', '--BAR', '"--VAL"'], _case_sensitive=True) with pytest.raises(SettingsError, match='error parsing CLI: unrecognized arguments: -f=-V -b "-V"'): Cfg(_cli_parse_args=['-f=-V', '-b', '"-V"'], _case_sensitive=True) with pytest.raises(SettingsError, match='Case-insensitive matching is only supported on the internal root parser'): CliSettingsSource(Cfg, root_parser=CliDummyParser(), case_sensitive=False) def test_cli_help_differentiation(capsys, monkeypatch): class Cfg(BaseSettings, cli_prog_name='example.py'): foo: str bar: int = 123 boo: int = Field(default_factory=lambda: 456) with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): CliApp.run(Cfg) assert ( re.sub(r'0x\w+', '0xffffffff', capsys.readouterr().out, flags=re.MULTILINE) == f"""usage: example.py [-h] [--foo str] [--bar int] [--boo int] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit --foo str (required) --bar int (default: 123) --boo int (default factory: ) """ ) def test_cli_help_string_format(capsys, monkeypatch): class Cfg(BaseSettings, cli_parse_args=True, cli_prog_name='example.py'): date_str: str = '%Y-%m-%d' class MultilineDoc(BaseSettings, cli_parse_args=True, cli_prog_name='example.py'): """ My Multiline Doc """ with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): Cfg() assert ( re.sub(r'0x\w+', '0xffffffff', capsys.readouterr().out, flags=re.MULTILINE) == f"""usage: example.py [-h] [--date_str str] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit --date_str str (default: %Y-%m-%d) """ ) with pytest.raises(SystemExit): MultilineDoc() assert ( capsys.readouterr().out == f"""usage: example.py [-h] My Multiline Doc {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit """ ) with pytest.raises(SystemExit): cli_settings_source = CliSettingsSource(MultilineDoc, formatter_class=argparse.HelpFormatter) MultilineDoc(_cli_settings_source=cli_settings_source(args=True)) assert ( capsys.readouterr().out == f"""usage: example.py [-h] My Multiline Doc {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit """ ) def test_cli_help_union_of_models(capsys, monkeypatch): class Cat(BaseModel): meow: str = 'meow' class Dog(BaseModel): bark: str = 'bark' class Bird(BaseModel): caww: str = 'caww' tweet: str class Tiger(Cat): roar: str = 'roar' class Car(BaseSettings, cli_parse_args=True, cli_prog_name='example.py'): driver: Cat | Dog | Bird = Tiger(meow='purr') with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): Car() assert ( capsys.readouterr().out == f"""usage: example.py [-h] [--driver [JSON]] [--driver.meow str] [--driver.bark str] [--driver.caww str] [--driver.tweet str] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit driver options: --driver [JSON] set driver from JSON string (default: {{}}) --driver.meow str (default: purr) --driver.bark str (default: bark) --driver.caww str (default: caww) --driver.tweet str (ifdef: required) """ ) def test_cli_help_default_or_none_model(capsys, monkeypatch): class DeeperSubModel(BaseModel): flag: bool class DeepSubModel(BaseModel): flag: bool deeper: DeeperSubModel | None = None class SubModel(BaseModel): flag: bool deep: DeepSubModel = DeepSubModel(flag=True) class Settings(BaseSettings, cli_parse_args=True, cli_prog_name='example.py'): flag: bool = True toggle: CliToggleFlag[bool] = True toggle_description: CliToggleFlag[bool] = Field(False, description='Bool Toggle') sub_model: SubModel = SubModel(flag=False) opt_model: DeepSubModel | None = Field(None, description='Group Doc') fact_model: SubModel = Field(default_factory=lambda: SubModel(flag=True)) with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): Settings() assert ( capsys.readouterr().out == f"""usage: example.py [-h] [--flag bool] [--no-toggle] [--toggle_description] [--sub_model [JSON]] [--sub_model.flag bool] [--sub_model.deep [JSON]] [--sub_model.deep.flag bool] [--sub_model.deep.deeper [{{JSON,null}}]] [--sub_model.deep.deeper.flag bool] [--opt_model [{{JSON,null}}]] [--opt_model.flag bool] [--opt_model.deeper [{{JSON,null}}]] [--opt_model.deeper.flag bool] [--fact_model [JSON]] [--fact_model.flag bool] [--fact_model.deep [JSON]] [--fact_model.deep.flag bool] [--fact_model.deep.deeper [{{JSON,null}}]] [--fact_model.deep.deeper.flag bool] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit --flag bool (default: True) --no-toggle --toggle_description Bool Toggle sub_model options: --sub_model [JSON] set sub_model from JSON string (default: {{}}) --sub_model.flag bool (default: False) sub_model.deep options: --sub_model.deep [JSON] set sub_model.deep from JSON string (default: {{}}) --sub_model.deep.flag bool (default: True) sub_model.deep.deeper options: default: null (undefined) --sub_model.deep.deeper [{{JSON,null}}] set sub_model.deep.deeper from JSON string (default: {{}}) --sub_model.deep.deeper.flag bool (ifdef: required) opt_model options: default: null (undefined) Group Doc --opt_model [{{JSON,null}}] set opt_model from JSON string (default: {{}}) --opt_model.flag bool (ifdef: required) opt_model.deeper options: default: null (undefined) --opt_model.deeper [{{JSON,null}}] set opt_model.deeper from JSON string (default: {{}}) --opt_model.deeper.flag bool (ifdef: required) fact_model options: --fact_model [JSON] set fact_model from JSON string (default: {{}}) --fact_model.flag bool (default factory: ) fact_model.deep options: --fact_model.deep [JSON] set fact_model.deep from JSON string (default: {{}}) --fact_model.deep.flag bool (default factory: ) fact_model.deep.deeper options: --fact_model.deep.deeper [{{JSON,null}}] set fact_model.deep.deeper from JSON string (default: {{}}) --fact_model.deep.deeper.flag bool (default factory: ) """ ) def test_cli_nested_dataclass_arg(): @pydantic_dataclasses.dataclass class MyDataclass: foo: int bar: str class Settings(BaseSettings): n: MyDataclass s = CliApp.run(Settings, cli_args=['--n.foo', '123', '--n.bar', 'bar value']) assert isinstance(s.n, MyDataclass) assert s.n.foo == 123 assert s.n.bar == 'bar value' def no_add_cli_arg_spaces(arg_str: str, has_quote_comma: bool = False) -> str: return arg_str def add_cli_arg_spaces(arg_str: str, has_quote_comma: bool = False) -> str: arg_str = arg_str.replace('[', ' [ ') arg_str = arg_str.replace(']', ' ] ') arg_str = arg_str.replace('{', ' { ') arg_str = arg_str.replace('}', ' } ') arg_str = arg_str.replace(':', ' : ') if not has_quote_comma: arg_str = arg_str.replace(',', ' , ') else: arg_str = arg_str.replace('",', '" , ') return f' {arg_str} ' @pytest.mark.parametrize('arg_spaces', [no_add_cli_arg_spaces, add_cli_arg_spaces]) @pytest.mark.parametrize('prefix', ['', 'child.']) def test_cli_list_arg(prefix, arg_spaces): class Obj(BaseModel): val: int class Child(BaseModel): num_list: list[int] | None = None obj_list: list[Obj] | None = None str_list: list[str] | None = None union_list: list[Obj | int] | None = None class Cfg(BaseSettings): num_list: list[int] | None = None obj_list: list[Obj] | None = None union_list: list[Obj | int] | None = None str_list: list[str] | None = None child: Child | None = None def check_answer(cfg, prefix, expected): if prefix: assert cfg.model_dump() == { 'num_list': None, 'obj_list': None, 'union_list': None, 'str_list': None, 'child': expected, } else: expected['child'] = None assert cfg.model_dump() == expected args: list[str] = [] args = [f'--{prefix}str_list', arg_spaces('["1","2"]')] args += [f'--{prefix}num_list', arg_spaces('["1","2"]')] args += [f'--{prefix}str_list', arg_spaces('"3","4"')] args += [f'--{prefix}num_list', arg_spaces('"3","4"')] args += [f'--{prefix}str_list', '"5"', f'--{prefix}str_list', '"6"'] args += [f'--{prefix}num_list', '"5"', f'--{prefix}num_list', '"6"'] cfg = CliApp.run(Cfg, cli_args=args) expected = { 'num_list': [1, 2, 3, 4, 5, 6], 'obj_list': None, 'union_list': None, 'str_list': ['1', '2', '3', '4', '5', '6'], } check_answer(cfg, prefix, expected) args = [arg.replace('"', '') for arg in args] cfg = CliApp.run(Cfg, cli_args=args) expected = { 'num_list': [1, 2, 3, 4, 5, 6], 'obj_list': None, 'union_list': None, 'str_list': ['1', '2', '3', '4', '5', '6'], } check_answer(cfg, prefix, expected) args = [f'--{prefix}obj_list', arg_spaces('[{"val":1},{"val":2}]')] args += [f'--{prefix}obj_list', arg_spaces('{"val":3},{"val":4}')] args += [f'--{prefix}obj_list', arg_spaces('{"val":5}'), f'--{prefix}obj_list', arg_spaces('{"val":6}')] cfg = CliApp.run(Cfg, cli_args=args) expected = { 'num_list': None, 'obj_list': [{'val': 1}, {'val': 2}, {'val': 3}, {'val': 4}, {'val': 5}, {'val': 6}], 'union_list': None, 'str_list': None, } check_answer(cfg, prefix, expected) args = [f'--{prefix}union_list', arg_spaces('[{"val":1},2]'), f'--{prefix}union_list', arg_spaces('[3,{"val":4}]')] args += [f'--{prefix}union_list', arg_spaces('{"val":5},6'), f'--{prefix}union_list', arg_spaces('7,{"val":8}')] args += [f'--{prefix}union_list', arg_spaces('{"val":9}'), f'--{prefix}union_list', '10'] cfg = CliApp.run(Cfg, cli_args=args) expected = { 'num_list': None, 'obj_list': None, 'union_list': [{'val': 1}, 2, 3, {'val': 4}, {'val': 5}, 6, 7, {'val': 8}, {'val': 9}, 10], 'str_list': None, } check_answer(cfg, prefix, expected) args = [f'--{prefix}str_list', arg_spaces('["0,0","1,1"]', has_quote_comma=True)] args += [f'--{prefix}str_list', arg_spaces('"2,2","3,3"', has_quote_comma=True)] args += [ f'--{prefix}str_list', arg_spaces('"4,4"', has_quote_comma=True), f'--{prefix}str_list', arg_spaces('"5,5"', has_quote_comma=True), ] cfg = CliApp.run(Cfg, cli_args=args) expected = { 'num_list': None, 'obj_list': None, 'union_list': None, 'str_list': ['0,0', '1,1', '2,2', '3,3', '4,4', '5,5'], } check_answer(cfg, prefix, expected) @pytest.mark.parametrize('arg_spaces', [no_add_cli_arg_spaces, add_cli_arg_spaces]) def test_cli_list_json_value_parsing(arg_spaces): class Cfg(BaseSettings): json_list: list[str | bool | None] assert CliApp.run( Cfg, cli_args=[ '--json_list', arg_spaces('true,"true"'), '--json_list', arg_spaces('false,"false"'), '--json_list', arg_spaces('null,"null"'), '--json_list', arg_spaces('hi,"bye"'), ], ).model_dump() == {'json_list': [True, 'true', False, 'false', None, 'null', 'hi', 'bye']} assert CliApp.run(Cfg, cli_args=['--json_list', '"","","",""']).model_dump() == {'json_list': ['', '', '', '']} assert CliApp.run(Cfg, cli_args=['--json_list', ',,,']).model_dump() == {'json_list': ['', '', '', '']} @pytest.mark.parametrize('arg_spaces', [no_add_cli_arg_spaces, add_cli_arg_spaces]) @pytest.mark.parametrize('prefix', ['', 'child.']) def test_cli_dict_arg(prefix, arg_spaces): class Child(BaseModel): check_dict: dict[str, str] class Cfg(BaseSettings): check_dict: dict[str, str] | None = None child: Child | None = None args: list[str] = [] args = [f'--{prefix}check_dict', arg_spaces('{"k1":"a","k2":"b"}')] args += [f'--{prefix}check_dict', arg_spaces('{"k3":"c"},{"k4":"d"}')] args += [f'--{prefix}check_dict', arg_spaces('{"k5":"e"}'), f'--{prefix}check_dict', arg_spaces('{"k6":"f"}')] args += [f'--{prefix}check_dict', arg_spaces('[k7=g,k8=h]')] args += [f'--{prefix}check_dict', arg_spaces('k9=i,k10=j')] args += [f'--{prefix}check_dict', arg_spaces('k11=k'), f'--{prefix}check_dict', arg_spaces('k12=l')] args += [ f'--{prefix}check_dict', arg_spaces('[{"k13":"m"},k14=n]'), f'--{prefix}check_dict', arg_spaces('[k15=o,{"k16":"p"}]'), ] args += [ f'--{prefix}check_dict', arg_spaces('{"k17":"q"},k18=r'), f'--{prefix}check_dict', arg_spaces('k19=s,{"k20":"t"}'), ] args += [f'--{prefix}check_dict', arg_spaces('{"k21":"u"},k22=v,{"k23":"w"}')] args += [f'--{prefix}check_dict', arg_spaces('k24=x,{"k25":"y"},k26=z')] args += [f'--{prefix}check_dict', arg_spaces('[k27="x,y",k28="x,y"]', has_quote_comma=True)] args += [f'--{prefix}check_dict', arg_spaces('k29="x,y",k30="x,y"', has_quote_comma=True)] args += [ f'--{prefix}check_dict', arg_spaces('k31="x,y"', has_quote_comma=True), f'--{prefix}check_dict', arg_spaces('k32="x,y"', has_quote_comma=True), ] cfg = CliApp.run(Cfg, cli_args=args) expected: dict[str, Any] = { 'check_dict': { 'k1': 'a', 'k2': 'b', 'k3': 'c', 'k4': 'd', 'k5': 'e', 'k6': 'f', 'k7': 'g', 'k8': 'h', 'k9': 'i', 'k10': 'j', 'k11': 'k', 'k12': 'l', 'k13': 'm', 'k14': 'n', 'k15': 'o', 'k16': 'p', 'k17': 'q', 'k18': 'r', 'k19': 's', 'k20': 't', 'k21': 'u', 'k22': 'v', 'k23': 'w', 'k24': 'x', 'k25': 'y', 'k26': 'z', 'k27': 'x,y', 'k28': 'x,y', 'k29': 'x,y', 'k30': 'x,y', 'k31': 'x,y', 'k32': 'x,y', } } if prefix: expected = {'check_dict': None, 'child': expected} else: expected['child'] = None assert cfg.model_dump() == expected with pytest.raises(SettingsError, match=f'Parsing error encountered for {prefix}check_dict: Mismatched quotes'): cfg = CliApp.run(Cfg, cli_args=[f'--{prefix}check_dict', 'k9="i']) with pytest.raises(SettingsError, match=f'Parsing error encountered for {prefix}check_dict: Mismatched quotes'): cfg = CliApp.run(Cfg, cli_args=[f'--{prefix}check_dict', 'k9=i"']) def test_cli_union_dict_arg(): class Cfg(BaseSettings): union_str_dict: str | dict[str, Any] with pytest.raises(ValidationError) as exc_info: args = ['--union_str_dict', 'hello world', '--union_str_dict', 'hello world'] cfg = CliApp.run(Cfg, cli_args=args) assert exc_info.value.errors(include_url=False) == [ { 'input': [ 'hello world', 'hello world', ], 'loc': ( 'union_str_dict', 'str', ), 'msg': 'Input should be a valid string', 'type': 'string_type', }, { 'input': [ 'hello world', 'hello world', ], 'loc': ( 'union_str_dict', 'dict[str,any]', ), 'msg': 'Input should be a valid dictionary', 'type': 'dict_type', }, ] args = ['--union_str_dict', 'hello world'] cfg = CliApp.run(Cfg, cli_args=args) assert cfg.model_dump() == {'union_str_dict': 'hello world'} args = ['--union_str_dict', '{"hello": "world"}'] cfg = CliApp.run(Cfg, cli_args=args) assert cfg.model_dump() == {'union_str_dict': {'hello': 'world'}} args = ['--union_str_dict', 'hello=world'] cfg = CliApp.run(Cfg, cli_args=args) assert cfg.model_dump() == {'union_str_dict': {'hello': 'world'}} args = ['--union_str_dict', '"hello=world"'] cfg = CliApp.run(Cfg, cli_args=args) assert cfg.model_dump() == {'union_str_dict': 'hello=world'} class Cfg(BaseSettings): union_list_dict: list[str] | dict[str, Any] with pytest.raises(ValidationError) as exc_info: args = ['--union_list_dict', 'hello,world'] cfg = CliApp.run(Cfg, cli_args=args) assert exc_info.value.errors(include_url=False) == [ { 'input': 'hello,world', 'loc': ( 'union_list_dict', 'list[str]', ), 'msg': 'Input should be a valid list', 'type': 'list_type', }, { 'input': 'hello,world', 'loc': ( 'union_list_dict', 'dict[str,any]', ), 'msg': 'Input should be a valid dictionary', 'type': 'dict_type', }, ] args = ['--union_list_dict', 'hello,world', '--union_list_dict', 'hello,world'] cfg = CliApp.run(Cfg, cli_args=args) assert cfg.model_dump() == {'union_list_dict': ['hello', 'world', 'hello', 'world']} args = ['--union_list_dict', '[hello,world]'] cfg = CliApp.run(Cfg, cli_args=args) assert cfg.model_dump() == {'union_list_dict': ['hello', 'world']} args = ['--union_list_dict', '{"hello": "world"}'] cfg = CliApp.run(Cfg, cli_args=args) assert cfg.model_dump() == {'union_list_dict': {'hello': 'world'}} args = ['--union_list_dict', 'hello=world'] cfg = CliApp.run(Cfg, cli_args=args) assert cfg.model_dump() == {'union_list_dict': {'hello': 'world'}} with pytest.raises(ValidationError) as exc_info: args = ['--union_list_dict', '"hello=world"'] cfg = CliApp.run(Cfg, cli_args=args) assert exc_info.value.errors(include_url=False) == [ { 'input': 'hello=world', 'loc': ( 'union_list_dict', 'list[str]', ), 'msg': 'Input should be a valid list', 'type': 'list_type', }, { 'input': 'hello=world', 'loc': ( 'union_list_dict', 'dict[str,any]', ), 'msg': 'Input should be a valid dictionary', 'type': 'dict_type', }, ] args = ['--union_list_dict', '["hello=world"]'] cfg = CliApp.run(Cfg, cli_args=args) assert cfg.model_dump() == {'union_list_dict': ['hello=world']} def test_cli_nested_dict_arg(): class Cfg(BaseSettings): check_dict: dict[str, Any] args = ['--check_dict', '{"k1":{"a": 1}},{"k2":{"b": 2}}'] cfg = CliApp.run(Cfg, cli_args=args) assert cfg.model_dump() == {'check_dict': {'k1': {'a': 1}, 'k2': {'b': 2}}} with pytest.raises( SettingsError, match=re.escape('Parsing error encountered for check_dict: not enough values to unpack (expected 2, got 1)'), ): args = ['--check_dict', '{"k1":{"a": 1}},"k2":{"b": 2}}'] cfg = CliApp.run(Cfg, cli_args=args) with pytest.raises(SettingsError, match='Parsing error encountered for check_dict: Missing end delimiter "}"'): args = ['--check_dict', '{"k1":{"a": 1}},{"k2":{"b": 2}'] cfg = CliApp.run(Cfg, cli_args=args) def test_cli_subcommand_union(capsys, monkeypatch): class AlphaCmd(BaseModel): """Alpha Help""" a: str class BetaCmd(BaseModel): """Beta Help""" b: str class GammaCmd(BaseModel): """Gamma Help""" g: str class Root1(BaseSettings, cli_prog_name='example.py'): """Root Help""" subcommand: CliSubCommand[AlphaCmd | BetaCmd | GammaCmd] = Field(description='Field Help') alpha = CliApp.run(Root1, cli_args=['AlphaCmd', '-a=alpha']) assert get_subcommand(alpha).model_dump() == {'a': 'alpha'} assert alpha.model_dump() == {'subcommand': {'a': 'alpha'}} beta = CliApp.run(Root1, cli_args=['BetaCmd', '-b=beta']) assert get_subcommand(beta).model_dump() == {'b': 'beta'} assert beta.model_dump() == {'subcommand': {'b': 'beta'}} gamma = CliApp.run(Root1, cli_args=['GammaCmd', '-g=gamma']) assert get_subcommand(gamma).model_dump() == {'g': 'gamma'} assert gamma.model_dump() == {'subcommand': {'g': 'gamma'}} with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): CliApp.run(Root1) assert ( capsys.readouterr().out == f"""usage: example.py [-h] {{AlphaCmd,BetaCmd,GammaCmd}} ... Root Help {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit subcommands: Field Help {{AlphaCmd,BetaCmd,GammaCmd}} AlphaCmd BetaCmd GammaCmd """ ) with pytest.raises(SystemExit): Root1(_cli_parse_args=True, _cli_use_class_docs_for_groups=True) assert ( capsys.readouterr().out == f"""usage: example.py [-h] {{AlphaCmd,BetaCmd,GammaCmd}} ... Root Help {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit subcommands: Field Help {{AlphaCmd,BetaCmd,GammaCmd}} AlphaCmd Alpha Help BetaCmd Beta Help GammaCmd Gamma Help """ ) class Root2(BaseSettings, cli_prog_name='example.py'): """Root Help""" subcommand: CliSubCommand[AlphaCmd | GammaCmd] = Field(description='Field Help') beta: CliSubCommand[BetaCmd] = Field(description='Field Beta Help') alpha = CliApp.run(Root2, cli_args=['AlphaCmd', '-a=alpha']) assert get_subcommand(alpha).model_dump() == {'a': 'alpha'} assert alpha.model_dump() == {'subcommand': {'a': 'alpha'}, 'beta': None} beta = CliApp.run(Root2, cli_args=['beta', '-b=beta']) assert get_subcommand(beta).model_dump() == {'b': 'beta'} assert beta.model_dump() == {'subcommand': None, 'beta': {'b': 'beta'}} gamma = CliApp.run(Root2, cli_args=['GammaCmd', '-g=gamma']) assert get_subcommand(gamma).model_dump() == {'g': 'gamma'} assert gamma.model_dump() == {'subcommand': {'g': 'gamma'}, 'beta': None} with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): CliApp.run(Root2, cli_args=True) assert ( capsys.readouterr().out == f"""usage: example.py [-h] {{AlphaCmd,GammaCmd,beta}} ... Root Help {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit subcommands: Field Help {{AlphaCmd,GammaCmd,beta}} AlphaCmd GammaCmd beta Field Beta Help """ ) with pytest.raises(SystemExit): Root2(_cli_parse_args=True, _cli_use_class_docs_for_groups=True) assert ( capsys.readouterr().out == f"""usage: example.py [-h] {{AlphaCmd,GammaCmd,beta}} ... Root Help {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit subcommands: Field Help {{AlphaCmd,GammaCmd,beta}} AlphaCmd Alpha Help GammaCmd Gamma Help beta Beta Help """ ) class Root3(BaseSettings, cli_prog_name='example.py'): """Root Help""" beta: CliSubCommand[BetaCmd] = Field(description='Field Beta Help') subcommand: CliSubCommand[AlphaCmd | GammaCmd] = Field(description='Field Help') alpha = CliApp.run(Root3, cli_args=['AlphaCmd', '-a=alpha']) assert get_subcommand(alpha).model_dump() == {'a': 'alpha'} assert alpha.model_dump() == {'subcommand': {'a': 'alpha'}, 'beta': None} beta = CliApp.run(Root3, cli_args=['beta', '-b=beta']) assert get_subcommand(beta).model_dump() == {'b': 'beta'} assert beta.model_dump() == {'subcommand': None, 'beta': {'b': 'beta'}} gamma = CliApp.run(Root3, cli_args=['GammaCmd', '-g=gamma']) assert get_subcommand(gamma).model_dump() == {'g': 'gamma'} assert gamma.model_dump() == {'subcommand': {'g': 'gamma'}, 'beta': None} with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): CliApp.run(Root3) assert ( capsys.readouterr().out == f"""usage: example.py [-h] {{beta,AlphaCmd,GammaCmd}} ... Root Help {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit subcommands: {{beta,AlphaCmd,GammaCmd}} beta Field Beta Help AlphaCmd GammaCmd """ ) with pytest.raises(SystemExit): Root3(_cli_parse_args=True, _cli_use_class_docs_for_groups=True) assert ( capsys.readouterr().out == f"""usage: example.py [-h] {{beta,AlphaCmd,GammaCmd}} ... Root Help {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit subcommands: {{beta,AlphaCmd,GammaCmd}} beta Beta Help AlphaCmd Alpha Help GammaCmd Gamma Help """ ) def test_cli_subcommand_with_positionals(): @pydantic_dataclasses.dataclass class FooPlugin: my_feature: bool = False @pydantic_dataclasses.dataclass class BarPlugin: my_feature: bool = False bar = BarPlugin() with pytest.raises(SystemExit, match='Error: CLI subcommand is required but no subcommands were found.'): get_subcommand(bar) with pytest.raises(SettingsError, match='Error: CLI subcommand is required but no subcommands were found.'): get_subcommand(bar, cli_exit_on_error=False) @pydantic_dataclasses.dataclass class Plugins: foo: CliSubCommand[FooPlugin] bar: CliSubCommand[BarPlugin] class Clone(BaseModel): repository: CliPositionalArg[str] directory: CliPositionalArg[str] local: bool = False shared: bool = False class Init(BaseModel): directory: CliPositionalArg[str] quiet: bool = False bare: bool = False class Git(BaseSettings): clone: CliSubCommand[Clone] init: CliSubCommand[Init] plugins: CliSubCommand[Plugins] git = CliApp.run(Git, cli_args=[]) assert git.model_dump() == { 'clone': None, 'init': None, 'plugins': None, } assert get_subcommand(git, is_required=False) is None with pytest.raises(SystemExit, match='Error: CLI subcommand is required {clone, init, plugins}'): get_subcommand(git) with pytest.raises(SettingsError, match='Error: CLI subcommand is required {clone, init, plugins}'): get_subcommand(git, cli_exit_on_error=False) git = CliApp.run(Git, cli_args=['init', '--quiet', 'true', 'dir/path']) assert git.model_dump() == { 'clone': None, 'init': {'directory': 'dir/path', 'quiet': True, 'bare': False}, 'plugins': None, } assert get_subcommand(git) == git.init assert get_subcommand(git, is_required=False) == git.init git = CliApp.run(Git, cli_args=['clone', 'repo', '.', '--shared', 'true']) assert git.model_dump() == { 'clone': {'repository': 'repo', 'directory': '.', 'local': False, 'shared': True}, 'init': None, 'plugins': None, } assert get_subcommand(git) == git.clone assert get_subcommand(git, is_required=False) == git.clone git = CliApp.run(Git, cli_args=['plugins', 'bar']) assert git.model_dump() == { 'clone': None, 'init': None, 'plugins': {'foo': None, 'bar': {'my_feature': False}}, } assert get_subcommand(git) == git.plugins assert get_subcommand(git, is_required=False) == git.plugins assert get_subcommand(get_subcommand(git)) == git.plugins.bar assert get_subcommand(get_subcommand(git), is_required=False) == git.plugins.bar class NotModel: ... with pytest.raises( SettingsError, match='Error: NotModel is not subclass of BaseModel or pydantic.dataclasses.dataclass' ): get_subcommand(NotModel()) class NotSettingsConfigDict(BaseModel): model_config = ConfigDict(cli_exit_on_error='not a bool') with pytest.raises(SystemExit, match='Error: CLI subcommand is required but no subcommands were found.'): get_subcommand(NotSettingsConfigDict()) with pytest.raises(SettingsError, match='Error: CLI subcommand is required but no subcommands were found.'): get_subcommand(NotSettingsConfigDict(), cli_exit_on_error=False) def test_cli_union_similar_sub_models(): class ChildA(BaseModel): name: str = 'child a' diff_a: str = 'child a difference' class ChildB(BaseModel): name: str = 'child b' diff_b: str = 'child b difference' class Cfg(BaseSettings): child: ChildA | ChildB cfg = CliApp.run(Cfg, cli_args=['--child.name', 'new name a', '--child.diff_a', 'new diff a']) assert cfg.model_dump() == {'child': {'name': 'new name a', 'diff_a': 'new diff a'}} def test_cli_optional_positional_arg(env): class Main(BaseSettings): model_config = SettingsConfigDict( cli_parse_args=True, cli_enforce_required=True, ) value: CliPositionalArg[int] = 123 assert CliApp.run(Main, cli_args=[]).model_dump() == {'value': 123} env.set('VALUE', '456') assert CliApp.run(Main, cli_args=[]).model_dump() == {'value': 456} assert CliApp.run(Main, cli_args=['789']).model_dump() == {'value': 789} def test_cli_variadic_positional_arg(env): class MainRequired(BaseSettings): model_config = SettingsConfigDict(cli_parse_args=True) values: CliPositionalArg[list[int]] class MainOptional(MainRequired): values: CliPositionalArg[list[int]] = [1, 2, 3] assert CliApp.run(MainOptional, cli_args=[]).model_dump() == {'values': [1, 2, 3]} with pytest.raises(SettingsError, match='error parsing CLI: the following arguments are required: VALUES'): CliApp.run(MainRequired, cli_args=[], cli_exit_on_error=False) env.set('VALUES', '[4,5,6]') assert CliApp.run(MainOptional, cli_args=[]).model_dump() == {'values': [4, 5, 6]} with pytest.raises(SettingsError, match='error parsing CLI: the following arguments are required: VALUES'): CliApp.run(MainRequired, cli_args=[], cli_exit_on_error=False) assert CliApp.run(MainOptional, cli_args=['7', '8', '9']).model_dump() == {'values': [7, 8, 9]} assert CliApp.run(MainRequired, cli_args=['7', '8', '9']).model_dump() == {'values': [7, 8, 9]} def test_cli_enums(capsys, monkeypatch): class Pet(IntEnum): dog = 0 cat = 1 bird = 2 crow = 2 class Cfg(BaseSettings, cli_prog_name='example.py'): pet: Pet = Pet.dog union_pet: Pet | int = 43 cfg = CliApp.run(Cfg, cli_args=['--pet', 'cat', '--union_pet', 'dog']) assert cfg.model_dump() == {'pet': Pet.cat, 'union_pet': Pet.dog} cfg = CliApp.run(Cfg, cli_args=['--pet', 'crow', '--union_pet', 'dog']) assert cfg.model_dump() == {'pet': Pet.crow, 'union_pet': Pet.dog} with pytest.raises(ValidationError) as exc_info: CliApp.run(Cfg, cli_args=['--pet', 'rock']) assert exc_info.value.errors(include_url=False) == [ { 'type': 'enum', 'loc': ('pet',), 'msg': 'Input should be 0, 1, 2 or 2', 'input': 'rock', 'ctx': {'expected': '0, 1, 2 or 2'}, } ] with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): CliApp.run(Cfg) assert ( capsys.readouterr().out == f"""usage: example.py [-h] [--pet {{dog,cat,bird,crow}}] [--union_pet {{{{dog,cat,bird,crow}},int}}] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit --pet {{dog,cat,bird,crow}} (default: dog) --union_pet {{{{dog,cat,bird,crow}},int}} (default: 43) """ ) def test_cli_literals(): class Cfg(BaseSettings): pet: Literal['dog', 'cat', 'bird'] cfg = CliApp.run(Cfg, cli_args=['--pet', 'cat']) assert cfg.model_dump() == {'pet': 'cat'} with pytest.raises(ValidationError) as exc_info: CliApp.run(Cfg, cli_args=['--pet', 'rock']) assert exc_info.value.errors(include_url=False) == [ { 'ctx': {'expected': "'dog', 'cat' or 'bird'"}, 'type': 'literal_error', 'loc': ('pet',), 'msg': "Input should be 'dog', 'cat' or 'bird'", 'input': 'rock', } ] def test_cli_annotation_exceptions(monkeypatch): class SubCmdAlt(BaseModel): pass class SubCmd(BaseModel): pass with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises( SettingsError, match='CliSubCommand is not outermost annotation for SubCommandNotOutermost.subcmd' ): class SubCommandNotOutermost(BaseSettings, cli_parse_args=True): subcmd: int | CliSubCommand[SubCmd] SubCommandNotOutermost() with pytest.raises(SettingsError, match='subcommand argument SubCommandHasDefault.subcmd has a default value'): class SubCommandHasDefault(BaseSettings, cli_parse_args=True): subcmd: CliSubCommand[SubCmd] = SubCmd() SubCommandHasDefault() with pytest.raises( SettingsError, match='subcommand argument SubCommandMultipleTypes.subcmd has type not derived from BaseModel', ): class SubCommandMultipleTypes(BaseSettings, cli_parse_args=True): subcmd: CliSubCommand[SubCmd | str] SubCommandMultipleTypes() with pytest.raises( SettingsError, match='subcommand argument SubCommandNotModel.subcmd has type not derived from BaseModel' ): class SubCommandNotModel(BaseSettings, cli_parse_args=True): subcmd: CliSubCommand[str] SubCommandNotModel() with pytest.raises( SettingsError, match='CliPositionalArg is not outermost annotation for PositionalArgNotOutermost.pos_arg' ): class PositionalArgNotOutermost(BaseSettings, cli_parse_args=True): pos_arg: int | CliPositionalArg[str] PositionalArgNotOutermost() with pytest.raises( SettingsError, match='MultipleVariadicPositionalArgs has multiple variadic positional arguments: strings, numbers', ): class MultipleVariadicPositionalArgs(BaseSettings, cli_parse_args=True): strings: CliPositionalArg[list[str]] numbers: CliPositionalArg[list[int]] MultipleVariadicPositionalArgs() with pytest.raises( SettingsError, match='VariadicPositionalArgAndSubCommand has variadic positional arguments and subcommand arguments: strings, sub_cmd', ): class VariadicPositionalArgAndSubCommand(BaseSettings, cli_parse_args=True): strings: CliPositionalArg[list[str]] sub_cmd: CliSubCommand[SubCmd] VariadicPositionalArgAndSubCommand() with pytest.raises( SettingsError, match=re.escape("cli_parse_args must be a list or tuple of strings, received ") ): class InvalidCliParseArgsType(BaseSettings, cli_parse_args='invalid type'): val: int InvalidCliParseArgsType() with pytest.raises(SettingsError, match='CliExplicitFlag argument CliFlagNotBool.flag is not of type bool'): class CliFlagNotBool(BaseSettings, cli_parse_args=True): flag: CliExplicitFlag[int] = False CliFlagNotBool() with pytest.raises( SettingsError, match='CliToggleFlag argument CliToggleNoDefault.flag must have a default bool value' ): class CliToggleNoDefault(BaseSettings, cli_parse_args=True): flag: CliToggleFlag[bool] CliToggleNoDefault() @pytest.mark.parametrize('enforce_required', [True, False]) @pytest.mark.parametrize('implicit_flags_mode', [None, True, 'dual', 'toggle']) def test_cli_bool_flags(monkeypatch, enforce_required, implicit_flags_mode): class FlagSettings(BaseSettings, cli_implicit_flags=implicit_flags_mode, cli_enforce_required=enforce_required): explicit_req: CliExplicitFlag[bool] explicit_opt: CliExplicitFlag[bool] = False dual_req: CliDualFlag[bool] dual_opt: CliDualFlag[bool] = False toggle_def_t: CliToggleFlag[bool] = True toggle_def_f: CliToggleFlag[bool] = False implicit_req: bool implicit_opt: bool = False expected = { 'explicit_req': True, 'explicit_opt': False, 'implicit_req': True, 'implicit_opt': False, 'dual_req': True, 'dual_opt': False, 'toggle_def_t': True, 'toggle_def_f': False, } flag_args = [ '--explicit_req', 'True', '--dual_req', ] flag_args += ['--implicit_req'] if implicit_flags_mode is not None else ['--implicit_req', 'True'] implicit_settings = CliApp.run(FlagSettings, cli_args=flag_args) assert implicit_settings.model_dump() == expected serialized_args = CliApp.serialize(implicit_settings) assert serialized_args == flag_args expected = { 'explicit_req': False, 'explicit_opt': False, 'implicit_req': False, 'implicit_opt': False, 'dual_req': False, 'dual_opt': False, 'toggle_def_t': False, 'toggle_def_f': False, } flag_args = [ '--explicit_req', 'False', '--no-dual_req', '--no-toggle_def_t', ] flag_args += ['--no-implicit_req'] if implicit_flags_mode is not None else ['--implicit_req', 'False'] implicit_settings = CliApp.run(FlagSettings, cli_args=flag_args) assert implicit_settings.model_dump() == expected serialized_args = CliApp.serialize(implicit_settings) assert serialized_args == flag_args expected = { 'explicit_req': True, 'explicit_opt': True, 'implicit_req': True, 'implicit_opt': True, 'dual_req': True, 'dual_opt': True, 'toggle_def_t': True, 'toggle_def_f': True, } flag_args = [ '--explicit_req', 'True', '--explicit_opt', 'True', '--dual_req', '--dual_opt', '--toggle_def_f', ] flag_args += ['--implicit_req'] if implicit_flags_mode is not None else ['--implicit_req', 'True'] flag_args += ['--implicit_opt'] if implicit_flags_mode is not None else ['--implicit_opt', 'True'] implicit_settings = CliApp.run(FlagSettings, cli_args=flag_args) assert implicit_settings.model_dump() == expected serialized_args = CliApp.serialize(implicit_settings) assert serialized_args == flag_args def test_cli_avoid_json(capsys, monkeypatch): class SubModel(BaseModel): v1: int class Settings(BaseSettings, cli_prog_name='example.py'): sub_model: SubModel model_config = SettingsConfigDict(cli_parse_args=True) with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): Settings(_cli_avoid_json=False) assert ( capsys.readouterr().out == f"""usage: example.py [-h] [--sub_model [JSON]] [--sub_model.v1 int] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit sub_model options: --sub_model [JSON] set sub_model from JSON string (default: {{}}) --sub_model.v1 int (required) """ ) with pytest.raises(SystemExit): Settings(_cli_avoid_json=True) assert ( capsys.readouterr().out == f"""usage: example.py [-h] [--sub_model.v1 int] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit sub_model options: --sub_model.v1 int (required) """ ) def test_cli_remove_empty_groups(capsys, monkeypatch): class SubModel(BaseModel): pass class Settings(BaseSettings, cli_prog_name='example.py'): sub_model: SubModel model_config = SettingsConfigDict(cli_parse_args=True) with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): Settings(_cli_avoid_json=False) assert ( capsys.readouterr().out == f"""usage: example.py [-h] [--sub_model [JSON]] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit sub_model options: --sub_model [JSON] set sub_model from JSON string (default: {{}}) """ ) with pytest.raises(SystemExit): Settings(_cli_avoid_json=True) assert ( capsys.readouterr().out == f"""usage: example.py [-h] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit """ ) def test_cli_hide_none_type(capsys, monkeypatch): class Settings(BaseSettings, cli_prog_name='example.py'): v0: str | None model_config = SettingsConfigDict(cli_parse_args=True) with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): Settings(_cli_hide_none_type=False) assert ( capsys.readouterr().out == f"""usage: example.py [-h] [--v0 {{str,null}}] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit --v0 {{str,null}} (required) """ ) with pytest.raises(SystemExit): Settings(_cli_hide_none_type=True) assert ( capsys.readouterr().out == f"""usage: example.py [-h] [--v0 str] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit --v0 str (required) """ ) def test_cli_use_class_docs_for_groups(capsys, monkeypatch): class SubModel(BaseModel): """The help text from the class docstring""" v1: int class Settings(BaseSettings, cli_prog_name='example.py'): """My application help text.""" sub_model: SubModel = Field(description='The help text from the field description') model_config = SettingsConfigDict(cli_parse_args=True) with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): Settings(_cli_use_class_docs_for_groups=False) assert ( capsys.readouterr().out == f"""usage: example.py [-h] [--sub_model [JSON]] [--sub_model.v1 int] My application help text. {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit sub_model options: The help text from the field description --sub_model [JSON] set sub_model from JSON string (default: {{}}) --sub_model.v1 int (required) """ ) with pytest.raises(SystemExit): Settings(_cli_use_class_docs_for_groups=True) assert ( capsys.readouterr().out == f"""usage: example.py [-h] [--sub_model [JSON]] [--sub_model.v1 int] My application help text. {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit sub_model options: The help text from the class docstring --sub_model [JSON] set sub_model from JSON string (default: {{}}) --sub_model.v1 int (required) """ ) def test_cli_enforce_required(env): class MyRootModel(RootModel[str]): root: str class Settings(BaseSettings, cli_exit_on_error=False): my_required_field: str my_root_model_required_field: MyRootModel env.set('MY_REQUIRED_FIELD', 'hello from environment') env.set('MY_ROOT_MODEL_REQUIRED_FIELD', 'hi from environment') assert Settings(_cli_parse_args=[], _cli_enforce_required=False).model_dump() == { 'my_required_field': 'hello from environment', 'my_root_model_required_field': 'hi from environment', } with pytest.raises( SettingsError, match='error parsing CLI: the following arguments are required: --my_required_field' ): Settings(_cli_parse_args=[], _cli_enforce_required=True).model_dump() with pytest.raises( SettingsError, match='error parsing CLI: the following arguments are required: --my_root_model_required_field' ): Settings(_cli_parse_args=['--my_required_field', 'hello from cli'], _cli_enforce_required=True).model_dump() def test_cli_exit_on_error(capsys, monkeypatch): class Settings(BaseSettings, cli_parse_args=True, cli_prog_name='example.py'): ... with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--bad-arg']) with pytest.raises(SystemExit): Settings() assert ( capsys.readouterr().err == """usage: example.py [-h] example.py: error: unrecognized arguments: --bad-arg """ ) with pytest.raises(SettingsError, match='error parsing CLI: unrecognized arguments: --bad-arg'): CliApp.run(Settings, cli_exit_on_error=False) def test_cli_ignore_unknown_args(): class Cfg(BaseSettings, cli_ignore_unknown_args=True): this: str = 'hello' that: int = 123 ignored_args: CliUnknownArgs cfg = CliApp.run(Cfg, cli_args=['--this=hi', '--that=456']) assert cfg.model_dump() == {'this': 'hi', 'that': 456, 'ignored_args': []} cfg = CliApp.run(Cfg, cli_args=['not_my_positional_arg', '--not-my-optional-arg=456']) assert cfg.model_dump() == { 'this': 'hello', 'that': 123, 'ignored_args': ['not_my_positional_arg', '--not-my-optional-arg=456'], } cfg = CliApp.run( Cfg, cli_args=['not_my_positional_arg', '--not-my-optional-arg=456', '--this=goodbye', '--that=789'] ) assert cfg.model_dump() == { 'this': 'goodbye', 'that': 789, 'ignored_args': ['not_my_positional_arg', '--not-my-optional-arg=456'], } def test_cli_flag_prefix_char(): class Cfg(BaseSettings, cli_flag_prefix_char='+'): my_var: str = Field(validation_alias=AliasChoices('m', 'my-var')) cfg = CliApp.run(Cfg, cli_args=['++my-var=hello']) assert cfg.model_dump() == {'my_var': 'hello'} cfg = CliApp.run(Cfg, cli_args=['+m=hello']) assert cfg.model_dump() == {'my_var': 'hello'} @pytest.mark.parametrize('parser_type', [pytest.Parser, argparse.ArgumentParser, CliDummyParser]) @pytest.mark.parametrize('prefix', ['', 'cfg']) def test_cli_user_settings_source(parser_type, prefix): class Cfg(BaseSettings): pet: Literal['dog', 'cat', 'bird'] = 'bird' if parser_type is pytest.Parser: parser = pytest.Parser(_ispytest=True) parse_args = parser.parse add_arg = parser.addoption cli_cfg_settings = CliSettingsSource( Cfg, cli_prefix=prefix, root_parser=parser, parse_args_method=pytest.Parser.parse, add_argument_method=pytest.Parser.addoption, add_argument_group_method=pytest.Parser.getgroup, add_parser_method=None, add_subparsers_method=None, formatter_class=None, ) elif parser_type is CliDummyParser: parser = CliDummyParser() parse_args = parser.parse_args add_arg = parser.add_argument cli_cfg_settings = CliSettingsSource( Cfg, cli_prefix=prefix, root_parser=parser, parse_args_method=CliDummyParser.parse_args, add_argument_method=CliDummyParser.add_argument, add_argument_group_method=CliDummyParser.add_argument_group, add_parser_method=CliDummySubParsers.add_parser, add_subparsers_method=CliDummyParser.add_subparsers, ) else: parser = argparse.ArgumentParser() parse_args = parser.parse_args add_arg = parser.add_argument cli_cfg_settings = CliSettingsSource(Cfg, cli_prefix=prefix, root_parser=parser) add_arg('--fruit', choices=['pear', 'kiwi', 'lime']) add_arg('--num-list', action='append', type=int) add_arg('--num', type=int) args = ['--fruit', 'pear', '--num', '0', '--num-list', '1', '--num-list', '2', '--num-list', '3'] parsed_args = parse_args(args) assert CliApp.run(Cfg, cli_args=parsed_args, cli_settings_source=cli_cfg_settings).model_dump() == {'pet': 'bird'} assert CliApp.run(Cfg, cli_args=args, cli_settings_source=cli_cfg_settings).model_dump() == {'pet': 'bird'} assert Cfg(_cli_settings_source=cli_cfg_settings(parsed_args=parsed_args)).model_dump() == {'pet': 'bird'} assert Cfg(_cli_settings_source=cli_cfg_settings(args=args)).model_dump() == {'pet': 'bird'} assert Cfg(_cli_settings_source=cli_cfg_settings(args=False)).model_dump() == {'pet': 'bird'} arg_prefix = f'{prefix}.' if prefix else '' args = [ '--fruit', 'kiwi', '--num', '0', '--num-list', '1', '--num-list', '2', '--num-list', '3', f'--{arg_prefix}pet', 'dog', ] parsed_args = parse_args(args) assert CliApp.run(Cfg, cli_args=parsed_args, cli_settings_source=cli_cfg_settings).model_dump() == {'pet': 'dog'} assert CliApp.run(Cfg, cli_args=args, cli_settings_source=cli_cfg_settings).model_dump() == {'pet': 'dog'} assert Cfg(_cli_settings_source=cli_cfg_settings(parsed_args=parsed_args)).model_dump() == {'pet': 'dog'} assert Cfg(_cli_settings_source=cli_cfg_settings(args=args)).model_dump() == {'pet': 'dog'} assert Cfg(_cli_settings_source=cli_cfg_settings(args=False)).model_dump() == {'pet': 'bird'} parsed_args = parse_args( [ '--fruit', 'kiwi', '--num', '0', '--num-list', '1', '--num-list', '2', '--num-list', '3', f'--{arg_prefix}pet', 'cat', ] ) assert CliApp.run(Cfg, cli_args=vars(parsed_args), cli_settings_source=cli_cfg_settings).model_dump() == { 'pet': 'cat' } assert Cfg(_cli_settings_source=cli_cfg_settings(parsed_args=vars(parsed_args))).model_dump() == {'pet': 'cat'} assert Cfg(_cli_settings_source=cli_cfg_settings(args=False)).model_dump() == {'pet': 'bird'} @pytest.mark.parametrize('prefix', ['', 'cfg']) def test_cli_dummy_user_settings_with_subcommand(prefix): class DogCommands(BaseModel): name: str = 'Bob' command: Literal['roll', 'bark', 'sit'] = 'sit' class Cfg(BaseSettings): pet: Literal['dog', 'cat', 'bird'] = 'bird' command: CliSubCommand[DogCommands] parser = CliDummyParser() cli_cfg_settings = CliSettingsSource( Cfg, root_parser=parser, cli_prefix=prefix, parse_args_method=CliDummyParser.parse_args, add_argument_method=CliDummyParser.add_argument, add_argument_group_method=CliDummyParser.add_argument_group, add_parser_method=CliDummySubParsers.add_parser, add_subparsers_method=CliDummyParser.add_subparsers, ) parser.add_argument('--fruit', choices=['pear', 'kiwi', 'lime']) args = ['--fruit', 'pear'] parsed_args = parser.parse_args(args) assert CliApp.run(Cfg, cli_args=parsed_args, cli_settings_source=cli_cfg_settings).model_dump() == { 'pet': 'bird', 'command': None, } assert CliApp.run(Cfg, cli_args=args, cli_settings_source=cli_cfg_settings).model_dump() == { 'pet': 'bird', 'command': None, } arg_prefix = f'{prefix}.' if prefix else '' args = ['--fruit', 'kiwi', f'--{arg_prefix}pet', 'dog'] parsed_args = parser.parse_args(args) assert CliApp.run(Cfg, cli_args=parsed_args, cli_settings_source=cli_cfg_settings).model_dump() == { 'pet': 'dog', 'command': None, } assert CliApp.run(Cfg, cli_args=args, cli_settings_source=cli_cfg_settings).model_dump() == { 'pet': 'dog', 'command': None, } parsed_args = parser.parse_args(['--fruit', 'kiwi', f'--{arg_prefix}pet', 'cat']) assert CliApp.run(Cfg, cli_args=vars(parsed_args), cli_settings_source=cli_cfg_settings).model_dump() == { 'pet': 'cat', 'command': None, } args = ['--fruit', 'kiwi', f'--{arg_prefix}pet', 'dog', 'command', '--name', 'ralph', '--command', 'roll'] parsed_args = parser.parse_args(args) assert CliApp.run(Cfg, cli_args=vars(parsed_args), cli_settings_source=cli_cfg_settings).model_dump() == { 'pet': 'dog', 'command': {'name': 'ralph', 'command': 'roll'}, } assert CliApp.run(Cfg, cli_args=args, cli_settings_source=cli_cfg_settings).model_dump() == { 'pet': 'dog', 'command': {'name': 'ralph', 'command': 'roll'}, } def test_cli_user_settings_source_exceptions(): class Cfg(BaseSettings): pet: Literal['dog', 'cat', 'bird'] = 'bird' with pytest.raises(SettingsError, match='`args` and `parsed_args` are mutually exclusive'): args = ['--pet', 'dog'] parsed_args = {'pet': 'dog'} cli_cfg_settings = CliSettingsSource(Cfg) Cfg(_cli_settings_source=cli_cfg_settings(args=args, parsed_args=parsed_args)) with pytest.raises(SettingsError, match='CLI settings source prefix is invalid: .cfg'): CliSettingsSource(Cfg, cli_prefix='.cfg') with pytest.raises(SettingsError, match='CLI settings source prefix is invalid: cfg.'): CliSettingsSource(Cfg, cli_prefix='cfg.') with pytest.raises(SettingsError, match='CLI settings source prefix is invalid: 123'): CliSettingsSource(Cfg, cli_prefix='123') class Food(BaseModel): fruit: FruitsEnum = FruitsEnum.kiwi class CfgWithSubCommand(BaseSettings): pet: Literal['dog', 'cat', 'bird'] = 'bird' food: CliSubCommand[Food] with pytest.raises( SettingsError, match='cannot connect CLI settings source root parser: add_subparsers_method is set to `None` but is needed for connecting', ): CliSettingsSource(CfgWithSubCommand, add_subparsers_method=None) @pytest.mark.parametrize( 'value,expected', [ (str, 'str'), ('foobar', 'str'), ('SomeForwardRefString', 'str'), # included to document current behavior; could be changed (List['SomeForwardRef'], "List[ForwardRef('SomeForwardRef')]"), # noqa: F821, UP006 (str | int, '{str,int}'), (list, 'list'), (List, 'List'), # noqa: UP006 ([1, 2, 3], 'list'), (List[Dict[str, int]], 'List[Dict[str,int]]'), # noqa: UP006 (Tuple[str, int, float], 'Tuple[str,int,float]'), # noqa: UP006 (Tuple[str, ...], 'Tuple[str,...]'), # noqa: UP006 (int | List[str] | Tuple[str, int], '{int,List[str],Tuple[str,int]}'), # noqa: UP006 (foobar, 'foobar'), (LoggedVar, 'LoggedVar'), (LoggedVar(), 'LoggedVar'), (Representation(), 'Representation()'), (typing.Literal[1, 2, 3], '{1,2,3}'), (typing_extensions.Literal[1, 2, 3], '{1,2,3}'), (typing.Literal['a', 'b', 'c'], '{a,b,c}'), (typing_extensions.Literal['a', 'b', 'c'], '{a,b,c}'), (SimpleSettings, 'JSON'), (SimpleSettings | SettingWithIgnoreEmpty, 'JSON'), (Union[SimpleSettings, str, SettingWithIgnoreEmpty], '{JSON,str}'), # noqa: UP007 (Union[str, SimpleSettings, SettingWithIgnoreEmpty], '{str,JSON}'), # noqa: UP007 (Annotated[SimpleSettings, 'annotation'], 'JSON'), (DirectoryPath, 'Path'), (FruitsEnum, '{pear,kiwi,lime}'), (time.time_ns, 'time_ns'), (foobar, 'foobar'), (CliDummyParser.add_argument, 'CliDummyParser.add_argument'), (lambda: str | int, '{str,int}'), (lambda: list[int], 'list[int]'), (lambda: List[int], 'List[int]'), # noqa: UP006 (lambda: list[dict[str, int]], 'list[dict[str,int]]'), (lambda: list[str | int], 'list[{str,int}]'), (lambda: list[str | int], 'list[{str,int}]'), (lambda: LoggedVar[int], 'LoggedVar[int]'), (lambda: LoggedVar[Dict[int, str]], 'LoggedVar[Dict[int,str]]'), # noqa: UP006 ], ) @pytest.mark.parametrize('hide_none_type', [True, False]) def test_cli_metavar_format(hide_none_type, value, expected): if callable(value) and value.__name__ == '': value = value() cli_settings = CliSettingsSource(SimpleSettings, cli_hide_none_type=hide_none_type) if hide_none_type: if value == [1, 2, 3] or isinstance(value, LoggedVar) or isinstance(value, Representation): pytest.skip() if value in ('foobar', 'SomeForwardRefString'): expected = f"ForwardRef('{value}')" # forward ref implicit cast if typing_extensions.get_origin(value) is Union: args = typing_extensions.get_args(value) value = Union[args + (None,) if args else (value, None)] # noqa: UP007 else: value = Union[(value, None)] # noqa: UP007 assert cli_settings._metavar_format(value) == expected @pytest.mark.skipif(sys.version_info < (3, 12), reason='requires python 3.12 or higher') def test_cli_metavar_format_type_alias_312(): exec( """ type TypeAliasInt = int assert CliSettingsSource(SimpleSettings)._metavar_format(TypeAliasInt) == 'TypeAliasInt' """ ) def test_cli_app(): class Init(BaseModel): directory: CliPositionalArg[str] def cli_cmd(self) -> None: self.directory = 'ran Init.cli_cmd' def alt_cmd(self) -> None: self.directory = 'ran Init.alt_cmd' class Clone(BaseModel): repository: CliPositionalArg[str] directory: CliPositionalArg[str] def cli_cmd(self) -> None: self.repository = 'ran Clone.cli_cmd' def alt_cmd(self) -> None: self.repository = 'ran Clone.alt_cmd' class Git(BaseModel): clone: CliSubCommand[Clone] init: CliSubCommand[Init] def cli_cmd(self) -> None: CliApp.run_subcommand(self) def alt_cmd(self) -> None: CliApp.run_subcommand(self, cli_cmd_method_name='alt_cmd') assert CliApp.run(Git, cli_args=['init', 'dir']).model_dump() == { 'clone': None, 'init': {'directory': 'ran Init.cli_cmd'}, } assert CliApp.run(Git, cli_args=['init', 'dir'], cli_cmd_method_name='alt_cmd').model_dump() == { 'clone': None, 'init': {'directory': 'ran Init.alt_cmd'}, } assert CliApp.run(Git, cli_args=['clone', 'repo', 'dir']).model_dump() == { 'clone': {'repository': 'ran Clone.cli_cmd', 'directory': 'dir'}, 'init': None, } assert CliApp.run(Git, cli_args=['clone', 'repo', 'dir'], cli_cmd_method_name='alt_cmd').model_dump() == { 'clone': {'repository': 'ran Clone.alt_cmd', 'directory': 'dir'}, 'init': None, } def test_cli_app_async_method_no_existing_loop(): class Command(BaseSettings): called: bool = False async def cli_cmd(self) -> None: self.called = True assert CliApp.run(Command, cli_args=[]).called def test_cli_app_async_method_with_existing_loop(): class Command(BaseSettings): called: bool = False async def cli_cmd(self) -> None: self.called = True async def run_as_coro(): return CliApp.run(Command, cli_args=[]) assert asyncio.run(run_as_coro()).called def test_cli_app_exceptions(): with pytest.raises( SettingsError, match='Error: NotPydanticModel is not subclass of BaseModel or pydantic.dataclasses.dataclass' ): class NotPydanticModel: ... CliApp.run(NotPydanticModel) with pytest.raises( SettingsError, match=re.escape('Error: `cli_args` must be list[str] or None when `cli_settings_source` is not used'), ): class Cfg(BaseModel): ... CliApp.run(Cfg, cli_args={'my_arg': 'hello'}) with pytest.raises(SettingsError, match='Error: Child class is missing cli_cmd entrypoint'): class Child(BaseModel): val: str class Root(BaseModel): child: CliSubCommand[Child] def cli_cmd(self) -> None: CliApp.run_subcommand(self) CliApp.run(Root, cli_args=['child', '--val=hello']) def test_cli_suppress(capsys, monkeypatch): class DeepHiddenSubModel(BaseModel): deep_hidden_a: int deep_hidden_b: int class HiddenSubModel(BaseModel): hidden_a: int hidden_b: int deep_hidden_obj: DeepHiddenSubModel = Field(description='deep_hidden_obj description') class SubModel(BaseModel): visible_a: int visible_b: int deep_hidden_obj: CliSuppress[DeepHiddenSubModel] = Field(description='deep_hidden_obj description') class Settings(BaseSettings, cli_parse_args=True, cli_prog_name='example.py'): field_a: CliSuppress[int] = 0 field_b: str = Field(default='hi', description=CLI_SUPPRESS) hidden_obj: CliSuppress[HiddenSubModel] = Field(description='hidden_obj description') visible_obj: SubModel with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): CliApp.run(Settings) assert ( capsys.readouterr().out == f"""usage: example.py [-h] [--visible_obj [JSON]] [--visible_obj.visible_a int] [--visible_obj.visible_b int] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit visible_obj options: --visible_obj [JSON] set visible_obj from JSON string (default: {{}}) --visible_obj.visible_a int (required) --visible_obj.visible_b int (required) """ ) def test_cli_mutually_exclusive_group(capsys, monkeypatch): class Circle(CliMutuallyExclusiveGroup): radius: float | None = 21 diameter: float | None = 22 perimeter: float | None = 23 class Settings(BaseModel, cli_prog_name='example.py'): circle_optional: Circle = Circle(radius=None, diameter=None, perimeter=24) circle_required: Circle CliApp.run(Settings, cli_args=['--circle-required.radius=1', '--circle-optional.radius=1']).model_dump() == { 'circle_optional': {'radius': 1, 'diameter': 22, 'perimeter': 24}, 'circle_required': {'radius': 1, 'diameter': 22, 'perimeter': 23}, } with pytest.raises(SystemExit): CliApp.run(Settings, cli_args=['--circle-required.radius=1', '--circle-required.diameter=2']) assert ( 'error: argument --circle-required.diameter: not allowed with argument --circle-required.radius' in capsys.readouterr().err ) with pytest.raises(SystemExit): CliApp.run( Settings, cli_args=['--circle-required.radius=1', '--circle-optional.radius=1', '--circle-optional.diameter=2'], ) assert ( 'error: argument --circle-optional.diameter: not allowed with argument --circle-optional.radius' in capsys.readouterr().err ) with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): CliApp.run(Settings) usage = ( """usage: example.py [-h] [--circle-optional.radius float | --circle-optional.diameter float | --circle-optional.perimeter float] (--circle-required.radius float | --circle-required.diameter float | --circle-required.perimeter float)""" if sys.version_info >= (3, 13) else """usage: example.py [-h] [--circle-optional.radius float | --circle-optional.diameter float | --circle-optional.perimeter float] (--circle-required.radius float | --circle-required.diameter float | --circle-required.perimeter float)""" ) assert ( capsys.readouterr().out == f"""{usage} {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit circle-optional options (mutually exclusive): --circle-optional.radius float (default: None) --circle-optional.diameter float (default: None) --circle-optional.perimeter float (default: 24.0) circle-required options (mutually exclusive): --circle-required.radius float (default: 21) --circle-required.diameter float (default: 22) --circle-required.perimeter float (default: 23) """ ) def test_cli_mutually_exclusive_group_exceptions(): class Circle(CliMutuallyExclusiveGroup): radius: float | None = 21 diameter: float | None = 22 perimeter: float | None = 23 class Settings(BaseSettings): circle: Circle parser = CliDummyParser() with pytest.raises( SettingsError, match='cannot connect CLI settings source root parser: group object is missing add_mutually_exclusive_group but is needed for connecting', ): CliSettingsSource( Settings, root_parser=parser, parse_args_method=CliDummyParser.parse_args, add_argument_method=CliDummyParser.add_argument, add_argument_group_method=CliDummyParser.add_argument_group, add_parser_method=CliDummySubParsers.add_parser, add_subparsers_method=CliDummyParser.add_subparsers, ) class SubModel(BaseModel): pass class SettingsInvalidUnion(BaseSettings): union: Circle | SubModel with pytest.raises(SettingsError, match='cannot use union with CliMutuallyExclusiveGroup'): CliApp.run(SettingsInvalidUnion) class CircleInvalidSubModel(Circle): square: SubModel | None = None class SettingsInvalidOptSubModel(BaseModel): circle: CircleInvalidSubModel = CircleInvalidSubModel() class SettingsInvalidReqSubModel(BaseModel): circle: CircleInvalidSubModel for settings in [SettingsInvalidOptSubModel, SettingsInvalidReqSubModel]: with pytest.raises(SettingsError, match='cannot have nested models in a CliMutuallyExclusiveGroup'): CliApp.run(settings) class CircleRequiredField(Circle): length: float class SettingsOptCircleReqField(BaseModel): circle: CircleRequiredField = CircleRequiredField(length=2) assert CliApp.run(SettingsOptCircleReqField, cli_args=[]).model_dump() == { 'circle': {'diameter': 22.0, 'length': 2.0, 'perimeter': 23.0, 'radius': 21.0} } class SettingsInvalidReqCircleReqField(BaseModel): circle: CircleRequiredField with pytest.raises(ValueError, match='mutually exclusive arguments must be optional'): CliApp.run(SettingsInvalidReqCircleReqField) def test_cli_invalid_abbrev(): class MySettings(BaseSettings): bacon: str = '' badger: str = '' with pytest.raises( SettingsError, match='error parsing CLI: unrecognized arguments: --bac cli abbrev are invalid for internal parser', ): CliApp.run( MySettings, cli_args=['--bac', 'cli abbrev are invalid for internal parser'], cli_exit_on_error=False ) def test_cli_subcommand_invalid_abbrev(): class Child(BaseModel): bacon: str = '' badger: str = '' class MySettings(BaseSettings): child: CliSubCommand[Child] with pytest.raises( SettingsError, match='error parsing CLI: unrecognized arguments: --bac cli abbrev are invalid for internal parser', ): CliApp.run( MySettings, cli_args=['child', '--bac', 'cli abbrev are invalid for internal parser'], cli_exit_on_error=False, ) def test_cli_submodels_strip_annotated(): class PolyA(BaseModel): a: int = 1 type: Literal['a'] = 'a' class PolyB(BaseModel): b: str = '2' type: Literal['b'] = 'b' def _get_type(model: BaseModel | dict) -> str: if isinstance(model, dict): return model.get('type', 'a') return model.type # type: ignore Poly = Annotated[Annotated[PolyA, Tag('a')] | Annotated[PolyB, Tag('b')], Discriminator(_get_type)] class WithUnion(BaseSettings): poly: Poly assert CliApp.run(WithUnion, ['--poly.type=a']).model_dump() == {'poly': {'a': 1, 'type': 'a'}} def test_cli_kebab_case(capsys, monkeypatch): class DeepSubModel(BaseModel): deep_pos_arg: CliPositionalArg[str] deep_arg: str class SubModel(BaseModel): sub_subcmd: CliSubCommand[DeepSubModel] sub_other_subcmd: CliSubCommand[DeepSubModel] sub_arg: str class Root(BaseModel, cli_prog_name='example.py'): root_subcmd: CliSubCommand[SubModel] other_subcmd: CliSubCommand[SubModel] root_arg: str root = CliApp.run( Root, cli_args=[ '--root-arg=hi', 'root-subcmd', '--sub-arg=hello', 'sub-subcmd', 'hey', '--deep-arg=bye', ], ) assert root.model_dump() == { 'root_arg': 'hi', 'other_subcmd': None, 'root_subcmd': { 'sub_arg': 'hello', 'sub_subcmd': {'deep_pos_arg': 'hey', 'deep_arg': 'bye'}, 'sub_other_subcmd': None, }, } serialized_cli_args = CliApp.serialize(root) assert serialized_cli_args == [ '--root-arg', 'hi', 'root-subcmd', '--sub-arg', 'hello', 'sub-subcmd', '--deep-arg', 'bye', 'hey', ] with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) with pytest.raises(SystemExit): CliApp.run(Root) assert ( capsys.readouterr().out == f"""usage: example.py [-h] --root-arg str {{root-subcmd,other-subcmd}} ... {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit --root-arg str (required) subcommands: {{root-subcmd,other-subcmd}} root-subcmd other-subcmd """ ) m.setattr(sys, 'argv', ['example.py', 'root-subcmd', '--help']) with pytest.raises(SystemExit): CliApp.run(Root) assert ( capsys.readouterr().out == f"""usage: example.py root-subcmd [-h] --sub-arg str {{sub-subcmd,sub-other-subcmd}} ... {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit --sub-arg str (required) subcommands: {{sub-subcmd,sub-other-subcmd}} sub-subcmd sub-other-subcmd """ ) m.setattr(sys, 'argv', ['example.py', 'root-subcmd', 'sub-subcmd', '--help']) with pytest.raises(SystemExit): CliApp.run(Root) assert ( capsys.readouterr().out == f"""usage: example.py root-subcmd sub-subcmd [-h] --deep-arg str DEEP-POS-ARG positional arguments: DEEP-POS-ARG {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit --deep-arg str (required) """ ) def test_cli_kebab_case_enums(): class Example1(IntEnum): example_a = 0 example_b = 1 class Example2(IntEnum): example_c = 2 example_d = 3 class SettingsNoEnum(BaseSettings): model_config = SettingsConfigDict(cli_kebab_case='no_enums') example: Example1 | Example2 mybool: bool class SettingsAll(BaseSettings): model_config = SettingsConfigDict(cli_kebab_case='all') example: Example1 | Example2 mybool: bool assert CliApp.run( SettingsNoEnum, cli_args=['--example', 'example_a', '--mybool=true'], ).model_dump() == {'example': Example1.example_a, 'mybool': True} assert CliApp.run(SettingsAll, cli_args=['--example', 'example-c', '--mybool=true']).model_dump() == { 'example': Example2.example_c, 'mybool': True, } with pytest.raises(ValueError, match='Input should be kebab-case "example-a", not "example_a"'): CliApp.run(SettingsAll, cli_args=['--example', 'example_a', '--mybool=true']) def test_cli_kebab_case_all_with_implicit_flag(): class Settings(BaseSettings): model_config = SettingsConfigDict(cli_kebab_case='all') test_bool_flag_a: CliImplicitFlag[bool] test_bool_flag_b: CliToggleFlag[bool] = True test_bool_flag_c: CliToggleFlag[bool] = False test_bool_flag_d: CliDualFlag[bool] = False assert CliApp.run( Settings, cli_args=['--test-bool-flag-a', '--test-bool-flag-c', '--test-bool-flag-d'], ).model_dump() == { 'test_bool_flag_a': True, 'test_bool_flag_b': True, 'test_bool_flag_c': True, 'test_bool_flag_d': True, } assert CliApp.run( Settings, cli_args=['--no-test-bool-flag-a', '--no-test-bool-flag-b', '--no-test-bool-flag-d'], ).model_dump() == { 'test_bool_flag_a': False, 'test_bool_flag_b': False, 'test_bool_flag_c': False, 'test_bool_flag_d': False, } def test_cli_with_unbalanced_brackets_in_json_string(): class StrToStrDictOptions(BaseSettings): nested: dict[str, str] assert CliApp.run(StrToStrDictOptions, cli_args=['--nested={"test": "{"}']).model_dump() == { 'nested': {'test': '{'} } assert CliApp.run(StrToStrDictOptions, cli_args=['--nested={"test": "}"}']).model_dump() == { 'nested': {'test': '}'} } assert CliApp.run(StrToStrDictOptions, cli_args=['--nested={"test": "["}']).model_dump() == { 'nested': {'test': '['} } assert CliApp.run(StrToStrDictOptions, cli_args=['--nested={"test": "]"}']).model_dump() == { 'nested': {'test': ']'} } class StrToListDictOptions(BaseSettings): nested: dict[str, list[str]] assert CliApp.run(StrToListDictOptions, cli_args=['--nested={"test": ["{"]}']).model_dump() == { 'nested': {'test': ['{']} } assert CliApp.run(StrToListDictOptions, cli_args=['--nested={"test": ["}"]}']).model_dump() == { 'nested': {'test': ['}']} } assert CliApp.run(StrToListDictOptions, cli_args=['--nested={"test": ["["]}']).model_dump() == { 'nested': {'test': ['[']} } assert CliApp.run(StrToListDictOptions, cli_args=['--nested={"test": ["]"]}']).model_dump() == { 'nested': {'test': [']']} } def test_cli_json_optional_default(): class Nested(BaseModel): foo: int = 1 bar: int = 2 class Options(BaseSettings): nested: Nested = Nested(foo=3, bar=4) assert CliApp.run(Options, cli_args=[]).model_dump() == {'nested': {'foo': 3, 'bar': 4}} assert CliApp.run(Options, cli_args=['--nested']).model_dump() == {'nested': {'foo': 1, 'bar': 2}} assert CliApp.run(Options, cli_args=['--nested={}']).model_dump() == {'nested': {'foo': 1, 'bar': 2}} assert CliApp.run(Options, cli_args=['--nested.foo=5']).model_dump() == {'nested': {'foo': 5, 'bar': 2}} def test_cli_parse_args_from_model_config_is_respected_with_settings_customise_sources( monkeypatch: pytest.MonkeyPatch, ): class MySettings(BaseSettings): model_config = SettingsConfigDict(cli_parse_args=True) foo: str @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (CliSettingsSource(settings_cls),) with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--foo', 'bar']) cfg = CliApp.run(MySettings) assert cfg.model_dump() == {'foo': 'bar'} def test_cli_shortcuts_on_flat_object(): class Settings(BaseSettings): option: str = Field(default='foo') list_option: str = Field(default='fizz') model_config = SettingsConfigDict(cli_shortcuts={'option': 'option2', 'list_option': ['list_option2']}) assert CliApp.run(Settings, cli_args=['--option2', 'bar', '--list_option2', 'buzz']).model_dump() == { 'option': 'bar', 'list_option': 'buzz', } def test_cli_shortcuts_on_nested_object(): class TwiceNested(BaseModel): option: str = Field(default='foo') class Nested(BaseModel): twice_nested_option: TwiceNested = TwiceNested() option: str = Field(default='foo') class Settings(BaseSettings): nested: Nested = Nested() model_config = SettingsConfigDict( cli_shortcuts={'nested.option': 'option2', 'nested.twice_nested_option.option': 'twice_nested_option'} ) assert CliApp.run(Settings, cli_args=['--option2', 'bar', '--twice_nested_option', 'baz']).model_dump() == { 'nested': {'option': 'bar', 'twice_nested_option': {'option': 'baz'}} } def test_cli_shortcuts_alias_collision_applies_to_first_target_field(): class Nested(BaseModel): option: str = Field(default='foo') class Settings(BaseSettings): nested: Nested = Nested() option2: str = Field(default='foo2') model_config = SettingsConfigDict(cli_shortcuts={'option2': 'abc', 'nested.option': 'abc'}) assert CliApp.run(Settings, cli_args=['--abc', 'bar']).model_dump() == { 'nested': {'option': 'bar'}, 'option2': 'foo2', } def test_cli_serialize_positional_args(): class Nested(BaseModel): deep: CliPositionalArg[int] class Cfg(BaseSettings): top: CliPositionalArg[int] variadic: CliPositionalArg[list[int]] nested_0: Nested nested_1: Nested cfg = CliApp.run(Cfg, cli_args=['0', '1', '2', '3', '4', '5']) assert cfg.model_dump() == { 'top': 0, 'variadic': [ 1, 2, 3, ], 'nested_0': { 'deep': 4, }, 'nested_1': { 'deep': 5, }, } serialized_cli_args = CliApp.serialize(cfg) assert serialized_cli_args == ['0', '1', '2', '3', '4', '5'] assert CliApp.run(Cfg, cli_args=serialized_cli_args).model_dump() == cfg.model_dump() def test_cli_app_with_separate_parser(monkeypatch): class Cfg(BaseSettings): model_config = SettingsConfigDict(cli_parse_args=True) pet: Literal['dog', 'cat', 'bird'] parser = argparse.ArgumentParser() # The actual parsing of command line argument should not happen here. cli_settings = CliSettingsSource(Cfg, root_parser=parser) parser.add_argument('-e', '--extra', dest='extra', default=0, action='count') parser.add_argument('--num-list', action='append', default=[1]) parser.add_argument('--str-list', action='append', default=['abc']) with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--pet', 'dog', '-eeee']) parsed_args = parser.parse_args() assert parsed_args.extra == 4 assert parsed_args.num_list == [1] assert parsed_args.str_list == ['abc'] # With parsed arguments passed to CliApp.run, the parser should not need to be called again. assert CliApp.run(Cfg, cli_args=parsed_args, cli_settings_source=cli_settings).model_dump() == {'pet': 'dog'} assert parsed_args.extra == 4 assert parsed_args.num_list == [1] assert parsed_args.str_list == ['abc'] def test_cli_serialize_non_default_values(): class Cfg(BaseSettings): default_val: int = 123 non_default_val: int cfg = Cfg(non_default_val=456) assert cfg.model_dump() == {'default_val': 123, 'non_default_val': 456} serialized_cli_args = CliApp.serialize(cfg) assert serialized_cli_args == ['--non_default_val', '456'] assert CliApp.run(Cfg, cli_args=serialized_cli_args).model_dump() == cfg.model_dump() def test_cli_serialize_ordering(): class NestedCfg(BaseSettings): positional: CliPositionalArg[str] optional: int class Cfg(BaseSettings): command: CliSubCommand[NestedCfg] positional: CliPositionalArg[str] optional: int cfg = Cfg(optional=0, positional='pos_1', command=NestedCfg(optional=2, positional='pos_3')) assert cfg.model_dump() == {'command': {'optional': 2, 'positional': 'pos_3'}, 'optional': 0, 'positional': 'pos_1'} serialized_cli_args = CliApp.serialize(cfg) assert CliApp.run(Cfg, cli_args=serialized_cli_args).model_dump() == cfg.model_dump() assert serialized_cli_args == [ '--optional', '0', 'pos_1', 'command', '--optional', '2', 'pos_3', ] serialized_cli_args = CliApp.serialize(cfg, positionals_first=True) assert CliApp.run(Cfg, cli_args=serialized_cli_args).model_dump() == cfg.model_dump() assert serialized_cli_args == [ 'pos_1', '--optional', '0', 'command', 'pos_3', '--optional', '2', ] def test_cli_serialize_styles(): class Cfg(BaseModel): my_list: list[int] my_dict: dict[str, int] cfg = Cfg(my_list=[1, 2, 3], my_dict={'a': 1, 'b': 2, 'c': 3}) serialized_cli_args = CliApp.serialize(cfg, list_style='lazy') assert CliApp.run(Cfg, cli_args=serialized_cli_args).model_dump() == cfg.model_dump() assert serialized_cli_args == ['--my-list', '1,2,3', '--my-dict', '{"a": 1, "b": 2, "c": 3}'] serialized_cli_args = CliApp.serialize(cfg, list_style='argparse') assert CliApp.run(Cfg, cli_args=serialized_cli_args).model_dump() == cfg.model_dump() assert serialized_cli_args == [ '--my-list', '1', '--my-list', '2', '--my-list', '3', '--my-dict', '{"a": 1, "b": 2, "c": 3}', ] serialized_cli_args = CliApp.serialize(cfg, dict_style='env') assert CliApp.run(Cfg, cli_args=serialized_cli_args).model_dump() == cfg.model_dump() assert serialized_cli_args == [ '--my-list', '[1, 2, 3]', '--my-dict', 'a=1', '--my-dict', 'b=2', '--my-dict', 'c=3', ] def test_cli_decoding(): PATH_A_STR = str(PureWindowsPath(Path.cwd())) PATH_B_STR = str(PureWindowsPath(Path.cwd() / 'subdir')) class PathsDecode(BaseSettings): path_a: Path = Field(validation_alias=AliasPath('paths', 0)) path_b: Path = Field(validation_alias=AliasPath('paths', 1)) num_a: int = Field(validation_alias=AliasPath('nums', 0)) num_b: int = Field(validation_alias=AliasPath('nums', 1)) assert CliApp.run( PathsDecode, cli_args=['--paths', PATH_A_STR, '--paths', PATH_B_STR, '--nums', '1', '--nums', '2'] ).model_dump() == { 'path_a': Path(PATH_A_STR), 'path_b': Path(PATH_B_STR), 'num_a': 1, 'num_b': 2, } class PathsListNoDecode(BaseSettings): paths: Annotated[list[Path], NoDecode] nums: Annotated[list[int], NoDecode] @field_validator('paths', mode='before') @classmethod def decode_path_a(cls, paths: str) -> list[Path]: return [Path(p) for p in paths.split(',')] @field_validator('nums', mode='before') @classmethod def decode_nums(cls, nums: str) -> list[int]: return [int(n) for n in nums.split(',')] assert CliApp.run( PathsListNoDecode, cli_args=['--paths', f'{PATH_A_STR},{PATH_B_STR}', '--nums', '1,2'] ).model_dump() == {'paths': [Path(PATH_A_STR), Path(PATH_B_STR)], 'nums': [1, 2]} class PathsAliasNoDecode(BaseSettings): path_a: Annotated[Path, NoDecode] = Field(validation_alias=AliasPath('paths', 0)) path_b: Annotated[Path, NoDecode] = Field(validation_alias=AliasPath('paths', 1)) num_a: Annotated[int, NoDecode] = Field(validation_alias=AliasPath('nums', 0)) num_b: Annotated[int, NoDecode] = Field(validation_alias=AliasPath('nums', 1)) @model_validator(mode='before') @classmethod def intercept_kwargs(cls, data: Any) -> Any: data['paths'] = [Path(p) for p in data['paths'].split(',')] data['nums'] = [int(n) for n in data['nums'].split(',')] return data assert CliApp.run( PathsAliasNoDecode, cli_args=['--paths', f'{PATH_A_STR},{PATH_B_STR}', '--nums', '1,2'] ).model_dump() == { 'path_a': Path(PATH_A_STR), 'path_b': Path(PATH_B_STR), 'num_a': 1, 'num_b': 2, } with pytest.raises( SettingsError, match='Parsing error encountered for paths: Mixing Decode and NoDecode across different AliasPath fields is not allowed', ): class PathsMixedDecode(BaseSettings): path_a: Annotated[Path, ForceDecode] = Field(validation_alias=AliasPath('paths', 0)) path_b: Annotated[Path, NoDecode] = Field(validation_alias=AliasPath('paths', 1)) CliApp.run(PathsMixedDecode, cli_args=['--paths', PATH_A_STR, '--paths', PATH_B_STR]) def test_cli_custom_help(capsys, monkeypatch): class Cfg(BaseSettings): my_help: CliToggleFlag[bool] = Field(False, description='my help string', alias='help') def cli_cmd(self) -> None: if self.my_help: print('custom help no exit') with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) CliApp.run(Cfg) assert capsys.readouterr().out == 'custom help no exit\n' def test_cli_format_help(): class Init(BaseModel, cli_prog_name='example.py'): repo: Path def cli_cmd(self) -> None: print(f'repo: {self.repo}') class RootCommand(BaseSettings, cli_prog_name='example.py'): init: CliSubCommand[Init] def cli_cmd(self) -> None: CliApp.run_subcommand(self) assert ( CliApp.format_help(RootCommand, strip_ansi_color=True) == f"""usage: example.py [-h] {{init}} ... {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit subcommands: {{init}} init """ ) assert ( CliApp.format_help(Init, strip_ansi_color=True) == f"""usage: example.py [-h] --repo Path {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit --repo Path (required) """ ) with pytest.raises( SettingsError, match=re.escape(f'Error: CLI subcommand is required {{init}}\n{CliApp.format_help(RootCommand)}'), ): CliApp.run(RootCommand, cli_args=[], cli_exit_on_error=False) def test_cli_disriminator_choices(): class DivModel(BaseModel): el_type: Literal['div'] = 'div' class_name: str | None = None children: list[Any] | None = None class SpanModel(BaseModel): el_type: Literal['span'] = 'span' class_name: str | None = None contents: str | None = None class ButtonModel(BaseModel): el_type: Literal['button'] = 'button' class_name: str | None = None contents: str | None = None class InputModel(BaseModel): el_type: Literal['input'] = 'input' class_name: str | None = None value: str | None = None class Html(BaseSettings, cli_prog_name='example.py'): contents: DivModel | SpanModel | ButtonModel | InputModel = Field(discriminator='el_type') assert CliApp.format_help(Html, strip_ansi_color=True) == ( f"""usage: example.py [-h] [--contents [JSON]] [--contents.class_name {{str,null}}] [--contents.children {{list[Any],null}}] [--contents.contents {{str,null}}] [--contents.el_type {{button,div,input,span}}] [--contents.value {{str,null}}] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit contents options: --contents [JSON] set contents from JSON string (default: {{}}) --contents.class_name {{str,null}} (default: null) --contents.children {{list[Any],null}} (default: null) --contents.contents {{str,null}} (default: null) --contents.el_type {{button,div,input,span}} (default: input) --contents.value {{str,null}} (default: null) """ ) pydantic-pydantic-settings-198e71c/tests/test_source_gcp_secret_manager.py000066400000000000000000000754161514433345000273210ustar00rootroot00000000000000""" Test pydantic_settings.GoogleSecretSettingsSource """ from typing import Annotated import pytest from pydantic import Field from pytest_mock import MockerFixture from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict from pydantic_settings.sources import GoogleSecretManagerSettingsSource from pydantic_settings.sources.providers.gcp import GoogleSecretManagerMapping, import_gcp_secret_manager from pydantic_settings.sources.types import SecretVersion try: gcp_secret_manager = True import_gcp_secret_manager() from google.cloud.secretmanager import SecretManagerServiceClient except ImportError: gcp_secret_manager = False SECRET_VALUES = {'test-secret': 'test-value'} @pytest.fixture def mock_secret_client_factory(mocker: MockerFixture): def _create_client(secrets_config: list[dict] | None = None): client = mocker.Mock(spec=SecretManagerServiceClient) # Default config if generic usage if secrets_config is None: secrets_config = [ {'name': 'test-secret', 'project': 'test-project', 'version': 'latest', 'value': 'test-value'} ] # Helper to normalize access path def _get_path(project, secret, version): # If secret is already a path, extract the name if '/' in secret: secret = secret.split('/')[-1] return f'projects/{project}/secrets/{secret}/versions/{version}' client.secret_version_path = _get_path client.common_project_path.return_value = 'projects/test-project' client.parse_secret_path = SecretManagerServiceClient.parse_secret_path # Prepare data for list_secrets and access_secret_version known_secrets = [] secret_values = {} for cfg in secrets_config: project = cfg.get('project', 'test-project') secret_name = cfg['name'] version = cfg.get('version', 'latest') value = cfg.get('value', 'test-value') full_secret_path = f'projects/{project}/secrets/{secret_name}' full_version_path = _get_path(project, secret_name, version) # Create secret mock for list_secrets s_mock = mocker.Mock() s_mock.name = full_secret_path # Avoid duplicates in listing if not any(s.name == full_secret_path for s in known_secrets): known_secrets.append(s_mock) # Store value for access secret_values[full_version_path] = value client.list_secrets.return_value = known_secrets def mock_access_secret_version(name: str): if name in secret_values: resp = mocker.Mock() resp.payload.data.decode.return_value = secret_values[name] return resp raise Exception(f'Secret not found or access denied: {name}') client.access_secret_version = mocker.Mock(side_effect=mock_access_secret_version) return client return _create_client @pytest.fixture def mock_secret_client(mock_secret_client_factory): """Legacy fixture support for tests that just need the default 'test-secret'.""" return mock_secret_client_factory() @pytest.fixture def secret_manager_mapping(mock_secret_client): return GoogleSecretManagerMapping(mock_secret_client, project_id='test-project', case_sensitive=True) @pytest.fixture def test_settings(): class TestSettings(BaseSettings): test_secret: str another_secret: str return TestSettings @pytest.fixture(autouse=True) def mock_google_auth(mocker: MockerFixture): mocker.patch( 'pydantic_settings.sources.providers.gcp.google_auth_default', return_value=(mocker.Mock(), 'test-project') ) @pytest.mark.skipif(not gcp_secret_manager, reason='pydantic-settings[gcp-secret-manager] is not installed') class TestGoogleSecretManagerSettingsSource: """Test GoogleSecretManagerSettingsSource.""" def test_secret_manager_mapping_init(self, secret_manager_mapping): assert secret_manager_mapping._project_id == 'test-project' assert len(secret_manager_mapping._loaded_secrets) == 0 def test_secret_manager_mapping_gcp_project_path(self, secret_manager_mapping, mock_secret_client): secret_manager_mapping._gcp_project_path mock_secret_client.common_project_path.assert_called_once_with('test-project') def test_secret_manager_mapping_secret_names(self, secret_manager_mapping): names = secret_manager_mapping._secret_names assert names == ['test-secret'] def test_secret_manager_mapping_getitem_access_error(self, secret_manager_mapping, mocker): secret_manager_mapping._secret_client.access_secret_version = mocker.Mock( side_effect=Exception('Access denied') ) assert secret_manager_mapping['test-secret'] is None def test_secret_manager_mapping_iter(self, secret_manager_mapping): assert list(secret_manager_mapping) == ['test-secret'] @pytest.mark.parametrize( 'project_id, credentials, expected_project_id', [ pytest.param(None, None, 'test-project', id='Init: Default Project ID from Auth'), pytest.param('custom-project', 'mock', 'custom-project', id='Init: Custom Project ID and Credentials'), ], ) def test_settings_source_init( self, mocker, mock_google_auth, test_settings, project_id, credentials, expected_project_id ): if credentials == 'mock': credentials = mocker.Mock() source = GoogleSecretManagerSettingsSource(test_settings, credentials=credentials, project_id=project_id) assert source._project_id == expected_project_id if credentials: assert source._credentials == credentials def test_settings_source_init_with_custom_values_no_project_raises_error(self, mocker, test_settings): credentials = mocker.Mock() mocker.patch('pydantic_settings.sources.providers.gcp.google_auth_default', return_value=(mocker.Mock(), None)) with pytest.raises(AttributeError): _ = GoogleSecretManagerSettingsSource(test_settings, credentials=credentials) def test_settings_source_load_env_vars(self, mock_secret_client, mocker, test_settings): credentials = mocker.Mock() source = GoogleSecretManagerSettingsSource(test_settings, credentials=credentials, project_id='test-project') source._secret_client = mock_secret_client env_vars = source._load_env_vars() assert isinstance(env_vars, GoogleSecretManagerMapping) assert env_vars.get('test-secret') == 'test-value' assert env_vars.get('another_secret') is None def test_settings_source_repr(self, test_settings): source = GoogleSecretManagerSettingsSource(test_settings, project_id='test-project') assert 'test-project' in repr(source) assert 'GoogleSecretManagerSettingsSource' in repr(source) def test_pydantic_base_settings(self, mock_secret_client, monkeypatch, mocker): monkeypatch.setenv('ANOTHER_SECRET', 'yep_this_one') class Settings(BaseSettings, case_sensitive=False): test_secret: str = Field(..., alias='test-secret') another_secret: str = Field(..., alias='ANOTHER_SECRET') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: google_secret_manager_settings = GoogleSecretManagerSettingsSource( settings_cls, secret_client=mock_secret_client ) return ( init_settings, env_settings, dotenv_settings, file_secret_settings, google_secret_manager_settings, ) settings = Settings() # type: ignore assert settings.another_secret == 'yep_this_one' assert settings.test_secret == 'test-value' def test_pydantic_base_settings_with_unknown_attribute(self, mock_secret_client, monkeypatch, mocker): from pydantic_core._pydantic_core import ValidationError class Settings(BaseSettings, case_sensitive=False): test_secret: str = Field(..., alias='test-secret') another_secret: str = Field(..., alias='ANOTHER_SECRET') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: google_secret_manager_settings = GoogleSecretManagerSettingsSource( settings_cls, secret_client=mock_secret_client ) return ( init_settings, env_settings, dotenv_settings, file_secret_settings, google_secret_manager_settings, ) with pytest.raises(ValidationError): _ = Settings() # type: ignore def test_pydantic_base_settings_with_default_value(self, mock_secret_client): class Settings(BaseSettings): my_field: str | None = Field(default='foo') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: google_secret_manager_settings = GoogleSecretManagerSettingsSource( settings_cls, secret_client=mock_secret_client ) return ( init_settings, env_settings, dotenv_settings, file_secret_settings, google_secret_manager_settings, ) settings = Settings() assert settings.my_field == 'foo' def test_secret_manager_mapping_list_secrets_error(self, secret_manager_mapping, mocker): secret_manager_mapping._secret_client.list_secrets = mocker.Mock(side_effect=Exception('Permission denied')) with pytest.raises(Exception, match='Permission denied'): _ = secret_manager_mapping._secret_names @pytest.mark.parametrize( 'case_sensitive, secret_name_in_gcp, requested_key, expected_value, expected_error', [ pytest.param( True, 'test-secret', 'test-secret', 'test-value', None, id='Retrieval: CS=True - Exact Match (Lower)' ), pytest.param( True, 'TEST-SECRET', 'TEST-SECRET', 'test-value', None, id='Retrieval: CS=True - Exact Match (Upper)' ), pytest.param( True, 'testSecret', 'testSecret', 'test-value', None, id='Retrieval: CS=True - Exact Match (Camel)' ), pytest.param( True, 'TEST-SECRET', 'test-secret', None, KeyError, id='Retrieval: CS=True - Case Mismatch Raises KeyError', ), pytest.param( True, 'test-secret', 'TEST_SECRET', None, KeyError, id='Retrieval: CS=True - Key Mismatch Raises KeyError', ), pytest.param( False, 'test-secret', 'TEST-SECRET', 'test-value', None, id='Retrieval: CS=False - Uppercase Key / Lowercase Secret', ), pytest.param( False, 'TEST-SECRET', 'test-secret', 'test-value', None, id='Retrieval: CS=False - Lowercase Key / Uppercase Secret', ), pytest.param( False, 'TEST-SECRET', 'TEST-SECRET', 'test-value', None, id='Retrieval: CS=False - Exact Match (Upper)' ), pytest.param( False, 'testSecret', 'testSecret', 'test-value', None, id='Retrieval: CS=False - Exact Match (Camel)' ), pytest.param( False, 'testSecret', 'TESTSECRET', 'test-value', None, id='Retrieval: CS=False - Uppercase Key / Camel Case Secret', ), pytest.param( True, 'test-secret', 'nonexistent-secret', None, KeyError, id='Retrieval: CS=True - Nonexistent Secret Raises KeyError', ), pytest.param( False, 'test-secret', 'nonexistent-secret', None, KeyError, id='Retrieval: CS=False - Nonexistent Secret Raises KeyError', ), ], ) def test_secret_manager_mapping_retrieval_cases( self, mock_secret_client_factory, case_sensitive, secret_name_in_gcp, requested_key, expected_value, expected_error, ): """ Tests various combinations of case sensitivity, secret naming, and error raising (when the Key doesn't exist). """ client = mock_secret_client_factory([{'name': secret_name_in_gcp, 'value': 'test-value'}]) mapping = GoogleSecretManagerMapping(client, project_id='test-project', case_sensitive=case_sensitive) if expected_error: with pytest.raises(expected_error): _ = mapping[requested_key] else: assert mapping[requested_key] == expected_value @pytest.mark.parametrize( 'case_sensitive, requested_key, expected_value', [ pytest.param( True, 'TEST-SECRET', 'UPPER_VAL', id='Collision Test: CS=True - Uppercase Key Returns Correct Value' ), pytest.param( True, 'test-secret', 'lower_val', id='Collision Test: CS=True - Lowercase Key Returns Correct Value' ), # Case insensitive collision with "Prefer Exact Match" logic: pytest.param( False, 'TEST-SECRET', 'UPPER_VAL', id='Collision Test: CS=False - Uppercase Key Prefers Exact Match' ), pytest.param( False, 'test-secret', 'lower_val', id='Collision Test: CS=False - Lowercase Key Prefers Exact Match' ), pytest.param( False, 'Test-Secret', 'lower_val', id='Collision Test: CS=False - Mixed Case Key Fallback to Last Loaded', ), ], ) def test_secret_manager_mapping_collision( self, mock_secret_client_factory, case_sensitive, requested_key, expected_value ): client = mock_secret_client_factory( [ {'name': 'TEST-SECRET', 'value': 'UPPER_VAL'}, {'name': 'test-secret', 'value': 'lower_val'}, ] ) mapping = GoogleSecretManagerMapping(client, project_id='test-project', case_sensitive=case_sensitive) if not case_sensitive: with pytest.warns(UserWarning, match='Secret collision'): _ = mapping._secret_name_map else: _ = mapping._secret_name_map assert mapping[requested_key] == expected_value @pytest.mark.parametrize( 'case_sensitive', [ # Case Sensitive = True: We expect exact alias match to work pytest.param(True, id='Version Annotation: CS=True - Exact Alias Match'), # Case Sensitive = False: We expect case-insensitive match (alias mismatch) to work pytest.param(False, id='Version Annotation: CS=False - Case Insensitive Alias Match'), ], ) def test_secret_version_annotation(self, mock_secret_client, mocker, case_sensitive): mock_secret_client.secret_version_path = ( lambda project, secret, version: f'projects/{project}/secrets/{secret}/versions/{version}' ) resp_latest = mocker.Mock() resp_latest.payload.data.decode.return_value = 'latest-value' resp_v1 = mocker.Mock() resp_v1.payload.data.decode.return_value = 'v1-value' def mock_access(name: str): if name.endswith('/versions/latest'): return resp_latest if name.endswith('/versions/1'): return resp_v1 raise Exception(f'Not found: {name}') mock_secret_client.access_secret_version = mock_access alias = 'test-secret' if case_sensitive else 'TEST-SECRET' class Settings(BaseSettings): model_config = SettingsConfigDict(populate_by_name=True, case_sensitive=case_sensitive) test_secret_v1: Annotated[str, Field(alias=alias), SecretVersion('1')] test_secret_latest: str = Field(alias='test-secret') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( GoogleSecretManagerSettingsSource( settings_cls, secret_client=mock_secret_client, case_sensitive=case_sensitive ), ) s = Settings() assert s.test_secret_v1 == 'v1-value' assert s.test_secret_latest == 'latest-value' def test_secret_version_annotation_missing_secret(self, mock_secret_client, mocker): """Test SecretVersion annotation when secret is missing""" mock_secret_client.secret_version_path.return_value = 'path/to/missing' # Only test-secret exists mock_secret_client.list_secrets.return_value = [] class Settings(BaseSettings): missing_secret: Annotated[str, SecretVersion('1')] = 'default' @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (GoogleSecretManagerSettingsSource(settings_cls, secret_client=mock_secret_client),) s = Settings() assert s.missing_secret == 'default' def test_secret_version_annotation_access_failure(self, mock_secret_client, mocker): """Test SecretVersion annotation when secret access fails (covers branch 209->202).""" mock_secret_client.secret_version_path.return_value = 'path/to/secret' # Secret exists in list secret = mocker.Mock() secret.name = 'projects/test-project/secrets/existing-secret' mock_secret_client.list_secrets.return_value = [secret] # Access fails (returns None from mapping._get_secret_value due to exception) mock_secret_client.access_secret_version.side_effect = Exception('Access denied') class Settings(BaseSettings): model_config = SettingsConfigDict(populate_by_name=True) existing_secret: Annotated[str, Field(alias='existing-secret'), SecretVersion('1')] = 'default' @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (GoogleSecretManagerSettingsSource(settings_cls, secret_client=mock_secret_client),) s = Settings() assert s.existing_secret == 'default' def test_secret_version_annotation_case_insensitive_multiple_versions(self, mock_secret_client_factory, mocker): """ Test fetching multiple versions of the same secret with case-insensitive, matching different aliases. """ client = mock_secret_client_factory( [ {'name': 'test-secret', 'version': '1', 'value': 'v1-val'}, {'name': 'test-secret', 'version': '2', 'value': 'v2-val'}, ] ) class Settings(BaseSettings): model_config = SettingsConfigDict(populate_by_name=True, case_sensitive=False) # Both should map to 'test-secret' in GCP, but requesting different versions v1: Annotated[str, Field(alias='test-secret'), SecretVersion('1')] v2: Annotated[str, Field(alias='TEST-SECRET'), SecretVersion('2')] @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (GoogleSecretManagerSettingsSource(settings_cls, secret_client=client, case_sensitive=False),) s = Settings() assert s.v1 == 'v1-val' assert s.v2 == 'v2-val' def test_secret_version_annotation_case_sensitive_failure(self, mock_secret_client, mocker): """Test that case sensitive lookup fails when case mismatches.""" mock_secret_client.secret_version_path.return_value = 'path/to/secret' # Secret exists as 'test-secret' secret = mocker.Mock() secret.name = 'projects/test-project/secrets/test-secret' mock_secret_client.list_secrets.return_value = [secret] class Settings(BaseSettings): model_config = SettingsConfigDict(populate_by_name=True, case_sensitive=True) # Alias mismatch case my_secret: Annotated[str, Field(alias='TEST-SECRET'), SecretVersion('1')] @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( GoogleSecretManagerSettingsSource( settings_cls, secret_client=mock_secret_client, case_sensitive=True ), ) from pydantic import ValidationError with pytest.raises(ValidationError) as excinfo: Settings() # Expecting validation error because the secret was not found due to case mismatch # and no default value was provided. assert 'TEST-SECRET' in str(excinfo.value) def test_secret_manager_cache_behavior(self, mock_secret_client_factory, mocker): """Test that accessing the same secret twice does not fetch it again from GCP.""" client = mock_secret_client_factory([{'name': 'test-secret', 'value': 'secret-value'}]) mapping = GoogleSecretManagerMapping(client, project_id='test-project', case_sensitive=True) # First access val1 = mapping['test-secret'] assert val1 == 'secret-value' assert client.access_secret_version.call_count == 1 # Second access should hit cache val2 = mapping['test-secret'] assert val2 == 'secret-value' assert client.access_secret_version.call_count == 1 def test_init_triggers_import(self, mocker, test_settings): """Test that initializing the source triggers the import if globals are None.""" mocker.patch('pydantic_settings.sources.providers.gcp.SecretManagerServiceClient', None) mock_import = mocker.patch('pydantic_settings.sources.providers.gcp.import_gcp_secret_manager') # Side effect to restore the class so initialization continues def side_effect(): mocker.patch('pydantic_settings.sources.providers.gcp.SecretManagerServiceClient', mocker.Mock()) mocker.patch('pydantic_settings.sources.providers.gcp.Credentials', mocker.Mock()) mocker.patch( 'pydantic_settings.sources.providers.gcp.google_auth_default', return_value=(mocker.Mock(), 'p') ) mock_import.side_effect = side_effect credentials = mocker.Mock() GoogleSecretManagerSettingsSource(test_settings, credentials=credentials, project_id='p') def test_secret_version_no_fallback(self, mock_secret_client_factory, mocker): """ Test that we do NOT fallback to 'latest' if a specific version is requested but missing. """ # Client has 'latest' but NOT version '1' # We simulate this by having the mock client raise an exception or return None for v1 path # but work for latest. client = mock_secret_client_factory([{'name': 'test-secret', 'value': 'latest-val'}]) # We need to ensure accessing version '1' fails. # The factory's access_secret_version mocks per path. original_access = client.access_secret_version def access_side_effect(name, **kwargs): if 'versions/1' in name: raise Exception('Version 1 missing') return original_access(name, **kwargs) client.access_secret_version.side_effect = access_side_effect class Settings(BaseSettings): model_config = SettingsConfigDict(populate_by_name=True) # This should fail if we don't fallback (because v1 is missing) # If we DO fallback (bug), it will get 'latest-val' v1: Annotated[str, Field(alias='test-secret'), SecretVersion('1')] @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (GoogleSecretManagerSettingsSource(cls, secret_client=client, project_id='test-project'),) from pydantic import ValidationError # EXPECTATION: ValidationError because v1 is missing. # REALITY (Bug): It gets 'latest-val' and succeeds. # We assert that it raises ValidationError. # If the bug is present, this assertion will FAIL (no exception raised). with pytest.raises(ValidationError) as excinfo: Settings() assert 'Field required' in str(excinfo.value) or 'test-secret' in str(excinfo.value) """ Test that duplicate aliases with different versions work correctly when populate_by_name is True. """ client = mock_secret_client_factory( [ {'name': 'test-secret', 'version': '1', 'value': 'v1-val'}, {'name': 'test-secret', 'version': '2', 'value': 'v2-val'}, ] ) class Settings(BaseSettings): model_config = SettingsConfigDict(populate_by_name=True, case_sensitive=True) v1: Annotated[str, Field(alias='test-secret'), SecretVersion('1')] v2: Annotated[str, Field(alias='test-secret'), SecretVersion('2')] @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (GoogleSecretManagerSettingsSource(cls, secret_client=client, project_id='test-project'),) s = Settings() assert s.v1 == 'v1-val' assert s.v2 == 'v2-val' def test_secret_version_annotation_duplicate_alias_fail(self, mock_secret_client_factory, mocker): """ Test that duplicate aliases fail when populate_by_name is False. """ client = mock_secret_client_factory( [ {'name': 'test-secret', 'version': '1', 'value': 'v1-val'}, {'name': 'test-secret', 'version': '2', 'value': 'v2-val'}, ] ) class Settings(BaseSettings): model_config = SettingsConfigDict(populate_by_name=False) v1: Annotated[str, Field(alias='test-secret'), SecretVersion('1')] v2: Annotated[str, Field(alias='test-secret'), SecretVersion('2')] @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (GoogleSecretManagerSettingsSource(cls, secret_client=client, project_id='test-project'),) s = Settings() # With populate_by_name=False, we return the alias as the key. # Since both fields have the same alias, they collide in the input dictionary. # Pydantic assigns the single value to both fields. # This proves we need populate_by_name=True to distinguish them. assert s.v1 == s.v2 # One of them is definitely wrong (or both if overridden by something else, but here one wins) assert s.v1 != 'v1-val' or s.v2 != 'v2-val' pydantic-pydantic-settings-198e71c/tests/test_source_json.py000066400000000000000000000155741514433345000244610ustar00rootroot00000000000000""" Test pydantic_settings.JsonConfigSettingsSource. """ import importlib.resources import json import sys if sys.version_info < (3, 11): from importlib.abc import Traversable else: from importlib.resources.abc import Traversable from pathlib import Path import pytest from pydantic import BaseModel from pydantic_settings import ( BaseSettings, JsonConfigSettingsSource, PydanticBaseSettingsSource, SettingsConfigDict, ) def test_repr() -> None: source = JsonConfigSettingsSource(BaseSettings, Path('config.json')) assert repr(source) == 'JsonConfigSettingsSource(json_file=config.json)' def test_json_file(tmp_path): p = tmp_path / '.env' p.write_text( """ {"foobar": "Hello", "nested": {"nested_field": "world!"}, "null_field": null} """ ) class Nested(BaseModel): nested_field: str class Settings(BaseSettings): model_config = SettingsConfigDict(json_file=p) foobar: str nested: Nested null_field: str | None @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (JsonConfigSettingsSource(settings_cls),) s = Settings() assert s.foobar == 'Hello' assert s.nested.nested_field == 'world!' def test_json_no_file(): class Settings(BaseSettings): model_config = SettingsConfigDict(json_file=None) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (JsonConfigSettingsSource(settings_cls),) s = Settings() assert s.model_dump() == {} def test_multiple_file_json(tmp_path): p5 = tmp_path / '.env.json5' p6 = tmp_path / '.env.json6' with open(p5, 'w') as f5: json.dump({'json5': 5}, f5) with open(p6, 'w') as f6: json.dump({'json6': 6}, f6) class Settings(BaseSettings): json5: int json6: int @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (JsonConfigSettingsSource(settings_cls, json_file=[p5, p6]),) s = Settings() assert s.model_dump() == {'json5': 5, 'json6': 6} @pytest.mark.parametrize('deep_merge', [False, True]) def test_multiple_file_json_merge(tmp_path, deep_merge): p5 = tmp_path / '.env.json5' p6 = tmp_path / '.env.json6' with open(p5, 'w') as f5: json.dump({'hello': 'world', 'nested': {'foo': 1, 'bar': 2}}, f5) with open(p6, 'w') as f6: json.dump({'nested': {'foo': 3}}, f6) class Nested(BaseModel): foo: int bar: int = 0 class Settings(BaseSettings): hello: str nested: Nested @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (JsonConfigSettingsSource(settings_cls, json_file=[p5, p6], deep_merge=deep_merge),) s = Settings() assert s.model_dump() == {'hello': 'world', 'nested': {'foo': 3, 'bar': 2 if deep_merge else 0}} class TestTraversableSupport: FILENAME = 'example_test_config.json' @pytest.fixture(params=['importlib_resources', 'custom', 'custom_with_path']) def json_config_path(self, request, tmp_path): tests_package_dir = importlib.resources.files('tests') if request.param == 'importlib_resources': # get Traversable object using importlib.resources return tests_package_dir / self.FILENAME # Create a custom Traversable implementation class CustomTraversable(Traversable): def __init__(self, path): self._path = path def __truediv__(self, child): return CustomTraversable(self._path / child) def is_file(self): return self._path.is_file() def is_dir(self): return self._path.is_dir() def iterdir(self): raise NotImplementedError('iterdir not implemented for this test') def open(self, mode='r', *args, **kwargs): return self._path.open(mode, *args, **kwargs) def read_bytes(self): return self._path.read_bytes() def read_text(self, encoding=None): return self._path.read_text(encoding=encoding) @property def name(self): return self._path.name def joinpath(self, *descendants): return CustomTraversable(self._path.joinpath(*descendants)) if request.param == 'custom': custom_traversable = CustomTraversable(tests_package_dir) return custom_traversable / self.FILENAME filepath = tmp_path / self.FILENAME with filepath.open('w') as f: json.dump({'foobar': 'test'}, f) return CustomTraversable(filepath) def test_traversable_support(self, json_config_path: Traversable): assert json_config_path.is_file() class Settings(BaseSettings): foobar: str model_config = SettingsConfigDict( # Traversable is not added in annotation, but is supported json_file=json_config_path, ) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (JsonConfigSettingsSource(settings_cls),) s = Settings() # "test" value in file assert s.foobar == 'test' pydantic-pydantic-settings-198e71c/tests/test_source_nested_secrets.py000066400000000000000000000305511514433345000265120ustar00rootroot00000000000000from enum import Enum from os import sep import pytest from pydantic import BaseModel from pydantic_settings import ( BaseSettings, NestedSecretsSettingsSource, SecretsSettingsSource, SettingsConfigDict, SettingsError, ) from pydantic_settings.sources.providers.nested_secrets import SECRETS_DIR_MAX_SIZE class DbSettings(BaseModel): user: str passwd: str | None = None class AppSettings(BaseSettings): app_key: str | None = None db: DbSettings @classmethod def settings_customise_sources( cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings, ): return ( init_settings, env_settings, NestedSecretsSettingsSource(file_secret_settings), ) class SampleEnum(str, Enum): TEST = 'test' def test_repr(tmp_path): class Settings(BaseSettings): model_config = SettingsConfigDict( secrets_dir=tmp_path, ) src = NestedSecretsSettingsSource(SecretsSettingsSource(Settings)) assert f'{src!r}'.startswith(f'{src.__class__.__name__}(') def test_source_off(env, tmp_files): env.set('DB__USER', 'user') tmp_files.write( { 'app_key': 'secret1', 'db__passwd': 'secret2', } ) class Settings(AppSettings): model_config = SettingsConfigDict( env_nested_delimiter='__', ) assert Settings().model_dump() == { 'app_key': None, 'db': {'user': 'user', 'passwd': None}, } def test_delimited_name(env, tmp_files): env.set('DB__USER', 'user') tmp_files.write( { 'app_key': 'secret1', 'db___passwd': 'secret2', } ) class Settings(AppSettings): model_config = SettingsConfigDict( env_nested_delimiter='__', secrets_dir=tmp_files.basedir, secrets_nested_delimiter='___', ) assert Settings().model_dump() == { 'app_key': 'secret1', 'db': {'user': 'user', 'passwd': 'secret2'}, } def test_secrets_dir_as_arg(env, tmp_files): env.set('DB__USER', 'user') tmp_files.write( { 'app_key': 'secret1', 'db__passwd': 'secret2', } ) class Settings(AppSettings): model_config = SettingsConfigDict( env_nested_delimiter='__', secrets_nested_delimiter='__', ) assert Settings(_secrets_dir=tmp_files.basedir).model_dump() == { 'app_key': 'secret1', 'db': {'user': 'user', 'passwd': 'secret2'}, } @pytest.mark.parametrize( 'conf,secrets', ( ( dict(secrets_nested_delimiter='___', secrets_prefix='prefix_'), {'prefix_app_key': 'secret1', 'prefix_db___passwd': 'secret2'}, ), ( dict(secrets_nested_subdir=True, secrets_prefix='prefix_'), {'prefix_app_key': 'secret1', 'prefix_db/passwd': 'secret2'}, ), ( dict(secrets_nested_subdir=True, secrets_prefix=f'dir1{sep}dir2{sep}'), {'dir1/dir2/app_key': 'secret1', 'dir1/dir2/db/passwd': 'secret2'}, ), ), ) def test_prefix(conf: SettingsConfigDict, secrets, env, tmp_files): env.set('DB__USER', 'user') tmp_files.write(secrets) class Settings(AppSettings): model_config = SettingsConfigDict( env_nested_delimiter='__', secrets_dir=tmp_files.basedir, **conf, ) assert Settings().model_dump() == { 'app_key': 'secret1', 'db': {'user': 'user', 'passwd': 'secret2'}, } def test_subdir(env, tmp_files): env.set('DB__USER', 'user') tmp_files.write( { 'app_key': 'secret1', 'db/passwd': 'secret2', # file in subdir } ) class Settings(AppSettings): model_config = SettingsConfigDict( secrets_dir=tmp_files.basedir, env_nested_delimiter='__', secrets_nested_subdir=True, ) assert Settings().model_dump() == { 'app_key': 'secret1', 'db': {'user': 'user', 'passwd': 'secret2'}, } def test_symlink_subdir(env, tmp_files): env.set('DB__USER', 'user') # fmt: off tmp_files.write({ 'app_key': 'secret1', 'db_random/passwd': 'secret2', # file in subdir that is not directly referenced in our settings }) tmp_files.basedir.joinpath('db').symlink_to(tmp_files.basedir.joinpath('db_random')) # fmt: on class Settings(AppSettings): model_config = SettingsConfigDict( secrets_dir=tmp_files.basedir, env_nested_delimiter='__', secrets_nested_subdir=True, ) assert Settings().model_dump() == { 'app_key': 'secret1', 'db': {'user': 'user', 'passwd': 'secret2'}, } @pytest.mark.parametrize( 'conf,secrets,dirs,expected', ( ( # when multiple secrets_dir values are given, their values are merged dict(), {'dir1/key1': 'a', 'dir1/key2': 'b', 'dir2/key2': 'c'}, ['dir1', 'dir2'], {'key1': 'a', 'key2': 'c'}, ), ( # when secrets_dir is not a directory, error is raised dict(), {'some_file': ''}, 'some_file', (SettingsError, 'must reference a directory'), ), ( # missing secrets_dir emits warning by default dict(), {'key1': 'value'}, 'missing_subdir', (UserWarning, 1, 'does not exist', {'key1': None, 'key2': None}), ), ( # ...or expect warning explicitly (identical behaviour) dict(secrets_dir_missing='warn'), {'key1': 'value'}, 'missing_subdir', (UserWarning, 1, 'does not exist', {'key1': None, 'key2': None}), ), ( # missing secrets_dir warning can be suppressed dict(secrets_dir_missing='ok'), {'key1': 'value'}, 'missing_subdir', {'key1': None, 'key2': None}, ), ( # missing secrets_dir can raise error dict(secrets_dir_missing='error'), {'key1': 'value'}, 'missing_subdir', (SettingsError, 'does not exist'), ), ( # invalid secrets_dir_missing value raises error dict(secrets_dir_missing='uNeXpEcTeD'), {'key1': 'value'}, 'missing_subdir', (SettingsError, 'invalid secrets_dir_missing value'), ), ( # when multiple secrets_dir do not exist, multiple warnings are emitted dict(), {'key1': 'value'}, ['missing_subdir1', 'missing_subdir2'], (UserWarning, 2, 'does not exist', {'key1': None, 'key2': None}), ), ( # secrets_dir size is limited dict(), {'key1': 'x' * SECRETS_DIR_MAX_SIZE}, '.', {'key1': 'x' * SECRETS_DIR_MAX_SIZE, 'key2': None}, ), ( # ...and raises error if file is larger than the limit dict(), {'key1': 'x' * (SECRETS_DIR_MAX_SIZE + 1)}, '.', (SettingsError, 'secrets_dir size'), ), ( # secrets_dir size limit can be adjusted dict(secrets_dir_max_size=100), {'key1': 'x' * 100}, '.', {'key1': 'x' * 100, 'key2': None}, ), ( # ...and raises error if file is larger than the limit dict(secrets_dir_max_size=100), {'key1': 'x' * 101}, '.', (SettingsError, 'secrets_dir size'), ), ( # ...even if secrets_dir size exceeds limit because of another file dict(secrets_dir_max_size=100), {'another_file': 'x' * 101}, '.', (SettingsError, 'secrets_dir size'), ), ( # when multiple secrets_dir values are given, their sizes are not added dict(secrets_dir_max_size=100), {'dir1/key1': 'x' * 100, 'dir2/key2': 'y' * 100}, ['dir1', 'dir2'], {'key1': 'x' * 100, 'key2': 'y' * 100}, ), ), ) def test_multiple_secrets_dirs(conf: SettingsConfigDict, secrets, dirs, expected, tmp_files): secrets_dirs = [tmp_files.basedir / d for d in dirs] if isinstance(dirs, list) else tmp_files.basedir / dirs tmp_files.write(secrets) class Settings(BaseSettings): key1: str | None = None key2: str | None = None model_config = SettingsConfigDict(secrets_dir=secrets_dirs, **conf) @classmethod def settings_customise_sources( cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings, ): return (NestedSecretsSettingsSource(file_secret_settings),) # clean execution if isinstance(expected, dict): assert Settings().model_dump() == expected # error elif isinstance(expected, tuple) and len(expected) == 2: error_type, msg_fragment = expected with pytest.raises(error_type, match=msg_fragment): Settings() # warnings elif isinstance(expected, tuple) and len(expected) == 4: warning_type, warning_count, msg_fragment, value = expected with pytest.warns(warning_type) as warninfo: settings = Settings() assert len(warninfo) == warning_count assert all(msg_fragment in str(w.message) for w in warninfo) assert settings.model_dump() == value # unexpected else: raise AssertionError('unreachable') def test_strip_whitespace(env, tmp_files): env.set('DB__USER', 'user') tmp_files.write( { 'app_key': ' secret1 ', 'db__passwd': '\tsecret2\n', } ) class Settings(AppSettings): model_config = SettingsConfigDict( env_nested_delimiter='__', secrets_dir=tmp_files.basedir, secrets_nested_delimiter='__', ) assert Settings(_secrets_dir=tmp_files.basedir).model_dump() == { 'app_key': 'secret1', 'db': {'user': 'user', 'passwd': 'secret2'}, } def test_invalid_options(tmp_path): class Settings(AppSettings): model_config = SettingsConfigDict( secrets_dir=tmp_path, env_nested_delimiter='__', secrets_nested_subdir=True, secrets_nested_delimiter='__', ) with pytest.raises(SettingsError, match='mutually exclusive'): Settings() @pytest.mark.parametrize( 'conf,expected', ( # default settings ({}, dict(field_empty='', field_none='null', field_enum=SampleEnum.TEST)), # env_ignore_empty has no effect on secrets ({'env_ignore_empty': True}, dict(field_empty='')), ({'env_ignore_empty': False}, dict(field_empty='')), # env_parse_none_str has no effect on secrets ({'env_parse_none_str': 'null'}, dict(field_none='null')), # env_parse_enums has no effect on secrets ({'env_parse_enums': True}, dict(field_enum=SampleEnum.TEST)), ({'env_parse_enums': False}, dict(field_enum=SampleEnum.TEST)), ), ) def test_env_ignore_empty(conf: SettingsConfigDict, expected, tmp_files): tmp_files.write( { 'field_empty': '', 'field_none': 'null', 'field_enum': 'test', } ) class Settings(BaseSettings): field_empty: str | None = None field_none: str | None = None field_enum: SampleEnum | None = None class Original(Settings): model_config = SettingsConfigDict(secrets_dir=tmp_files.basedir, **conf) class Evaluated(Settings): model_config = SettingsConfigDict(secrets_dir=tmp_files.basedir, **conf) @classmethod def settings_customise_sources( cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings, ): return (NestedSecretsSettingsSource(file_secret_settings),) original = Original() evaluated = Evaluated() assert original.model_dump() == evaluated.model_dump() for k, v in expected.items(): assert getattr(original, k) == getattr(evaluated, k) == v pydantic-pydantic-settings-198e71c/tests/test_source_pyproject_toml.py000066400000000000000000000246301514433345000265530ustar00rootroot00000000000000""" Test pydantic_settings.PyprojectTomlConfigSettingsSource. """ import sys from pathlib import Path import pytest from pydantic import BaseModel from pytest_mock import MockerFixture from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, PyprojectTomlConfigSettingsSource, SettingsConfigDict, ) try: import tomli except ImportError: tomli = None MODULE = 'pydantic_settings.sources.providers.pyproject' SOME_TOML_DATA = """ field = "top-level" [some] [some.table] field = "some" [other.table] field = "other" """ class SimpleSettings(BaseSettings): """Simple settings.""" model_config = SettingsConfigDict(pyproject_toml_depth=1, pyproject_toml_table_header=('some', 'table')) @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') class TestPyprojectTomlConfigSettingsSource: """Test PyprojectTomlConfigSettingsSource.""" def test___init__(self, mocker: MockerFixture, tmp_path: Path) -> None: """Test __init__.""" mocker.patch(f'{MODULE}.Path.cwd', return_value=tmp_path) pyproject = tmp_path / 'pyproject.toml' pyproject.write_text(SOME_TOML_DATA) obj = PyprojectTomlConfigSettingsSource(SimpleSettings) assert obj.toml_table_header == ('some', 'table') assert obj.toml_data == {'field': 'some'} assert obj.toml_file_path == tmp_path / 'pyproject.toml' def test___init___explicit(self, mocker: MockerFixture, tmp_path: Path) -> None: """Test __init__ explicit file.""" mocker.patch(f'{MODULE}.Path.cwd', return_value=tmp_path) pyproject = tmp_path / 'child' / 'pyproject.toml' pyproject.parent.mkdir() pyproject.write_text(SOME_TOML_DATA) obj = PyprojectTomlConfigSettingsSource(SimpleSettings, pyproject) assert obj.toml_table_header == ('some', 'table') assert obj.toml_data == {'field': 'some'} assert obj.toml_file_path == pyproject def test___init___explicit_missing(self, mocker: MockerFixture, tmp_path: Path) -> None: """Test __init__ explicit file missing.""" mocker.patch(f'{MODULE}.Path.cwd', return_value=tmp_path) pyproject = tmp_path / 'child' / 'pyproject.toml' obj = PyprojectTomlConfigSettingsSource(SimpleSettings, pyproject) assert obj.toml_table_header == ('some', 'table') assert not obj.toml_data assert obj.toml_file_path == pyproject @pytest.mark.parametrize('depth', [0, 99]) def test___init___no_file(self, depth: int, mocker: MockerFixture, tmp_path: Path) -> None: """Test __init__ no file.""" class Settings(BaseSettings): model_config = SettingsConfigDict(pyproject_toml_depth=depth) mocker.patch(f'{MODULE}.Path.cwd', return_value=tmp_path / 'foo') obj = PyprojectTomlConfigSettingsSource(Settings) assert obj.toml_table_header == ('tool', 'pydantic-settings') assert not obj.toml_data assert obj.toml_file_path == tmp_path / 'foo' / 'pyproject.toml' def test___init___parent(self, mocker: MockerFixture, tmp_path: Path) -> None: """Test __init__ parent directory.""" mocker.patch(f'{MODULE}.Path.cwd', return_value=tmp_path / 'child') pyproject = tmp_path / 'pyproject.toml' pyproject.write_text(SOME_TOML_DATA) obj = PyprojectTomlConfigSettingsSource(SimpleSettings) assert obj.toml_table_header == ('some', 'table') assert obj.toml_data == {'field': 'some'} assert obj.toml_file_path == tmp_path / 'pyproject.toml' @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') def test_pyproject_toml_file(cd_tmp_path: Path): pyproject = cd_tmp_path / 'pyproject.toml' pyproject.write_text( """ [tool.pydantic-settings] foobar = "Hello" [tool.pydantic-settings.nested] nested_field = "world!" """ ) class Nested(BaseModel): nested_field: str class Settings(BaseSettings): foobar: str nested: Nested model_config = SettingsConfigDict() @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) s = Settings() assert s.foobar == 'Hello' assert s.nested.nested_field == 'world!' @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') def test_pyproject_toml_file_explicit(cd_tmp_path: Path): pyproject = cd_tmp_path / 'child' / 'grandchild' / 'pyproject.toml' pyproject.parent.mkdir(parents=True) pyproject.write_text( """ [tool.pydantic-settings] foobar = "Hello" [tool.pydantic-settings.nested] nested_field = "world!" """ ) (cd_tmp_path / 'pyproject.toml').write_text( """ [tool.pydantic-settings] foobar = "fail" [tool.pydantic-settings.nested] nested_field = "fail" """ ) class Nested(BaseModel): nested_field: str class Settings(BaseSettings): foobar: str nested: Nested model_config = SettingsConfigDict() @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls, pyproject),) s = Settings() assert s.foobar == 'Hello' assert s.nested.nested_field == 'world!' @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') def test_pyproject_toml_file_parent(mocker: MockerFixture, tmp_path: Path): cwd = tmp_path / 'child' / 'grandchild' / 'cwd' cwd.mkdir(parents=True) mocker.patch('pydantic_settings.sources.providers.toml.Path.cwd', return_value=cwd) (cwd.parent.parent / 'pyproject.toml').write_text( """ [tool.pydantic-settings] foobar = "Hello" [tool.pydantic-settings.nested] nested_field = "world!" """ ) (tmp_path / 'pyproject.toml').write_text( """ [tool.pydantic-settings] foobar = "fail" [tool.pydantic-settings.nested] nested_field = "fail" """ ) class Nested(BaseModel): nested_field: str class Settings(BaseSettings): foobar: str nested: Nested model_config = SettingsConfigDict(pyproject_toml_depth=2) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) s = Settings() assert s.foobar == 'Hello' assert s.nested.nested_field == 'world!' @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') def test_pyproject_toml_file_header(cd_tmp_path: Path): pyproject = cd_tmp_path / 'subdir' / 'pyproject.toml' pyproject.parent.mkdir() pyproject.write_text( """ [tool.pydantic-settings] foobar = "Hello" [tool.pydantic-settings.nested] nested_field = "world!" [tool."my.tool".foo] status = "success" """ ) class Settings(BaseSettings): status: str model_config = SettingsConfigDict(extra='forbid', pyproject_toml_table_header=('tool', 'my.tool', 'foo')) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls, pyproject),) s = Settings() assert s.status == 'success' @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') @pytest.mark.parametrize('depth', [0, 99]) def test_pyproject_toml_no_file(cd_tmp_path: Path, depth: int): class Settings(BaseSettings): model_config = SettingsConfigDict(pyproject_toml_depth=depth) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) s = Settings() assert s.model_dump() == {} @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') def test_pyproject_toml_no_file_explicit(tmp_path: Path): pyproject = tmp_path / 'child' / 'pyproject.toml' (tmp_path / 'pyproject.toml').write_text('[tool.pydantic-settings]\nfield = "fail"') class Settings(BaseSettings): model_config = SettingsConfigDict() field: str | None = None @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls, pyproject),) s = Settings() assert s.model_dump() == {'field': None} @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') @pytest.mark.parametrize('depth', [0, 1, 2]) def test_pyproject_toml_no_file_too_shallow(depth: int, mocker: MockerFixture, tmp_path: Path): cwd = tmp_path / 'child' / 'grandchild' / 'cwd' cwd.mkdir(parents=True) mocker.patch('pydantic_settings.sources.providers.toml.Path.cwd', return_value=cwd) (tmp_path / 'pyproject.toml').write_text( """ [tool.pydantic-settings] foobar = "fail" [tool.pydantic-settings.nested] nested_field = "fail" """ ) class Nested(BaseModel): nested_field: str | None = None class Settings(BaseSettings): foobar: str | None = None nested: Nested = Nested() model_config = SettingsConfigDict(pyproject_toml_depth=depth) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) s = Settings() assert not s.foobar assert not s.nested.nested_field pydantic-pydantic-settings-198e71c/tests/test_source_toml.py000066400000000000000000000106001514433345000244440ustar00rootroot00000000000000""" Test pydantic_settings.TomlConfigSettingsSource. """ import sys from pathlib import Path import pytest from pydantic import BaseModel from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource, ) try: import tomli except ImportError: tomli = None def test_repr() -> None: source = TomlConfigSettingsSource(BaseSettings, Path('config.toml')) assert repr(source) == 'TomlConfigSettingsSource(toml_file=config.toml)' @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') def test_toml_file(tmp_path): p = tmp_path / '.env' p.write_text( """ foobar = "Hello" [nested] nested_field = "world!" """ ) class Nested(BaseModel): nested_field: str class Settings(BaseSettings): foobar: str nested: Nested model_config = SettingsConfigDict(toml_file=p) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (TomlConfigSettingsSource(settings_cls),) s = Settings() assert s.foobar == 'Hello' assert s.nested.nested_field == 'world!' @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') def test_toml_no_file(): class Settings(BaseSettings): model_config = SettingsConfigDict(toml_file=None) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (TomlConfigSettingsSource(settings_cls),) s = Settings() assert s.model_dump() == {} @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') def test_multiple_file_toml(tmp_path): p1 = tmp_path / '.env.toml1' p2 = tmp_path / '.env.toml2' p1.write_text( """ toml1=1 """ ) p2.write_text( """ toml2=2 """ ) class Settings(BaseSettings): toml1: int toml2: int @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (TomlConfigSettingsSource(settings_cls, toml_file=[p1, p2]),) s = Settings() assert s.model_dump() == {'toml1': 1, 'toml2': 2} @pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed') @pytest.mark.parametrize('deep_merge', [False, True]) def test_multiple_file_toml_merge(tmp_path, deep_merge): p1 = tmp_path / '.env.toml1' p2 = tmp_path / '.env.toml2' p1.write_text( """ hello = "world" [nested] foo=1 bar=2 """ ) p2.write_text( """ [nested] foo=3 """ ) class Nested(BaseModel): foo: int bar: int = 0 class Settings(BaseSettings): hello: str nested: Nested @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (TomlConfigSettingsSource(settings_cls, toml_file=[p1, p2], deep_merge=deep_merge),) s = Settings() assert s.model_dump() == {'hello': 'world', 'nested': {'foo': 3, 'bar': 2 if deep_merge else 0}} pydantic-pydantic-settings-198e71c/tests/test_source_yaml.py000066400000000000000000000454731514433345000244530ustar00rootroot00000000000000""" Test pydantic_settings.YamlConfigSettingsSource. """ from pathlib import Path import pytest from pydantic import BaseModel from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, YamlConfigSettingsSource, ) try: import yaml except ImportError: yaml = None def test_repr() -> None: source = YamlConfigSettingsSource(BaseSettings, Path('config.yaml')) assert repr(source) == 'YamlConfigSettingsSource(yaml_file=config.yaml)' @pytest.mark.skipif(yaml, reason='PyYAML is installed') def test_yaml_not_installed(tmp_path): p = tmp_path / '.env' p.write_text( """ foobar: "Hello" """ ) class Settings(BaseSettings): foobar: str model_config = SettingsConfigDict(yaml_file=p) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) with pytest.raises(ImportError, match=r'^PyYAML is not installed, run `pip install pydantic-settings\[yaml\]`$'): Settings() @pytest.mark.skipif(yaml is None, reason='pyYaml is not installed') def test_yaml_file(tmp_path): p = tmp_path / '.env' p.write_text( """ foobar: "Hello" null_field: nested: nested_field: "world!" """ ) class Nested(BaseModel): nested_field: str class Settings(BaseSettings): foobar: str nested: Nested null_field: str | None model_config = SettingsConfigDict(yaml_file=p) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s = Settings() assert s.foobar == 'Hello' assert s.nested.nested_field == 'world!' @pytest.mark.skipif(yaml is None, reason='pyYaml is not installed') def test_yaml_no_file(): class Settings(BaseSettings): model_config = SettingsConfigDict(yaml_file=None) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s = Settings() assert s.model_dump() == {} @pytest.mark.skipif(yaml is None, reason='pyYaml is not installed') def test_yaml_empty_file(tmp_path): p = tmp_path / '.env' p.write_text('') class Settings(BaseSettings): model_config = SettingsConfigDict(yaml_file=p) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s = Settings() assert s.model_dump() == {} @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') def test_multiple_file_yaml(tmp_path): p3 = tmp_path / '.env.yaml3' p4 = tmp_path / '.env.yaml4' p3.write_text( """ yaml3: 3 """ ) p4.write_text( """ yaml4: 4 """ ) class Settings(BaseSettings): yaml3: int yaml4: int @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls, yaml_file=[p3, p4]),) s = Settings() assert s.model_dump() == {'yaml3': 3, 'yaml4': 4} @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') @pytest.mark.parametrize('deep_merge', [False, True]) def test_multiple_file_yaml_deep_merge(tmp_path, deep_merge): p3 = tmp_path / '.env.yaml3' p4 = tmp_path / '.env.yaml4' p3.write_text( """ hello: world nested: foo: 1 bar: 2 """ ) p4.write_text( """ nested: foo: 3 """ ) class Nested(BaseModel): foo: int bar: int = 0 class Settings(BaseSettings): hello: str nested: Nested @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls, yaml_file=[p3, p4], deep_merge=deep_merge),) s = Settings() assert s.model_dump() == {'hello': 'world', 'nested': {'foo': 3, 'bar': 2 if deep_merge else 0}} @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') def test_yaml_config_section(tmp_path): p = tmp_path / '.env' p.write_text( """ foobar: "Hello" nested: nested_field: "world!" """ ) class Settings(BaseSettings): nested_field: str model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='nested') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s = Settings() assert s.nested_field == 'world!' @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') def test_invalid_yaml_config_section(tmp_path): p = tmp_path / '.env' p.write_text( """ foobar: "Hello" nested: nested_field: "world!" """ ) class Settings(BaseSettings): nested_field: str model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='invalid_key') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) with pytest.raises(KeyError, match='yaml_config_section key "invalid_key" not found in .+'): Settings() @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') def test_yaml_config_section_nested_path(tmp_path): p = tmp_path / 'config.yaml' p.write_text( """ config: app: settings: database_url: "postgresql://localhost/db" api_key: "secret123" logging: level: "INFO" """ ) class Settings(BaseSettings): database_url: str api_key: str model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='config.app.settings') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s = Settings() assert s.database_url == 'postgresql://localhost/db' assert s.api_key == 'secret123' @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') def test_yaml_config_section_nested_path_two_levels(tmp_path): p = tmp_path / 'config.yaml' p.write_text( """ app: settings: host: "localhost" port: 8000 """ ) class Settings(BaseSettings): host: str port: int model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='app.settings') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s = Settings() assert s.host == 'localhost' assert s.port == 8000 @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') def test_invalid_yaml_config_section_nested_path(tmp_path): p = tmp_path / 'config.yaml' p.write_text( """ config: app: settings: database_url: "postgresql://localhost/db" """ ) class Settings(BaseSettings): database_url: str model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='config.app.invalid') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) with pytest.raises(KeyError, match='yaml_config_section key "config.app.invalid" not found in .+'): Settings() @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') def test_yaml_config_section_with_literal_dots(tmp_path): """Test that keys containing literal dots can be accessed using greedy matching.""" p = tmp_path / 'config.yaml' p.write_text( """ "app.settings": database_url: "postgresql://localhost/db" api_key: "secret123" config: "server.prod": host: "prod.example.com" port: 443 """ ) # Test accessing a top-level key with literal dots class Settings1(BaseSettings): database_url: str api_key: str model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='app.settings') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s1 = Settings1() assert s1.database_url == 'postgresql://localhost/db' assert s1.api_key == 'secret123' # Test accessing a nested key where the child has literal dots class Settings2(BaseSettings): host: str port: int model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='config.server.prod') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s2 = Settings2() assert s2.host == 'prod.example.com' assert s2.port == 443 @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') def test_yaml_config_section_empty_path(tmp_path): """Test that empty section path is rejected.""" p = tmp_path / 'config.yaml' p.write_text( """ app: settings: host: "localhost" """ ) class Settings(BaseSettings): host: str model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) with pytest.raises(ValueError, match='yaml_config_section cannot be empty'): Settings() @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') def test_yaml_config_section_unusual_literal_keys(tmp_path): """Test that keys with leading/trailing/consecutive dots can be accessed as literal keys.""" p = tmp_path / 'config.yaml' p.write_text( """ ".leading": value: "has leading dot" "trailing.": value: "has trailing dot" "double..dots": value: "has consecutive dots" "": value: "empty key" """ ) # Test leading dot key class Settings1(BaseSettings): value: str model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='.leading') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s1 = Settings1() assert s1.value == 'has leading dot' # Test trailing dot key class Settings2(BaseSettings): value: str model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='trailing.') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s2 = Settings2() assert s2.value == 'has trailing dot' # Test consecutive dots key class Settings3(BaseSettings): value: str model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='double..dots') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s3 = Settings3() assert s3.value == 'has consecutive dots' @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') def test_yaml_config_section_complex_unusual_keys(tmp_path): """Test complex scenario with multiple unusual characters in nested keys.""" p = tmp_path / 'config.yaml' p.write_text( """ "..leading..double.trailing..": "value..double": normal: "regular value" number: 42 """ ) # Test accessing deeply nested path with unusual keys at each level # Path: "..leading..double.trailing.." (literal key) -> "value..double" (literal key) class Settings(BaseSettings): normal: str number: int model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='..leading..double.trailing...value..double') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s = Settings() assert s.normal == 'regular value' assert s.number == 42 @pytest.mark.skipif(yaml is None, reason='pyYAML is not installed') def test_yaml_config_section_non_dict_intermediate(tmp_path): """Test that traversing through non-dict intermediate values raises clear error.""" p = tmp_path / 'config.yaml' p.write_text( """ app: name: "MyApp" settings: host: "localhost" """ ) # Try to traverse through a string value (app.name.something) class Settings(BaseSettings): host: str model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='app.name.host') @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) with pytest.raises(TypeError, match='yaml_config_section path.*cannot be traversed.*not a dictionary'): Settings() pydantic-pydantic-settings-198e71c/tests/test_utils.py000066400000000000000000000002421514433345000232520ustar00rootroot00000000000000from pydantic_settings.utils import path_type_label def test_path_type_label(tmp_path): result = path_type_label(tmp_path) assert result == 'directory' pydantic-pydantic-settings-198e71c/uv.lock000066400000000000000000011721101514433345000206500ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", "python_full_version >= '3.11' and python_full_version < '3.13'", "python_full_version < '3.11'", ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "azure-core" version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/e503e08e755ea94e7d3419c9242315f888fc664211c90d032e40479022bf/azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993", size = 363033, upload-time = "2026-01-12T17:03:05.535Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825, upload-time = "2026-01-12T17:03:07.291Z" }, ] [[package]] name = "azure-identity" version = "1.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, { name = "cryptography" }, { name = "msal" }, { name = "msal-extensions" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/8d/1a6c41c28a37eab26dc85ab6c86992c700cd3f4a597d9ed174b0e9c69489/azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456", size = 279826, upload-time = "2025-10-06T20:30:02.194Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/7b/5652771e24fff12da9dde4c20ecf4682e606b104f26419d139758cc935a6/azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651", size = 191317, upload-time = "2025-10-06T20:30:04.251Z" }, ] [[package]] name = "azure-keyvault-secrets" version = "4.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, { name = "isodate" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/97/e5/3074e581b6e8923c4a1f2e42192ea6f390bb52de3600c68baaaed529ef05/azure_keyvault_secrets-4.10.0.tar.gz", hash = "sha256:666fa42892f9cee749563e551a90f060435ab878977c95265173a8246d546a36", size = 129695, upload-time = "2025-06-16T22:52:20.986Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/26/94/7c902e966b28e7cb5080a8e0dd6bffc22ba44bc907f09c4c633d2b7c4f6a/azure_keyvault_secrets-4.10.0-py3-none-any.whl", hash = "sha256:9dbde256077a4ee1a847646671580692e3f9bea36bcfc189c3cf2b9a94eb38b9", size = 125237, upload-time = "2025-06-16T22:52:22.489Z" }, ] [[package]] name = "black" version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "mypy-extensions" }, { name = "packaging" }, { name = "pathspec" }, { name = "platformdirs" }, { name = "pytokens" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" }, { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" }, { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" }, { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" }, { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" }, { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, ] [[package]] name = "boto3" version = "1.42.39" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/ea/b96c77da49fed28744ee0347374d8223994a2b8570e76e8380a4064a8c4a/boto3-1.42.39.tar.gz", hash = "sha256:d03f82363314759eff7f84a27b9e6428125f89d8119e4588e8c2c1d79892c956", size = 112783, upload-time = "2026-01-30T20:38:31.226Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b2/c4/3493b5c86e32d6dd558b30d16b55503e24a6e6cd7115714bc102b247d26e/boto3-1.42.39-py3-none-any.whl", hash = "sha256:d9d6ce11df309707b490d2f5f785b761cfddfd6d1f665385b78c9d8ed097184b", size = 140606, upload-time = "2026-01-30T20:38:28.635Z" }, ] [[package]] name = "boto3-stubs" version = "1.42.39" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/97/ae49a2b78402d7f9e3859cf86908d5abe53a85573d935fb6818a0cdc3e00/boto3_stubs-1.42.39.tar.gz", hash = "sha256:f41331e1830ed22e6524fa6e3f95897e99b0df790efb63798b3d43180f3985a1", size = 100851, upload-time = "2026-01-30T20:49:38.881Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/44/28/040b36d4a0c58b20e8c313eea9cbe8b8c802de9179c12a64673afce9ff0c/boto3_stubs-1.42.39-py3-none-any.whl", hash = "sha256:45a0df8d43d7ba2acea9dbaec8109f4428e3c097d91b1a240f123cc4b4e60a4b", size = 69783, upload-time = "2026-01-30T20:49:29.53Z" }, ] [package.optional-dependencies] secretsmanager = [ { name = "mypy-boto3-secretsmanager" }, ] [[package]] name = "botocore" version = "1.42.39" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/a6/3a34d1b74effc0f759f5ff4e91c77729d932bc34dd3207905e9ecbba1103/botocore-1.42.39.tar.gz", hash = "sha256:0f00355050821e91a5fe6d932f7bf220f337249b752899e3e4cf6ed54326249e", size = 14914927, upload-time = "2026-01-30T20:38:19.265Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/71/9a2c88abb5fe47b46168b262254d5b5d635de371eba4bd01ea5c8c109575/botocore-1.42.39-py3-none-any.whl", hash = "sha256:9e0d0fed9226449cc26fcf2bbffc0392ac698dd8378e8395ce54f3ec13f81d58", size = 14591958, upload-time = "2026-01-30T20:38:14.814Z" }, ] [[package]] name = "botocore-stubs" version = "1.42.39" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-awscrt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/51/18/dfacc2b7fdbace7d85526e7ec3acabb79253495f58baa47c736e60f6f4a1/botocore_stubs-1.42.39.tar.gz", hash = "sha256:7a75265cd59fb93fea4a6a02ac5e90cbb44d14f182627ad58db1425690bc883d", size = 42413, upload-time = "2026-01-30T21:34:24.293Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/2e/cbcc97042679bdabc654a6e536e65581e4294a56e0b8bf87f455d95d77a9/botocore_stubs-1.42.39-py3-none-any.whl", hash = "sha256:5540aa52b5071f84e4edba4bdaffb7a24d02bd1272df534eb38289c0d9a3e22a", size = 66761, upload-time = "2026-01-30T21:34:22.366Z" }, ] [[package]] name = "certifi" version = "2026.1.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "cfgv" version = "3.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] name = "chardet" version = "5.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.13.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/2d/63e37369c8e81a643afe54f76073b020f7b97ddbe698c5c944b51b0a2bc5/coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b", size = 218842, upload-time = "2026-01-25T12:57:15.3Z" }, { url = "https://files.pythonhosted.org/packages/57/06/86ce882a8d58cbcb3030e298788988e618da35420d16a8c66dac34f138d0/coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2", size = 219360, upload-time = "2026-01-25T12:57:17.572Z" }, { url = "https://files.pythonhosted.org/packages/cd/84/70b0eb1ee19ca4ef559c559054c59e5b2ae4ec9af61398670189e5d276e9/coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896", size = 246123, upload-time = "2026-01-25T12:57:19.087Z" }, { url = "https://files.pythonhosted.org/packages/35/fb/05b9830c2e8275ebc031e0019387cda99113e62bb500ab328bb72578183b/coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c", size = 247930, upload-time = "2026-01-25T12:57:20.929Z" }, { url = "https://files.pythonhosted.org/packages/81/aa/3f37858ca2eed4f09b10ca3c6ddc9041be0a475626cd7fd2712f4a2d526f/coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc", size = 249804, upload-time = "2026-01-25T12:57:22.904Z" }, { url = "https://files.pythonhosted.org/packages/b6/b3/c904f40c56e60a2d9678a5ee8df3d906d297d15fb8bec5756c3b0a67e2df/coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5", size = 246815, upload-time = "2026-01-25T12:57:24.314Z" }, { url = "https://files.pythonhosted.org/packages/41/91/ddc1c5394ca7fd086342486440bfdd6b9e9bda512bf774599c7c7a0081e0/coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31", size = 247843, upload-time = "2026-01-25T12:57:26.544Z" }, { url = "https://files.pythonhosted.org/packages/87/d2/cdff8f4cd33697883c224ea8e003e9c77c0f1a837dc41d95a94dd26aad67/coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad", size = 245850, upload-time = "2026-01-25T12:57:28.507Z" }, { url = "https://files.pythonhosted.org/packages/f5/42/e837febb7866bf2553ab53dd62ed52f9bb36d60c7e017c55376ad21fbb05/coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f", size = 246116, upload-time = "2026-01-25T12:57:30.16Z" }, { url = "https://files.pythonhosted.org/packages/09/b1/4a3f935d7df154df02ff4f71af8d61298d713a7ba305d050ae475bfbdde2/coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8", size = 246720, upload-time = "2026-01-25T12:57:32.165Z" }, { url = "https://files.pythonhosted.org/packages/e1/fe/538a6fd44c515f1c5197a3f078094cbaf2ce9f945df5b44e29d95c864bff/coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c", size = 221465, upload-time = "2026-01-25T12:57:33.511Z" }, { url = "https://files.pythonhosted.org/packages/5e/09/4b63a024295f326ec1a40ec8def27799300ce8775b1cbf0d33b1790605c4/coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99", size = 222397, upload-time = "2026-01-25T12:57:34.927Z" }, { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "cryptography" version = "46.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" }, { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" }, { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" }, { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" }, { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" }, { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" }, { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" }, { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" }, { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" }, { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" }, { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" }, { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" }, { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" }, { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" }, { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" }, { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" }, { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" }, { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" }, { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" }, { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" }, { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" }, { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" }, { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" }, { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, ] [[package]] name = "diff-cover" version = "10.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "chardet" }, { name = "jinja2" }, { name = "pluggy" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/99/b4/eee71d1e338bc1f9bd3539b46b70e303dac061324b759c9a80fa3c96d90d/diff_cover-10.2.0.tar.gz", hash = "sha256:61bf83025f10510c76ef6a5820680cf61b9b974e8f81de70c57ac926fa63872a", size = 102473, upload-time = "2026-01-09T01:59:07.605Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/2c/61eeb887055a37150db824b6bf830e821a736580769ac2fea4eadb0d613f/diff_cover-10.2.0-py3-none-any.whl", hash = "sha256:59c328595e0b8948617cc5269af9e484c86462e2844bfcafa3fb37f8fca0af87", size = 56748, upload-time = "2026-01-09T01:59:06.028Z" }, ] [[package]] name = "distlib" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "filelock" version = "3.20.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] name = "google-api-core" version = "2.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "googleapis-common-protos" }, { name = "proto-plus" }, { name = "protobuf" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/10/05572d33273292bac49c2d1785925f7bc3ff2fe50e3044cf1062c1dde32e/google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", size = 177828, upload-time = "2026-01-08T22:21:39.269Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9", size = 173906, upload-time = "2026-01-08T22:21:36.093Z" }, ] [package.optional-dependencies] grpc = [ { name = "grpcio" }, { name = "grpcio-status" }, ] [[package]] name = "google-auth" version = "2.48.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, ] [[package]] name = "google-cloud-secret-manager" version = "2.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, { name = "grpc-google-iam-v1" }, { name = "grpcio" }, { name = "proto-plus" }, { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/9c/a6c7144bc96df77376ae3fcc916fb639c40814c2e4bba2051d31dc136cd0/google_cloud_secret_manager-2.26.0.tar.gz", hash = "sha256:0d1d6f76327685a0ed78a4cf50f289e1bfbbe56026ed0affa98663b86d6d50d6", size = 277603, upload-time = "2025-12-18T00:29:31.065Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/30/a58739dd12cec0f7f761ed1efb518aed2250a407d4ed14c5a0eeee7eaaf9/google_cloud_secret_manager-2.26.0-py3-none-any.whl", hash = "sha256:940a5447a6ec9951446fd1a0f22c81a4303fde164cd747aae152c5f5c8e6723e", size = 223623, upload-time = "2025-12-18T00:29:29.311Z" }, ] [[package]] name = "googleapis-common-protos" version = "1.72.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, ] [package.optional-dependencies] grpc = [ { name = "grpcio" }, ] [[package]] name = "grpc-google-iam-v1" version = "0.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos", extra = ["grpc"] }, { name = "grpcio" }, { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/1e/1011451679a983f2f5c6771a1682542ecb027776762ad031fd0d7129164b/grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389", size = 23745, upload-time = "2025-10-15T21:14:53.318Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4a/bd/330a1bbdb1afe0b96311249e699b6dc9cfc17916394fd4503ac5aca2514b/grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6", size = 32690, upload-time = "2025-10-15T21:14:51.72Z" }, ] [[package]] name = "grpcio" version = "1.76.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" }, { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" }, { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" }, { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" }, { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" }, { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" }, { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" }, { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" }, { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" }, { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" }, { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, ] [[package]] name = "grpcio-status" version = "1.76.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3f/46/e9f19d5be65e8423f886813a2a9d0056ba94757b0c5007aa59aed1a961fa/grpcio_status-1.76.0.tar.gz", hash = "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd", size = 13679, upload-time = "2025-10-21T16:28:52.545Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8c/cc/27ba60ad5a5f2067963e6a858743500df408eb5855e98be778eaef8c9b02/grpcio_status-1.76.0-py3-none-any.whl", hash = "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18", size = 14425, upload-time = "2025-10-21T16:28:40.853Z" }, ] [[package]] name = "identify" version = "2.6.16" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "isodate" version = "0.7.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "jmespath" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] [[package]] name = "librt" version = "0.7.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, ] [[package]] name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "moto" version = "5.1.20" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3" }, { name = "botocore" }, { name = "cryptography" }, { name = "jinja2" }, { name = "python-dateutil" }, { name = "requests" }, { name = "responses" }, { name = "werkzeug" }, { name = "xmltodict" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/93/6b696aab5174721696a17716a488086e21f7b2547b4c9517f799a9b25e9e/moto-5.1.20.tar.gz", hash = "sha256:6d12d781e26a550d80e4b7e01d5538178e3adec6efbdec870e06e84750f13ec0", size = 8318716, upload-time = "2026-01-17T21:49:00.101Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/2f/f50892fdb28097917b87d358a5fcefd30976289884ff142893edcb0243ba/moto-5.1.20-py3-none-any.whl", hash = "sha256:58c82c8e6b2ef659ef3a562fa415dce14da84bc7a797943245d9a338496ea0ea", size = 6392751, upload-time = "2026-01-17T21:48:57.099Z" }, ] [[package]] name = "msal" version = "1.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/c857c46d653e104019a84f22d4494f2119b4fe9f896c92b4b864b3b045cc/msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f", size = 153961, upload-time = "2025-09-22T23:05:48.989Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/dc/18d48843499e278538890dc709e9ee3dea8375f8be8e82682851df1b48b5/msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1", size = 116987, upload-time = "2025-09-22T23:05:47.294Z" }, ] [[package]] name = "msal-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "msal" }, ] sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, ] [[package]] name = "mypy" version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] name = "mypy-boto3-secretsmanager" version = "1.42.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f7/58/ccae71b7b7f550eab01d600e956d57e6e6bb9148dbf5d116696d0dc43369/mypy_boto3_secretsmanager-1.42.8.tar.gz", hash = "sha256:5ab42f35ce932765ebb1684146f478a87cc4b83bef950fd1aa0e268b88d59c81", size = 19863, upload-time = "2025-12-11T22:12:51.045Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1a/42/90cef7241c98f6e504cabc9a99d89dd38b84e4d40ff0774c89bc871ffb18/mypy_boto3_secretsmanager-1.42.8-py3-none-any.whl", hash = "sha256:50c891a88e725a8dba7444018e47590ea63d8e938abe2b1c0b25e5413f39d51d", size = 27243, upload-time = "2025-12-11T22:12:44.389Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "nodeenv" version = "1.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pathspec" version = "1.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "platformdirs" version = "4.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pre-commit" version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] [[package]] name = "proto-plus" version = "1.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/01/89/9cbe2f4bba860e149108b683bc2efec21f14d5f7ed6e25562ad86acbc373/proto_plus-1.27.0.tar.gz", hash = "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4", size = 56158, upload-time = "2025-12-16T13:46:25.729Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl", hash = "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82", size = 50205, upload-time = "2025-12-16T13:46:24.76Z" }, ] [[package]] name = "protobuf" version = "6.33.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] [[package]] name = "pyasn1" version = "0.6.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, ] [[package]] name = "pyasn1-modules" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] [[package]] name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pydantic" version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] name = "pydantic-core" version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] [[package]] name = "pydantic-settings" source = { editable = "." } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] [package.optional-dependencies] aws-secrets-manager = [ { name = "boto3" }, { name = "boto3-stubs", extra = ["secretsmanager"] }, ] azure-key-vault = [ { name = "azure-identity" }, { name = "azure-keyvault-secrets" }, ] gcp-secret-manager = [ { name = "google-cloud-secret-manager" }, ] toml = [ { name = "tomli" }, ] yaml = [ { name = "pyyaml" }, ] [package.dev-dependencies] linting = [ { name = "black" }, { name = "boto3-stubs", extra = ["secretsmanager"] }, { name = "mypy" }, { name = "pre-commit" }, { name = "pyyaml" }, { name = "ruff" }, { name = "types-pyyaml" }, ] testing = [ { name = "coverage", extra = ["toml"] }, { name = "diff-cover" }, { name = "moto" }, { name = "pytest" }, { name = "pytest-examples" }, { name = "pytest-mock" }, { name = "pytest-pretty" }, ] [package.metadata] requires-dist = [ { name = "azure-identity", marker = "extra == 'azure-key-vault'", specifier = ">=1.16.0" }, { name = "azure-keyvault-secrets", marker = "extra == 'azure-key-vault'", specifier = ">=4.8.0" }, { name = "boto3", marker = "extra == 'aws-secrets-manager'", specifier = ">=1.35.0" }, { name = "boto3-stubs", extras = ["secretsmanager"], marker = "extra == 'aws-secrets-manager'" }, { name = "google-cloud-secret-manager", marker = "extra == 'gcp-secret-manager'", specifier = ">=2.23.1" }, { name = "pydantic", specifier = ">=2.7.0" }, { name = "python-dotenv", specifier = ">=0.21.0" }, { name = "pyyaml", marker = "extra == 'yaml'", specifier = ">=6.0.1" }, { name = "tomli", marker = "extra == 'toml'", specifier = ">=2.0.1" }, { name = "typing-inspection", specifier = ">=0.4.0" }, ] provides-extras = ["aws-secrets-manager", "azure-key-vault", "gcp-secret-manager", "toml", "yaml"] [package.metadata.requires-dev] linting = [ { name = "black" }, { name = "boto3-stubs", extras = ["secretsmanager"] }, { name = "mypy" }, { name = "pre-commit" }, { name = "pyyaml" }, { name = "ruff" }, { name = "types-pyyaml" }, ] testing = [ { name = "coverage", extras = ["toml"] }, { name = "diff-cover", specifier = ">=9.2.0" }, { name = "moto", extras = ["secretsmanager"] }, { name = "pytest" }, { name = "pytest-examples" }, { name = "pytest-mock" }, { name = "pytest-pretty" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyjwt" version = "2.11.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, ] [package.optional-dependencies] crypto = [ { name = "cryptography" }, ] [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] name = "pytest-examples" version = "0.0.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "black" }, { name = "pytest" }, { name = "ruff" }, ] sdist = { url = "https://files.pythonhosted.org/packages/af/71/4ae972fd95f474454aa450108ee1037830e7ba11840363e981b8d48fd16a/pytest_examples-0.0.18.tar.gz", hash = "sha256:9a464f007f805b113677a15e2f8942ebb92d7d3eb5312e9a405d018478ec9801", size = 21237, upload-time = "2025-05-06T07:46:10.705Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/09/52/7bbfb6e987d9a8a945f22941a8da63e3529465f1b106ef0e26f5df7c780d/pytest_examples-0.0.18-py3-none-any.whl", hash = "sha256:86c195b98c4e55049a0df3a0a990ca89123b7280473ab57608eecc6c47bcfe9c", size = 18169, upload-time = "2025-05-06T07:46:09.349Z" }, ] [[package]] name = "pytest-mock" version = "3.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] name = "pytest-pretty" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "rich" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ba/d7/c699e0be5401fe9ccad484562f0af9350b4e48c05acf39fb3dab1932128f/pytest_pretty-1.3.0.tar.gz", hash = "sha256:97e9921be40f003e40ae78db078d4a0c1ea42bf73418097b5077970c2cc43bf3", size = 219297, upload-time = "2025-06-04T12:54:37.322Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/85/2f97a1b65178b0f11c9c77c35417a4cc5b99a80db90dad4734a129844ea5/pytest_pretty-1.3.0-py3-none-any.whl", hash = "sha256:074b9d5783cef9571494543de07e768a4dda92a3e85118d6c7458c67297159b7", size = 5620, upload-time = "2025-06-04T12:54:36.229Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] [[package]] name = "pytokens" version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "requests" version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] name = "responses" version = "0.25.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, { name = "requests" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320, upload-time = "2025-08-08T19:01:46.709Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" }, ] [[package]] name = "rich" version = "14.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, ] [[package]] name = "rsa" version = "4.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] [[package]] name = "ruff" version = "0.14.14" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] [[package]] name = "s3transfer" version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "tomli" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "types-awscrt" version = "0.31.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/97/be/589b7bba42b5681a72bac4d714287afef4e1bb84d07c859610ff631d449e/types_awscrt-0.31.1.tar.gz", hash = "sha256:08b13494f93f45c1a92eb264755fce50ed0d1dc75059abb5e31670feb9a09724", size = 17839, upload-time = "2026-01-16T02:01:23.394Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5e/fd/ddca80617f230bd833f99b4fb959abebffd8651f520493cae2e96276b1bd/types_awscrt-0.31.1-py3-none-any.whl", hash = "sha256:7e4364ac635f72bd57f52b093883640b1448a6eded0ecbac6e900bf4b1e4777b", size = 42516, upload-time = "2026-01-16T02:01:21.637Z" }, ] [[package]] name = "types-pyyaml" version = "6.0.12.20250915" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, ] [[package]] name = "types-s3transfer" version = "0.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fe/64/42689150509eb3e6e82b33ee3d89045de1592488842ddf23c56957786d05/types_s3transfer-0.16.0.tar.gz", hash = "sha256:b4636472024c5e2b62278c5b759661efeb52a81851cde5f092f24100b1ecb443", size = 13557, upload-time = "2025-12-08T08:13:09.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/27/e88220fe6274eccd3bdf95d9382918716d312f6f6cef6a46332d1ee2feff/types_s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:1c0cd111ecf6e21437cb410f5cddb631bfb2263b77ad973e79b9c6d0cb24e0ef", size = 19247, upload-time = "2025-12-08T08:13:08.426Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "urllib3" version = "2.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "virtualenv" version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]] name = "werkzeug" version = "3.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, ] [[package]] name = "xmltodict" version = "1.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6a/aa/917ceeed4dbb80d2f04dbd0c784b7ee7bba8ae5a54837ef0e5e062cd3cfb/xmltodict-1.0.2.tar.gz", hash = "sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649", size = 25725, upload-time = "2025-09-17T21:59:26.459Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c0/20/69a0e6058bc5ea74892d089d64dfc3a62ba78917ec5e2cfa70f7c92ba3a5/xmltodict-1.0.2-py3-none-any.whl", hash = "sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d", size = 13893, upload-time = "2025-09-17T21:59:24.859Z" }, ]