pax_global_header 0000666 0000000 0000000 00000000064 15136363550 0014521 g ustar 00root root 0000000 0000000 52 comment=cc1e7514d8194fd658277295f39ec89f848048ee
beetbox-confuse-cc1e751/ 0000775 0000000 0000000 00000000000 15136363550 0015223 5 ustar 00root root 0000000 0000000 beetbox-confuse-cc1e751/.git-blame-ignore-revs 0000664 0000000 0000000 00000001060 15136363550 0021320 0 ustar 00root root 0000000 0000000 # 2025
# adapt super() calls to Python 3
97c43eb36fa04c8dde2bb5acacdb00782697ad86
# clean up from Python 3 transition: class XYZ(object) -> class XYZ
142d3d9226228e8fc9cac73013990b1ff41d9782
# remove u'' string prefixes
d41faf0bdf0ba5e899625c0f2fd2421aaad8ff1c
# style
94309b91a070d0f64755fe0a7c83e9908b5de42e
# Reformat the codebase
2bb508ef08a0acfd0c8f8b6f8b48a240d2309740
# Fix linting issues
23bcec4bcabe3f1718b90e89d85cfa53d36a4445
# Format docs
424a17ceeccf93d92cb2e6b4062907af858854b1
# Update URLs in the docs
8aa6d237ac6479971ffe38608b2526ef0abe55f4
beetbox-confuse-cc1e751/.github/ 0000775 0000000 0000000 00000000000 15136363550 0016563 5 ustar 00root root 0000000 0000000 beetbox-confuse-cc1e751/.github/problem-matchers/ 0000775 0000000 0000000 00000000000 15136363550 0022027 5 ustar 00root root 0000000 0000000 beetbox-confuse-cc1e751/.github/problem-matchers/sphinx-build.json 0000664 0000000 0000000 00000000427 15136363550 0025333 0 ustar 00root root 0000000 0000000 {
"problemMatcher": [
{
"owner": "sphinx-build",
"severity": "error",
"pattern": [
{
"regexp": "^(/[^:]+):((\\d+):)?(\\sWARNING:)?\\s*(.+)$",
"file": 1,
"line": 3,
"message": 5
}
]
}
]
}
beetbox-confuse-cc1e751/.github/problem-matchers/sphinx-lint.json 0000664 0000000 0000000 00000000453 15136363550 0025201 0 ustar 00root root 0000000 0000000 {
"problemMatcher": [
{
"owner": "sphinx-lint",
"severity": "error",
"pattern": [
{
"regexp": "^([^:]+):(\\d+):\\s+(.*)\\s\\(([a-z-]+)\\)$",
"file": 1,
"line": 2,
"message": 3,
"code": 4
}
]
}
]
}
beetbox-confuse-cc1e751/.github/workflows/ 0000775 0000000 0000000 00000000000 15136363550 0020620 5 ustar 00root root 0000000 0000000 beetbox-confuse-cc1e751/.github/workflows/changelog_reminder.yaml 0000664 0000000 0000000 00000002051 15136363550 0025316 0 ustar 00root root 0000000 0000000 name: Verify changelog updated
on:
pull_request_target:
types:
- opened
- ready_for_review
jobs:
check_changes:
permissions:
pull-requests: write
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Get all updated Python files
id: changed-python-files
uses: tj-actions/changed-files@v47
with:
files: |
**.py
- name: Check for the changelog update
id: changelog-update
uses: tj-actions/changed-files@v47
with:
files: docs/changelog.rst
- name: Comment under the PR with a reminder
if: steps.changed-python-files.outputs.any_changed == 'true' && steps.changelog-update.outputs.any_changed == 'false'
uses: thollander/actions-comment-pull-request@v3
with:
message: 'Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry.'
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' beetbox-confuse-cc1e751/.github/workflows/lint.yaml 0000664 0000000 0000000 00000010655 15136363550 0022461 0 ustar 00root root 0000000 0000000 name: Lint check
run-name: Lint code
on:
pull_request:
push:
branches:
- master
concurrency:
# Cancel previous workflow run when a new commit is pushed to a feature branch
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
env:
PYTHON_VERSION: "3.10"
jobs:
changed-files:
runs-on: ubuntu-latest
name: Get changed files
outputs:
any_docs_changed: ${{ steps.changed-doc-files.outputs.any_changed }}
any_python_changed: ${{ steps.raw-changed-python-files.outputs.any_changed }}
changed_doc_files: ${{ steps.changed-doc-files.outputs.all_changed_files }}
changed_python_files: ${{ steps.changed-python-files.outputs.all_changed_files }}
steps:
- uses: actions/checkout@v6
- name: Get changed docs files
id: changed-doc-files
uses: tj-actions/changed-files@v47
with:
files: |
docs/**
- name: Get changed python files
id: raw-changed-python-files
uses: tj-actions/changed-files@v47
with:
files: |
**.py
poetry.lock
- name: Check changed python files
id: changed-python-files
env:
CHANGED_PYTHON_FILES: ${{ steps.raw-changed-python-files.outputs.all_changed_files }}
run: |
if [[ " $CHANGED_PYTHON_FILES " == *" poetry.lock "* ]]; then
# if poetry.lock is changed, we need to check everything
CHANGED_PYTHON_FILES="."
fi
echo "all_changed_files=$CHANGED_PYTHON_FILES" >> "$GITHUB_OUTPUT"
format:
if: needs.changed-files.outputs.any_python_changed == 'true'
runs-on: ubuntu-latest
name: Check formatting
needs: changed-files
steps:
- uses: actions/checkout@v6
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Install dependencies
run: poetry install
- name: Check code formatting
# the job output will contain colored diffs with what needs adjusting
run: poe check-format --output-format=github ${{ needs.changed-files.outputs.changed_python_files }}
lint:
if: needs.changed-files.outputs.any_python_changed == 'true'
runs-on: ubuntu-latest
name: Check linting
needs: changed-files
steps:
- uses: actions/checkout@v6
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Install dependencies
run: poetry install
- name: Lint code
run: poe lint --output-format=github ${{ needs.changed-files.outputs.changed_python_files }}
mypy:
if: needs.changed-files.outputs.any_python_changed == 'true'
runs-on: ubuntu-latest
name: Check types with mypy
needs: changed-files
steps:
- uses: actions/checkout@v6
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Install dependencies
run: poetry install
- name: Type check code
uses: liskin/gh-problem-matcher-wrap@v3
with:
linters: mypy
run: poe check-types --show-column-numbers --no-error-summary .
docs:
if: needs.changed-files.outputs.any_docs_changed == 'true'
runs-on: ubuntu-latest
name: Check docs
needs: changed-files
steps:
- uses: actions/checkout@v6
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Install dependencies
run: poetry install --extras=docs
- name: Add Sphinx problem matchers
run: |
echo "::add-matcher::.github/problem-matchers/sphinx-build.json"
echo "::add-matcher::.github/problem-matchers/sphinx-lint.json"
- name: Check docs formatting
run: poe format-docs --check
- name: Lint docs
run: poe lint-docs
- name: Build docs
run: poe docs -- -e 'SPHINXOPTS=--fail-on-warning --keep-going'
beetbox-confuse-cc1e751/.github/workflows/main.yml 0000664 0000000 0000000 00000002376 15136363550 0022277 0 ustar 00root root 0000000 0000000 name: Test
on:
pull_request:
push:
branches:
- main
jobs:
test:
name: Run tests
permissions:
id-token: write
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v6
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- name: Setup Python with poetry caching
# poetry cache requires poetry to already be installed, weirdly
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: poetry
- name: Test
run: |-
poetry install
poe test-with-coverage
- name: Upload test results to Codecov
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload code coverage
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
flags: ${{ matrix.platform}}_python${{ matrix.python-version }}
use_oidc: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) }}
beetbox-confuse-cc1e751/.github/workflows/make_release.yaml 0000664 0000000 0000000 00000004121 15136363550 0024117 0 ustar 00root root 0000000 0000000 name: Make a release
on:
workflow_dispatch:
inputs:
version:
description: 'Version of the new release, just as a number with no prepended "v"'
required: true
env:
PYTHON_VERSION: "3.10"
NEW_VERSION: ${{ inputs.version }}
NEW_TAG: v${{ inputs.version }}
jobs:
increment-version:
name: Bump version, commit and create tag
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Bump project version
run: poetry version "${{ env.NEW_VERSION }}"
- uses: EndBug/add-and-commit@v9
id: commit_and_tag
name: Commit the changes and create tag
with:
message: "Increment version to ${{ env.NEW_VERSION }}"
tag: "${{ env.NEW_TAG }} --force"
build:
name: Build the distribution package
runs-on: ubuntu-latest
needs: increment-version
steps:
- uses: actions/checkout@v6
with:
ref: ${{ env.NEW_TAG }}
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Build a binary wheel and a source tarball
run: poetry build
- name: Store the package
uses: actions/upload-artifact@v6
with:
name: python-package-distributions
path: dist/
publish-to-pypi:
name: Publish distribution 📦 to PyPI
runs-on: ubuntu-latest
needs: build
environment:
name: pypi
url: https://pypi.org/p/confuse
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
beetbox-confuse-cc1e751/.gitignore 0000664 0000000 0000000 00000000352 15136363550 0017213 0 ustar 00root root 0000000 0000000 .DS_Store
.idea
*~
.python-version
__pycache__/
*.py[cod]
_build/
build/
dist/
docs/_build/
*.egg-info/
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
beetbox-confuse-cc1e751/.readthedocs.yaml 0000664 0000000 0000000 00000000306 15136363550 0020451 0 ustar 00root root 0000000 0000000 version: 2
build:
os: ubuntu-22.04
tools:
python: "3.10"
sphinx:
configuration: docs/conf.py
python:
install:
- method: pip
path: .
extra_requirements:
- docs
beetbox-confuse-cc1e751/LICENSE 0000664 0000000 0000000 00000002063 15136363550 0016231 0 ustar 00root root 0000000 0000000 The MIT License
Copyright (c) 2015 Adrian Sampson
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.
beetbox-confuse-cc1e751/README.rst 0000664 0000000 0000000 00000005645 15136363550 0016724 0 ustar 00root root 0000000 0000000 Confuse: painless YAML config files
===================================
.. image:: https://github.com/beetbox/confuse/actions/workflows/main.yml/badge.svg
:target: https://github.com/beetbox/confuse/actions
.. image:: https://img.shields.io/pypi/v/confuse.svg
:target: https://pypi.org/project/confuse/
**Confuse** is a configuration library for Python that uses YAML_. It takes care
of defaults, overrides, type checking, command-line integration, environment
variable support, human-readable errors, and standard OS-specific locations.
What It Does
------------
Here's what Confuse brings to the table:
- An **utterly sensible API** resembling dictionary-and-list structures but
providing **transparent validation** without lots of boilerplate code. Type
``config['num_goats'].get(int)`` to get the configured number of goats and
ensure that it's an integer.
- Combine configuration data from **multiple sources**. Using *layering*,
Confuse allows user-specific configuration to seamlessly override system-wide
configuration, which in turn overrides built-in defaults. An in-package
``config_default.yaml`` can be used to provide bottom-layer defaults using the
same syntax that users will see. A runtime overlay allows the program to
programmatically override and add configuration values.
- Look for configuration files in **platform-specific paths**. Like
``$XDG_CONFIG_HOME`` or ``~/.config`` on Unix; "Application Support" on macOS;
``%APPDATA%`` on Windows. Your program gets its own directory, which you can
use to store additional data. You can transparently create this directory on
demand if, for example, you need to initialize the configuration file on first
run. And an environment variable can be used to override the directory's
location.
- Integration with **command-line arguments** via argparse_ or optparse_ from
the standard library. Use argparse's declarative API to allow command-line
options to override configured defaults.
- Include configuration values from **environment variables**. Values undergo
automatic type conversion, and nested dicts and lists are supported.
Installation
------------
Confuse is available on `PyPI `_ and can be
installed using ``pip``:
.. code-block:: sh
pip install confuse
Using Confuse
-------------
`Confuse's documentation`_ describes its API in detail.
Credits
-------
Confuse was made to power beets_. Like beets, it is available under the `MIT
license`_.
.. _argparse: https://docs.python.org/dev/library/argparse.html
.. _beets: https://github.com/beetbox/beets
.. _configparser: https://docs.python.org/library/configparser.html
.. _confuse's documentation: https://confuse.readthedocs.io/en/latest/usage.html
.. _logging: https://docs.python.org/library/logging.html
.. _mit license: https://opensource.org/license/mit
.. _optparse: https://docs.python.org/dev/library/optparse.html
.. _yaml: https://yaml.org/
beetbox-confuse-cc1e751/codecov.yml 0000664 0000000 0000000 00000000260 15136363550 0017366 0 ustar 00root root 0000000 0000000 # Don't post a comment on pull requests.
comment: off
# I think this disables commit statuses?
coverage:
status:
project: no
patch: no
changes: no
beetbox-confuse-cc1e751/confuse/ 0000775 0000000 0000000 00000000000 15136363550 0016665 5 ustar 00root root 0000000 0000000 beetbox-confuse-cc1e751/confuse/__init__.py 0000664 0000000 0000000 00000000434 15136363550 0020777 0 ustar 00root root 0000000 0000000 """Painless YAML configuration."""
from .core import * # noqa: F403
from .exceptions import * # noqa: F403
from .sources import * # noqa: F403
from .templates import * # noqa: F403
from .util import * # type: ignore[no-redef] # noqa: F403
from .yaml_util import * # noqa: F403
beetbox-confuse-cc1e751/confuse/core.py 0000664 0000000 0000000 00000066134 15136363550 0020201 0 ustar 00root root 0000000 0000000 # This file is part of Confuse.
# Copyright 2016, Adrian Sampson.
#
# 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.
"""Worry-free YAML configuration files."""
from __future__ import annotations
__all__ = [
"CONFIG_FILENAME",
"DEFAULT_FILENAME",
"REDACTED_TOMBSTONE",
"ROOT_NAME",
"ConfigView",
"Configuration",
"LazyConfig",
"RootView",
"Subview",
]
import errno
import os
from collections import OrderedDict
from typing import TYPE_CHECKING, Any, TypeVar, overload
import yaml
from typing_extensions import Self
from . import templates, util, yaml_util
from .exceptions import ConfigError, ConfigTypeError, NotFoundError
from .sources import ConfigSource, EnvSource, YamlSource
if TYPE_CHECKING:
import builtins
from argparse import Namespace
from collections.abc import Iterable, Iterator, Mapping, Sequence
from optparse import Values
from pathlib import Path
from .templates import ConfigKey
CONFIG_FILENAME = "config.yaml"
DEFAULT_FILENAME = "config_default.yaml"
ROOT_NAME = "root"
REDACTED_TOMBSTONE = "REDACTED"
R = TypeVar("R")
# Views and sources.
class ConfigView:
"""A configuration "view" is a query into a program's configuration
data. A view represents a hypothetical location in the configuration
tree; to extract the data from the location, a client typically
calls the ``view.get()`` method. The client can access children in
the tree (subviews) by subscripting the parent view (i.e.,
``view[key]``).
"""
name: str
"""The name of the view, depicting the path taken through the
configuration in Python-like syntax (e.g., ``foo['bar'][42]``).
"""
def resolve(self) -> Iterator[tuple[dict[str, Any] | list[Any], ConfigSource]]:
"""The core (internal) data retrieval method. Generates (value,
source) pairs for each source that contains a value for this
view. May raise `ConfigTypeError` if a type error occurs while
traversing a source.
"""
raise NotImplementedError
def first(self) -> tuple[dict[str, Any] | list[Any], ConfigSource]:
"""Return a (value, source) pair for the first object found for
this view. This amounts to the first element returned by
`resolve`. If no values are available, a `NotFoundError` is
raised.
"""
pairs = self.resolve()
try:
return util.iter_first(pairs)
except ValueError:
raise NotFoundError(f"{self.name} not found")
def exists(self) -> bool:
"""Determine whether the view has a setting in any source."""
try:
self.first()
except NotFoundError:
return False
return True
def add(self, value: Any) -> None:
"""Set the *default* value for this configuration view. The
specified value is added as the lowest-priority configuration
data source.
"""
raise NotImplementedError
def set(self, value: Any) -> None:
"""*Override* the value for this configuration view. The
specified value is added as the highest-priority configuration
data source.
"""
raise NotImplementedError
def root(self) -> RootView:
"""The RootView object from which this view is descended."""
raise NotImplementedError
def __repr__(self) -> str:
return f"<{self.__class__.__name__}: {self.name}>"
def __iter__(self) -> Iterator[Subview | str]:
"""Iterate over the keys of a dictionary view or the *subviews*
of a list view.
"""
# Try iterating over the keys, if this is a dictionary view.
try:
yield from self.keys()
except ConfigTypeError:
# Otherwise, try iterating over a list view.
try:
yield from self.sequence()
except ConfigTypeError:
item, _ = self.first()
raise ConfigTypeError(
f"{self.name} must be a dictionary or a list, not "
f"{type(item).__name__}"
)
def __getitem__(self, key: ConfigKey) -> Subview:
"""Get a subview of this view."""
return Subview(self, key)
def __setitem__(self, key: ConfigKey, value: Any) -> None:
"""Create an overlay source to assign a given key under this
view.
"""
self.set({key: value})
def __contains__(self, key: ConfigKey) -> bool:
return self[key].exists()
def set_args(
self, namespace: dict[str, Any] | Namespace | Values, dots: bool = False
) -> None:
"""Overlay parsed command-line arguments, generated by a library
like argparse or optparse, onto this view's value.
:param namespace: Dictionary or Namespace to overlay this config with.
Supports nested Dictionaries and Namespaces.
:type namespace: dict or Namespace
:param dots: If True, any properties on namespace that contain dots (.)
will be broken down into child dictionaries.
:Example:
{'foo.bar': 'car'}
# Will be turned into
{'foo': {'bar': 'car'}}
:type dots: bool
"""
self.set(util.build_dict(namespace, sep="." if dots else ""))
# Magical conversions. These special methods make it possible to use
# View objects somewhat transparently in certain circumstances. For
# example, rather than using ``view.get(bool)``, it's possible to
# just say ``bool(view)`` or use ``view`` in a conditional.
def __str__(self) -> str:
"""Get the value for this view as a bytestring."""
return str(self.get())
def __bool__(self) -> bool:
"""Gets the value for this view as a bool."""
return bool(self.get())
# Dictionary emulation methods.
def keys(self) -> list[str]:
"""Returns a list containing all the keys available as subviews
of the current views. This enumerates all the keys in *all*
dictionaries matching the current view, in contrast to
``view.get(dict).keys()``, which gets all the keys for the
*first* dict matching the view. If the object for this view in
any source is not a dict, then a `ConfigTypeError` is raised. The
keys are ordered according to how they appear in each source.
"""
keys = []
for dic, _ in self.resolve():
try:
cur_keys = dic.keys() # type: ignore[union-attr]
except AttributeError:
raise ConfigTypeError(
f"{self.name} must be a dict, not {type(dic).__name__}"
)
for key in cur_keys:
if key not in keys:
keys.append(key)
return keys
def items(self) -> Iterator[tuple[str, Subview]]:
"""Iterates over (key, subview) pairs contained in dictionaries
from *all* sources at this view. If the object for this view in
any source is not a dict, then a `ConfigTypeError` is raised.
"""
for key in self.keys():
yield key, self[key]
def values(self) -> Iterator[Subview]:
"""Iterates over all the subviews contained in dictionaries from
*all* sources at this view. If the object for this view in any
source is not a dict, then a `ConfigTypeError` is raised.
"""
for key in self.keys():
yield self[key]
# List/sequence emulation.
def sequence(self) -> Iterator[Subview]:
"""Iterates over the subviews contained in lists from the *first*
source at this view. If the object for this view in the first source
is not a list or tuple, then a `ConfigTypeError` is raised.
"""
try:
collection, _ = self.first()
except NotFoundError:
return
if not isinstance(collection, (list, tuple)):
raise ConfigTypeError(
f"{self.name} must be a list, not {type(collection).__name__}"
)
# Yield all the indices in the sequence.
for index in range(len(collection)):
yield self[index]
def all_contents(self) -> Iterator[str]:
"""Iterates over all subviews from collections at this view from
*all* sources. If the object for this view in any source is not
iterable, then a `ConfigTypeError` is raised. This method is
intended to be used when the view indicates a list; this method
will concatenate the contents of the list from all sources.
"""
for collection, _ in self.resolve():
try:
it = iter(collection)
except TypeError:
raise ConfigTypeError(
f"{self.name} must be an iterable, not {type(collection).__name__}"
)
yield from it
# Validation and conversion.
def flatten(self, redact: bool = False) -> OrderedDict[str, Any]:
"""Create a hierarchy of OrderedDicts containing the data from
this view, recursively reifying all views to get their
represented values.
If `redact` is set, then sensitive values are replaced with
the string "REDACTED".
"""
od: OrderedDict[str, Any] = OrderedDict()
for key, view in self.items():
if redact and view.redact:
od[key] = REDACTED_TOMBSTONE
else:
try:
od[key] = view.flatten(redact=redact)
except ConfigTypeError:
od[key] = view.get()
return od
@overload
def get(self, template: templates.Path) -> Path: ...
@overload
def get(self, template: templates.Template[R]) -> R: ...
@overload
def get(self, template: type[R]) -> R: ...
@overload
def get(self, template: Mapping[str, object]) -> templates.AttrDict[str, Any]: ...
@overload
def get(self, template: list[R]) -> R: ...
@overload
def get(self, template: templates._Required = ...) -> Any: ...
def get(self, template: object = templates.REQUIRED) -> Any:
"""Retrieve the value for this view according to the template.
The `template` against which the values are checked can be
anything convertible to a `Template` using `as_template`. This
means you can pass in a default integer or string value, for
example, or a type to just check that something matches the type
you expect.
May raise a `ConfigValueError` (or its subclass,
`ConfigTypeError`) or a `NotFoundError` when the configuration
doesn't satisfy the template.
"""
return templates.as_template(template).value(self, template)
# Shortcuts for common templates.
def as_filename(self) -> str:
"""Get the value as a path. Equivalent to `get(Filename())`."""
return self.get(templates.Filename())
def as_path(self) -> Path:
"""Get the value as a `pathlib.Path` object. Equivalent to `get(Path())`."""
return self.get(templates.Path())
def as_choice(self, choices: Sequence[R] | dict[str, R] | type[R]) -> R:
"""Get the value from a list of choices. Equivalent to
`get(Choice(choices))`.
Sequences, dictionaries and :class:`Enum` types are supported,
see :class:`confuse.templates.Choice` for more details.
"""
return self.get(templates.Choice(choices))
def as_number(self) -> int | float:
"""Get the value as any number type: int or float. Equivalent to
`get(Number())`.
"""
return self.get(templates.Number())
def as_str_seq(self, split: bool = True) -> list[str]:
"""Get the value as a sequence of strings. Equivalent to
`get(StrSeq(split=split))`.
"""
return self.get(templates.StrSeq(split=split))
@overload
def as_pairs(self, default_value: str) -> list[tuple[str, str]]: ...
@overload
def as_pairs(self, default_value: None = None) -> list[tuple[str, None]]: ...
def as_pairs(
self, default_value: str | None = None
) -> list[tuple[str, str]] | list[tuple[str, None]]:
"""Get the value as a sequence of pairs of two strings. Equivalent to
`get(Pairs(default_value=default_value))`.
"""
return self.get(templates.Pairs(default_value=default_value)) # type: ignore[return-value]
def as_str(self) -> str:
"""Get the value as a (Unicode) string. Equivalent to
`get(unicode)` on Python 2 and `get(str)` on Python 3.
"""
return self.get(templates.String())
def as_str_expanded(self) -> str:
"""Get the value as a (Unicode) string, with env vars
expanded by `os.path.expandvars()`.
"""
return self.get(templates.String(expand_vars=True))
# Redaction.
@property
def redact(self) -> bool:
"""Whether the view contains sensitive information and should be
redacted from output.
"""
return () in self.get_redactions()
@redact.setter
def redact(self, flag: bool) -> None:
self.set_redaction((), flag)
def set_redaction(self, path: tuple[ConfigKey, ...], flag: bool) -> None:
"""Add or remove a redaction for a key path, which should be an
iterable of keys.
"""
raise NotImplementedError()
def get_redactions(self) -> Iterable[tuple[ConfigKey, ...]]:
"""Get the set of currently-redacted sub-key-paths at this view."""
raise NotImplementedError()
class RootView(ConfigView):
"""The base of a view hierarchy. This view keeps track of the
sources that may be accessed by subviews.
"""
def __init__(self, sources: Iterable[ConfigSource]) -> None:
"""Create a configuration hierarchy for a list of sources. At
least one source must be provided. The first source in the list
has the highest priority.
"""
self.sources: list[ConfigSource] = list(sources)
self.name = ROOT_NAME
self.redactions: set[tuple[ConfigKey, ...]] = set()
def add(self, value: Any) -> None:
self.sources.append(ConfigSource.of(value))
def set(self, value: Any) -> None:
self.sources.insert(0, ConfigSource.of(value))
def resolve(self) -> Iterator[tuple[dict[str, Any] | list[Any], ConfigSource]]:
return ((dict(s), s) for s in self.sources)
def clear(self) -> None:
"""Remove all sources (and redactions) from this
configuration.
"""
del self.sources[:]
self.redactions.clear()
def root(self) -> Self:
return self
def set_redaction(self, path: tuple[ConfigKey, ...], flag: bool) -> None:
if flag:
self.redactions.add(path)
elif path in self.redactions:
self.redactions.remove(path)
def get_redactions(self) -> builtins.set[tuple[ConfigKey, ...]]:
return self.redactions
class Subview(ConfigView):
"""A subview accessed via a subscript of a parent view."""
def __init__(self, parent: ConfigView, key: ConfigKey) -> None:
"""Make a subview of a parent view for a given subscript key."""
self.parent = parent
self.key = key
# Choose a human-readable name for this view.
if isinstance(self.parent, RootView):
self.name = ""
else:
self.name = self.parent.name
if not isinstance(self.key, int):
self.name += "."
if isinstance(self.key, int):
self.name += f"#{self.key}"
elif isinstance(self.key, bytes):
self.name += self.key.decode("utf-8")
elif isinstance(self.key, str):
self.name += self.key
else:
self.name += repr(self.key)
def resolve(self) -> Iterator[tuple[dict[str, Any] | list[Any], ConfigSource]]:
for collection, source in self.parent.resolve():
try:
value = collection[self.key] # type: ignore[index]
except IndexError:
# List index out of bounds.
continue
except KeyError:
# Dict key does not exist.
continue
except TypeError:
# Not subscriptable.
raise ConfigTypeError(
f"{self.parent.name} must be a collection, not "
f"{type(collection).__name__}"
)
yield value, source
def set(self, value: Any) -> None:
self.parent.set({self.key: value})
def add(self, value: Any) -> None:
self.parent.add({self.key: value})
def root(self) -> RootView:
return self.parent.root()
def set_redaction(self, path: tuple[ConfigKey, ...], flag: bool) -> None:
self.parent.set_redaction((self.key, *path), flag)
def get_redactions(self) -> Iterable[tuple[ConfigKey, ...]]:
return (
kp[1:] for kp in self.parent.get_redactions() if kp and kp[0] == self.key
)
# Main interface.
class Configuration(RootView):
def __init__(
self,
appname: str,
modname: str | None = None,
read: bool = True,
loader: type[yaml_util.Loader] = yaml_util.Loader,
):
"""Create a configuration object by reading the
automatically-discovered config files for the application for a
given name. If `modname` is specified, it should be the import
name of a module whose package will be searched for a default
config file. (Otherwise, no defaults are used.) Pass `False` for
`read` to disable automatic reading of all discovered
configuration files. Use this when creating a configuration
object at module load time and then call the `read` method
later. Specify the Loader class as `loader`.
"""
super().__init__([])
self.appname = appname
self.modname = modname
self.loader = loader
# Resolve default source location. We do this ahead of time to
# avoid unexpected problems if the working directory changes.
if self.modname:
self._package_path = util.find_package_path(self.modname)
else:
self._package_path = None
self._env_var = f"{self.appname.upper()}DIR"
if read:
self.read()
def user_config_path(self) -> str:
"""Points to the location of the user configuration.
The file may not exist.
"""
return os.path.join(self.config_dir(), CONFIG_FILENAME)
def _add_user_source(self) -> None:
"""Add the configuration options from the YAML file in the
user's configuration directory (given by `config_dir`) if it
exists.
"""
filename = self.user_config_path()
self.add(YamlSource(filename, loader=self.loader, optional=True))
def _add_default_source(self) -> None:
"""Add the package's default configuration settings. This looks
for a YAML file located inside the package for the module
`modname` if it was given.
"""
if self.modname:
if self._package_path:
filename = os.path.join(self._package_path, DEFAULT_FILENAME)
self.add(
YamlSource(
filename, loader=self.loader, optional=True, default=True
)
)
def read(self, user: bool = True, defaults: bool = True) -> None:
"""Find and read the files for this configuration and set them
as the sources for this configuration. To disable either
discovered user configuration files or the in-package defaults,
set `user` or `defaults` to `False`.
"""
if user:
self._add_user_source()
if defaults:
self._add_default_source()
def config_dir(self) -> str:
"""Get the path to the user configuration directory. The
directory is guaranteed to exist as a postcondition (one may be
created if none exist).
If the application's ``...DIR`` environment variable is set, it
is used as the configuration directory. Otherwise,
platform-specific standard configuration locations are searched
for a ``config.yaml`` file. If no configuration file is found, a
fallback path is used.
"""
# If environment variable is set, use it.
if self._env_var in os.environ:
appdir = os.environ[self._env_var]
appdir = os.path.abspath(os.path.expanduser(appdir))
if os.path.isfile(appdir):
raise ConfigError(f"{self._env_var} must be a directory")
else:
# Search platform-specific locations. If no config file is
# found, fall back to the first directory in the list.
configdirs = util.config_dirs()
for confdir in configdirs:
appdir = os.path.join(confdir, self.appname)
if os.path.isfile(os.path.join(appdir, CONFIG_FILENAME)):
break
else:
appdir = os.path.join(configdirs[0], self.appname)
# Ensure that the directory exists.
try:
os.makedirs(appdir)
except OSError as e:
if e.errno != errno.EEXIST:
raise
return appdir
def set_file(self, filename: str, base_for_paths: bool = False) -> None:
"""Parses the file as YAML and inserts it into the configuration
sources with highest priority.
:param filename: Filename of the YAML file to load.
:param base_for_paths: Indicates whether the directory containing the
YAML file will be used as the base directory for resolving relative
path values stored in the YAML file. Otherwise, by default, the
directory returned by `config_dir()` will be used as the base.
"""
self.set(
YamlSource(filename, base_for_paths=base_for_paths, loader=self.loader)
)
def set_env(self, prefix: str | None = None, sep: str = "__") -> None:
"""Create a configuration overlay at the highest priority from
environment variables.
After prefix matching and removal, environment variable names will be
converted to lowercase for use as keys within the configuration. If
there are nested keys, list-like dicts (ie, `{0: 'a', 1: 'b'}`) will
be converted into corresponding lists (ie, `['a', 'b']`). The values
of all environment variables will be parsed as YAML scalars using the
`self.loader` Loader class to ensure type conversion is consistent
with YAML file sources. Use the `EnvSource` class directly to load
environment variables using non-default behavior and to enable full
YAML parsing of values.
:param prefix: The prefix to identify the environment variables to use.
Defaults to uppercased `self.appname` followed by an underscore.
:param sep: Separator within variable names to define nested keys.
"""
if prefix is None:
prefix = f"{self.appname.upper()}_"
self.set(EnvSource(prefix, sep=sep, loader=self.loader))
def dump(self, full: bool = True, redact: bool = False) -> str:
"""Dump the Configuration object to a YAML file.
The order of the keys is determined from the default
configuration file. All keys not in the default configuration
will be appended to the end of the file.
:param full: Dump settings that don't differ from the defaults
as well
:param redact: Remove sensitive information (views with the `redact`
flag set) from the output
"""
if full:
out_dict = self.flatten(redact=redact)
else:
# Exclude defaults when flattening.
sources = [s for s in self.sources if not s.default]
temp_root = RootView(sources)
temp_root.redactions = self.redactions
out_dict = temp_root.flatten(redact=redact)
yaml_out = yaml.dump(
out_dict,
Dumper=yaml_util.Dumper,
default_flow_style=None,
indent=4,
width=1000,
)
# Restore comments to the YAML text.
default_source = None
for source in self.sources:
if source.default:
default_source = source
break
if default_source and default_source.filename:
with open(default_source.filename, "rb") as fp:
default_data = fp.read()
yaml_out = yaml_util.restore_yaml_comments(
yaml_out, default_data.decode("utf-8")
)
return yaml_out
def reload(self) -> None:
"""Reload all sources from the file system.
This only affects sources that come from files (i.e.,
`YamlSource` objects); other sources, such as dictionaries
inserted with `add` or `set`, will remain unchanged.
"""
for source in self.sources:
if isinstance(source, YamlSource):
source.load()
class LazyConfig(Configuration):
"""A Configuration at reads files on demand when it is first
accessed. This is appropriate for using as a global config object at
the module level.
"""
def __init__(self, appname: str, modname: str | None = None) -> None:
super().__init__(appname, modname, False)
self._materialized = False # Have we read the files yet?
self._lazy_prefix: list[
ConfigSource
] = [] # Pre-materialization calls to set().
self._lazy_suffix: list[ConfigSource] = [] # Calls to add().
def read(self, user: bool = True, defaults: bool = True) -> None:
self._materialized = True
super().read(user, defaults)
def resolve(self) -> Iterator[tuple[dict[str, Any] | list[Any], ConfigSource]]:
if not self._materialized:
# Read files and unspool buffers.
self.read()
self.sources += self._lazy_suffix
self.sources[:0] = self._lazy_prefix
return super().resolve()
def add(self, value: Any) -> None:
super().add(value)
if not self._materialized:
# Buffer additions to end.
self._lazy_suffix += self.sources
del self.sources[:]
def set(self, value: Any) -> None:
super().set(value)
if not self._materialized:
# Buffer additions to beginning.
self._lazy_prefix[:0] = self.sources
del self.sources[:]
def clear(self) -> None:
"""Remove all sources from this configuration."""
super().clear()
self._lazy_suffix = []
self._lazy_prefix = []
# "Validated" configuration views: experimental!
beetbox-confuse-cc1e751/confuse/exceptions.py 0000664 0000000 0000000 00000003170 15136363550 0021421 0 ustar 00root root 0000000 0000000 from yaml.scanner import ScannerError
__all__ = [
"ConfigError",
"ConfigReadError",
"ConfigTemplateError",
"ConfigTypeError",
"ConfigValueError",
"NotFoundError",
]
YAML_TAB_PROBLEM = "found character '\\t' that cannot start any token"
# Exceptions.
class ConfigError(Exception):
"""Base class for exceptions raised when querying a configuration."""
class NotFoundError(ConfigError):
"""A requested value could not be found in the configuration trees."""
class ConfigValueError(ConfigError):
"""The value in the configuration is illegal."""
class ConfigTypeError(ConfigValueError):
"""The value in the configuration did not match the expected type."""
class ConfigTemplateError(ConfigError):
"""Base class for exceptions raised because of an invalid template."""
class ConfigReadError(ConfigError):
"""A configuration source could not be read."""
def __init__(self, name: str, reason: Exception | None = None) -> None:
self.name = name
self.reason = reason
message = f"{name} could not be read"
if (
isinstance(reason, ScannerError)
and reason.problem == YAML_TAB_PROBLEM
and reason.problem_mark
):
# Special-case error message for tab indentation in YAML markup.
message += (
f": found tab character at line {reason.problem_mark.line + 1}, "
f"column {reason.problem_mark.column + 1}"
)
elif reason:
# Generic error message uses exception's message.
message += f": {reason}"
super().__init__(message)
beetbox-confuse-cc1e751/confuse/py.typed 0000664 0000000 0000000 00000000000 15136363550 0020352 0 ustar 00root root 0000000 0000000 beetbox-confuse-cc1e751/confuse/sources.py 0000664 0000000 0000000 00000017717 15136363550 0020737 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
import yaml
from . import yaml_util
from .util import build_dict
class ConfigSource(dict[str, Any]):
"""A dictionary augmented with metadata about the source of the
configuration.
"""
def __init__(
self,
value: dict[str, Any],
filename: str | None = None,
default: bool = False,
base_for_paths: bool = False,
):
"""Create a configuration source from a dictionary.
:param filename: The file with the data for this configuration source.
:param default: Indicates whether this source provides the
application's default configuration settings.
:param base_for_paths: Indicates whether the source file's directory
(i.e., the directory component of `self.filename`) should be used as
the base directory for resolving relative path values provided by this
source, instead of using the application's configuration directory. If
no `filename` is provided, `base_for_paths` will be treated as False.
See `templates.Filename` for details of the relative path resolution
behavior.
"""
super().__init__(value)
if filename is not None and not isinstance(filename, str):
raise TypeError("filename must be a string or None")
self.filename = filename
self.default = default
self.base_for_paths = base_for_paths if filename is not None else False
def __repr__(self) -> str:
return (
f"ConfigSource({super()!r}, {self.filename!r}, {self.default!r}, "
f"{self.base_for_paths!r})"
)
@classmethod
def of(cls, value: dict[str, Any] | ConfigSource) -> ConfigSource:
"""Given either a dictionary or a `ConfigSource` object, return
a `ConfigSource` object. This lets a function accept either type
of object as an argument.
"""
if isinstance(value, ConfigSource):
return value
elif isinstance(value, dict):
return ConfigSource(value)
else:
raise TypeError("source value must be a dict")
class YamlSource(ConfigSource):
"""A configuration data source that reads from a YAML file."""
def __init__(
self,
filename: str | None = None,
default: bool = False,
base_for_paths: bool = False,
optional: bool = False,
loader: type[yaml.SafeLoader] = yaml_util.Loader,
):
"""Create a YAML data source by reading data from a file.
May raise a `ConfigReadError`. However, if `optional` is
enabled, this exception will not be raised in the case when the
file does not exist---instead, the source will be silently
empty.
"""
if filename is not None:
filename = os.path.abspath(filename)
super().__init__({}, filename, default, base_for_paths)
self.loader = loader
self.optional = optional
self.load()
def load(self) -> None:
"""Load YAML data from the source's filename."""
if self.optional and (
self.filename is None or not os.path.isfile(self.filename)
):
value: object = {}
elif self.filename is None:
raise TypeError("filename is required for YamlSource")
else:
value = yaml_util.load_yaml(self.filename, loader=self.loader) or {}
if isinstance(value, dict):
self.update(value)
else:
# We enforce that the loaded YAML is a mapping (dict)
raise TypeError(f"YAML config must be a mapping, got {type(value)}")
class EnvSource(ConfigSource):
"""A configuration data source loaded from environment variables."""
def __init__(
self,
prefix: str,
sep: str = "__",
lower: bool = True,
handle_lists: bool = True,
parse_yaml_docs: bool = False,
loader: type[yaml.SafeLoader] = yaml_util.Loader,
):
"""Create a configuration source from the environment.
:param prefix: The prefix used to identify the environment variables
to be loaded into this configuration source.
:param sep: Separator within variable names to define nested keys.
:param lower: Indicates whether to convert variable names to lowercase
after prefix matching.
:param handle_lists: If variables are split into nested keys, indicates
whether to search for sub-dicts with keys that are sequential
integers starting from 0 and convert those dicts to lists.
:param parse_yaml_docs: Enable parsing the values of environment
variables as full YAML documents. By default, when False, values
are parsed only as YAML scalars.
:param loader: PyYAML Loader class to use to parse YAML values.
"""
super().__init__({}, filename=None, default=False, base_for_paths=False)
self.prefix = prefix
self.sep = sep
self.lower = lower
self.handle_lists = handle_lists
self.parse_yaml_docs = parse_yaml_docs
self.loader = loader
self.load()
def load(self) -> None:
"""Load configuration data from the environment."""
# Read config variables with prefix from the environment.
config_vars: dict[str, object] = {}
for var, value in os.environ.items():
if var.startswith(self.prefix):
key = var[len(self.prefix) :]
if self.lower:
key = key.lower()
if self.parse_yaml_docs:
# Parse the value as a YAML document, which will convert
# string representations of dicts and lists into the
# appropriate object (ie, '{foo: bar}' to {'foo': 'bar'}).
# Will raise a ConfigReadError if YAML parsing fails.
val = yaml_util.load_yaml_string(
value, f"env variable {var}", loader=self.loader
)
else:
# Parse the value as a YAML scalar so that values are type
# converted using the same rules as the YAML Loader (ie,
# numeric string to int/float, 'true' to True, etc.). Will
# not raise a ConfigReadError.
val = yaml_util.parse_as_scalar(value, loader=self.loader)
config_vars[key] = val
if self.sep:
# Build a nested dict, keeping keys with `None` values to allow
# environment variables to unset values from lower priority sources
config_vars = build_dict(config_vars, self.sep, keep_none=True)
if self.handle_lists:
for k, v in config_vars.items():
config_vars[k] = self._convert_dict_lists(v)
self.update(config_vars)
@classmethod
def _convert_dict_lists(cls, obj: object) -> object:
"""Recursively search for dicts where all of the keys are integers
from 0 to the length of the dict, and convert them to lists.
"""
# We only deal with dictionaries
if not isinstance(obj, dict):
return obj
# Recursively search values for additional dicts to convert to lists
for k, v in obj.items():
obj[k] = cls._convert_dict_lists(v)
try:
# Convert the keys to integers, mapping the ints back to the keys
int_to_key = {int(k): k for k in obj.keys()}
except ValueError:
# Not all of the keys represent integers
return obj
try:
# For the integers from 0 to the length of the dict, try to create
# a list from the dict values using the integer to key mapping
return [obj[int_to_key[i]] for i in range(len(obj))]
except KeyError:
# At least one integer within the range is not a key of the dict
return obj
beetbox-confuse-cc1e751/confuse/templates.py 0000664 0000000 0000000 00000074332 15136363550 0021246 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import enum
import os
import pathlib
import re
from collections import abc
from collections.abc import Hashable, Iterable, Mapping
from functools import singledispatchmethod
from typing import TYPE_CHECKING, Any, Generic, NoReturn, overload
from typing_extensions import TypeVar
from . import exceptions, util
if TYPE_CHECKING:
from .core import ConfigView, Subview
T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True)
K = TypeVar("K", bound=Hashable, default=str)
P = TypeVar("P", bound=pathlib.PurePath | str, default=str)
V = TypeVar("V", default=object)
ConfigKey = int | str | bytes
ConfigKeyT = TypeVar("ConfigKeyT", bound=ConfigKey, default=str)
class _Required:
"""Marker class for required sentinel."""
pass
REQUIRED = _Required()
"""A sentinel indicating that there is no default value and an exception
should be raised when the value is missing.
"""
class AttrDict(dict[ConfigKeyT, V]):
"""A `dict` subclass that can be accessed via attributes (dot
notation) for convenience.
"""
def __getattr__(self, key: str) -> V:
if key in self:
return self[key] # type: ignore[index]
else:
raise AttributeError(key)
def __setattr__(self, key: str, value: V) -> None:
self[key] = value # type: ignore[index]
class Template(Generic[T_co]):
"""A value template for configuration fields.
The template works like a type and instructs Confuse about how to
interpret a deserialized YAML value. This includes type conversions,
providing a default value, and validating for errors. For example, a
filepath type might expand tildes and check that the file exists.
"""
default: T_co | _Required
@overload
def __init__(self: Template[T], default: T) -> None: ...
@overload
def __init__(self: Template[Any], default: _Required = ...) -> None: ...
def __init__(self, default: object = REQUIRED) -> None:
"""Create a template with a given default value.
If `default` is the sentinel `REQUIRED` (as it is by default),
then an error will be raised when a value is missing. Otherwise,
missing values will instead return `default`.
"""
self.default = default # type: ignore[assignment]
def __call__(self, view: ConfigView) -> T_co:
"""Invoking a template on a view gets the view's value according
to the template.
"""
return self.value(view, self)
def value(
self, view: ConfigView, template: Template[T_co] | object | None = None
) -> T_co:
"""Get the value for a `ConfigView`.
May raise a `NotFoundError` if the value is missing (and the
template requires it) or a `ConfigValueError` for invalid values.
"""
try:
value, _ = view.first()
return self.convert(value, view)
except exceptions.NotFoundError:
pass
# Get default value, or raise if required.
return self.get_default_value(view.name)
def get_default_value(self, key_name: str = "default") -> T_co:
"""Get the default value to return when the value is missing.
May raise a `NotFoundError` if the value is required.
"""
if not hasattr(self, "default") or self.default is REQUIRED:
# The value is required. A missing value is an error.
raise exceptions.NotFoundError(f"{key_name} not found")
# The value is not required.
return self.default # type: ignore[return-value]
def convert(self, value: Any, view: ConfigView) -> T_co:
"""Convert the YAML-deserialized value to a value of the desired
type.
Subclasses should override this to provide useful conversions.
May raise a `ConfigValueError` when the configuration is wrong.
"""
# Default implementation does no conversion.
return value # type: ignore[no-any-return]
def fail(
self, message: str, view: ConfigView, type_error: bool = False
) -> NoReturn:
"""Raise an exception indicating that a value cannot be
accepted.
`type_error` indicates whether the error is due to a type
mismatch rather than a malformed value. In this case, a more
specific exception is raised.
"""
exc_class = (
exceptions.ConfigTypeError if type_error else exceptions.ConfigValueError
)
raise exc_class(f"{view.name}: {message}")
def __repr__(self) -> str:
return "{}({})".format(
type(self).__name__,
"" if self.default is REQUIRED else repr(self.default),
)
class Integer(Template[int]):
"""An integer configuration value template."""
def convert(self, value: int | float, view: ConfigView) -> int:
"""Check that the value is an integer. Floats are rounded."""
if isinstance(value, int):
return value
elif isinstance(value, float):
return int(value)
else:
self.fail("must be a number", view, True)
Numeric = TypeVar("Numeric", int, float)
class Number(Template[Numeric]):
"""A numeric type: either an integer or a floating-point number."""
def convert(self, value: Numeric, view: ConfigView) -> Numeric:
"""Check that the value is an int or a float."""
if isinstance(value, (int, float)):
return value
else:
self.fail(f"must be numeric, not {type(value).__name__}", view, True)
class MappingTemplate(Template[AttrDict[ConfigKeyT, V]]):
"""A template that uses a dictionary to specify other types for the
values for a set of keys and produce a validated `AttrDict`.
"""
def __init__(self, mapping: Mapping[ConfigKeyT, Template[V] | type[V]]) -> None:
"""Create a template according to a dict (mapping). The
mapping's values should themselves either be Types or
convertible to Types.
"""
subtemplates: dict[ConfigKeyT, Template[V]] = {}
for key, typ in mapping.items():
subtemplates[key] = as_template(typ)
self.subtemplates = subtemplates
def value(
self,
view: ConfigView,
template: Template[AttrDict[ConfigKeyT, V]] | object | None = None,
) -> AttrDict[ConfigKeyT, V]:
"""Get a dict with the same keys as the template and values
validated according to the value types.
"""
return AttrDict(
{k: v.value(view[k], self) for k, v in self.subtemplates.items()}
)
def __repr__(self) -> str:
return f"MappingTemplate({self.subtemplates!r})"
class Sequence(Template[list[T]]):
"""A template used to validate lists of similar items,
based on a given subtemplate.
"""
subtemplate: Template[T]
@overload
def __init__(
self: Sequence[dict[str, V]], subtemplate: Mapping[str, Template[V] | type[V]]
) -> None: ...
@overload
def __init__(self, subtemplate: type[T]) -> None: ...
@overload
def __init__(self, subtemplate: Template[T]) -> None: ...
def __init__(self, subtemplate: Template[T] | type[T] | Mapping[str, object]):
"""Create a template for a list with items validated
on a given subtemplate.
"""
super().__init__()
self.subtemplate = as_template(subtemplate) # type: ignore[assignment]
def value(
self, view: ConfigView, template: Template[list[T]] | object | None = None
) -> list[T]:
"""Get a list of items validated against the template."""
out = []
for item in view.sequence():
out.append(self.subtemplate.value(item, self))
return out
def __repr__(self) -> str:
return f"Sequence({self.subtemplate!r})"
class MappingValues(Template[dict[str, T]]):
"""A template used to validate mappings of similar items,
based on a given subtemplate applied to the values.
All keys in the mapping are considered valid, but values
must pass validation by the subtemplate. Similar to the
Sequence template but for mappings.
"""
subtemplate: Template[T]
def __init__(self, subtemplate: Template[T] | type[T] | Mapping[str, object]):
"""Create a template for a mapping with variable keys
and item values validated on a given subtemplate.
"""
super().__init__()
self.subtemplate = as_template(subtemplate) # type: ignore[assignment]
def value(
self, view: ConfigView, template: Template[dict[str, T]] | object | None = None
) -> dict[str, T]:
"""Get a dict with the same keys as the view and the
value of each item validated against the subtemplate.
"""
out = {}
for key, item in view.items():
out[key] = self.subtemplate.value(item, self)
return out
def __repr__(self) -> str:
return f"MappingValues({self.subtemplate!r})"
class String(Template[str]):
"""A string configuration value template."""
def __init__(
self,
default: str | _Required = REQUIRED,
pattern: str | None = None,
expand_vars: bool = False,
):
"""Create a template with the added optional `pattern` argument,
a regular expression string that the value should match.
"""
super().__init__(default)
self.pattern = pattern
self.expand_vars = expand_vars
if pattern:
self.regex = re.compile(pattern)
def __repr__(self) -> str:
args = []
if self.default is not REQUIRED:
args.append(repr(self.default))
if self.pattern is not None:
args.append("pattern=" + repr(self.pattern))
return f"String({', '.join(args)})"
def convert(self, value: object, view: ConfigView) -> str:
"""Check that the value is a string and matches the pattern."""
if not isinstance(value, str):
self.fail("must be a string", view, True)
if self.pattern and not self.regex.match(value):
self.fail(f"must match the pattern {self.pattern}", view)
if self.expand_vars:
return os.path.expandvars(value)
else:
return value
class Choice(Template[T], Generic[T, K]):
"""A template that permits values from a sequence of choices.
Sequences, dictionaries and :class:`Enum` types are supported,
see :meth:`__init__` for usage.
"""
choices: abc.Sequence[T] | dict[K, T] | type[T]
def __init__(
self,
choices: abc.Sequence[T] | dict[K, T] | type[T],
default: T | _Required = REQUIRED,
) -> None:
"""Create a template that validates any of the values from the
iterable `choices`.
If `choices` is a map, then the corresponding value is emitted.
Otherwise, the value itself is emitted.
If `choices` is a `Enum`, then the enum entry with the value is
emitted.
"""
super().__init__(default)
self.choices = choices
@singledispatchmethod
def convert_choices(
self, choices: abc.Sequence[T] | dict[K, T] | type[T], value: str
) -> T:
raise NotImplementedError
@convert_choices.register(type)
def _(self, choices: type[T], value: str) -> T:
return choices(value) # type: ignore[call-arg]
@convert_choices.register(dict)
def _(self, choices: dict[K, T], value: K) -> T:
return choices[value]
@convert_choices.register(abc.Sequence)
def _(self, choices: abc.Sequence[T], value: T) -> T:
return choices[choices.index(value)]
@singledispatchmethod
def format_choices(self, choices: abc.Sequence[T] | enum.Enum) -> list[str]:
raise NotImplementedError
@format_choices.register(type)
def _(self, choices: type[enum.Enum]) -> list[str]:
return [c.value for c in choices]
@format_choices.register(abc.Sequence)
@format_choices.register(Mapping)
def _(self, choices: Iterable[T]) -> list[str]:
return list(map(str, choices))
def convert(self, value: object, view: ConfigView) -> T:
"""Ensure that the value is among the choices (and remap if the
choices are a mapping).
"""
try:
return self.convert_choices(self.choices, value)
except (KeyError, ValueError):
self.fail(
f"must be one of {self.format_choices(self.choices)!r}, not {value!r}",
view,
)
def __repr__(self) -> str:
return f"Choice({self.choices!r})"
class OneOf(Template[T]):
"""A template that permits values complying to one of the given templates.
When using templates that produce different types, explicitly specify
the type parameter: ``OneOf[bool | str]([bool, String()])``
"""
allowed: list[Template[Any]]
template: Template[Any] | None
def __init__(
self,
allowed: Iterable[Template[Any] | type[Any] | Mapping[str, object] | T],
default: T | _Required = REQUIRED,
) -> None:
super().__init__(default)
self.allowed = [as_template(t) for t in allowed]
self.template = None
def __repr__(self) -> str:
args = []
if self.allowed is not None:
args.append("allowed=" + repr(self.allowed))
if self.default is not REQUIRED:
args.append(repr(self.default))
return f"OneOf({', '.join(args)})"
def value(
self, view: ConfigView, template: Template[T] | object | None = None
) -> T:
self.template = template if isinstance(template, Template) else None
return super().value(view, template)
def convert(self, value: object, view: Subview) -> T: # type: ignore[override]
"""Ensure that the value follows at least one template."""
is_mapping = isinstance(self.template, MappingTemplate)
for candidate in self.allowed:
result: T
try:
if is_mapping:
assert self.template is not None
from .core import Subview
if isinstance(view, Subview):
# Use a new MappingTemplate to check the sibling value
next_template = MappingTemplate({view.key: candidate})
result = view.parent.get(next_template)[view.key]
return result
else:
self.fail("MappingTemplate must be used with a Subview", view)
else:
result = view.get(candidate)
return result
except exceptions.ConfigTemplateError:
raise
except exceptions.ConfigError:
pass
except ValueError as exc:
raise exceptions.ConfigTemplateError(exc)
self.fail(f"must be one of {self.allowed!r}, not {value!r}", view)
class BytesToStrMixin:
@staticmethod
def normalize_bytes(x: str | bytes) -> str:
if isinstance(x, bytes):
return x.decode("utf-8", "ignore")
return x
class StrSeq(BytesToStrMixin, Template[list[str]]):
"""A template for values that are lists of strings.
Validates both actual YAML string lists and single strings. Strings
can optionally be split on whitespace.
"""
def __init__(
self, split: bool = True, default: list[str] | _Required = REQUIRED
) -> None:
"""Create a new template.
`split` indicates whether, when the underlying value is a single
string, it should be split on whitespace. Otherwise, the
resulting value is a list containing a single string.
"""
super().__init__(default)
self.split = split
def _convert_value(self, x: object, view: ConfigView) -> str:
if not isinstance(x, (str, bytes)):
self.fail("must be a list of strings", view, True)
return self.normalize_bytes(x)
def convert(
self, value: str | bytes | list[str | bytes], view: ConfigView
) -> list[str]:
if isinstance(value, bytes):
value = value.decode("utf-8", "ignore")
if isinstance(value, str):
if self.split:
values: Iterable[object] = value.split()
else:
values = [value]
elif isinstance(value, Iterable):
values = value
else:
self.fail("must be a whitespace-separated string or a list", view, True)
return [self._convert_value(v, view) for v in values]
class Pairs(BytesToStrMixin, Template[list[tuple[str, V]]]):
"""A template for ordered key-value pairs.
This can either be given with the same syntax as for `StrSeq` (i.e. without
values), or as a list of strings and/or single-element mappings such as::
- key: value
- [key, value]
- key
The result is a list of two-element tuples. If no value is provided, the
`default_value` will be returned as the second element.
"""
default_value: V
@overload
def __init__(
self: Pairs[str],
default_value: str,
default: list[tuple[str, str]] | _Required = REQUIRED,
) -> None: ...
@overload
def __init__(
self: Pairs[None],
default_value: None = None,
default: list[tuple[str, None]] | _Required = REQUIRED,
) -> None: ...
def __init__(
self,
default_value: str | None = None,
default: list[tuple[str, str]] | list[tuple[str, None]] | _Required = REQUIRED,
) -> None:
"""Create a new template.
`default_value` is the dictionary value returned for items that are not
a mapping, but a single string.
"""
super().__init__(default) # type: ignore[arg-type]
self.default_value = default_value # type: ignore[assignment]
def _convert_value(self, x: object, view: ConfigView) -> tuple[str, V]:
if isinstance(x, (str, bytes)):
return self.normalize_bytes(x), self.default_value
if isinstance(x, Mapping):
if len(x) != 1:
self.fail("must be a single-element mapping", view, True)
k, v = util.iter_first(x.items())
elif isinstance(x, abc.Sequence):
if len(x) != 2:
self.fail("must be a two-element list", view, True)
k, v = x
else:
# Is this even possible? -> Likely, if some !directive cause
# YAML to parse this to some custom type.
self.fail(f"must be a single string, mapping, or a list{x}", view, True)
return self.normalize_bytes(k), self.normalize_bytes(v) # type: ignore[return-value]
def convert(
self, value: list[abc.Sequence[str] | Mapping[str, str]], view: ConfigView
) -> list[tuple[str, V]]:
return [self._convert_value(v, view) for v in value]
class Filename(Template[P]):
"""A template that validates strings as filenames.
Filenames are returned as absolute, tilde-free paths.
Relative paths are relative to the template's `cwd` argument
when it is specified. Otherwise, if the paths come from a file,
they will be relative to the configuration directory (see the
`config_dir` method) by default or to the base directory of the
config file if either the source has `base_for_paths` set to True
or the template has `in_source_dir` set to True. Paths from sources
without a file are relative to the current working directory. This
helps attain the expected behavior when using command-line options.
"""
def __init__(
self,
default: P | str | None | _Required = REQUIRED,
cwd: str | None = None,
relative_to: str | None = None,
in_app_dir: bool = False,
in_source_dir: bool = False,
) -> None:
"""`relative_to` is the name of a sibling value that is
being validated at the same time.
`in_app_dir` indicates whether the path should be resolved
inside the application's config directory (even when the setting
does not come from a file).
`in_source_dir` indicates whether the path should be resolved
relative to the directory containing the source file, if there is
one, taking precedence over the application's config directory.
"""
if default is None:
self.default: P | _Required = default # type: ignore[assignment]
else:
super().__init__(default) # type: ignore[arg-type]
self.cwd = cwd
self.relative_to = relative_to
self.in_app_dir = in_app_dir
self.in_source_dir = in_source_dir
def __repr__(self) -> str:
args = []
if self.default is not REQUIRED:
args.append(repr(self.default))
if self.cwd is not None:
args.append("cwd=" + repr(self.cwd))
if self.relative_to is not None:
args.append("relative_to=" + repr(self.relative_to))
if self.in_app_dir:
args.append("in_app_dir=True")
if self.in_source_dir:
args.append("in_source_dir=True")
return f"Filename({', '.join(args)})"
def resolve_relative_to(
self, view: Subview, template: MappingTemplate | Mapping[str, Any] | None
) -> str:
if not isinstance(template, (Mapping, MappingTemplate)):
# disallow config.get(Filename(relative_to='foo'))
raise exceptions.ConfigTemplateError(
"relative_to may only be used when getting multiple values."
)
elif self.relative_to == view.key:
raise exceptions.ConfigTemplateError(f"{view.name} is relative to itself")
elif self.relative_to not in view.parent.keys():
# self.relative_to is not in the config
self.fail(
(f'needs sibling value "{self.relative_to}" to expand relative path'),
view,
)
# Use a safe way to access subtemplates
if isinstance(template, MappingTemplate):
subtemplates = template.subtemplates
else:
# template is a Mapping
subtemplates = {k: as_template(v) for k, v in template.items()}
old_template = dict(subtemplates)
# save time by skipping MappingTemplate's init loop
next_template = MappingTemplate({})
next_relative: str | None = self.relative_to
# gather all the needed templates and nothing else
while next_relative is not None:
try:
# pop to avoid infinite loop because of recursive
# relative paths
rel_to_template = old_template.pop(next_relative)
except KeyError:
if next_relative in subtemplates:
# we encountered this config key previously
raise exceptions.ConfigTemplateError(
f"{view.name} and {self.relative_to} are recursively relative"
)
else:
raise exceptions.ConfigTemplateError(
f"missing template for {self.relative_to}, needed to expand "
f"{view.name}'s relative path"
)
next_template.subtemplates[next_relative] = rel_to_template
next_relative_val = getattr(rel_to_template, "relative_to", None)
next_relative = (
next_relative_val if isinstance(next_relative_val, str) else None
)
return view.parent.get(next_template)[self.relative_to] # type: ignore[return-value]
def value(
self, view: ConfigView, template: Template[T] | object | None = None
) -> P:
try:
path, source = view.first()
except exceptions.NotFoundError:
return self.get_default_value(view.name)
if not isinstance(path, (str, bytes)):
self.fail(f"must be a filename, not {type(path).__name__}", view, True)
if isinstance(path, bytes):
path_str = path.decode("utf-8", "ignore")
else:
path_str = path
path_str = os.path.expanduser(path_str)
if not os.path.isabs(path_str):
if self.cwd is not None:
# relative to the template's argument
path_str = os.path.join(self.cwd, path_str)
elif self.relative_to is not None:
path_str = os.path.join(
self.resolve_relative_to(view, template),
path_str,
)
elif (source.filename and self.in_source_dir) or (
source.base_for_paths and not self.in_app_dir
):
# relative to the directory the source file is in.
path_str = os.path.join(os.path.dirname(source.filename), path_str)
elif source.filename or self.in_app_dir:
# From defaults: relative to the app's directory.
path_str = os.path.join(view.root().config_dir(), path_str)
return os.path.abspath(path_str)
class Path(Filename[pathlib.PurePath]):
"""A template that validates strings as `pathlib.Path` objects.
Filenames are parsed equivalent to the `Filename` template and then
converted to `pathlib.Path` objects.
"""
def value(
self, view: ConfigView, template: Template[pathlib.Path] | object | None = None
) -> pathlib.Path:
val = super().value(view, template)
return pathlib.Path(val) if val is not None else None
class Optional(Template[T | None]):
"""A template that makes a subtemplate optional.
If the value is present and not null, it must validate against the
subtemplate. However, if the value is null or missing, the template will
still validate, returning a default value. If `allow_missing` is False,
the template will not allow missing values while still permitting null.
"""
subtemplate: Template[T]
default: T | None
@overload
def __init__(
self,
subtemplate: type[T],
default: T | None = None,
allow_missing: bool = True,
) -> None: ...
@overload
def __init__(
self,
subtemplate: Template[T],
default: T | None = None,
allow_missing: bool = True,
) -> None: ...
@overload
def __init__(
self,
subtemplate: Mapping[str, object],
default: Mapping[str, Any] | None = None,
allow_missing: bool = True,
) -> None: ...
def __init__(
self,
subtemplate: Template[T] | type[T] | Mapping[str, object],
default: T | Mapping[str, Any] | None = None,
allow_missing: bool = True,
) -> None:
self.subtemplate: Template[T] = as_template(subtemplate) # type: ignore[assignment]
if default is None:
# When no default is passed, try to use the subtemplate's
# default value as the default for this template
try:
default = self.subtemplate.get_default_value()
except exceptions.NotFoundError:
pass
self.default = default # type: ignore[assignment]
self.allow_missing = allow_missing
def value(
self, view: ConfigView, template: Template[T | None] | object | None = None
) -> T | None:
try:
value, _ = view.first()
except exceptions.NotFoundError:
if self.allow_missing:
# Value is missing but not required
return self.default
# Value must be present even though it can be null. Raise an error.
raise exceptions.NotFoundError(f"{view.name} not found")
if value is None:
# None (ie, null) is always a valid value
return self.default
return self.subtemplate.value(view, self)
def __repr__(self) -> str:
return (
f"Optional({self.subtemplate!r}, {self.default!r}, "
f"allow_missing={self.allow_missing})"
)
class TypeTemplate(Template[T]):
"""A simple template that checks that a value is an instance of a
desired Python type.
"""
def __init__(self, typ: type[T], default: T | _Required = REQUIRED) -> None:
"""Create a template that checks that the value is an instance
of `typ`.
"""
super().__init__(default)
self.typ = typ
def convert(self, value: Any, view: ConfigView) -> T:
if isinstance(value, self.typ):
return value
self.fail(
f"must be a {self.typ.__name__}, not {type(value).__name__}",
view,
True,
)
@overload
def as_template(value: Template[T]) -> Template[T]: ...
@overload
def as_template(value: type[T]) -> Template[T]: ...
@overload
def as_template(value: Mapping[str, object]) -> MappingTemplate: ...
@overload
def as_template(value: set[T]) -> Choice[T, T]: ...
@overload
def as_template(value: list[T]) -> OneOf[T]: ...
@overload
def as_template(value: pathlib.PurePath) -> Path: ...
@overload
def as_template(value: None) -> Template[None]: ...
@overload
def as_template(value: T) -> Template[T]: ...
def as_template(value: Any) -> Template[Any]:
"""Convert a simple "shorthand" Python value to a `Template`."""
if isinstance(value, Template):
# If it's already a Template, pass it through.
return value
elif isinstance(value, abc.Mapping):
# Dictionaries work as templates.
return MappingTemplate(value)
elif value is int:
return Integer()
elif isinstance(value, int):
return Integer(value)
elif isinstance(value, type) and issubclass(value, str):
return String()
elif isinstance(value, str):
return String(value)
elif isinstance(value, set):
# convert to list to avoid hash related problems
return Choice(list(value))
elif isinstance(value, type) and issubclass(value, enum.Enum):
return Choice(value)
elif isinstance(value, list):
return OneOf(value)
elif value is float:
return Number()
elif isinstance(value, float):
return Number(value)
elif isinstance(value, pathlib.PurePath):
return Path(value)
elif value is None:
return Template(None)
elif value is REQUIRED:
return Template()
elif value is dict:
return TypeTemplate(abc.Mapping)
elif value is list:
return TypeTemplate(abc.Sequence)
elif isinstance(value, type):
return TypeTemplate(value)
else:
raise ValueError(f"cannot convert to template: {value!r}")
beetbox-confuse-cc1e751/confuse/util.py 0000664 0000000 0000000 00000013773 15136363550 0020227 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import argparse
import importlib.util
import optparse
import os
import platform
import sys
from typing import TYPE_CHECKING, Any, TypeVar
if TYPE_CHECKING:
from collections.abc import Iterable
UNIX_DIR_FALLBACK = "~/.config"
WINDOWS_DIR_VAR = "APPDATA"
WINDOWS_DIR_FALLBACK = "~\\AppData\\Roaming"
MAC_DIR = "~/Library/Application Support"
T = TypeVar("T")
def iter_first(sequence: Iterable[T]) -> T:
"""Get the first element from an iterable or raise a ValueError if
the iterator generates no values.
"""
it = iter(sequence)
try:
return next(it)
except StopIteration:
raise ValueError()
def namespace_to_dict(
obj: argparse.Namespace | optparse.Values | T,
) -> dict[str, Any] | T:
"""If obj is argparse.Namespace or optparse.Values we'll return
a dict representation of it, else return the original object.
Redefine this method if using other parsers.
:param obj: *
:return:
:rtype: dict or *
"""
if isinstance(obj, (argparse.Namespace, optparse.Values)):
return vars(obj)
return obj
def build_dict(
values_obj: argparse.Namespace | optparse.Values | T,
sep: str = "",
keep_none: bool = False,
) -> dict[str, Any] | T:
"""Recursively builds a dictionary from an argparse.Namespace,
optparse.Values, or dict object.
Additionally, if `sep` is a non-empty string, the keys will be split
by `sep` and expanded into a nested dict. Keys with a `None` value
are dropped by default to avoid unsetting options but can be kept
by setting `keep_none` to `True`.
:param obj: Namespace, Values, or dict to iterate over. Other
values will simply be returned.
:type obj: argparse.Namespace or optparse.Values or dict or *
:param sep: Separator to use for splitting properties/keys of `obj`
for expansion into nested dictionaries.
:type sep: str
:param keep_none: Whether to keep keys whose value is `None`.
:type keep_none: bool
:return: A new dictionary or the value passed if obj was not a
dict, Namespace, or Values.
:rtype: dict or *
"""
# We expect our root object to be a dict, but it may come in as
# a namespace
obj = namespace_to_dict(values_obj)
# We only deal with dictionaries
if not isinstance(obj, dict):
return obj
# Get keys iterator
keys: Iterable[str] = obj.keys()
if sep:
# Splitting keys by `sep` needs sorted keys to prevent parents
# from clobbering children
keys = sorted(list(keys))
output: dict[str, Any] = {}
for key in keys:
value = obj[key]
if value is None and not keep_none: # Avoid unset options.
continue
save_to = output
result = build_dict(value, sep, keep_none)
if sep:
# Split keys by `sep` as this signifies nesting
split = key.split(sep)
if len(split) > 1:
# The last index will be the key we assign result to
key = split.pop()
# Build the dict tree if needed and change where
# we're saving to
for child_key in split:
if child_key in save_to and isinstance(save_to[child_key], dict):
save_to = save_to[child_key]
else:
# Clobber or create
save_to[child_key] = {}
save_to = save_to[child_key]
# Save
if key in save_to:
save_to[key].update(result)
else:
save_to[key] = result
return output
# Config file paths, including platform-specific paths and in-package
# defaults.
def find_package_path(name: str) -> str | None:
"""Returns the path to the package containing the named module or
None if the path could not be identified (e.g., if
``name == "__main__"``).
"""
# Based on get_root_path from Flask by Armin Ronacher, cf.
# https://github.com/pallets/flask/blob/85c5d93cbd049c4bd0679c36fd1ddcae8c37b642/src/flask/helpers.py#L570
try:
spec = importlib.util.find_spec(name)
except (ImportError, ValueError):
return None
if not spec or (loader := spec.loader) is None or name == "__main__":
return None
filepath: str
if hasattr(loader, "get_filename"):
filepath = loader.get_filename(name)
else:
# Fall back to importing the specified module.
__import__(name)
filepath = sys.modules[name].__file__ # type: ignore[assignment]
return os.path.dirname(os.path.abspath(filepath))
def xdg_config_dirs() -> list[str]:
"""Returns a list of paths taken from the XDG_CONFIG_DIRS
and XDG_CONFIG_HOME environment varibables if they exist
"""
paths = []
if "XDG_CONFIG_HOME" in os.environ:
paths.append(os.environ["XDG_CONFIG_HOME"])
if "XDG_CONFIG_DIRS" in os.environ:
paths.extend(os.environ["XDG_CONFIG_DIRS"].split(":"))
else:
paths.append("/etc/xdg")
paths.append("/etc")
return paths
def config_dirs() -> list[str]:
"""Return a platform-specific list of candidates for user
configuration directories on the system.
The candidates are in order of priority, from highest to lowest. The
last element is the "fallback" location to be used when no
higher-priority config file exists.
"""
paths = []
if platform.system() == "Darwin":
paths.append(UNIX_DIR_FALLBACK)
paths.append(MAC_DIR)
paths.extend(xdg_config_dirs())
elif platform.system() == "Windows":
paths.append(WINDOWS_DIR_FALLBACK)
if WINDOWS_DIR_VAR in os.environ:
paths.append(os.environ[WINDOWS_DIR_VAR])
else:
# Assume Unix.
paths.append(UNIX_DIR_FALLBACK)
paths.extend(xdg_config_dirs())
# Expand and deduplicate paths.
out = []
for path in paths:
path = os.path.abspath(os.path.expanduser(path))
if path not in out:
out.append(path)
return out
beetbox-confuse-cc1e751/confuse/yaml_util.py 0000664 0000000 0000000 00000021274 15136363550 0021244 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from collections import OrderedDict
from typing import TYPE_CHECKING, Any
import yaml
if TYPE_CHECKING:
from collections.abc import Hashable, Iterable, Iterator
from _typeshed import SupportsItems
from .exceptions import ConfigReadError
# YAML loading.
class Loader(yaml.SafeLoader):
"""A customized YAML loader. This loader deviates from the official
YAML spec in a few convenient ways:
- All strings as are Unicode objects.
- All maps are OrderedDicts.
- Strings can begin with % without quotation.
"""
# All strings should be Unicode objects, regardless of contents.
def _construct_unicode(self, node: yaml.ScalarNode) -> str:
return self.construct_scalar(node)
# Use ordered dictionaries for every YAML map.
# From https://gist.github.com/844388
def construct_yaml_map(
self, node: yaml.MappingNode
) -> Iterator[OrderedDict[object, object]]:
data: OrderedDict[object, object] = OrderedDict()
yield data
value = self.construct_mapping(node)
data.update(value)
def construct_mapping(
self, node: yaml.MappingNode, deep: bool = False
) -> dict[Hashable, Any]:
if isinstance(node, yaml.MappingNode):
self.flatten_mapping(node)
else:
raise yaml.constructor.ConstructorError(
None,
None,
f"expected a mapping node, but found {node.id}",
node.start_mark,
)
mapping: OrderedDict[object, object] = OrderedDict()
for key_node, value_node in node.value:
key = self.construct_object(key_node, deep=deep)
try:
hash(key)
except TypeError as exc:
raise yaml.constructor.ConstructorError(
"while constructing a mapping",
node.start_mark,
f"found unacceptable key ({exc})",
key_node.start_mark,
)
value = self.construct_object(value_node, deep=deep)
mapping[key] = value
return mapping
# Allow bare strings to begin with %. Directives are still detected.
def check_plain(self) -> bool:
plain = super().check_plain()
return plain or self.peek() == "%" # type: ignore[no-any-return]
@staticmethod
def add_constructors(loader: type[yaml.SafeLoader]) -> None:
"""Modify a PyYAML Loader class to add extra constructors for strings
and maps. Call this method on a custom Loader class to make it behave
like Confuse's own Loader
"""
loader.add_constructor("tag:yaml.org,2002:str", Loader._construct_unicode)
loader.add_constructor("tag:yaml.org,2002:map", Loader.construct_yaml_map)
loader.add_constructor("tag:yaml.org,2002:omap", Loader.construct_yaml_map)
Loader.add_constructors(Loader)
def load_yaml(filename: str, loader: type[yaml.SafeLoader] = Loader) -> Any:
"""Read a YAML document from a file. If the file cannot be read or
parsed, a ConfigReadError is raised.
loader is the PyYAML Loader class to use to parse the YAML. By default,
this is Confuse's own Loader class, which is like SafeLoader with
extra constructors.
"""
try:
with open(filename, "rb") as f:
return yaml.load(f, Loader=loader)
except (OSError, yaml.error.YAMLError) as exc:
raise ConfigReadError(filename, exc)
def load_yaml_string(
yaml_string: str | bytes, name: str, loader: type[yaml.SafeLoader] = Loader
) -> Any:
"""Read a YAML document from a string. If the string cannot be parsed,
a ConfigReadError is raised.
`yaml_string` is a string to be parsed as a YAML document.
`name` is the name to use in error messages.
`loader` is the PyYAML Loader class to use to parse the YAML. By default,
this is Confuse's own Loader class, which is like SafeLoader with
extra constructors.
"""
try:
return yaml.load(yaml_string, Loader=loader)
except yaml.error.YAMLError as exc:
raise ConfigReadError(name, exc)
def parse_as_scalar(value: object, loader: type[yaml.SafeLoader] = Loader) -> object:
"""Parse a value as if it were a YAML scalar to perform type conversion
that is consistent with YAML documents.
`value` should be a string. Non-string inputs or strings that raise YAML
errors will be returned unchanged.
`Loader` is the PyYAML Loader class to use for parsing, defaulting to
Confuse's own Loader class.
Examples with the default Loader:
- '1' will return 1 as an integer
- '1.0' will return 1 as a float
- 'true' will return True
- The empty string '' will return None
"""
# We only deal with strings
if not isinstance(value, str):
return value
try:
loader_instance = loader("")
tag = loader_instance.resolve(yaml.ScalarNode, value, (True, False))
node = yaml.ScalarNode(tag, value)
return loader_instance.construct_object(node)
except yaml.error.YAMLError:
# Fallback to returning the value unchanged
return value
# YAML dumping.
class Dumper(yaml.SafeDumper):
"""A PyYAML Dumper that represents OrderedDicts as ordinary mappings
(in order, of course).
"""
# From http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py
def represent_mapping(
self,
tag: str,
mapping: SupportsItems[Any, Any] | Iterable[tuple[Any, Any]],
flow_style: bool | None = None,
) -> yaml.MappingNode:
value: list[tuple[yaml.Node, yaml.Node]] = []
node = yaml.MappingNode(tag, value, flow_style=flow_style)
if self.alias_key is not None:
self.represented_objects[self.alias_key] = node
best_style = False
if hasattr(mapping, "items"):
mapping = list(mapping.items())
for item_key, item_value in mapping:
node_key = self.represent_data(item_key)
node_value = self.represent_data(item_value)
if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style):
best_style = False
if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style):
best_style = False
value.append((node_key, node_value))
if flow_style is None:
if self.default_flow_style is not None:
node.flow_style = self.default_flow_style
else:
node.flow_style = best_style
return node
def represent_list(self, data: Iterable[Any]) -> yaml.SequenceNode:
"""If a list has less than 4 items, represent it in inline style
(i.e. comma separated, within square brackets).
"""
node = super().represent_list(data)
if self.default_flow_style is None and len(list(data)) < 4:
node.flow_style = True
elif self.default_flow_style is None:
node.flow_style = False
return node
def represent_bool(self, data: bool) -> yaml.ScalarNode:
"""Represent bool as 'yes' or 'no' instead of 'true' or 'false'."""
if data:
value = "yes"
else:
value = "no"
return self.represent_scalar("tag:yaml.org,2002:bool", value)
def represent_none(self, data: Any) -> yaml.ScalarNode:
"""Represent a None value with nothing instead of 'none'."""
return self.represent_scalar("tag:yaml.org,2002:null", "")
Dumper.add_representer(OrderedDict, Dumper.represent_dict)
Dumper.add_representer(bool, Dumper.represent_bool)
Dumper.add_representer(type(None), Dumper.represent_none)
Dumper.add_representer(list, Dumper.represent_list)
def restore_yaml_comments(data: str, default_data: str) -> str:
"""Scan default_data for comments (we include empty lines in our
definition of comments) and place them before the same keys in data.
Only works with comments that are on one or more own lines, i.e.
not next to a yaml mapping.
"""
comment_map = dict()
default_lines = iter(default_data.splitlines())
for line in default_lines:
if not line:
comment = "\n"
elif line.startswith("#"):
comment = f"{line}\n"
else:
continue
while True:
line = next(default_lines)
if line and not line.startswith("#"):
break
comment += f"{line}\n"
key = line.split(":")[0].strip()
comment_map[key] = comment
out_lines = iter(data.splitlines())
out_data = ""
for line in out_lines:
key = line.split(":")[0].strip()
if key in comment_map:
out_data += comment_map[key]
out_data += f"{line}\n"
return out_data
beetbox-confuse-cc1e751/docs/ 0000775 0000000 0000000 00000000000 15136363550 0016153 5 ustar 00root root 0000000 0000000 beetbox-confuse-cc1e751/docs/Makefile 0000664 0000000 0000000 00000011306 15136363550 0017614 0 ustar 00root root 0000000 0000000 # Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
SOURCEDIR = .
# When both are available, use Sphinx 2.x for autodoc compatibility.
ifeq ($(shell which sphinx-build2 >/dev/null 2>&1 ; echo $$?),0)
SPHINXBUILD = sphinx-build2
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest auto
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/* $(SOURCEDIR)/api/generated/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/beets.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/beets.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/beets"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/beets"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
make -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
beetbox-confuse-cc1e751/docs/api.rst 0000664 0000000 0000000 00000001470 15136363550 0017460 0 ustar 00root root 0000000 0000000 API Documentation
=================
This part of the documentation covers the interfaces used to develop with
``confuse``.
Core
----
.. automodule:: confuse.core
:members:
:private-members:
:show-inheritance:
Exceptions
----------
.. automodule:: confuse.exceptions
:members:
:private-members:
:show-inheritance:
Sources
-------
.. automodule:: confuse.sources
:members:
:private-members:
:show-inheritance:
Templates
---------
.. automodule:: confuse.templates
:members:
:private-members:
:special-members: __init__
:show-inheritance:
Utility
-------
.. automodule:: confuse.util
:members:
:private-members:
:show-inheritance:
YAML Utility
------------
.. automodule:: confuse.yaml_util
:members:
:private-members:
:show-inheritance:
beetbox-confuse-cc1e751/docs/changelog.rst 0000664 0000000 0000000 00000006202 15136363550 0020634 0 ustar 00root root 0000000 0000000 Changelog
=========
v2.2.0
------
- Drop support for Python 3.9.
- Ship inline type information via a `py.typed` marker for type checkers.
- Tighten and extend type hints across the public API, templates, and sources.
- Add strict mypy configuration and align tests/examples with the new typing.
- Include `docs` and `tests` directory in source distributions.
v2.1.0
------
- Drop support for versions of Python below 3.9.
- Removed 'u' prefix from string literals for Python 3.0+ compatibility.
- Removed a number of python 2 leftovers.
- Removed deprecated `pkgutil.get_loader` usage in favor of
`importlib..util.find_spec` for better compatibility with modern Python
versions.
- Added typehints to `as_*` functions which allows for enhanced type checking
and IDE support.
- Added a minimal release workflow for GitHub Actions to automate the release
process.
- Added support for Python 3.13 and Python 3.14.
- Modernized package and tests setup.
v2.0.1
------
- Remove a `<4` Python version requirement bound.
v2.0.0
------
- Drop support for versions of Python below 3.6.
v1.7.0
------
- Add support for reading configuration values from environment variables (see
`EnvSource`).
- Resolve a possible race condition when creating configuration directories.
v1.6.0
------
- A new `Configuration.reload` method makes it convenient to reload and re-parse
all YAML files from the file system.
v1.5.0
------
- A new `MappingValues` template behaves like `Sequence` but for mappings with
arbitrary keys.
- A new `Optional` template allows other templates to be null.
- `Filename` templates now have an option to resolve relative to a specific
directory. Also, configuration sources now have a corresponding global option
to resolve relative to the base configuration directory instead of the
location of the specific configuration file.
- There is a better error message for `Sequence` templates when the data from
the configuration is not a sequence.
v1.4.0
------
- `pathlib.PurePath` objects can now be converted to `Path` templates.
- `AttrDict` now properly supports (over)writing attributes via dot notation.
v1.3.0
------
- Break up the `confuse` module into a package. (All names should still be
importable from `confuse`.)
- When using `None` as a template, the result is a value whose default is
`None`. Previously, this was equivalent to leaving the key off entirely, i.e.,
a template with no default. To get the same effect now, use `confuse.REQUIRED`
in the template.
v1.2.0
------
- `float` values (like ``4.2``) can now be used in templates (just like ``42``
works as an `int` template).
- The `Filename` and `Path` templates now correctly accept default values.
- It's now possible to provide custom PyYAML `Loader` objects for parsing config
files.
v1.1.0
------
- A new ``Path`` template produces a pathlib_ Path object.
- Drop support for Python 3.4 (following in the footsteps of PyYAML).
- String templates support environment variable expansion.
.. _pathlib: https://docs.python.org/3/library/pathlib.html
v1.0.0
------
The first stable release, and the first that beets_ depends on externally.
.. _beets: https://beets.io
beetbox-confuse-cc1e751/docs/conf.py 0000664 0000000 0000000 00000001631 15136363550 0017453 0 ustar 00root root 0000000 0000000 import datetime as dt
import os
import re
import sys
from pathlib import Path
MATCH_VERSION_LINE = re.compile(r"version = \W((\d+\.\d+)\.\d.*?)\W").fullmatch
pyproject = Path(__file__).parent.parent / "pyproject.toml"
version_line_match = next(
filter(None, map(MATCH_VERSION_LINE, pyproject.read_text().splitlines()))
)
release, version = version_line_match.groups()
sys.path.insert(0, os.path.abspath(".."))
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
"sphinx.ext.autosectionlabel",
"sphinx_rtd_theme",
]
source_suffix = ".rst"
master_doc = "index"
project = "Confuse"
copyright = f"2012-{dt.date.today().year}, Adrian Sampson & contributors"
exclude_patterns = ["_build"]
pygments_style = "sphinx"
# -- Options for HTML output --------------------------------------------------
html_theme = "sphinx_rtd_theme"
htmlhelp_basename = "Confusedoc"
beetbox-confuse-cc1e751/docs/dev.rst 0000664 0000000 0000000 00000001716 15136363550 0017470 0 ustar 00root root 0000000 0000000 Development Guide
=================
This document provides guidelines for developers working on the ``confuse``
library.
Version Bumps
-------------
This section outlines how to create a new version of the ``confuse`` library and
publish it on PyPi. The versioning follows semantic versioning principles, where
the version number is structured as ``MAJOR.MINOR.PATCH``.
To create a new version, follow these steps:
1. Navigate to `Make release
`_
action in the GitHub repository.
2. Press **Run workflow**, enter the new version number in the format
``MAJOR.MINOR.PATCH``, e.g., ``1.8.0`` and submit it.
3. Refresh the page to see the status of the workflow.
4. Once it succeeds, create a GitHub release with notes from the
``docs/changelog.rst`` file.
Note: This workflow does not update the changelog version numbers; this must be
done manually before running the release workflow.
beetbox-confuse-cc1e751/docs/examples.rst 0000664 0000000 0000000 00000055767 15136363550 0020547 0 ustar 00root root 0000000 0000000 Template Examples
=================
These examples demonstrate how the confuse templates work to validate
configuration values.
Sequence
--------
A ``Sequence`` template allows validation of a sequence of configuration items
that all must match a subtemplate. The items in the sequence can be simple
values or more complex objects, as defined by the subtemplate. When the view is
defined in multiple sources, the highest priority source will override the
entire list of items, rather than appending new items to the list from lower
sources. If the view is not defined in any source of the configuration, an empty
list will be returned.
As an example of using the ``Sequence`` template, consider a configuration that
includes a list of servers, where each server is required to have a host string
and an optional port number that defaults to 80. For this example, an initial
configuration file named ``servers_example.yaml`` has the following contents:
.. code-block:: yaml
servers:
- host: one.example.com
- host: two.example.com
port: 8000
- host: three.example.com
port: 8080
Validation of this configuration could be performed like this:
>>> import confuse
>>> import pprint
>>> source = confuse.YamlSource("servers_example.yaml")
>>> config = confuse.RootView([source])
>>> template = {
... "servers": confuse.Sequence(
... {
... "host": str,
... "port": 80,
... }
... ),
... }
>>> valid_config = config.get(template)
>>> pprint.pprint(valid_config)
{'servers': [{'host': 'one.example.com', 'port': 80},
{'host': 'two.example.com', 'port': 8000},
{'host': 'three.example.com', 'port': 8080}]}
The list of items in the initial configuration can be overridden by setting a
higher priority source. Continuing the previous example:
>>> config.set(
... {
... "servers": [
... {"host": "four.example.org"},
... {"host": "five.example.org", "port": 9000},
... ],
... }
... )
>>> updated_config = config.get(template)
>>> pprint.pprint(updated_config)
{'servers': [{'host': 'four.example.org', 'port': 80},
{'host': 'five.example.org', 'port': 9000}]}
If the requested view is missing, ``Sequence`` returns an empty list:
>>> config.clear()
>>> config.get(template)
{'servers': []}
However, if an item within the sequence does not match the subtemplate provided
to ``Sequence``, then an error will be raised:
>>> config.set(
... {"servers": [{"host": "bad_port.example.net", "port": "default"}]}
... )
>>> try:
... config.get(template)
... except confuse.ConfigError as err:
... print(err)
servers#0.port: must be a number
.. note::
A python list is not the shortcut for defining a ``Sequence`` template but
will instead produce a ``OneOf`` template. For example,
``config.get([str])`` is equivalent to ``config.get(confuse.OneOf([str]))``
and *not* ``config.get(confuse.Sequence(str))``.
MappingValues
-------------
A ``MappingValues`` template allows validation of a mapping of configuration
items where the keys can be arbitrary but all the values need to match a
subtemplate. Use cases include simple user-defined key:value pairs or larger
configuration blocks that all follow the same structure, but where the keys
naming each block are user-defined. In addition, individual items in the mapping
can be overridden and new items can be added by higher priority configuration
sources. This is in contrast to the ``Sequence`` template, in which a higher
priority source overrides the entire list of configuration items provided by a
lower source.
In the following example, a hypothetical todo list program can be configured
with user-defined colors and category labels. Colors are required to be in hex
format. For each category, a description is required and a priority level is
optional, with a default value of 0. An initial configuration file named
``todo_example.yaml`` has the following contents:
.. code-block:: yaml
colors:
red: '#FF0000'
green: '#00FF00'
blue: '#0000FF'
categories:
default:
description: Things to do
high:
description: These are important
priority: 50
low:
description: Will get to it eventually
priority: -10
Validation of this configuration could be performed like this:
>>> import confuse
>>> import pprint
>>> source = confuse.YamlSource("todo_example.yaml")
>>> config = confuse.RootView([source])
>>> template = {
... "colors": confuse.MappingValues(
... confuse.String(pattern="#[0-9a-fA-F]{6,6}")
... ),
... "categories": confuse.MappingValues(
... {
... "description": str,
... "priority": 0,
... }
... ),
... }
>>> valid_config = config.get(template)
>>> pprint.pprint(valid_config)
{'categories': {'default': {'description': 'Things to do', 'priority': 0},
'high': {'description': 'These are important', 'priority': 50},
'low': {'description': 'Will get to it eventually',
'priority': -10}},
'colors': {'blue': '#0000FF', 'green': '#00FF00', 'red': '#FF0000'}}
Items in the initial configuration can be overridden and the mapping can be
extended by setting a higher priority source. Continuing the previous example:
>>> config.set(
... {
... "colors": {
... "green": "#008000",
... "orange": "#FFA500",
... },
... "categories": {
... "urgent": {
... "description": "Must get done now",
... "priority": 100,
... },
... "high": {
... "description": "Important, but not urgent",
... "priority": 20,
... },
... },
... }
... )
>>> updated_config = config.get(template)
>>> pprint.pprint(updated_config)
{'categories': {'default': {'description': 'Things to do', 'priority': 0},
'high': {'description': 'Important, but not urgent',
'priority': 20},
'low': {'description': 'Will get to it eventually',
'priority': -10},
'urgent': {'description': 'Must get done now',
'priority': 100}},
'colors': {'blue': '#0000FF',
'green': '#008000',
'orange': '#FFA500',
'red': '#FF0000'}}
If the requested view is missing, ``MappingValues`` returns an empty dict:
>>> config.clear()
>>> config.get(template)
{'colors': {}, 'categories': {}}
However, if an item within the mapping does not match the subtemplate provided
to ``MappingValues``, then an error will be raised:
>>> config.set(
... {
... "categories": {
... "no_description": {
... "priority": 10,
... },
... },
... }
... )
>>> try:
... config.get(template)
... except confuse.ConfigError as err:
... print(err)
categories.no_description.description not found
Filename
--------
A ``Filename`` template validates a string as a filename, which is normalized
and returned as an absolute, tilde-free path. By default, relative path values
that are provided in config files are resolved relative to the application's
configuration directory, as returned by ``Configuration.config_dir()``, while
relative paths from command-line options are resolved from the current working
directory. However, these default relative path behaviors can be changed using
the ``cwd``, ``relative_to``, ``in_app_dir``, or ``in_source_dir`` parameters to
the ``Filename`` template. In addition, relative path resolution for an entire
source file can be changed by creating a ``ConfigSource`` with the
``base_for_paths`` parameter set to True. Setting the behavior at the
source-level can be useful when all ``Filename`` templates should be relative to
the source. The template-level parameters provide more fine-grained control.
While the directory used for resolving relative paths can be controlled, the
``Filename`` template should not be used to guarantee that a file is contained
within a given directory, because an absolute path may be provided and will not
be subject to resolution. In addition, ``Filename`` validation only ensures that
the filename is a valid path on the platform where the application is running,
not that the file or any parent directories exist or could be created.
.. note::
Running the example below will create the application config directory
``~/.config/ExampleApp/`` on MacOS and Unix machines or
``%APPDATA%\ExampleApp\`` on Windows machines. The filenames in the sample
output will also be different on your own machine because the paths to the
config files and the current working directory will be different.
For this example, we will validate a configuration with filenames that should be
resolved as follows:
- ``library``: a filename that should always be resolved relative to the
application's config directory
- ``media_dir``: a directory that should always be resolved relative to the
source config file that provides that value
- ``photo_dir`` and ``video_dir``: subdirectories that should be resolved
relative of the value of ``media_dir``
- ``temp_dir``: a directory that should be resolved relative to ``/tmp/``
- ``log``: a filename that follows the default ``Filename`` template behavior
The initial user config file will be at ``~/.config/ExampleApp/config.yaml``,
where it will be discovered automatically using the :ref:`Search Paths`, and has
the following contents:
.. code-block:: yaml
library: library.db
media_dir: media
photo_dir: my_photos
video_dir: my_videos
temp_dir: example_tmp
log: example.log
Validation of this initial user configuration could be performed as follows:
>>> import confuse
>>> import pprint
>>> config = confuse.Configuration("ExampleApp", __name__) # Loads user config
>>> print(config.config_dir()) # Application config directory
/home/user/.config/ExampleApp
>>> template = {
... "library": confuse.Filename(in_app_dir=True),
... "media_dir": confuse.Filename(in_source_dir=True),
... "photo_dir": confuse.Filename(relative_to="media_dir"),
... "video_dir": confuse.Filename(relative_to="media_dir"),
... "temp_dir": confuse.Filename(cwd="/tmp"),
... "log": confuse.Filename(),
... }
>>> valid_config = config.get(template)
>>> pprint.pprint(valid_config)
{'library': '/home/user/.config/ExampleApp/library.db',
'log': '/home/user/.config/ExampleApp/example.log',
'media_dir': '/home/user/.config/ExampleApp/media',
'photo_dir': '/home/user/.config/ExampleApp/media/my_photos',
'temp_dir': '/tmp/example_tmp',
'video_dir': '/home/user/.config/ExampleApp/media/my_videos'}
Because the user configuration file ``config.yaml`` was in the application's
configuration directory of ``/home/user/.config/ExampleApp/``, all of the
filenames are below ``/home/user/.config/ExampleApp/`` except for ``temp_dir``,
whose template used the ``cwd`` parameter. However, if the following YAML file
is then loaded from ``/var/tmp/example/config.yaml`` as a higher-level source,
some of the paths will no longer be relative to the application config
directory:
.. code-block:: yaml
library: new_library.db
media_dir: new_media
photo_dir: new_photos
# video_dir: my_videos # Not overridden
temp_dir: ./new_example_tmp
log: new_example.log
Continuing the example code from above:
>>> config.set_file("/var/tmp/example/config.yaml")
>>> updated_config = config.get(template)
>>> pprint.pprint(updated_config)
{'library': '/home/user/.config/ExampleApp/new_library.db',
'log': '/home/user/.config/ExampleApp/new_example.log',
'media_dir': '/var/tmp/example/new_media',
'photo_dir': '/var/tmp/example/new_media/new_photos',
'temp_dir': '/tmp/new_example_tmp',
'video_dir': '/var/tmp/example/new_media/my_videos'}
Now, the ``media_dir`` and its subdirectories are relative to the directory
containing the new source file, because the ``media_dir`` template used the
``in_source_dir`` parameter. However, ``log`` remains in the application config
directory because it uses the default ``Filename`` template behavior. The base
directories for the ``library`` and ``temp_dir`` items are also not affected.
If the previous YAML file is instead loaded with the ``base_for_paths``
parameter set to True, then a default ``Filename`` template will use that config
file's directory as the base for resolving relative paths:
>>> config.set_file("/var/tmp/example/config.yaml", base_for_paths=True)
>>> updated_config = config.get(template)
>>> pprint.pprint(updated_config)
{'library': '/home/user/.config/ExampleApp/new_library.db',
'log': '/var/tmp/example/new_example.log',
'media_dir': '/var/tmp/example/new_media',
'photo_dir': '/var/tmp/example/new_media/new_photos',
'temp_dir': '/tmp/new_example_tmp',
'video_dir': '/var/tmp/example/new_media/my_videos'}
The filename for ``log`` is now within the directory containing the new source
file. However, the directory for the ``library`` file has not changed since its
template uses the ``in_app_dir`` parameter, which takes precedence over the
source's ``base_for_paths`` setting. The template-level ``cwd`` parameter, used
with ``temp_dir``, also takes precedence over the source setting.
For configuration values set from command-line options, relative paths will be
resolved from the current working directory by default, but the ``cwd``,
``relative_to``, and ``in_app_dir`` template parameters alter that behavior.
Continuing the example code from above, command-line options are mimicked here
by splitting a mock command line string and parsing it with ``argparse``:
>>> import os
>>> print(os.getcwd()) # Current working directory
/home/user
>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument("--library")
>>> parser.add_argument("--media_dir")
>>> parser.add_argument("--photo_dir")
>>> parser.add_argument("--temp_dir")
>>> parser.add_argument("--log")
>>> cmd_line = (
... "--library cmd_line_library --media_dir cmd_line_media "
... "--photo_dir cmd_line_photo --temp_dir cmd_line_tmp "
... "--log cmd_line_log"
... )
>>> args = parser.parse_args(cmd_line.split())
>>> config.set_args(args)
>>> config_with_cmdline = config.get(template)
>>> pprint.pprint(config_with_cmdline)
{'library': '/home/user/.config/ExampleApp/cmd_line_library',
'log': '/home/user/cmd_line_log',
'media_dir': '/home/user/cmd_line_media',
'photo_dir': '/home/user/cmd_line_media/cmd_line_photo',
'temp_dir': '/tmp/cmd_line_tmp',
'video_dir': '/home/user/cmd_line_media/my_videos'}
Now the ``log`` and ``media_dir`` paths are relative to the current working
directory of ``/home/user``, while the ``photo_dir`` and ``video_dir`` paths
remain relative to the updated ``media_dir`` path. The ``library`` and
``temp_dir`` paths are still resolved as before, because those templates used
``in_app_dir`` and ``cwd``, respectively.
If a configuration value is provided as an absolute path, the path will be
normalized but otherwise unchanged. Here is an example of overridding earlier
values with absolute paths:
>>> config.set(
... {
... "library": "~/home_library.db",
... "media_dir": "/media",
... "video_dir": "/video_not_under_media",
... "temp_dir": "/var/./remove_me/..//tmp",
... "log": "/var/log/example.log",
... }
... )
>>> absolute_config = config.get(template)
>>> pprint.pprint(absolute_config)
{'library': '/home/user/home_library.db',
'log': '/var/log/example.log',
'media_dir': '/media',
'photo_dir': '/media/cmd_line_photo',
'temp_dir': '/var/tmp',
'video_dir': '/video_not_under_media'}
The paths for ``library`` and ``temp_dir`` have been normalized, but are not
impacted by their template parameters. Since ``photo_dir`` was not overridden,
the previous relative path value is now being resolved from the new
``media_dir`` absolute path. However, the ``video_dir`` was set to an absolute
path and is no longer a subdirectory of ``media_dir``.
Path
----
A ``Path`` template works the same as a ``Filename`` template, but returns a
``pathlib.Path`` object instead of a string. Using the same initial example as
above for ``Filename`` but with ``Path`` templates gives the following:
>>> import confuse
>>> import pprint
>>> config = confuse.Configuration("ExampleApp", __name__)
>>> print(config.config_dir()) # Application config directory
/home/user/.config/ExampleApp
>>> template = {
... "library": confuse.Path(in_app_dir=True),
... "media_dir": confuse.Path(in_source_dir=True),
... "photo_dir": confuse.Path(relative_to="media_dir"),
... "video_dir": confuse.Path(relative_to="media_dir"),
... "temp_dir": confuse.Path(cwd="/tmp"),
... "log": confuse.Path(),
... }
>>> valid_config = config.get(template)
>>> pprint.pprint(valid_config)
{'library': PosixPath('/home/user/.config/ExampleApp/library.db'),
'log': PosixPath('/home/user/.config/ExampleApp/example.log'),
'media_dir': PosixPath('/home/user/.config/ExampleApp/media'),
'photo_dir': PosixPath('/home/user/.config/ExampleApp/media/my_photos'),
'temp_dir': PosixPath('/tmp/example_tmp'),
'video_dir': PosixPath('/home/user/.config/ExampleApp/media/my_videos')}
Optional
--------
While many templates like ``Integer`` and ``String`` can be configured to return
a default value if the requested view is missing, validation with these
templates will fail if the value is left blank in the YAML file or explicitly
set to ``null`` in YAML (ie, ``None`` in python). The ``Optional`` template can
be used with other templates to allow its subtemplate to accept ``null`` as
valid and return a default value. The default behavior of ``Optional`` allows
the requested view to be missing, but this behavior can be changed by passing
``allow_missing=False``, in which case the view must be present but its value
can still be ``null``. In all cases, any value other than ``null`` will be
passed to the subtemplate for validation, and an appropriate ``ConfigError``
will be raised if validation fails. ``Optional`` can also be used with more
complex templates like ``MappingTemplate`` to make entire sections of the
configuration optional.
Consider a configuration where ``log`` can be set to a filename to enable
logging to that file or set to ``null`` or not included in the configuration to
indicate logging to the console. All of the following are valid configurations
using the ``Optional`` template with ``Filename`` as the subtemplate:
>>> import sys
>>> import confuse
>>> def get_log_output(config):
... output = config["log"].get(confuse.Optional(confuse.Filename()))
... if output is None:
... return sys.stderr
... return output
>>> config = confuse.RootView([])
>>> config.set({"log": "/tmp/log.txt"}) # `log` set to a filename
>>> get_log_output(config)
'/tmp/log.txt'
>>> config.set({"log": None}) # `log` set to None (ie, null in YAML)
>>> get_log_output(config)
<_io.TextIOWrapper name='' mode='w' encoding='UTF-8'>
>>> config.clear() # Clear config so that `log` is missing
>>> get_log_output(config)
<_io.TextIOWrapper name='' mode='w' encoding='UTF-8'>
However, validation will still fail with ``Optional`` if a value is given that
is invalid for the subtemplate:
>>> config.set({"log": True})
>>> try:
... get_log_output(config)
... except confuse.ConfigError as err:
... print(err)
log: must be a filename, not bool
And without wrapping the ``Filename`` subtemplate in ``Optional``, ``null``
values are not valid:
>>> config.set({"log": None})
>>> try:
... config["log"].get(confuse.Filename())
... except confuse.ConfigError as err:
... print(err)
log: must be a filename, not NoneType
If a program wants to require an item to be present in the configuration, while
still allowing ``null`` to be valid, pass ``allow_missing=False`` when creating
the ``Optional`` template:
>>> def get_log_output_no_missing(config):
... output = config["log"].get(
... confuse.Optional(confuse.Filename(), allow_missing=False)
... )
... if output is None:
... return sys.stderr
... return output
>>> config.set({"log": None}) # `log` set to None is still OK...
>>> get_log_output_no_missing(config)
<_io.TextIOWrapper name='' mode='w' encoding='UTF-8'>
>>> config.clear() # but `log` missing now raises an error
>>> try:
... get_log_output_no_missing(config)
... except confuse.ConfigError as err:
... print(err)
log not found
The default value returned by ``Optional`` can be set explicitly by passing a
value to its ``default`` parameter. However, if no explicit default is passed to
``Optional`` and the subtemplate has a default value defined, then ``Optional``
will return the subtemplate's default value. For subtemplates that do not define
default values, like ``MappingTemplate``, ``None`` will be returned as the
default unless an explicit default is provided.
In the following example, ``Optional`` is used to make an ``Integer`` template
more lenient, allowing blank values to validate. In addition, the entire
``extra_config`` block can be left out without causing validation errors. If we
have a file named ``optional.yaml`` with the following contents:
.. code-block:: yaml
favorite_number: # No favorite number provided, but that's OK
# This part of the configuration is optional. Uncomment to include.
# extra_config:
# fruit: apple
# number: 10
Then the configuration can be validated as follows:
>>> import confuse
>>> source = confuse.YamlSource("optional.yaml")
>>> config = confuse.RootView([source])
>>> config["favorite_number"].get(confuse.Optional(5))
5
>>> config["favorite_number"].get(confuse.Optional(confuse.Integer(5)))
5
>>> config["favorite_number"].get(confuse.Optional(int, default=5))
5
>>> config["favorite_number"].get(confuse.Optional(5, default="five"))
'five'
>>> extra_config = config["extra_config"].get(
... confuse.Optional(
... {"fruit": str, "number": int},
... )
... )
>>> print(extra_config is None)
True
>>> config["extra_config"].get(
... confuse.Optional(
... {"fruit": str, "number": int},
... default={},
... )
... )
{}
>>> config["extra_config"].get(
... confuse.Optional(
... {"fruit": str, "number": int},
... default={"fruit": "orange", "number": 3},
... )
... )
{'fruit': 'orange', 'number': 3}
Without the ``Optional`` template wrapping the ``Integer``, the blank value in
the YAML file will cause an error:
>>> try:
... config["favorite_number"].get(5)
... except confuse.ConfigError as err:
... print(err)
favorite_number: must be a number
If the ``extra_config`` for this example configuration is supplied, it must
still match the subtemplate. Therefore, this will fail:
>>> config.set({"extra_config": {}})
>>> try:
... config["extra_config"].get(
... confuse.Optional(
... {"fruit": str, "number": int},
... )
... )
... except confuse.ConfigError as err:
... print(err)
extra_config.fruit not found
But this override of the example configuration will validate:
>>> config.set({"extra_config": {"fruit": "banana", "number": 1}})
>>> config["extra_config"].get(
... confuse.Optional(
... {"fruit": str, "number": int},
... )
... )
{'fruit': 'banana', 'number': 1}
beetbox-confuse-cc1e751/docs/index.rst 0000664 0000000 0000000 00000000175 15136363550 0020017 0 ustar 00root root 0000000 0000000 .. include:: ../README.rst
.. toctree::
:maxdepth: 3
:hidden:
usage
examples
changelog
api
dev
beetbox-confuse-cc1e751/docs/usage.rst 0000664 0000000 0000000 00000052245 15136363550 0020021 0 ustar 00root root 0000000 0000000 Confuse: Painless Configuration
===============================
Confuse_ is a straightforward, full-featured configuration system for Python.
.. _confuse: https://github.com/beetbox/confuse
Basic Usage
-----------
Set up your Configuration object, which provides unified access to all of your
application's config settings:
.. code-block:: python
config = confuse.Configuration("MyGreatApp", __name__)
The first parameter is required; it's the name of your application, which will
be used to search the system for a config file named ``config.yaml``. See
:ref:`Search Paths` for the specific locations searched.
The second parameter is optional: it's the name of a module that will guide the
search for a *defaults* file. Use this if you want to include a
``config_default.yaml`` file inside your package. (The included ``example``
package does exactly this.)
Now, you can access your configuration data as if it were a simple structure
consisting of nested dicts and lists—except that you need to call the method
``.get()`` on the leaf of this tree to get the result as a value:
.. code-block:: python
value = config["foo"][2]["bar"].get()
Under the hood, accessing items in your configuration tree builds up a *view*
into your app's configuration. Then, ``get()`` flattens this view into a value,
performing a search through each configuration data source to find an answer.
(More on views later.)
If you know that a configuration value should have a specific type, just pass
that type to ``get()``:
.. code-block:: python
int_value = config["number_of_goats"].get(int)
This way, Confuse will either give you an integer or raise a ``ConfigTypeError``
if the user has messed up the configuration. You're safe to assume after this
call that ``int_value`` has the right type. If the key doesn't exist in any
configuration file, Confuse will raise a ``NotFoundError``. Together, catching
these exceptions (both subclasses of ``confuse.ConfigError``) lets you
painlessly validate the user's configuration as you go.
View Theory
-----------
The Confuse API is based on the concept of *views*. You can think of a view as a
*place to look* in a config file: for example, one view might say *get the value
for key ``number_of_goats``*. Another might say *get the value at index 8 inside
the sequence for key ``animal_counts``*. To get the value for a given view, you
*resolve* it by calling the ``get()`` method.
This concept separates the specification of a location from the mechanism for
retrieving data from a location. (In this sense, it's a little like XPath_: you
specify a path to data you want and *then* you retrieve it.)
Using views, you can write ``config['animal_counts'][8]`` and know that no
exceptions will be raised until you call ``get()``, even if the
``animal_counts`` key does not exist. More importantly, it lets you write a
single expression to search many different data sources without preemptively
merging all sources together into a single data structure.
Views also solve an important problem with overriding collections. Imagine, for
example, that you have a dictionary called ``deliciousness`` in your config file
that maps food names to tastiness ratings. If the default configuration gives
carrots a rating of 8 and the user's config rates them a 10, then clearly
``config['deliciousness']['carrots'].get()`` should return 10. But what if the
two data sources have different sets of vegetables? If the user provides a value
for broccoli and zucchini but not carrots, should carrots have a default
deliciousness value of 8 or should Confuse just throw an exception? With
Confuse's views, the application gets to decide.
The above expression, ``config['deliciousness']['carrots'].get()``, returns 8
(falling back on the default). However, you can also write
``config['deliciousness'].get()``. This expression will cause the *entire*
user-specified mapping to override the default one, providing a dict object like
``{'broccoli': 7, 'zucchini': 9}``. As a rule, then, resolve a view at the same
granularity you want config files to override each other.
.. warning::
It may appear that calling ``config.get()`` would retrieve the entire
configuration at once. However, this will return only the *highest-priority*
configuration source, masking any lower-priority values for keys that are
not present in the top source. This pitfall is especially likely when using
:ref:`Command-Line Options` or :ref:`Environment Variables`, which may place
an empty configuration at the top of the stack. A subsequent call to
``config.get()`` might then return no configuration at all.
.. _xpath: https://www.w3.org/TR/xpath/
Validation
----------
We saw above that you can easily assert that a configuration value has a certain
type by passing that type to ``get()``. But sometimes you need to do more than
just type checking. For this reason, Confuse provides a few methods on views
that perform fancier validation or even conversion:
- ``as_filename()``: Normalize a filename, substituting tildes and
absolute-ifying relative paths. For filenames defined in a config file, by
default the filename is relative to the application's config directory
(``Configuration.config_dir()``, as described below). However, if the config
file was loaded with the ``base_for_paths`` parameter set to ``True`` (see
:ref:`Manually Specifying Config Files`), then a relative path refers to the
directory containing the config file. A relative path from any other source
(e.g., command-line options) is relative to the working directory. For full
control over relative path resolution, use the ``Filename`` template directly
(see :ref:`Filename`).
- ``as_choice(choices)``: Check that a value is one of the provided choices. The
argument should be a sequence of possible values. If the sequence is a
``dict``, then this method returns the associated value instead of the key.
- ``as_number()``: Raise an exception unless the value is of a numeric type.
- ``as_pairs()``: Get a collection as a list of pairs. The collection should be
a list of elements that are either pairs (i.e., two-element lists) already or
single-entry dicts. This can be helpful because, in YAML, lists of
single-element mappings have a simple syntax (``- key: value``) and, unlike
real mappings, preserve order.
- ``as_str_seq()``: Given either a string or a list of strings, return a list of
strings. A single string is split on whitespace.
- ``as_str_expanded()``: Expand any environment variables contained in a string
using `os.path.expandvars()`_.
.. _os.path.expandvars(): https://docs.python.org/3/library/os.path.html#os.path.expandvars
For example, ``config['path'].as_filename()`` ensures that you get a reasonable
filename string from the configuration. And calling
``config['direction'].as_choice(['up', 'down'])`` will raise a
``ConfigValueError`` unless the ``direction`` value is either "up" or "down".
Command-Line Options
--------------------
Arguments to command-line programs can be seen as just another *source* for
configuration options. Just as options in a user-specific configuration file
should override those from a system-wide config, command-line options should
take priority over all configuration files.
You can use the argparse_ and optparse_ modules from the standard library with
Confuse to accomplish this. Just call the ``set_args`` method on any view and
pass in the object returned by the command-line parsing library. Values from the
command-line option namespace object will be added to the overlay for the view
in question. For example, with argparse:
.. code-block:: python
args = parser.parse_args()
config.set_args(args)
Correspondingly, with optparse:
.. code-block:: python
options, args = parser.parse_args()
config.set_args(options)
This call will turn all of the command-line options into a top-level source in
your configuration. The key associated with each option in the parser will
become a key available in your configuration. For example, consider this
argparse script:
.. code-block:: python
config = confuse.Configuration("myapp")
parser = argparse.ArgumentParser()
parser.add_argument("--foo", help="a parameter")
args = parser.parse_args()
config.set_args(args)
print(config["foo"].get())
This will allow the user to override the configured value for key ``foo`` by
passing ``--foo `` on the command line.
Overriding nested values can be accomplished by passing `dots=True` and have
dot-delimited properties on the incoming object.
.. code-block:: python
parser.add_argument("--bar", help="nested parameter", dest="foo.bar")
args = parser.parse_args() # args looks like: {'foo.bar': 'value'}
config.set_args(args, dots=True)
print(config["foo"]["bar"].get())
`set_args` works with generic dictionaries too.
.. code-block:: python
args = {"foo": {"bar": 1}}
config.set_args(args, dots=True)
print(config["foo"]["bar"].get())
.. _argparse: https://docs.python.org/dev/library/argparse.html
.. _optparse: https://docs.python.org/3/library/optparse.html
.. _parse_args: https://docs.python.org/library/argparse.html#the-parse-args-method
Note that, while you can use the full power of your favorite command-line
parsing library, you'll probably want to avoid specifying defaults in your
argparse or optparse setup. This way, Confuse can use other configuration
sources---possibly your ``config_default.yaml``---to fill in values for
unspecified command-line switches. Otherwise, the argparse/optparse default
value will hide options configured elsewhere.
Environment Variables
---------------------
Confuse supports using environment variables as another source to provide an
additional layer of configuration. The environment variables to include are
identified by a prefix, which defaults to the uppercased name of your
application followed by an underscore. Matching environment variable names are
first stripped of this prefix and then lowercased to determine the corresponding
configuration option. To load the environment variables for your application
using the default prefix, just call ``set_env`` on your ``Configuration``
object. Config values from the environment will then be added as an overlay at
the highest precedence. For example:
.. code-block:: sh
export MYAPP_FOO=something
.. code-block:: python
import confuse
config = confuse.Configuration("myapp", __name__)
config.set_env()
print(config["foo"].get())
Nested config values can be overridden by using a separator string in the
environment variable name. By default, double underscores are used as the
separator for nesting, to avoid clashes with config options that contain single
underscores. Note that most shells restrict environment variable names to
alphanumeric and underscore characters, so dots are not a valid separator.
.. code-block:: sh
export MYAPP_FOO__BAR=something
.. code-block:: python
import confuse
config = confuse.Configuration("myapp", __name__)
config.set_env()
print(config["foo"]["bar"].get())
Both the prefix and the separator can be customized when using ``set_env``. Note
that prefix matching is done to the environment variables *prior* to
lowercasing, while the separator is matched *after* lowercasing.
.. code-block:: sh
export APPFOO_NESTED_BAR=something
.. code-block:: python
import confuse
config = confuse.Configuration("myapp", __name__)
config.set_env(prefix="APP", sep="_nested_")
print(config["foo"]["bar"].get())
For configurations that include lists, use integers starting from 0 as nested
keys to invoke "list conversion." If any of the sibling nested keys are not
integers or the integers are not sequential starting from 0, then conversion
will not be performed. Nested lists and combinations of nested dicts and lists
are supported.
.. code-block:: sh
export MYAPP_FOO__0=first
export MYAPP_FOO__1=second
export MYAPP_FOO__2__BAR__0=nested
.. code-block:: python
import confuse
config = confuse.Configuration("myapp", __name__)
config.set_env()
print(config["foo"].get()) # ['first', 'second', {'bar': ['nested']}]
For consistency with YAML config files, the values of environment variables are
type converted using the same YAML parser used for file-based configs. This
means that numeric strings will be converted to integers or floats, "true" and
"false" will be converted to booleans, and the empty string or "null" will be
converted to ``None``. Setting an environment variable to the empty string or
"null" allows unsetting a config value from a lower-precedence source.
To change the lowercasing and list handling behaviors when loading environment
variables or to enable full YAML parsing of environment variables, you can
initialize an ``EnvSource`` configuration source directly.
If you use config overlays from both command-line args and environment
variables, the order of calls to ``set_args`` and ``set_env`` will determine the
precedence, with the last call having the highest precedence.
Search Paths
------------
Confuse looks in a number of locations for your application's configurations.
The locations are determined by the platform. For each platform, Confuse has a
list of directories in which it looks for a directory named after the
application. For example, the first search location on Unix-y systems is
``$XDG_CONFIG_HOME/AppName`` for an application called ``AppName``.
Here are the default search paths for each platform:
- macOS: ``~/.config/app`` and ``~/Library/Application Support/app``
- Other Unix: ``~/.config/app`` and ``/etc/app``
- Windows: ``%APPDATA%\app`` where the `APPDATA` environment variable falls back
to ``%HOME%\AppData\Roaming`` if undefined
Both macOS and other Unix operating sytems also try to use the
``XDG_CONFIG_HOME`` and ``XDG_CONFIG_DIRS`` environment variables if set then
search those directories as well.
Users can also add an override configuration directory with an environment
variable. The variable name is the application name in capitals with "DIR"
appended: for an application named ``AppName``, the environment variable is
``APPNAMEDIR``.
Manually Specifying Config Files
--------------------------------
You may want to leverage Confuse's features without :ref:`Search Paths`. This
can be done by manually specifying the YAML files you want to include, which
also allows changing how relative paths in the file will be resolved:
.. code-block:: python
import confuse
# Instantiates config. Confuse searches for a config_default.yaml
config = confuse.Configuration("MyGreatApp", __name__)
# Add config items from specified file. Relative path values within the
# file are resolved relative to the application's configuration directory.
config.set_file("subdirectory/default_config.yaml")
# Add config items from a second file. If some items were already defined,
# they will be overwritten (new file precedes the previous ones). With
# `base_for_paths` set to True, relative path values in this file will be
# resolved relative to the config file's directory (i.e., 'subdirectory').
config.set_file("subdirectory/local_config.yaml", base_for_paths=True)
val = config["foo"]["bar"].get(int)
Your Application Directory
--------------------------
Confuse provides a simple helper, ``Configuration.config_dir()``, that gives you
a directory used to store your application's configuration. If a configuration
file exists in any of the searched locations, then the highest-priority
directory containing a config file is used. Otherwise, a directory is created
for you and returned. So you can always expect this method to give you a
directory that actually exists.
As an example, you may want to migrate a user's settings to Confuse from an
older configuration system such as ConfigParser_. Just do something like this:
.. code-block:: python
config_filename = os.path.join(config.config_dir(), confuse.CONFIG_FILENAME)
with open(config_filename, "w") as f:
yaml.dump(migrated_config, f)
.. _configparser: https://docs.python.org/3/library/configparser.html
Dynamic Updates
---------------
Occasionally, a program will need to modify its configuration while it's
running. For example, an interactive prompt from the user might cause the
program to change a setting for the current execution only. Or the program might
need to add a *derived* configuration value that the user doesn't specify.
To facilitate this, Confuse lets you *assign* to view objects using ordinary
Python assignment. Assignment will add an overlay source that precedes all other
configuration sources in priority. Here's an example of programmatically setting
a configuration value based on a ``DEBUG`` constant:
.. code-block:: python
if DEBUG:
config["verbosity"] = 100
...
my_logger.setLevel(config["verbosity"].get(int))
This example allows the constant to override the default verbosity level, which
would otherwise come from a configuration file.
Assignment works by creating a new "source" for configuration data at the top of
the stack. This new source takes priority over all other, previously-loaded
sources. You can cause this explicitly by calling the ``set()`` method on any
view. A related method, ``add()``, works similarly but instead adds a new
*lowest-priority* source to the bottom of the stack. This can be used to provide
defaults for options that may be overridden by previously-loaded configuration
files.
YAML Tweaks
-----------
Confuse uses the PyYAML_ module to parse YAML configuration files. However, it
deviates very slightly from the official YAML specification to provide a few
niceties suited to human-written configuration files. Those tweaks are:
.. _pyyaml: https://pyyaml.org/
- All strings are returned as Python Unicode objects.
- YAML maps are parsed as Python OrderedDict_ objects. This means that you can
recover the order that the user wrote down a dictionary.
- Bare strings can begin with the % character. In stock PyYAML, this will throw
a parse error.
.. _ordereddict: https://docs.python.org/2/library/collections.html#collections.OrderedDict
To produce a YAML string reflecting a configuration, just call
``config.dump()``. This does not cleanly round-trip YAML, but it does play some
tricks to preserve comments and spacing in the original file.
Custom YAML Loaders
~~~~~~~~~~~~~~~~~~~
You can also specify your own PyYAML_ `Loader` object to parse YAML files.
Supply the `loader` parameter to a `Configuration` constructor, like this:
.. code-block:: python
config = confuse.Configuration("name", loader=yaml.Loaded)
To imbue a loader with Confuse's special parser overrides, use its
`add_constructors` method:
.. code-block:: python
class MyLoader(yaml.Loader): ...
confuse.Loader.add_constructors(MyLoader)
config = confuse.Configuration("name", loader=MyLoader)
Configuring Large Programs
--------------------------
One problem that must be solved by a configuration system is the issue of global
configuration for complex applications. In a large program with many components
and many config options, it can be unwieldy to explicitly pass configuration
values from component to component. You quickly end up with monstrous function
signatures with dozens of keyword arguments, decreasing code legibility and
testability.
In such systems, one option is to pass a single `Configuration` object through
to each component. To avoid even this, however, it's sometimes appropriate to
use a little bit of shared global state. As evil as shared global state usually
is, configuration is (in my opinion) one valid use: since configuration is
mostly read-only, it's relatively unlikely to cause the sorts of problems that
global values sometimes can. And having a global repository for configuration
option can vastly reduce the amount of boilerplate threading-through needed to
explicitly pass configuration from call to call.
To use global configuration, consider creating a configuration object in a
well-known module (say, the root of a package). But since this object will be
initialized at module load time, Confuse provides a `LazyConfig` object that
loads your configuration files on demand instead of when the object is
constructed. (Doing complicated stuff like parsing YAML at module load time is
generally considered a Bad Idea.)
Global state can cause problems for unit testing. To alleviate this, consider
adding code to your test fixtures (e.g., setUp_ in the unittest_ module) that
clears out the global configuration before each test is run. Something like
this:
.. code-block:: python
config.clear()
config.read(user=False)
These lines will empty out the current configuration and then re-load the
defaults (but not the user's configuration files). Your tests can then modify
the global configuration values without affecting other tests since these
modifications will be cleared out before the next test runs.
.. _setup: https://docs.python.org/2/library/unittest.html#unittest.TestCase.setUp
.. _unittest: https://docs.python.org/2/library/unittest.html
Redaction
---------
You can also mark certain configuration values as "sensitive" and avoid
including them in output. Just set the `redact` flag:
.. code-block:: python
config["key"].redact = True
Then flatten or dump the configuration like so:
.. code-block:: python
config.dump(redact=True)
The resulting YAML will contain "key: REDACTED" instead of the original data.
beetbox-confuse-cc1e751/example.py 0000775 0000000 0000000 00000000065 15136363550 0017234 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import example
example.main()
beetbox-confuse-cc1e751/example/ 0000775 0000000 0000000 00000000000 15136363550 0016656 5 ustar 00root root 0000000 0000000 beetbox-confuse-cc1e751/example/__init__.py 0000664 0000000 0000000 00000003425 15136363550 0020773 0 ustar 00root root 0000000 0000000 """An example application using Confuse for configuration."""
import argparse
import confuse
template = {
"library": confuse.Filename(),
"import_write": confuse.OneOf([bool, "ask", "skip"]),
"ignore": confuse.StrSeq(),
"plugins": list,
"paths": {
"directory": confuse.Filename(),
"default": confuse.Filename(relative_to="directory"),
},
"servers": confuse.Sequence(
{
"hostname": str,
"options": confuse.StrSeq(),
}
),
}
config = confuse.LazyConfig("ConfuseExample", __name__)
def main() -> None:
parser = argparse.ArgumentParser(description="example Confuse program")
parser.add_argument(
"--library",
"-l",
dest="library",
metavar="LIBPATH",
help="library database file",
)
parser.add_argument(
"--directory",
"-d",
dest="paths.directory",
metavar="DIRECTORY",
help="destination music directory",
)
parser.add_argument(
"--verbose",
"-v",
dest="verbose",
action="store_true",
help="print debugging messages",
)
args = parser.parse_args()
config.set_args(args, dots=True)
print("configuration directory is", config.config_dir())
# Use a boolean flag and the transient overlay.
if config["verbose"]:
print("verbose mode")
config["log"]["level"] = 2
else:
config["log"]["level"] = 0
print("logging level is", config["log"]["level"].get(int))
valid = config.get(template)
# Some validated/converted values.
print("library is", valid.library)
print("directory is", valid.paths.directory)
print("paths.default is", valid.paths.default)
print("servers are", [s.hostname for s in valid.servers])
beetbox-confuse-cc1e751/example/config_default.yaml 0000664 0000000 0000000 00000001511 15136363550 0022511 0 ustar 00root root 0000000 0000000 library: library.db
import_write: yes
import_copy: yes
import_move: no
import_resume: ask
import_incremental: yes
import_quiet_fallback: skip
import_timid: no
import_log:
ignore: [".*", "*~"]
replace:
'[\\/]': _
'^\.': _
'[\x00-\x1f]': _
'[<>:"\?\*\|]': _
'\.$': _
'\s+$': ''
art_filename: cover
plugins: []
pluginpath: []
threaded: yes
color: yes
timeout: 5.0
per_disc_numbering: no
verbose: no
list_format_item: $artist - $album - $title
list_format_album: $albumartist - $album
paths:
directory: ~/Music
default: $albumartist/$album%aunique{}/$track $title
singleton: Non-Album/$artist/$title
comp: Compilations/$album%aunique{}/$track $title
servers:
- hostname: test1.example.com
options:
- foo
- hostname: test2.example.com
options:
- bar
- baz
beetbox-confuse-cc1e751/poetry.lock 0000664 0000000 0000000 00000476150 15136363550 0017434 0 ustar 00root root 0000000 0000000 # This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "alabaster"
version = "1.0.0"
description = "A light, configurable Sphinx theme"
optional = false
python-versions = ">=3.10"
groups = ["main", "lint"]
files = [
{file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"},
{file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"},
]
markers = {main = "extra == \"docs\""}
[[package]]
name = "babel"
version = "2.17.0"
description = "Internationalization utilities"
optional = false
python-versions = ">=3.8"
groups = ["main", "lint"]
files = [
{file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"},
{file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"},
]
markers = {main = "extra == \"docs\""}
[package.extras]
dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""]
[[package]]
name = "black"
version = "26.1.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.10"
groups = ["lint"]
files = [
{file = "black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168"},
{file = "black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d"},
{file = "black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0"},
{file = "black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24"},
{file = "black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89"},
{file = "black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5"},
{file = "black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68"},
{file = "black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14"},
{file = "black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c"},
{file = "black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4"},
{file = "black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f"},
{file = "black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6"},
{file = "black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a"},
{file = "black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791"},
{file = "black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954"},
{file = "black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304"},
{file = "black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9"},
{file = "black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b"},
{file = "black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b"},
{file = "black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca"},
{file = "black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115"},
{file = "black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79"},
{file = "black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af"},
{file = "black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f"},
{file = "black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0"},
{file = "black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede"},
{file = "black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=1.0.0"
platformdirs = ">=2"
pytokens = ">=0.3.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.10)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "certifi"
version = "2026.1.4"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.7"
groups = ["main", "lint"]
files = [
{file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"},
{file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"},
]
markers = {main = "extra == \"docs\""}
[[package]]
name = "charset-normalizer"
version = "3.4.4"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7"
groups = ["main", "lint"]
files = [
{file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"},
{file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"},
{file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"},
{file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"},
{file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"},
{file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"},
{file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"},
{file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"},
{file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"},
{file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"},
{file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"},
{file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"},
{file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"},
{file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"},
{file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"},
{file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"},
{file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"},
{file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"},
{file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"},
{file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"},
{file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"},
{file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"},
{file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"},
{file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"},
{file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"},
{file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"},
{file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"},
{file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"},
{file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"},
]
markers = {main = "extra == \"docs\""}
[[package]]
name = "click"
version = "8.3.1"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
groups = ["lint"]
files = [
{file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"},
{file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main", "dev", "lint"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
markers = {main = "extra == \"docs\" and sys_platform == \"win32\"", dev = "sys_platform == \"win32\"", lint = "sys_platform == \"win32\" or platform_system == \"Windows\""}
[[package]]
name = "coverage"
version = "7.13.2"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.10"
groups = ["dev", "lint"]
files = [
{file = "coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b"},
{file = "coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2"},
{file = "coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896"},
{file = "coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c"},
{file = "coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc"},
{file = "coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5"},
{file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31"},
{file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad"},
{file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f"},
{file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8"},
{file = "coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c"},
{file = "coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99"},
{file = "coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e"},
{file = "coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e"},
{file = "coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508"},
{file = "coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b"},
{file = "coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b"},
{file = "coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f"},
{file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3"},
{file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b"},
{file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1"},
{file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059"},
{file = "coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031"},
{file = "coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e"},
{file = "coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28"},
{file = "coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d"},
{file = "coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3"},
{file = "coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99"},
{file = "coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f"},
{file = "coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f"},
{file = "coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa"},
{file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce"},
{file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94"},
{file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5"},
{file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b"},
{file = "coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41"},
{file = "coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e"},
{file = "coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894"},
{file = "coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6"},
{file = "coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc"},
{file = "coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f"},
{file = "coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1"},
{file = "coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9"},
{file = "coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c"},
{file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5"},
{file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4"},
{file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c"},
{file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31"},
{file = "coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8"},
{file = "coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb"},
{file = "coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557"},
{file = "coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e"},
{file = "coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7"},
{file = "coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3"},
{file = "coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3"},
{file = "coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421"},
{file = "coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5"},
{file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23"},
{file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c"},
{file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f"},
{file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573"},
{file = "coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343"},
{file = "coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47"},
{file = "coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7"},
{file = "coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef"},
{file = "coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f"},
{file = "coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5"},
{file = "coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4"},
{file = "coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27"},
{file = "coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548"},
{file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660"},
{file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92"},
{file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82"},
{file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892"},
{file = "coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe"},
{file = "coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859"},
{file = "coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6"},
{file = "coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b"},
{file = "coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417"},
{file = "coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee"},
{file = "coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1"},
{file = "coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d"},
{file = "coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6"},
{file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a"},
{file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04"},
{file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f"},
{file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f"},
{file = "coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3"},
{file = "coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba"},
{file = "coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c"},
{file = "coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5"},
{file = "coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3"},
]
[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
[package.extras]
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "docstrfmt"
version = "2.0.1"
description = "docstrfmt: A formatter for Sphinx flavored reStructuredText."
optional = false
python-versions = ">=3.10"
groups = ["lint"]
files = [
{file = "docstrfmt-2.0.1-py3-none-any.whl", hash = "sha256:88f7c4bda3683b2464ef38f339a6f30c5fb36b94cfe1c2853a225037d198447a"},
{file = "docstrfmt-2.0.1.tar.gz", hash = "sha256:98895f70168aaf9ca92e6dc946e1f965e79d83d5966144f6f1a78c46184967de"},
]
[package.dependencies]
black = ">=24"
click = ">=8"
coverage = ">=7.11.0"
docutils = ">=0.21"
docutils-stubs = "0.0.22"
libcst = ">=1"
platformdirs = ">=4"
roman = "*"
sphinx = ">=7"
tabulate = ">=0.9"
tomli = {version = ">=0.10", markers = "python_version < \"3.11\""}
types-docutils = "0.22.3.20251115"
[package.extras]
d = ["aiohttp"]
[[package]]
name = "docutils"
version = "0.21.2"
description = "Docutils -- Python Documentation Utilities"
optional = false
python-versions = ">=3.9"
groups = ["main", "lint"]
files = [
{file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"},
{file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"},
]
markers = {main = "python_version < \"3.13\" and extra == \"docs\"", lint = "python_version < \"3.13\""}
[[package]]
name = "docutils"
version = "0.22.4"
description = "Docutils -- Python Documentation Utilities"
optional = false
python-versions = ">=3.9"
groups = ["main", "lint"]
files = [
{file = "docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"},
{file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"},
]
markers = {main = "python_version >= \"3.13\" and extra == \"docs\"", lint = "python_version >= \"3.13\""}
[[package]]
name = "docutils-stubs"
version = "0.0.22"
description = "PEP 561 type stubs for docutils"
optional = false
python-versions = ">=3.5"
groups = ["lint"]
files = [
{file = "docutils-stubs-0.0.22.tar.gz", hash = "sha256:1736d9650cfc20cff8c72582806c33a5c642694e2df9e430717e7da7e73efbdf"},
{file = "docutils_stubs-0.0.22-py3-none-any.whl", hash = "sha256:157807309de24e8c96af9a13afe207410f1fc6e5aab5d974fd6b9191f04de327"},
]
[package.dependencies]
docutils = ">=0.14"
[[package]]
name = "exceptiongroup"
version = "1.3.1"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version == \"3.10\""
files = [
{file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"},
{file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"},
]
[package.dependencies]
typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""}
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "idna"
version = "3.11"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.8"
groups = ["main", "lint"]
files = [
{file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"},
{file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"},
]
markers = {main = "extra == \"docs\""}
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "imagesize"
version = "1.4.1"
description = "Getting image size from png/jpeg/jpeg2000/gif file"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["main", "lint"]
files = [
{file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"},
{file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"},
]
markers = {main = "extra == \"docs\""}
[[package]]
name = "iniconfig"
version = "2.3.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
]
[[package]]
name = "jinja2"
version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
groups = ["main", "lint"]
files = [
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
]
markers = {main = "extra == \"docs\""}
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "libcst"
version = "1.8.6"
description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.14 programs."
optional = false
python-versions = ">=3.9"
groups = ["lint"]
files = [
{file = "libcst-1.8.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a20c5182af04332cc94d8520792befda06d73daf2865e6dddc5161c72ea92cb9"},
{file = "libcst-1.8.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36473e47cb199b7e6531d653ee6ffed057de1d179301e6c67f651f3af0b499d6"},
{file = "libcst-1.8.6-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:06fc56335a45d61b7c1b856bfab4587b84cfe31e9d6368f60bb3c9129d900f58"},
{file = "libcst-1.8.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6b23d14a7fc0addd9795795763af26b185deb7c456b1e7cc4d5228e69dab5ce8"},
{file = "libcst-1.8.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16cfe0cfca5fd840e1fb2c30afb628b023d3085b30c3484a79b61eae9d6fe7ba"},
{file = "libcst-1.8.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:455f49a93aea4070132c30ebb6c07c2dea0ba6c1fde5ffde59fc45dbb9cfbe4b"},
{file = "libcst-1.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:72cca15800ffc00ba25788e4626189fe0bc5fe2a0c1cb4294bce2e4df21cc073"},
{file = "libcst-1.8.6-cp310-cp310-win_arm64.whl", hash = "sha256:6cad63e3a26556b020b634d25a8703b605c0e0b491426b3e6b9e12ed20f09100"},
{file = "libcst-1.8.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3649a813660fbffd7bc24d3f810b1f75ac98bd40d9d6f56d1f0ee38579021073"},
{file = "libcst-1.8.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cbe17067055829607c5ba4afa46bfa4d0dd554c0b5a583546e690b7367a29b6"},
{file = "libcst-1.8.6-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:59a7e388c57d21d63722018978a8ddba7b176e3a99bd34b9b84a576ed53f2978"},
{file = "libcst-1.8.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b6c1248cc62952a3a005792b10cdef2a4e130847be9c74f33a7d617486f7e532"},
{file = "libcst-1.8.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6421a930b028c5ef4a943b32a5a78b7f1bf15138214525a2088f11acbb7d3d64"},
{file = "libcst-1.8.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6d8b67874f2188399a71a71731e1ba2d1a2c3173b7565d1cc7ffb32e8fbaba5b"},
{file = "libcst-1.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:b0d8c364c44ae343937f474b2e492c1040df96d94530377c2f9263fb77096e4f"},
{file = "libcst-1.8.6-cp311-cp311-win_arm64.whl", hash = "sha256:5dcaaebc835dfe5755bc85f9b186fb7e2895dda78e805e577fef1011d51d5a5c"},
{file = "libcst-1.8.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c13d5bd3d8414a129e9dccaf0e5785108a4441e9b266e1e5e9d1f82d1b943c9"},
{file = "libcst-1.8.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1472eeafd67cdb22544e59cf3bfc25d23dc94058a68cf41f6654ff4fcb92e09"},
{file = "libcst-1.8.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:089c58e75cb142ec33738a1a4ea7760a28b40c078ab2fd26b270dac7d2633a4d"},
{file = "libcst-1.8.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c9d7aeafb1b07d25a964b148c0dda9451efb47bbbf67756e16eeae65004b0eb5"},
{file = "libcst-1.8.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207481197afd328aa91d02670c15b48d0256e676ce1ad4bafb6dc2b593cc58f1"},
{file = "libcst-1.8.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86"},
{file = "libcst-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d"},
{file = "libcst-1.8.6-cp312-cp312-win_arm64.whl", hash = "sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7"},
{file = "libcst-1.8.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7445479ebe7d1aff0ee094ab5a1c7718e1ad78d33e3241e1a1ec65dcdbc22ffb"},
{file = "libcst-1.8.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fc3fef8a2c983e7abf5d633e1884c5dd6fa0dcb8f6e32035abd3d3803a3a196"},
{file = "libcst-1.8.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1a3a5e4ee870907aa85a4076c914ae69066715a2741b821d9bf16f9579de1105"},
{file = "libcst-1.8.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6609291c41f7ad0bac570bfca5af8fea1f4a27987d30a1fa8b67fe5e67e6c78d"},
{file = "libcst-1.8.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25eaeae6567091443b5374b4c7d33a33636a2d58f5eda02135e96fc6c8807786"},
{file = "libcst-1.8.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04030ea4d39d69a65873b1d4d877def1c3951a7ada1824242539e399b8763d30"},
{file = "libcst-1.8.6-cp313-cp313-win_amd64.whl", hash = "sha256:8066f1b70f21a2961e96bedf48649f27dfd5ea68be5cd1bed3742b047f14acde"},
{file = "libcst-1.8.6-cp313-cp313-win_arm64.whl", hash = "sha256:c188d06b583900e662cd791a3f962a8c96d3dfc9b36ea315be39e0a4c4792ebf"},
{file = "libcst-1.8.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c41c76e034a1094afed7057023b1d8967f968782433f7299cd170eaa01ec033e"},
{file = "libcst-1.8.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5432e785322aba3170352f6e72b32bea58d28abd141ac37cc9b0bf6b7c778f58"},
{file = "libcst-1.8.6-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:85b7025795b796dea5284d290ff69de5089fc8e989b25d6f6f15b6800be7167f"},
{file = "libcst-1.8.6-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:536567441182a62fb706e7aa954aca034827b19746832205953b2c725d254a93"},
{file = "libcst-1.8.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f04d3672bde1704f383a19e8f8331521abdbc1ed13abb349325a02ac56e5012"},
{file = "libcst-1.8.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f04febcd70e1e67917be7de513c8d4749d2e09206798558d7fe632134426ea4"},
{file = "libcst-1.8.6-cp313-cp313t-win_amd64.whl", hash = "sha256:1dc3b897c8b0f7323412da3f4ad12b16b909150efc42238e19cbf19b561cc330"},
{file = "libcst-1.8.6-cp313-cp313t-win_arm64.whl", hash = "sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42"},
{file = "libcst-1.8.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c"},
{file = "libcst-1.8.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661"},
{file = "libcst-1.8.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474"},
{file = "libcst-1.8.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8"},
{file = "libcst-1.8.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a"},
{file = "libcst-1.8.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47"},
{file = "libcst-1.8.6-cp314-cp314-win_amd64.whl", hash = "sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4"},
{file = "libcst-1.8.6-cp314-cp314-win_arm64.whl", hash = "sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9"},
{file = "libcst-1.8.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1"},
{file = "libcst-1.8.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4"},
{file = "libcst-1.8.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28"},
{file = "libcst-1.8.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa"},
{file = "libcst-1.8.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1"},
{file = "libcst-1.8.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996"},
{file = "libcst-1.8.6-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82"},
{file = "libcst-1.8.6-cp314-cp314t-win_arm64.whl", hash = "sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f"},
{file = "libcst-1.8.6-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cb2679ef532f9fa5be5c5a283b6357cb6e9888a8dd889c4bb2b01845a29d8c0b"},
{file = "libcst-1.8.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:203ec2a83f259baf686b9526268cd23d048d38be5589594ef143aee50a4faf7e"},
{file = "libcst-1.8.6-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6366ab2107425bf934b0c83311177f2a371bfc757ee8c6ad4a602d7cbcc2f363"},
{file = "libcst-1.8.6-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:6aa11df6c58812f731172b593fcb485d7ba09ccc3b52fea6c7f26a43377dc748"},
{file = "libcst-1.8.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:351ab879c2fd20d9cb2844ed1ea3e617ed72854d3d1e2b0880ede9c3eea43ba8"},
{file = "libcst-1.8.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98fa1ca321c81fb1f02e5c43f956ca543968cc1a30b264fd8e0a2e1b0b0bf106"},
{file = "libcst-1.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:25fc7a1303cad7639ad45ec38c06789b4540b7258e9a108924aaa2c132af4aca"},
{file = "libcst-1.8.6-cp39-cp39-win_arm64.whl", hash = "sha256:4d7bbdd35f3abdfb5ac5d1a674923572dab892b126a58da81ff2726102d6ec2e"},
{file = "libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b"},
]
[package.dependencies]
pyyaml = [
{version = ">=5.2", markers = "python_version < \"3.13\""},
{version = ">=6.0.3", markers = "python_version >= \"3.14\""},
]
pyyaml-ft = {version = ">=8.0.0", markers = "python_version == \"3.13\""}
[[package]]
name = "librt"
version = "0.7.8"
description = "Mypyc runtime library"
optional = false
python-versions = ">=3.9"
groups = ["lint"]
markers = "platform_python_implementation != \"PyPy\""
files = [
{file = "librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d"},
{file = "librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b"},
{file = "librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d"},
{file = "librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d"},
{file = "librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c"},
{file = "librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c"},
{file = "librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d"},
{file = "librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0"},
{file = "librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85"},
{file = "librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c"},
{file = "librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f"},
{file = "librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac"},
{file = "librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c"},
{file = "librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8"},
{file = "librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff"},
{file = "librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3"},
{file = "librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75"},
{file = "librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873"},
{file = "librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7"},
{file = "librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c"},
{file = "librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232"},
{file = "librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63"},
{file = "librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93"},
{file = "librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592"},
{file = "librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850"},
{file = "librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62"},
{file = "librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b"},
{file = "librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714"},
{file = "librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449"},
{file = "librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac"},
{file = "librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708"},
{file = "librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0"},
{file = "librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc"},
{file = "librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2"},
{file = "librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3"},
{file = "librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6"},
{file = "librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d"},
{file = "librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e"},
{file = "librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca"},
{file = "librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93"},
{file = "librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951"},
{file = "librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34"},
{file = "librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09"},
{file = "librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418"},
{file = "librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611"},
{file = "librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758"},
{file = "librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea"},
{file = "librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac"},
{file = "librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398"},
{file = "librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81"},
{file = "librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83"},
{file = "librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d"},
{file = "librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44"},
{file = "librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce"},
{file = "librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f"},
{file = "librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde"},
{file = "librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e"},
{file = "librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b"},
{file = "librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666"},
{file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581"},
{file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a"},
{file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca"},
{file = "librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365"},
{file = "librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32"},
{file = "librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06"},
{file = "librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6"},
{file = "librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b"},
{file = "librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c"},
{file = "librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5"},
{file = "librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71"},
{file = "librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e"},
{file = "librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63"},
{file = "librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94"},
{file = "librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb"},
{file = "librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be"},
{file = "librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862"},
]
[[package]]
name = "markupsafe"
version = "3.0.3"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.9"
groups = ["main", "lint"]
files = [
{file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
{file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
{file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"},
{file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"},
{file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"},
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"},
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"},
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"},
{file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"},
{file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"},
{file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"},
{file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"},
{file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"},
{file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"},
{file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"},
{file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"},
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"},
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"},
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"},
{file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"},
{file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"},
{file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"},
{file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"},
{file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"},
{file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"},
{file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"},
{file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"},
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"},
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"},
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"},
{file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"},
{file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"},
{file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"},
{file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"},
{file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"},
{file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"},
{file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"},
{file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"},
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"},
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"},
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"},
{file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"},
{file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"},
{file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"},
{file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"},
{file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"},
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"},
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"},
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"},
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"},
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"},
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"},
{file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"},
{file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"},
{file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"},
{file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"},
{file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"},
{file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"},
{file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"},
{file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"},
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"},
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"},
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"},
{file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"},
{file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"},
{file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"},
{file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"},
{file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"},
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"},
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"},
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"},
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"},
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"},
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"},
{file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"},
{file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"},
{file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"},
{file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"},
{file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"},
{file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"},
{file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"},
{file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"},
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"},
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"},
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"},
{file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"},
{file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"},
{file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"},
{file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"},
]
markers = {main = "extra == \"docs\""}
[[package]]
name = "mypy"
version = "1.19.1"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.9"
groups = ["lint"]
files = [
{file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"},
{file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"},
{file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"},
{file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"},
{file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"},
{file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"},
{file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"},
{file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"},
{file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"},
{file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"},
{file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"},
{file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"},
{file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"},
{file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"},
{file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"},
{file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"},
{file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"},
{file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"},
{file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"},
{file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"},
{file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"},
{file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"},
{file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"},
{file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"},
{file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"},
{file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"},
{file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"},
{file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"},
{file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"},
{file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"},
{file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"},
{file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"},
{file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"},
{file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"},
{file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"},
{file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"},
{file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"},
{file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"},
]
[package.dependencies]
librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""}
mypy_extensions = ">=1.0.0"
pathspec = ">=0.9.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing_extensions = ">=4.6.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
faster-cache = ["orjson"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.8"
groups = ["lint"]
files = [
{file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
]
[[package]]
name = "packaging"
version = "26.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev", "lint"]
files = [
{file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"},
{file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"},
]
markers = {main = "extra == \"docs\""}
[[package]]
name = "pathspec"
version = "1.0.4"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.9"
groups = ["lint"]
files = [
{file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"},
{file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"},
]
[package.extras]
hyperscan = ["hyperscan (>=0.7)"]
optional = ["typing-extensions (>=4)"]
re2 = ["google-re2 (>=1.1)"]
tests = ["pytest (>=9)", "typing-extensions (>=4.15)"]
[[package]]
name = "platformdirs"
version = "4.5.1"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.10"
groups = ["lint"]
files = [
{file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"},
{file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"},
]
[package.extras]
docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"]
type = ["mypy (>=1.18.2)"]
[[package]]
name = "pluggy"
version = "1.6.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]]
name = "polib"
version = "1.2.0"
description = "A library to manipulate gettext files (po and mo files)."
optional = false
python-versions = "*"
groups = ["lint"]
files = [
{file = "polib-1.2.0-py2.py3-none-any.whl", hash = "sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d"},
{file = "polib-1.2.0.tar.gz", hash = "sha256:f3ef94aefed6e183e342a8a269ae1fc4742ba193186ad76f175938621dbfc26b"},
]
[[package]]
name = "pygments"
version = "2.19.2"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev", "lint"]
files = [
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
]
markers = {main = "extra == \"docs\""}
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pytest"
version = "9.0.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"},
{file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"},
]
[package.dependencies]
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""}
iniconfig = ">=1.0.1"
packaging = ">=22"
pluggy = ">=1.5,<2"
pygments = ">=2.7.2"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-cov"
version = "7.0.0"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"},
{file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"},
]
[package.dependencies]
coverage = {version = ">=7.10.6", extras = ["toml"]}
pluggy = ">=1.2"
pytest = ">=7"
[package.extras]
testing = ["process-tests", "pytest-xdist", "virtualenv"]
[[package]]
name = "pytokens"
version = "0.4.0"
description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons."
optional = false
python-versions = ">=3.8"
groups = ["lint"]
files = [
{file = "pytokens-0.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:af0c3166aea367a9e755a283171befb92dd3043858b94ae9b3b7efbe9def26a3"},
{file = "pytokens-0.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae524ed14ca459932cbf51d74325bea643701ba8a8b0cc2d10f7cd4b3e2b63"},
{file = "pytokens-0.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e95cb158c44d642ed62f555bf8136bbe780dbd64d2fb0b9169e11ffb944664c3"},
{file = "pytokens-0.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df58d44630eaf25f587540e94bdf1fc50b4e6d5f212c786de0fb024bfcb8753a"},
{file = "pytokens-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55efcc36f9a2e0e930cfba0ce7f83445306b02f8326745585ed5551864eba73a"},
{file = "pytokens-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92eb3ef88f27c22dc9dbab966ace4d61f6826e02ba04dac8e2d65ea31df56c8e"},
{file = "pytokens-0.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4b77858a680635ee9904306f54b0ee4781effb89e211ba0a773d76539537165"},
{file = "pytokens-0.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25cacc20c2ad90acb56f3739d87905473c54ca1fa5967ffcd675463fe965865e"},
{file = "pytokens-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628fab535ebc9079e4db35cd63cb401901c7ce8720a9834f9ad44b9eb4e0f1d4"},
{file = "pytokens-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:4d0f568d7e82b7e96be56d03b5081de40e43c904eb6492bf09aaca47cd55f35b"},
{file = "pytokens-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd8da894e5a29ba6b6da8be06a4f7589d7220c099b5e363cb0643234b9b38c2a"},
{file = "pytokens-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:237ba7cfb677dbd3b01b09860810aceb448871150566b93cd24501d5734a04b1"},
{file = "pytokens-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01d1a61e36812e4e971cfe2c0e4c1f2d66d8311031dac8bf168af8a249fa04dd"},
{file = "pytokens-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47e2ef3ec6ee86909e520d79f965f9b23389fda47460303cf715d510a6fe544"},
{file = "pytokens-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d36954aba4557fd5a418a03cf595ecbb1cdcce119f91a49b19ef09d691a22ae"},
{file = "pytokens-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73eff3bdd8ad08da679867992782568db0529b887bed4c85694f84cdf35eafc6"},
{file = "pytokens-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d97cc1f91b1a8e8ebccf31c367f28225699bea26592df27141deade771ed0afb"},
{file = "pytokens-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c8952c537cb73a1a74369501a83b7f9d208c3cf92c41dd88a17814e68d48ce"},
{file = "pytokens-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dbf56f3c748aed9310b310d5b8b14e2c96d3ad682ad5a943f381bdbbdddf753"},
{file = "pytokens-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e131804513597f2dff2b18f9911d9b6276e21ef3699abeffc1c087c65a3d975e"},
{file = "pytokens-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0d7374c917197106d3c4761374718bc55ea2e9ac0fb94171588ef5840ee1f016"},
{file = "pytokens-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cd3fa1caf9e47a72ee134a29ca6b5bea84712724bba165d6628baa190c6ea5b"},
{file = "pytokens-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c6986576b7b07fe9791854caa5347923005a80b079d45b63b0be70d50cce5f1"},
{file = "pytokens-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9940f7c2e2f54fb1cb5fe17d0803c54da7a2bf62222704eb4217433664a186a7"},
{file = "pytokens-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:54691cf8f299e7efabcc25adb4ce715d3cef1491e1c930eaf555182f898ef66a"},
{file = "pytokens-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94ff5db97a0d3cd7248a5b07ba2167bd3edc1db92f76c6db00137bbaf068ddf8"},
{file = "pytokens-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0dd6261cd9cc95fae1227b1b6ebee023a5fd4a4b6330b071c73a516f5f59b63"},
{file = "pytokens-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdca8159df407dbd669145af4171a0d967006e0be25f3b520896bc7068f02c4"},
{file = "pytokens-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4b5770abeb2a24347380a1164a558f0ebe06e98aedbd54c45f7929527a5fb26e"},
{file = "pytokens-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:74500d72c561dad14c037a9e86a657afd63e277dd5a3bb7570932ab7a3b12551"},
{file = "pytokens-0.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e368e0749e4e9d86a6e08763310dc92bc69ad73d9b6db5243b30174c71a8a534"},
{file = "pytokens-0.4.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:865cc65c75c8f2e9e0d8330338f649b12bfd9442561900ebaf58c596a72107d2"},
{file = "pytokens-0.4.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbb9338663b3538f31c4ca7afe4f38d9b9b3a16a8be18a273a5704a1bc7a2367"},
{file = "pytokens-0.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:658f870523ac1a5f4733d7db61ce9af61a0c23b2aeea3d03d1800c93f760e15f"},
{file = "pytokens-0.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:d69a2491190a74e4b6f87f3b9dfce7a6873de3f3bf330d20083d374380becac0"},
{file = "pytokens-0.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8cd795191c4127fcb3d7b76d84006a07748c390226f47657869235092eedbc05"},
{file = "pytokens-0.4.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2bcbddb73ac18599a86c8c549d5145130f2cd9d83dc2b5482fd8322b7806cd"},
{file = "pytokens-0.4.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06ac081c1187389762b58823d90d6339e6880ce0df912f71fb9022d81d7fd429"},
{file = "pytokens-0.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:278129d54573efdc79e75c6082e73ebd19858e22a2e848359f93629323186ca6"},
{file = "pytokens-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:9380fb6d96fa5ab83ed606ebad27b6171930cc14a8a8d215f6adb187ba428690"},
{file = "pytokens-0.4.0-py3-none-any.whl", hash = "sha256:0508d11b4de157ee12063901603be87fb0253e8f4cb9305eb168b1202ab92068"},
{file = "pytokens-0.4.0.tar.gz", hash = "sha256:6b0b03e6ea7c9f9d47c5c61164b69ad30f4f0d70a5d9fe7eac4d19f24f77af2d"},
]
[package.extras]
dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"]
[[package]]
name = "pyyaml"
version = "6.0.3"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
groups = ["main", "lint"]
files = [
{file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"},
{file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"},
{file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"},
{file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"},
{file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
{file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
{file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
{file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
{file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
{file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
{file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
{file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
{file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
{file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
{file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
{file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
{file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"},
{file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"},
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"},
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"},
{file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"},
{file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"},
{file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"},
{file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"},
{file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"},
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"},
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"},
{file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"},
{file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"},
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"},
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"},
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"},
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"},
{file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"},
{file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"},
{file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"},
{file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"},
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"},
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"},
{file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"},
{file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"},
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
]
markers = {lint = "python_version < \"3.13\" or python_version >= \"3.14\""}
[[package]]
name = "pyyaml-ft"
version = "8.0.0"
description = "YAML parser and emitter for Python with support for free-threading"
optional = false
python-versions = ">=3.13"
groups = ["lint"]
markers = "python_version == \"3.13\""
files = [
{file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6"},
{file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69"},
{file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0"},
{file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42"},
{file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b"},
{file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254"},
{file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8"},
{file = "pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96"},
{file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb"},
{file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1"},
{file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49"},
{file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b"},
{file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a"},
{file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e"},
{file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255"},
{file = "pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793"},
{file = "pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab"},
]
[[package]]
name = "regex"
version = "2026.1.15"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.9"
groups = ["lint"]
files = [
{file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"},
{file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"},
{file = "regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b"},
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c"},
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9"},
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c"},
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106"},
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618"},
{file = "regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4"},
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79"},
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9"},
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220"},
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13"},
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3"},
{file = "regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218"},
{file = "regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a"},
{file = "regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3"},
{file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"},
{file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"},
{file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"},
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"},
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"},
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"},
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"},
{file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"},
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"},
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"},
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"},
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"},
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"},
{file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"},
{file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"},
{file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"},
{file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"},
{file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"},
{file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"},
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"},
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"},
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"},
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"},
{file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"},
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"},
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"},
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"},
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"},
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"},
{file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"},
{file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"},
{file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"},
{file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"},
{file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"},
{file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"},
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"},
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"},
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"},
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"},
{file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"},
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"},
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"},
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"},
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"},
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"},
{file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"},
{file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"},
{file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"},
{file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"},
{file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"},
{file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"},
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"},
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"},
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"},
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"},
{file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"},
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"},
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"},
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"},
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"},
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"},
{file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"},
{file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"},
{file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"},
{file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"},
{file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"},
{file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"},
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"},
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"},
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"},
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"},
{file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"},
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"},
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"},
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"},
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"},
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"},
{file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"},
{file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"},
{file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"},
{file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"},
{file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"},
{file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"},
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"},
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"},
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"},
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"},
{file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"},
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"},
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"},
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"},
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"},
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"},
{file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"},
{file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"},
{file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"},
{file = "regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5"},
{file = "regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815"},
{file = "regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b"},
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6"},
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b"},
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df"},
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e"},
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c"},
{file = "regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839"},
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c"},
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077"},
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4"},
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a"},
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3"},
{file = "regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a"},
{file = "regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc"},
{file = "regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5"},
{file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"},
]
[[package]]
name = "requests"
version = "2.32.5"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.9"
groups = ["main", "lint"]
files = [
{file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"},
{file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"},
]
markers = {main = "extra == \"docs\""}
[package.dependencies]
certifi = ">=2017.4.17"
charset_normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "roman"
version = "5.2"
description = "Integer to Roman numerals converter"
optional = false
python-versions = ">=3.10"
groups = ["lint"]
files = [
{file = "roman-5.2-py3-none-any.whl", hash = "sha256:89d3b47400388806d06ff77ea77c79ab080bc127820dea6bf34e1f1c1b8e676e"},
{file = "roman-5.2.tar.gz", hash = "sha256:275fe9f46290f7d0ffaea1c33251b92b8e463ace23660508ceef522e7587cb6f"},
]
[[package]]
name = "roman-numerals"
version = "4.1.0"
description = "Manipulate well-formed Roman numerals"
optional = false
python-versions = ">=3.10"
groups = ["main", "lint"]
files = [
{file = "roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7"},
{file = "roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2"},
]
markers = {main = "python_version >= \"3.13\" and extra == \"docs\"", lint = "python_version >= \"3.13\""}
[[package]]
name = "ruff"
version = "0.14.14"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["lint"]
files = [
{file = "ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed"},
{file = "ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c"},
{file = "ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de"},
{file = "ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e"},
{file = "ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8"},
{file = "ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906"},
{file = "ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480"},
{file = "ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df"},
{file = "ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b"},
{file = "ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974"},
{file = "ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66"},
{file = "ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13"},
{file = "ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412"},
{file = "ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3"},
{file = "ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b"},
{file = "ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167"},
{file = "ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd"},
{file = "ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c"},
{file = "ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b"},
]
[[package]]
name = "snowballstemmer"
version = "3.0.1"
description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*"
groups = ["main", "lint"]
files = [
{file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"},
{file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"},
]
markers = {main = "extra == \"docs\""}
[[package]]
name = "sphinx"
version = "8.1.3"
description = "Python documentation generator"
optional = false
python-versions = ">=3.10"
groups = ["main", "lint"]
files = [
{file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"},
{file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"},
]
markers = {main = "python_version < \"3.13\" and extra == \"docs\"", lint = "python_version < \"3.13\""}
[package.dependencies]
alabaster = ">=0.7.14"
babel = ">=2.13"
colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""}
docutils = ">=0.20,<0.22"
imagesize = ">=1.3"
Jinja2 = ">=3.1"
packaging = ">=23.0"
Pygments = ">=2.17"
requests = ">=2.30.0"
snowballstemmer = ">=2.2"
sphinxcontrib-applehelp = ">=1.0.7"
sphinxcontrib-devhelp = ">=1.0.6"
sphinxcontrib-htmlhelp = ">=2.0.6"
sphinxcontrib-jsmath = ">=1.0.1"
sphinxcontrib-qthelp = ">=1.0.6"
sphinxcontrib-serializinghtml = ">=1.1.9"
tomli = {version = ">=2", markers = "python_version < \"3.11\""}
[package.extras]
docs = ["sphinxcontrib-websupport"]
lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"]
test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"]
[[package]]
name = "sphinx"
version = "9.1.0"
description = "Python documentation generator"
optional = false
python-versions = ">=3.12"
groups = ["main", "lint"]
files = [
{file = "sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978"},
{file = "sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb"},
]
markers = {main = "python_version >= \"3.13\" and extra == \"docs\"", lint = "python_version >= \"3.13\""}
[package.dependencies]
alabaster = ">=0.7.14"
babel = ">=2.13"
colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""}
docutils = ">=0.21,<0.23"
imagesize = ">=1.3"
Jinja2 = ">=3.1"
packaging = ">=23.0"
Pygments = ">=2.17"
requests = ">=2.30.0"
roman-numerals = ">=1.0.0"
snowballstemmer = ">=2.2"
sphinxcontrib-applehelp = ">=1.0.7"
sphinxcontrib-devhelp = ">=1.0.6"
sphinxcontrib-htmlhelp = ">=2.0.6"
sphinxcontrib-jsmath = ">=1.0.1"
sphinxcontrib-qthelp = ">=1.0.6"
sphinxcontrib-serializinghtml = ">=1.1.9"
[[package]]
name = "sphinx-lint"
version = "1.0.2"
description = "Check for stylistic and formal issues in .rst and .py files included in the documentation."
optional = false
python-versions = ">=3.10"
groups = ["lint"]
files = [
{file = "sphinx_lint-1.0.2-py3-none-any.whl", hash = "sha256:edcd0fa4d916386c5a3ef7ef0f5136f0bb4a15feefc83c1068ba15bc16eec652"},
{file = "sphinx_lint-1.0.2.tar.gz", hash = "sha256:4e7fc12f44f750b0006eaad237d7db9b1d8aba92adda9c838af891654b371d35"},
]
[package.dependencies]
polib = "*"
regex = "*"
[package.extras]
tests = ["pytest", "pytest-cov"]
[[package]]
name = "sphinx-rtd-theme"
version = "3.1.0"
description = "Read the Docs theme for Sphinx"
optional = true
python-versions = ">=3.8"
groups = ["main"]
markers = "extra == \"docs\""
files = [
{file = "sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89"},
{file = "sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c"},
]
[package.dependencies]
docutils = ">0.18,<0.23"
sphinx = ">=6,<10"
sphinxcontrib-jquery = ">=4,<5"
[package.extras]
dev = ["bump2version", "transifex-client", "twine", "wheel"]
[[package]]
name = "sphinxcontrib-applehelp"
version = "2.0.0"
description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books"
optional = false
python-versions = ">=3.9"
groups = ["main", "lint"]
files = [
{file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"},
{file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"},
]
markers = {main = "extra == \"docs\""}
[package.extras]
lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
standalone = ["Sphinx (>=5)"]
test = ["pytest"]
[[package]]
name = "sphinxcontrib-devhelp"
version = "2.0.0"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents"
optional = false
python-versions = ">=3.9"
groups = ["main", "lint"]
files = [
{file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"},
{file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"},
]
markers = {main = "extra == \"docs\""}
[package.extras]
lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
standalone = ["Sphinx (>=5)"]
test = ["pytest"]
[[package]]
name = "sphinxcontrib-htmlhelp"
version = "2.1.0"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
optional = false
python-versions = ">=3.9"
groups = ["main", "lint"]
files = [
{file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"},
{file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"},
]
markers = {main = "extra == \"docs\""}
[package.extras]
lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
standalone = ["Sphinx (>=5)"]
test = ["html5lib", "pytest"]
[[package]]
name = "sphinxcontrib-jquery"
version = "4.1"
description = "Extension to include jQuery on newer Sphinx releases"
optional = true
python-versions = ">=2.7"
groups = ["main"]
markers = "extra == \"docs\""
files = [
{file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"},
{file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"},
]
[package.dependencies]
Sphinx = ">=1.8"
[[package]]
name = "sphinxcontrib-jsmath"
version = "1.0.1"
description = "A sphinx extension which renders display math in HTML via JavaScript"
optional = false
python-versions = ">=3.5"
groups = ["main", "lint"]
files = [
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
{file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
]
markers = {main = "extra == \"docs\""}
[package.extras]
test = ["flake8", "mypy", "pytest"]
[[package]]
name = "sphinxcontrib-qthelp"
version = "2.0.0"
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents"
optional = false
python-versions = ">=3.9"
groups = ["main", "lint"]
files = [
{file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"},
{file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"},
]
markers = {main = "extra == \"docs\""}
[package.extras]
lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
standalone = ["Sphinx (>=5)"]
test = ["defusedxml (>=0.7.1)", "pytest"]
[[package]]
name = "sphinxcontrib-serializinghtml"
version = "2.0.0"
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)"
optional = false
python-versions = ">=3.9"
groups = ["main", "lint"]
files = [
{file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"},
{file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"},
]
markers = {main = "extra == \"docs\""}
[package.extras]
lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
standalone = ["Sphinx (>=5)"]
test = ["pytest"]
[[package]]
name = "tabulate"
version = "0.9.0"
description = "Pretty-print tabular data"
optional = false
python-versions = ">=3.7"
groups = ["lint"]
files = [
{file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"},
{file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"},
]
[package.extras]
widechars = ["wcwidth"]
[[package]]
name = "tomli"
version = "2.4.0"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev", "lint"]
files = [
{file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"},
{file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"},
{file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"},
{file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"},
{file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"},
{file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"},
{file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"},
{file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"},
{file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"},
{file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"},
{file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"},
{file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"},
{file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"},
{file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"},
{file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"},
{file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"},
{file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"},
{file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"},
{file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"},
{file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"},
{file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"},
{file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"},
{file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"},
{file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"},
{file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"},
{file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"},
{file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"},
{file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"},
{file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"},
{file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"},
{file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"},
{file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"},
{file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"},
{file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"},
{file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"},
{file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"},
{file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"},
{file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"},
{file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"},
{file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"},
{file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"},
{file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"},
{file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"},
{file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"},
{file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"},
{file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"},
{file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"},
]
markers = {main = "extra == \"docs\" and python_version == \"3.10\"", dev = "python_full_version <= \"3.11.0a6\"", lint = "python_version == \"3.10\""}
[[package]]
name = "types-docutils"
version = "0.22.3.20251115"
description = "Typing stubs for docutils"
optional = false
python-versions = ">=3.9"
groups = ["lint"]
files = [
{file = "types_docutils-0.22.3.20251115-py3-none-any.whl", hash = "sha256:c6e53715b65395d00a75a3a8a74e352c669bc63959e65a207dffaa22f4a2ad6e"},
{file = "types_docutils-0.22.3.20251115.tar.gz", hash = "sha256:0f79ea6a7bd4d12d56c9f824a0090ffae0ea4204203eb0006392906850913e16"},
]
[[package]]
name = "types-pyyaml"
version = "6.0.12.20250915"
description = "Typing stubs for PyYAML"
optional = false
python-versions = ">=3.9"
groups = ["lint"]
files = [
{file = "types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6"},
{file = "types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3"},
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
python-versions = ">=3.9"
groups = ["dev", "lint"]
files = [
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
]
markers = {dev = "python_version == \"3.10\""}
[[package]]
name = "urllib3"
version = "2.6.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
groups = ["main", "lint"]
files = [
{file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"},
{file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"},
]
markers = {main = "extra == \"docs\""}
[package.extras]
brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[extras]
docs = ["sphinx", "sphinx-rtd-theme"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10"
content-hash = "a66b26695a7b01066dcc6a13ae4fe8a2b09cf25853594d2b6adc3a63dcbdf4ac"
beetbox-confuse-cc1e751/pyproject.toml 0000664 0000000 0000000 00000006770 15136363550 0020151 0 ustar 00root root 0000000 0000000 [project]
name = "confuse"
version = "2.2.0"
description = "Painless YAML config files"
authors = [{ name = "Adrian Sampson", email = "adrian@radbox.org" }]
readme = "README.rst"
license = "MIT"
requires-python = ">=3.10"
classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"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",
]
dependencies = [
"pyyaml",
]
[dependency-groups]
dev = [
"pytest >= 8.4.2",
"pytest-cov >= 7.0.0",
]
lint = [
"docstrfmt >= 2.0.1",
"mypy >= 1.18.2",
"ruff >= 0.6.4",
"sphinx-lint >= 1.0.0",
"types-pyyaml >=6.0.12",
]
[project.optional-dependencies]
docs = ["sphinx >= 7.4.7", "sphinx-rtd-theme >= 3.0.2"]
[project.urls]
Homepage = "https://github.com/beetbox/confuse"
Repository = "https://github.com/beetbox/confuse"
"Bug Tracker" = "https://github.com/beetbox/confuse/issues"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.pipx-install]
poethepoet = ">=0.26"
poetry = ">=2.1"
[tool.poetry]
include = [
{ path = "docs", format = "sdist" },
{ path = "test", format = "sdist" },
]
[tool.poe.tasks.check-docs-links]
help = "Check the documentation for broken URLs"
cmd = "make -C docs linkcheck"
[tool.poe.tasks.check-format]
help = "Check the code for style issues"
cmd = "ruff format --check --diff"
[tool.poe.tasks.check-types]
help = "Check the code for typing issues. Accepts mypy options."
cmd = "mypy"
[tool.poe.tasks.docs]
help = "Build documentation"
args = [{ name = "COMMANDS", positional = true, multiple = true, default = "html" }]
cmd = "make -C docs $COMMANDS"
[tool.poe.tasks.format]
help = "Format the codebase"
cmd = "ruff format"
[tool.poe.tasks.format-docs]
help = "Format the documentation"
# TODO: Remove --no-format-python-code-blocks once https://github.com/LilSpazJoekp/docstrfmt/issues/171 is resolved
cmd = "docstrfmt --no-format-python-code-blocks --preserve-adornments docs *.rst"
[tool.poe.tasks.lint]
help = "Check the code for linting issues. Accepts ruff options."
cmd = "ruff check"
[tool.poe.tasks.lint-docs]
help = "Lint the documentation"
shell = "sphinx-lint --enable all --disable default-role $(git ls-files '*.rst')"
[tool.poe.tasks.test]
help = "Run tests with pytest"
cmd = "pytest $OPTS"
env.OPTS.default = "-p no:cov"
[tool.poe.tasks.test-with-coverage]
help = "Run tests and record coverage"
ref = "test"
# record coverage in confuse package
# save xml for coverage upload to codecov
# save html report for local dev use
# measure coverage across logical branches
# show which tests cover specific lines in the code (see the HTML report)
env.OPTS = """
--cov=confuse
--cov-report=xml:.reports/coverage.xml
--cov-report=html:.reports/html
--cov-branch
--cov-context=test
"""
[tool.docstrfmt]
line-length = 80
[tool.ruff]
target-version = "py310"
[tool.ruff.lint]
future-annotations = true
select = [
"E", # pycodestyle
"F", # pyflakes
"G", # flake8-logging-format
"I", # isort
"ISC", # flake8-implicit-str-concat
"N", # pep8-naming
"PT", # flake8-pytest-style
"RUF", # ruff
"UP", # pyupgrade
"TC", # flake8-type-checking
"W", # pycodestyle
]
ignore = [
"TC006", # no need to quote 'cast's since we use 'from __future__ import annotations'
]
[tool.ruff.format]
quote-style = "double"
beetbox-confuse-cc1e751/setup.cfg 0000664 0000000 0000000 00000001377 15136363550 0017054 0 ustar 00root root 0000000 0000000 [tool:pytest]
# slightly more verbose output
console_output_style = count
# pretty-print test names in the Codecov U
junit_family = legacy
addopts =
# show all skipped/failed/xfailed tests in the summary except passed
-ra
--strict-config
--junitxml=.reports/pytest.xml
[coverage:run]
data_file = .reports/coverage/data
branch = true
relative_files = true
[coverage:report]
precision = 2
skip_empty = true
show_missing = true
exclude_also =
@atexit.register
if TYPE_CHECKING
if typing.TYPE_CHECKING
raise AssertionError
raise NotImplementedError
[coverage:html]
show_contexts = true
[mypy]
allow_any_generics = false
allow_subclassing_any = true
allow_untyped_calls = true
strict = true
[mypy-test.*]
allow_untyped_defs = true
beetbox-confuse-cc1e751/test/ 0000775 0000000 0000000 00000000000 15136363550 0016202 5 ustar 00root root 0000000 0000000 beetbox-confuse-cc1e751/test/__init__.py 0000664 0000000 0000000 00000001536 15136363550 0020320 0 ustar 00root root 0000000 0000000 import os
import shutil
import tempfile
from typing import Any
import confuse
def _root(*sources: dict[str, Any]) -> confuse.RootView:
return confuse.RootView([confuse.ConfigSource.of(s) for s in sources])
class TempDir:
"""Context manager that creates and destroys a temporary directory."""
def __init__(self):
self.path = tempfile.mkdtemp()
def __enter__(self):
return self
def __exit__(self, *errstuff):
shutil.rmtree(self.path)
def sub(self, name, contents=None):
"""Get a path to a file named `name` inside this temporary
directory. If `contents` is provided, then the bytestring is
written to the file.
"""
path = os.path.join(self.path, name)
if contents:
with open(path, "wb") as f:
f.write(contents)
return path
beetbox-confuse-cc1e751/test/test_cli.py 0000664 0000000 0000000 00000013334 15136363550 0020366 0 ustar 00root root 0000000 0000000 import argparse
import optparse
import unittest
from argparse import Namespace
import pytest
import confuse
class ArgparseTest(unittest.TestCase):
def setUp(self):
self.config = confuse.Configuration("test", read=False)
self.parser = argparse.ArgumentParser()
def _parse(self, args, **kwargs):
args = self.parser.parse_args(args.split())
self.config.set_args(args, **kwargs)
def test_text_argument_parsed(self):
self.parser.add_argument("--foo", metavar="BAR")
self._parse("--foo bar")
assert self.config["foo"].get() == "bar"
def test_boolean_argument_parsed(self):
self.parser.add_argument("--foo", action="store_true")
self._parse("--foo")
assert self.config["foo"].get()
def test_missing_optional_argument_not_included(self):
self.parser.add_argument("--foo", metavar="BAR")
self._parse("")
with pytest.raises(confuse.NotFoundError):
self.config["foo"].get()
def test_argument_overrides_default(self):
self.config.add({"foo": "baz"})
self.parser.add_argument("--foo", metavar="BAR")
self._parse("--foo bar")
assert self.config["foo"].get() == "bar"
def test_nested_destination_single(self):
self.parser.add_argument("--one", dest="one.foo")
self.parser.add_argument("--two", dest="one.two.foo")
self._parse("--two TWO", dots=True)
assert self.config["one"]["two"]["foo"].get() == "TWO"
def test_nested_destination_nested(self):
self.parser.add_argument("--one", dest="one.foo")
self.parser.add_argument("--two", dest="one.two.foo")
self._parse("--two TWO --one ONE", dots=True)
assert self.config["one"]["foo"].get() == "ONE"
assert self.config["one"]["two"]["foo"].get() == "TWO"
def test_nested_destination_nested_rev(self):
self.parser.add_argument("--one", dest="one.foo")
self.parser.add_argument("--two", dest="one.two.foo")
# Reverse to ensure order doesn't matter
self._parse("--one ONE --two TWO", dots=True)
assert self.config["one"]["foo"].get() == "ONE"
assert self.config["one"]["two"]["foo"].get() == "TWO"
def test_nested_destination_clobber(self):
self.parser.add_argument("--one", dest="one.two")
self.parser.add_argument("--two", dest="one.two.foo")
self._parse("--two TWO --one ONE", dots=True)
# Clobbered
assert self.config["one"]["two"].get() == {"foo": "TWO"}
assert self.config["one"]["two"]["foo"].get() == "TWO"
def test_nested_destination_clobber_rev(self):
# Reversed order
self.parser.add_argument("--two", dest="one.two.foo")
self.parser.add_argument("--one", dest="one.two")
self._parse("--one ONE --two TWO", dots=True)
# Clobbered just the same
assert self.config["one"]["two"].get() == {"foo": "TWO"}
assert self.config["one"]["two"]["foo"].get() == "TWO"
class OptparseTest(unittest.TestCase):
def setUp(self):
self.config = confuse.Configuration("test", read=False)
self.parser = optparse.OptionParser()
def _parse(self, args, **kwargs):
options, _ = self.parser.parse_args(args.split())
self.config.set_args(options, **kwargs)
def test_text_argument_parsed(self):
self.parser.add_option("--foo", metavar="BAR")
self._parse("--foo bar")
assert self.config["foo"].get() == "bar"
def test_boolean_argument_parsed(self):
self.parser.add_option("--foo", action="store_true")
self._parse("--foo")
assert self.config["foo"].get()
def test_missing_optional_argument_not_included(self):
self.parser.add_option("--foo", metavar="BAR")
self._parse("")
with pytest.raises(confuse.NotFoundError):
self.config["foo"].get()
def test_argument_overrides_default(self):
self.config.add({"foo": "baz"})
self.parser.add_option("--foo", metavar="BAR")
self._parse("--foo bar")
assert self.config["foo"].get() == "bar"
def test_nested_destination_single(self):
self.parser.add_option("--one", dest="one.foo")
self.parser.add_option("--two", dest="one.two.foo")
self._parse("--two TWO", dots=True)
assert self.config["one"]["two"]["foo"].get() == "TWO"
def test_nested_destination_nested(self):
self.parser.add_option("--one", dest="one.foo")
self.parser.add_option("--two", dest="one.two.foo")
self._parse("--two TWO --one ONE", dots=True)
assert self.config["one"]["foo"].get() == "ONE"
assert self.config["one"]["two"]["foo"].get() == "TWO"
def test_nested_destination_nested_rev(self):
self.parser.add_option("--one", dest="one.foo")
self.parser.add_option("--two", dest="one.two.foo")
# Reverse to ensure order doesn't matter
self._parse("--one ONE --two TWO", dots=True)
assert self.config["one"]["foo"].get() == "ONE"
assert self.config["one"]["two"]["foo"].get() == "TWO"
class GenericNamespaceTest(unittest.TestCase):
def setUp(self):
self.config = confuse.Configuration("test", read=False)
def test_value_added_to_root(self):
self.config.set_args(Namespace(foo="bar"))
assert self.config["foo"].get() == "bar"
def test_value_added_to_subview(self):
self.config["baz"].set_args(Namespace(foo="bar"))
assert self.config["baz"]["foo"].get() == "bar"
def test_nested_namespace(self):
args = Namespace(first="Hello", nested=Namespace(second="World"))
self.config.set_args(args, dots=True)
assert self.config["first"].get() == "Hello"
assert self.config["nested"]["second"].get() == "World"
beetbox-confuse-cc1e751/test/test_dump.py 0000664 0000000 0000000 00000006313 15136363550 0020563 0 ustar 00root root 0000000 0000000 import textwrap
import unittest
from collections import OrderedDict
import confuse
from . import _root
class PrettyDumpTest(unittest.TestCase):
def test_dump_null(self):
config = confuse.Configuration("myapp", read=False)
config.add({"foo": None})
yaml = config.dump().strip()
assert yaml == "foo:"
def test_dump_true(self):
config = confuse.Configuration("myapp", read=False)
config.add({"foo": True})
yaml = config.dump().strip()
assert yaml == "foo: yes"
def test_dump_false(self):
config = confuse.Configuration("myapp", read=False)
config.add({"foo": False})
yaml = config.dump().strip()
assert yaml == "foo: no"
def test_dump_short_list(self):
config = confuse.Configuration("myapp", read=False)
config.add({"foo": ["bar", "baz"]})
yaml = config.dump().strip()
assert yaml == "foo: [bar, baz]"
def test_dump_ordered_dict(self):
odict = OrderedDict()
odict["foo"] = "bar"
odict["bar"] = "baz"
odict["baz"] = "qux"
config = confuse.Configuration("myapp", read=False)
config.add({"key": odict})
yaml = config.dump().strip()
assert (
yaml
== textwrap.dedent("""
key:
foo: bar
bar: baz
baz: qux
""").strip()
)
def test_dump_sans_defaults(self):
config = confuse.Configuration("myapp", read=False)
config.add({"foo": "bar"})
config.sources[0].default = True
config.add({"baz": "qux"})
yaml = config.dump().strip()
assert yaml == "foo: bar\nbaz: qux"
yaml = config.dump(full=False).strip()
assert yaml == "baz: qux"
class RedactTest(unittest.TestCase):
def test_no_redaction(self):
config = _root({"foo": "bar"})
data = config.flatten(redact=True)
assert data == {"foo": "bar"}
def test_redact_key(self):
config = _root({"foo": "bar"})
config["foo"].redact = True
data = config.flatten(redact=True)
assert data == {"foo": "REDACTED"}
def test_unredact(self):
config = _root({"foo": "bar"})
config["foo"].redact = True
config["foo"].redact = False
data = config.flatten(redact=True)
assert data == {"foo": "bar"}
def test_dump_redacted(self):
config = confuse.Configuration("myapp", read=False)
config.add({"foo": "bar"})
config["foo"].redact = True
yaml = config.dump(redact=True).strip()
assert yaml == "foo: REDACTED"
def test_dump_unredacted(self):
config = confuse.Configuration("myapp", read=False)
config.add({"foo": "bar"})
config["foo"].redact = True
yaml = config.dump(redact=False).strip()
assert yaml == "foo: bar"
def test_dump_redacted_sans_defaults(self):
config = confuse.Configuration("myapp", read=False)
config.add({"foo": "bar"})
config.sources[0].default = True
config.add({"baz": "qux"})
config["baz"].redact = True
yaml = config.dump(redact=True, full=False).strip()
assert yaml == "baz: REDACTED"
beetbox-confuse-cc1e751/test/test_env.py 0000664 0000000 0000000 00000024011 15136363550 0020401 0 ustar 00root root 0000000 0000000 import os
import unittest
from unittest.mock import patch
import pytest
import confuse
from . import _root
class EnvSourceTest(unittest.TestCase):
def setUp(self):
self.env_patcher = patch.dict("os.environ", {})
self.env_patcher.start()
def tearDown(self):
self.env_patcher.stop()
def test_prefix(self):
os.environ["TEST_FOO"] = "a"
os.environ["BAR"] = "b"
config = _root(confuse.EnvSource("TEST_"))
assert config.get() == {"foo": "a"}
def test_number_type_conversion(self):
os.environ["TEST_FOO"] = "1"
os.environ["TEST_BAR"] = "2.0"
config = _root(confuse.EnvSource("TEST_"))
foo = config["foo"].get()
bar = config["bar"].get()
assert isinstance(foo, int)
assert foo == 1
assert isinstance(bar, float)
assert bar == 2.0
def test_bool_type_conversion(self):
os.environ["TEST_FOO"] = "true"
os.environ["TEST_BAR"] = "FALSE"
config = _root(confuse.EnvSource("TEST_"))
assert config["foo"].get() is True
assert config["bar"].get() is False
def test_null_type_conversion(self):
os.environ["TEST_FOO"] = "null"
os.environ["TEST_BAR"] = ""
config = _root(confuse.EnvSource("TEST_"))
assert config["foo"].get() is None
assert config["bar"].get() is None
def test_unset_lower_config(self):
os.environ["TEST_FOO"] = "null"
config = _root({"foo": "bar"})
assert config["foo"].get() == "bar"
config.set(confuse.EnvSource("TEST_"))
assert config["foo"].get() is None
def test_sep_default(self):
os.environ["TEST_FOO__BAR"] = "a"
os.environ["TEST_FOO_BAZ"] = "b"
config = _root(confuse.EnvSource("TEST_"))
assert config["foo"]["bar"].get() == "a"
assert config["foo_baz"].get() == "b"
def test_sep_single_underscore_adjacent_seperators(self):
os.environ["TEST_FOO__BAR"] = "a"
os.environ["TEST_FOO_BAZ"] = "b"
config = _root(confuse.EnvSource("TEST_", sep="_"))
assert config["foo"][""]["bar"].get() == "a"
assert config["foo"]["baz"].get() == "b"
def test_nested(self):
os.environ["TEST_FOO__BAR"] = "a"
os.environ["TEST_FOO__BAZ__QUX"] = "b"
config = _root(confuse.EnvSource("TEST_"))
assert config["foo"]["bar"].get() == "a"
assert config["foo"]["baz"]["qux"].get() == "b"
def test_nested_rev(self):
# Reverse to ensure order doesn't matter
os.environ["TEST_FOO__BAZ__QUX"] = "b"
os.environ["TEST_FOO__BAR"] = "a"
config = _root(confuse.EnvSource("TEST_"))
assert config["foo"]["bar"].get() == "a"
assert config["foo"]["baz"]["qux"].get() == "b"
def test_nested_clobber(self):
os.environ["TEST_FOO__BAR"] = "a"
os.environ["TEST_FOO__BAR__BAZ"] = "b"
config = _root(confuse.EnvSource("TEST_"))
# Clobbered
assert config["foo"]["bar"].get() == {"baz": "b"}
assert config["foo"]["bar"]["baz"].get() == "b"
def test_nested_clobber_rev(self):
# Reverse to ensure order doesn't matter
os.environ["TEST_FOO__BAR__BAZ"] = "b"
os.environ["TEST_FOO__BAR"] = "a"
config = _root(confuse.EnvSource("TEST_"))
# Clobbered
assert config["foo"]["bar"].get() == {"baz": "b"}
assert config["foo"]["bar"]["baz"].get() == "b"
def test_lower_applied_after_prefix_match(self):
os.environ["TEST_FOO"] = "a"
config = _root(confuse.EnvSource("test_", lower=True))
assert config.get() == {}
def test_lower_already_lowercase(self):
os.environ["TEST_foo"] = "a"
config = _root(confuse.EnvSource("TEST_", lower=True))
assert config.get() == {"foo": "a"}
def test_lower_does_not_alter_value(self):
os.environ["TEST_FOO"] = "UPPER"
config = _root(confuse.EnvSource("TEST_", lower=True))
assert config.get() == {"foo": "UPPER"}
def test_lower_false(self):
os.environ["TEST_FOO"] = "a"
config = _root(confuse.EnvSource("TEST_", lower=False))
assert config.get() == {"FOO": "a"}
def test_handle_lists_good_list(self):
os.environ["TEST_FOO__0"] = "a"
os.environ["TEST_FOO__1"] = "b"
os.environ["TEST_FOO__2"] = "c"
config = _root(confuse.EnvSource("TEST_", handle_lists=True))
assert config["foo"].get() == ["a", "b", "c"]
def test_handle_lists_good_list_rev(self):
# Reverse to ensure order doesn't matter
os.environ["TEST_FOO__2"] = "c"
os.environ["TEST_FOO__1"] = "b"
os.environ["TEST_FOO__0"] = "a"
config = _root(confuse.EnvSource("TEST_", handle_lists=True))
assert config["foo"].get() == ["a", "b", "c"]
def test_handle_lists_nested_lists(self):
os.environ["TEST_FOO__0__0"] = "a"
os.environ["TEST_FOO__0__1"] = "b"
os.environ["TEST_FOO__1__0"] = "c"
config = _root(confuse.EnvSource("TEST_", handle_lists=True))
assert config["foo"].get() == [["a", "b"], ["c"]]
def test_handle_lists_bad_list_missing_index(self):
os.environ["TEST_FOO__0"] = "a"
os.environ["TEST_FOO__2"] = "b"
os.environ["TEST_FOO__3"] = "c"
config = _root(confuse.EnvSource("TEST_", handle_lists=True))
assert config["foo"].get() == {"0": "a", "2": "b", "3": "c"}
def test_handle_lists_bad_list_non_zero_start(self):
os.environ["TEST_FOO__1"] = "a"
os.environ["TEST_FOO__2"] = "b"
os.environ["TEST_FOO__3"] = "c"
config = _root(confuse.EnvSource("TEST_", handle_lists=True))
assert config["foo"].get() == {"1": "a", "2": "b", "3": "c"}
def test_handle_lists_bad_list_non_numeric(self):
os.environ["TEST_FOO__0"] = "a"
os.environ["TEST_FOO__ONE"] = "b"
os.environ["TEST_FOO__2"] = "c"
config = _root(confuse.EnvSource("TEST_", handle_lists=True))
assert config["foo"].get() == {"0": "a", "one": "b", "2": "c"}
def test_handle_lists_top_level_always_dict(self):
os.environ["TEST_0"] = "a"
os.environ["TEST_1"] = "b"
os.environ["TEST_2"] = "c"
config = _root(confuse.EnvSource("TEST_", handle_lists=True))
assert config.get() == {"0": "a", "1": "b", "2": "c"}
def test_handle_lists_not_a_list(self):
os.environ["TEST_FOO__BAR"] = "a"
os.environ["TEST_FOO__BAZ"] = "b"
config = _root(confuse.EnvSource("TEST_", handle_lists=True))
assert config["foo"].get() == {"bar": "a", "baz": "b"}
def test_parse_yaml_docs_scalar(self):
os.environ["TEST_FOO"] = "a"
config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True))
assert config["foo"].get() == "a"
def test_parse_yaml_docs_list(self):
os.environ["TEST_FOO"] = "[a, b]"
config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True))
assert config["foo"].get() == ["a", "b"]
def test_parse_yaml_docs_dict(self):
os.environ["TEST_FOO"] = "{bar: a, baz: b}"
config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True))
assert config["foo"].get() == {"bar": "a", "baz": "b"}
def test_parse_yaml_docs_nested(self):
os.environ["TEST_FOO"] = "{bar: [a, b], baz: {qux: c}}"
config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True))
assert config["foo"]["bar"].get() == ["a", "b"]
assert config["foo"]["baz"].get() == {"qux": "c"}
def test_parse_yaml_docs_number_conversion(self):
os.environ["TEST_FOO"] = "{bar: 1, baz: 2.0}"
config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True))
bar = config["foo"]["bar"].get()
baz = config["foo"]["baz"].get()
assert isinstance(bar, int)
assert bar == 1
assert isinstance(baz, float)
assert baz == 2.0
def test_parse_yaml_docs_bool_conversion(self):
os.environ["TEST_FOO"] = "{bar: true, baz: FALSE}"
config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True))
assert config["foo"]["bar"].get() is True
assert config["foo"]["baz"].get() is False
def test_parse_yaml_docs_null_conversion(self):
os.environ["TEST_FOO"] = "{bar: null, baz: }"
config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True))
assert config["foo"]["bar"].get() is None
assert config["foo"]["baz"].get() is None
def test_parse_yaml_docs_syntax_error(self):
os.environ["TEST_FOO"] = "{:}"
with pytest.raises(confuse.ConfigError, match="TEST_FOO"):
_root(confuse.EnvSource("TEST_", parse_yaml_docs=True))
def test_parse_yaml_docs_false(self):
os.environ["TEST_FOO"] = "{bar: a, baz: b}"
config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=False))
assert config["foo"].get() == "{bar: a, baz: b}"
with pytest.raises(confuse.ConfigError):
config["foo"]["bar"].get()
class ConfigEnvTest(unittest.TestCase):
def setUp(self):
self.env_patcher = patch.dict(
"os.environ",
{
"TESTAPP_FOO": "a",
"TESTAPP_BAR__NESTED": "b",
"TESTAPP_BAZ_SEP_NESTED": "c",
"MYAPP_QUX_SEP_NESTED": "d",
},
)
self.env_patcher.start()
self.config = confuse.Configuration("TestApp", read=False)
def test_defaults(self):
self.config.set_env()
assert self.config.get() == {
"foo": "a",
"bar": {"nested": "b"},
"baz_sep_nested": "c",
}
def test_with_prefix(self):
self.config.set_env(prefix="MYAPP_")
assert self.config.get() == {"qux_sep_nested": "d"}
def test_with_sep(self):
self.config.set_env(sep="_sep_")
assert self.config.get() == {
"foo": "a",
"bar__nested": "b",
"baz": {"nested": "c"},
}
def test_with_prefix_and_sep(self):
self.config.set_env(prefix="MYAPP_", sep="_sep_")
assert self.config.get() == {"qux": {"nested": "d"}}
beetbox-confuse-cc1e751/test/test_paths.py 0000664 0000000 0000000 00000013672 15136363550 0020743 0 ustar 00root root 0000000 0000000 import ntpath
import os
import platform
import posixpath
import shutil
import tempfile
import unittest
from typing import ClassVar
import confuse
import confuse.yaml_util
DEFAULT = (platform.system, os.environ, os.path)
SYSTEMS = {
"Linux": ({"HOME": "/home/test", "XDG_CONFIG_HOME": "~/xdgconfig"}, posixpath),
"Darwin": ({"HOME": "/Users/test"}, posixpath),
"Windows": (
{
"APPDATA": "~\\winconfig",
"HOME": "C:\\Users\\test",
"USERPROFILE": "C:\\Users\\test",
},
ntpath,
),
}
def _touch(path):
open(path, "a").close()
class FakeHome(unittest.TestCase):
def setUp(self):
super().setUp()
self.home = tempfile.mkdtemp()
os.environ["HOME"] = self.home
os.environ["USERPROFILE"] = self.home
def tearDown(self):
super().tearDown()
shutil.rmtree(self.home)
class FakeSystem(unittest.TestCase):
SYS_NAME: ClassVar[str]
def setUp(self):
super().setUp()
self.os_path = os.path
os.environ = {} # type: ignore[assignment]
environ, os.path = SYSTEMS[self.SYS_NAME]
os.environ.update(environ)
platform.system = lambda: self.SYS_NAME
def tearDown(self):
super().tearDown()
platform.system, os.environ, os.path = DEFAULT
class LinuxTestCases(FakeSystem):
SYS_NAME = "Linux"
def test_both_xdg_and_fallback_dirs(self):
assert confuse.config_dirs() == [
"/home/test/.config",
"/home/test/xdgconfig",
"/etc/xdg",
"/etc",
]
def test_fallback_only(self):
del os.environ["XDG_CONFIG_HOME"]
assert confuse.config_dirs() == ["/home/test/.config", "/etc/xdg", "/etc"]
def test_xdg_matching_fallback_not_duplicated(self):
os.environ["XDG_CONFIG_HOME"] = "~/.config"
assert confuse.config_dirs() == ["/home/test/.config", "/etc/xdg", "/etc"]
def test_xdg_config_dirs(self):
os.environ["XDG_CONFIG_DIRS"] = "/usr/local/etc/xdg:/etc/xdg"
assert confuse.config_dirs() == [
"/home/test/.config",
"/home/test/xdgconfig",
"/usr/local/etc/xdg",
"/etc/xdg",
"/etc",
]
class OSXTestCases(FakeSystem):
SYS_NAME = "Darwin"
def test_mac_dirs(self):
assert confuse.config_dirs() == [
"/Users/test/.config",
"/Users/test/Library/Application Support",
"/etc/xdg",
"/etc",
]
def test_xdg_config_dirs(self):
os.environ["XDG_CONFIG_DIRS"] = "/usr/local/etc/xdg:/etc/xdg"
assert confuse.config_dirs() == [
"/Users/test/.config",
"/Users/test/Library/Application Support",
"/usr/local/etc/xdg",
"/etc/xdg",
"/etc",
]
class WindowsTestCases(FakeSystem):
SYS_NAME = "Windows"
def test_dir_from_environ(self):
assert confuse.config_dirs() == [
"C:\\Users\\test\\AppData\\Roaming",
"C:\\Users\\test\\winconfig",
]
def test_fallback_dir(self):
del os.environ["APPDATA"]
assert confuse.config_dirs() == ["C:\\Users\\test\\AppData\\Roaming"]
class ConfigFilenamesTest(unittest.TestCase):
def setUp(self):
self._old = os.path.isfile, confuse.yaml_util.load_yaml
os.path.isfile = lambda x: True # type: ignore[assignment]
confuse.yaml_util.load_yaml = lambda *args, **kwargs: {}
def tearDown(self):
os.path.isfile, confuse.yaml_util.load_yaml = self._old
def test_no_sources_when_files_missing(self):
config = confuse.Configuration("myapp", read=False)
filenames = [s.filename for s in config.sources]
assert filenames == []
def test_search_package(self):
config = confuse.Configuration("myapp", __name__, read=False)
config._add_default_source()
for source in config.sources:
if source.default:
default_source = source
break
else:
self.fail("no default source")
assert default_source.filename == os.path.join(
os.path.dirname(__file__), "config_default.yaml"
)
assert source.default
class EnvVarTest(FakeHome):
def setUp(self):
super().setUp()
self.config = confuse.Configuration("myapp", read=False)
os.environ["MYAPPDIR"] = self.home # use the tmp home as a config dir
def test_env_var_name(self):
assert self.config._env_var == "MYAPPDIR"
def test_env_var_dir_has_first_priority(self):
assert self.config.config_dir() == self.home
def test_env_var_missing(self):
del os.environ["MYAPPDIR"]
assert self.config.config_dir() != self.home
@unittest.skipUnless(platform.system() == "Linux", "Linux-specific tests")
class PrimaryConfigDirTest(FakeHome, FakeSystem):
SYS_NAME = "Linux" # conversion from posix to nt is easy
def setUp(self):
super().setUp()
self.config = confuse.Configuration("test", read=False)
def test_create_dir_if_none_exists(self):
path = os.path.join(self.home, ".config", "test")
assert not os.path.exists(path)
assert self.config.config_dir() == path
assert os.path.isdir(path)
def test_return_existing_dir(self):
path = os.path.join(self.home, "xdgconfig", "test")
os.makedirs(path)
_touch(os.path.join(path, confuse.CONFIG_FILENAME))
assert self.config.config_dir() == path
def test_do_not_create_dir_if_lower_priority_exists(self):
path1 = os.path.join(self.home, "xdgconfig", "test")
path2 = os.path.join(self.home, ".config", "test")
os.makedirs(path2)
_touch(os.path.join(path2, confuse.CONFIG_FILENAME))
assert not os.path.exists(path1)
assert os.path.exists(path2)
assert self.config.config_dir() == path2
assert not os.path.isdir(path1)
assert os.path.isdir(path2)
beetbox-confuse-cc1e751/test/test_utils.py 0000664 0000000 0000000 00000005455 15136363550 0020764 0 ustar 00root root 0000000 0000000 import unittest
from argparse import Namespace
from collections import OrderedDict
from typing import Any
import pytest
import confuse
class BuildDictTests(unittest.TestCase):
def test_pure_dicts(self):
config = {"foo": {"bar": 1}}
result = confuse.util.build_dict(config)
assert 1 == result["foo"]["bar"]
def test_namespaces(self):
config = Namespace(foo=Namespace(bar=2), another=1)
result = confuse.util.build_dict(config)
assert 2 == result["foo"]["bar"]
assert 1 == result["another"]
def test_dot_sep_keys(self):
config: dict[str, Any] = {"foo.bar": 1}
result = confuse.util.build_dict(config.copy())
assert 1 == result["foo.bar"]
result = confuse.util.build_dict(config.copy(), sep=".")
assert 1 == result["foo"]["bar"]
def test_dot_sep_keys_clobber(self):
args: list[tuple[str, Any]] = [("foo.bar", 1), ("foo.bar.zar", 2)]
config = OrderedDict(args)
result = confuse.util.build_dict(config.copy(), sep=".")
assert {"zar": 2} == result["foo"]["bar"]
assert 2 == result["foo"]["bar"]["zar"]
# Reverse and do it again! (should be stable)
args.reverse()
config = OrderedDict(args)
result = confuse.util.build_dict(config.copy(), sep=".")
assert {"zar": 2} == result["foo"]["bar"]
assert 2 == result["foo"]["bar"]["zar"]
def test_dot_sep_keys_no_clobber(self):
args: list[tuple[str, Any]] = [
("foo.bar", 1),
("foo.far", 2),
("foo.zar.dar", 4),
]
config = OrderedDict(args)
result = confuse.util.build_dict(config.copy(), sep=".")
assert 1 == result["foo"]["bar"]
assert 2 == result["foo"]["far"]
assert 4 == result["foo"]["zar"]["dar"]
def test_adjacent_underscores_sep_keys(self):
config: dict[str, Any] = {"foo__bar_baz": 1}
result = confuse.util.build_dict(config.copy())
assert 1 == result["foo__bar_baz"]
result = confuse.util.build_dict(config.copy(), sep="_")
assert 1 == result["foo"][""]["bar"]["baz"]
result = confuse.util.build_dict(config.copy(), sep="__")
assert 1 == result["foo"]["bar_baz"]
def test_keep_none(self):
config = {"foo": None}
result = confuse.util.build_dict(config.copy())
with pytest.raises(KeyError):
result["foo"]
result = confuse.util.build_dict(config.copy(), keep_none=True)
assert None is result["foo"]
def test_keep_none_with_nested(self):
config = {"foo": {"bar": None}}
result = confuse.util.build_dict(config.copy())
assert {} == result["foo"]
result = confuse.util.build_dict(config.copy(), keep_none=True)
assert None is result["foo"]["bar"]
beetbox-confuse-cc1e751/test/test_valid.py 0000664 0000000 0000000 00000061653 15136363550 0020725 0 ustar 00root root 0000000 0000000 import enum
import os
import unittest
from collections.abc import Mapping, Sequence
from typing import Any
import pytest
import confuse
from . import _root
class ValidConfigTest(unittest.TestCase):
def test_validate_simple_dict(self):
config = _root({"foo": 5})
valid = config.get({"foo": confuse.Integer()})
assert valid["foo"] == 5
def test_default_value(self):
config = _root({})
valid = config.get({"foo": confuse.Integer(8)})
assert valid["foo"] == 8
def test_undeclared_key_raises_keyerror(self):
config = _root({"foo": 5})
valid = config.get({"foo": confuse.Integer()})
with pytest.raises(KeyError):
valid["bar"]
def test_undeclared_key_ignored_from_input(self):
config = _root({"foo": 5, "bar": 6})
valid = config.get({"foo": confuse.Integer()})
with pytest.raises(KeyError):
valid["bar"]
def test_int_template_shortcut(self):
config = _root({"foo": 5})
valid = config.get({"foo": int})
assert valid["foo"] == 5
def test_int_default_shortcut(self):
config = _root({})
valid = config.get({"foo": 9})
assert valid["foo"] == 9
def test_attribute_access(self):
config = _root({"foo": 5})
valid = config.get({"foo": confuse.Integer()})
assert valid.foo == 5
def test_missing_required_value_raises_error_on_validate(self):
config = _root({})
with pytest.raises(confuse.NotFoundError):
config.get({"foo": confuse.Integer()})
def test_none_as_default(self):
config = _root({})
valid = config.get({"foo": confuse.Integer(None)})
assert valid["foo"] is None
def test_wrong_type_raises_error_on_validate(self):
config = _root({"foo": "bar"})
with pytest.raises(confuse.ConfigTypeError):
config.get({"foo": confuse.Integer()})
def test_validate_individual_value(self):
config = _root({"foo": 5})
valid = config["foo"].get(confuse.Integer())
assert valid == 5
def test_nested_dict_template(self):
config = _root({"foo": {"bar": 9}})
valid = config.get({"foo": {"bar": confuse.Integer()}})
assert valid["foo"]["bar"] == 9
def test_nested_attribute_access(self):
config = _root({"foo": {"bar": 8}})
valid = config.get({"foo": {"bar": confuse.Integer()}})
assert valid.foo.bar == 8
class AsTemplateTest(unittest.TestCase):
def test_plain_int_as_template(self):
typ = confuse.as_template(int)
assert isinstance(typ, confuse.Integer)
assert typ.default == confuse.REQUIRED
def test_concrete_int_as_template(self):
typ = confuse.as_template(2)
assert isinstance(typ, confuse.Integer)
assert typ.default == 2
def test_plain_string_as_template(self):
typ = confuse.as_template(str)
assert isinstance(typ, confuse.String)
assert typ.default == confuse.REQUIRED
def test_concrete_string_as_template(self):
typ = confuse.as_template("foo")
assert isinstance(typ, confuse.String)
assert typ.default == "foo"
def test_dict_as_template(self):
typ = confuse.as_template({"key": 9})
assert isinstance(typ, confuse.MappingTemplate)
assert isinstance(typ.subtemplates["key"], confuse.Integer)
assert typ.subtemplates["key"].default == 9
def test_nested_dict_as_template(self):
typ = confuse.as_template({"outer": {"inner": 2}})
assert isinstance(typ, confuse.MappingTemplate)
assert isinstance(typ.subtemplates["outer"], confuse.MappingTemplate)
assert isinstance(
typ.subtemplates["outer"].subtemplates["inner"], confuse.Integer
)
assert typ.subtemplates["outer"].subtemplates["inner"].default == 2
def test_list_as_template(self):
typ: confuse.OneOf[Any] = confuse.as_template(list())
assert isinstance(typ, confuse.OneOf)
assert typ.default == confuse.REQUIRED
def test_set_as_template(self):
typ: confuse.Choice[Any] = confuse.as_template(set())
assert isinstance(typ, confuse.Choice)
def test_enum_type_as_template(self):
typ = confuse.as_template(enum.Enum)
assert isinstance(typ, confuse.Choice)
def test_float_type_as_tempalte(self):
typ = confuse.as_template(float)
assert isinstance(typ, confuse.Number)
assert typ.default == confuse.REQUIRED
def test_concrete_float_as_template(self):
typ = confuse.as_template(2.0)
assert isinstance(typ, confuse.Number)
assert typ.default == 2.0
def test_none_as_template(self):
typ = confuse.as_template(None)
assert type(typ) is confuse.Template
assert typ.default is None
def test_required_as_template(self):
typ = confuse.as_template(confuse.REQUIRED)
assert type(typ) is confuse.Template
assert typ.default == confuse.REQUIRED
def test_dict_type_as_template(self):
typ = confuse.as_template(dict)
assert isinstance(typ, confuse.TypeTemplate)
assert typ.typ == Mapping
assert typ.default == confuse.REQUIRED
def test_list_type_as_template(self):
typ = confuse.as_template(list)
assert isinstance(typ, confuse.TypeTemplate)
assert typ.typ == Sequence
assert typ.default == confuse.REQUIRED
def test_set_type_as_template(self):
typ = confuse.as_template(set)
assert isinstance(typ, confuse.TypeTemplate)
assert typ.typ is set
assert typ.default == confuse.REQUIRED
def test_other_type_as_template(self):
class MyClass:
pass
typ = confuse.as_template(MyClass)
assert isinstance(typ, confuse.TypeTemplate)
assert typ.typ == MyClass
assert typ.default == confuse.REQUIRED
class StringTemplateTest(unittest.TestCase):
def test_validate_string(self):
config = _root({"foo": "bar"})
valid = config.get({"foo": confuse.String()})
assert valid["foo"] == "bar"
def test_string_default_value(self):
config = _root({})
valid = config.get({"foo": confuse.String("baz")})
assert valid["foo"] == "baz"
def test_pattern_matching(self):
config = _root({"foo": "bar", "baz": "zab"})
valid = config.get({"foo": confuse.String(pattern="^ba.$")})
assert valid["foo"] == "bar"
with pytest.raises(confuse.ConfigValueError):
config.get({"baz": confuse.String(pattern="!")})
def test_string_template_shortcut(self):
config = _root({"foo": "bar"})
valid = config.get({"foo": str})
assert valid["foo"] == "bar"
def test_string_default_shortcut(self):
config = _root({})
valid = config.get({"foo": "bar"})
assert valid["foo"] == "bar"
def test_check_string_type(self):
config = _root({"foo": 5})
with pytest.raises(confuse.ConfigTypeError):
config.get({"foo": confuse.String()})
class NumberTest(unittest.TestCase):
def test_validate_int_as_number(self):
config = _root({"foo": 2})
valid = config["foo"].get(confuse.Number())
assert isinstance(valid, int)
assert valid == 2
def test_validate_float_as_number(self):
config = _root({"foo": 3.0})
valid = config["foo"].get(confuse.Number())
assert isinstance(valid, float)
assert valid == 3.0
def test_validate_string_as_number(self):
config = _root({"foo": "bar"})
with pytest.raises(confuse.ConfigTypeError):
config["foo"].get(confuse.Number())
class ChoiceTest(unittest.TestCase):
def test_validate_good_choice_in_list(self):
config = _root({"foo": 2})
valid = config["foo"].get(confuse.Choice([1, 2, 4, 8, 16]))
assert valid == 2
def test_validate_bad_choice_in_list(self):
config = _root({"foo": 3})
with pytest.raises(confuse.ConfigValueError):
config["foo"].get(confuse.Choice([1, 2, 4, 8, 16]))
def test_validate_good_choice_in_dict(self):
config = _root({"foo": 2})
valid = config["foo"].get(confuse.Choice({2: "two", 4: "four"}))
assert valid == "two"
def test_validate_bad_choice_in_dict(self):
config = _root({"foo": 3})
with pytest.raises(confuse.ConfigValueError):
config["foo"].get(confuse.Choice({2: "two", 4: "four"}))
class OneOfTest(unittest.TestCase):
def test_default_value(self):
config = _root({})
valid = config["foo"].get(confuse.OneOf([], default="bar"))
assert valid == "bar"
def test_validate_good_choice_in_list(self):
config = _root({"foo": 2})
valid: str | int = config["foo"].get(
confuse.OneOf(
[
confuse.String(),
confuse.Integer(),
]
)
)
assert valid == 2
def test_validate_first_good_choice_in_list(self):
config = _root({"foo": 3.14})
valid: str | int = config["foo"].get(
confuse.OneOf(
[
confuse.Integer(),
confuse.Number(),
]
)
)
assert valid == 3
def test_validate_no_choice_in_list(self):
config = _root({"foo": None})
with pytest.raises(confuse.ConfigValueError):
config["foo"].get(
confuse.OneOf(
[
confuse.String(),
confuse.Integer(),
]
)
)
def test_validate_bad_template(self):
class BadTemplate:
pass
config = _root({})
with pytest.raises(ValueError, match="cannot convert to template"):
config.get(confuse.OneOf([BadTemplate()]))
del BadTemplate
class StrSeqTest(unittest.TestCase):
def test_string_list(self):
config = _root({"foo": ["bar", "baz"]})
valid = config["foo"].get(confuse.StrSeq())
assert valid == ["bar", "baz"]
def test_string_tuple(self):
config = _root({"foo": ("bar", "baz")})
valid = config["foo"].get(confuse.StrSeq())
assert valid == ["bar", "baz"]
def test_whitespace_separated_string(self):
config = _root({"foo": "bar baz"})
valid = config["foo"].get(confuse.StrSeq())
assert valid == ["bar", "baz"]
def test_invalid_type(self):
config = _root({"foo": 9})
with pytest.raises(confuse.ConfigTypeError):
config["foo"].get(confuse.StrSeq())
def test_invalid_sequence_type(self):
config = _root({"foo": ["bar", 2126]})
with pytest.raises(confuse.ConfigTypeError):
config["foo"].get(confuse.StrSeq())
class FilenameTest(unittest.TestCase):
def test_default_value(self):
config = _root({})
valid = config["foo"].get(confuse.Filename("foo/bar"))
assert valid == "foo/bar"
def test_default_none(self):
config = _root({})
valid = config["foo"].get(confuse.Filename(None))
assert valid is None
def test_missing_required_value(self):
config = _root({})
with pytest.raises(confuse.NotFoundError):
config["foo"].get(confuse.Filename())
def test_filename_relative_to_working_dir(self):
config = _root({"foo": "bar"})
valid = config["foo"].get(confuse.Filename(cwd="/dev/null"))
assert valid == os.path.realpath("/dev/null/bar")
def test_filename_relative_to_sibling(self):
config = _root({"foo": "/", "bar": "baz"})
valid = config.get(
{"foo": confuse.Filename(), "bar": confuse.Filename(relative_to="foo")}
)
assert valid.foo == os.path.realpath("/")
assert valid.bar == os.path.realpath("/baz")
def test_filename_working_dir_overrides_sibling(self):
config = _root({"foo": "bar"})
valid = config.get(
{"foo": confuse.Filename(cwd="/dev/null", relative_to="baz")}
)
assert valid.foo == os.path.realpath("/dev/null/bar")
def test_filename_relative_to_sibling_with_recursion(self):
config = _root({"foo": "/", "bar": "r", "baz": "z"})
with pytest.raises(confuse.ConfigTemplateError):
config.get(
{
"foo": confuse.Filename(relative_to="bar"),
"bar": confuse.Filename(relative_to="baz"),
"baz": confuse.Filename(relative_to="foo"),
}
)
def test_filename_relative_to_self(self):
config = _root({"foo": "bar"})
with pytest.raises(confuse.ConfigTemplateError):
config.get({"foo": confuse.Filename(relative_to="foo")})
def test_filename_relative_to_sibling_needs_siblings(self):
config = _root({"foo": "bar"})
with pytest.raises(confuse.ConfigTemplateError):
config["foo"].get(confuse.Filename(relative_to="bar"))
def test_filename_relative_to_sibling_needs_template(self):
config = _root({"foo": "/", "bar": "baz"})
with pytest.raises(confuse.ConfigTemplateError):
config.get({"bar": confuse.Filename(relative_to="foo")})
def test_filename_with_non_file_source(self):
config = _root({"foo": "foo/bar"})
valid = config["foo"].get(confuse.Filename())
assert valid == os.path.join(os.getcwd(), "foo", "bar")
def test_filename_with_file_source(self):
source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml")
config = _root(source)
config.config_dir = lambda: "/config/path" # type: ignore[attr-defined]
valid = config["foo"].get(confuse.Filename())
assert valid == os.path.realpath("/config/path/foo/bar")
def test_filename_with_default_source(self):
source = confuse.ConfigSource(
{"foo": "foo/bar"}, filename="/baz/config.yaml", default=True
)
config = _root(source)
config.config_dir = lambda: "/config/path" # type: ignore[attr-defined]
valid = config["foo"].get(confuse.Filename())
assert valid == os.path.realpath("/config/path/foo/bar")
def test_filename_use_config_source_dir(self):
source = confuse.ConfigSource(
{"foo": "foo/bar"}, filename="/baz/config.yaml", base_for_paths=True
)
config = _root(source)
config.config_dir = lambda: "/config/path" # type: ignore[attr-defined]
valid = config["foo"].get(confuse.Filename())
assert valid == os.path.realpath("/baz/foo/bar")
def test_filename_in_source_dir(self):
source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml")
config = _root(source)
config.config_dir = lambda: "/config/path" # type: ignore[attr-defined]
valid = config["foo"].get(confuse.Filename(in_source_dir=True))
assert valid == os.path.realpath("/baz/foo/bar")
def test_filename_in_source_dir_overrides_in_app_dir(self):
source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml")
config = _root(source)
config.config_dir = lambda: "/config/path" # type: ignore[attr-defined]
valid = config["foo"].get(confuse.Filename(in_source_dir=True, in_app_dir=True))
assert valid == os.path.realpath("/baz/foo/bar")
def test_filename_in_app_dir_non_file_source(self):
source = confuse.ConfigSource({"foo": "foo/bar"})
config = _root(source)
config.config_dir = lambda: "/config/path" # type: ignore[attr-defined]
valid = config["foo"].get(confuse.Filename(in_app_dir=True))
assert valid == os.path.realpath("/config/path/foo/bar")
def test_filename_in_app_dir_overrides_config_source_dir(self):
source = confuse.ConfigSource(
{"foo": "foo/bar"}, filename="/baz/config.yaml", base_for_paths=True
)
config = _root(source)
config.config_dir = lambda: "/config/path" # type: ignore[attr-defined]
valid = config["foo"].get(confuse.Filename(in_app_dir=True))
assert valid == os.path.realpath("/config/path/foo/bar")
def test_filename_wrong_type(self):
config = _root({"foo": 8})
with pytest.raises(confuse.ConfigTypeError):
config["foo"].get(confuse.Filename())
class PathTest(unittest.TestCase):
def test_path_value(self):
import pathlib
config = _root({"foo": "foo/bar"})
valid = config["foo"].get(confuse.Path())
assert valid == pathlib.Path(os.path.abspath("foo/bar"))
def test_default_value(self):
import pathlib
config = _root({})
valid = config["foo"].get(confuse.Path("foo/bar"))
assert valid == pathlib.Path("foo/bar")
def test_default_none(self):
config = _root({})
valid = config["foo"].get(confuse.Path(None))
assert valid is None
def test_missing_required_value(self):
config = _root({})
with pytest.raises(confuse.NotFoundError):
config["foo"].get(confuse.Path())
class BaseTemplateTest(unittest.TestCase):
def test_base_template_accepts_any_value(self):
config = _root({"foo": 4.2})
valid: float = config["foo"].get(confuse.Template())
assert valid == 4.2
def test_base_template_required(self):
config = _root({})
with pytest.raises(confuse.NotFoundError):
config["foo"].get(confuse.Template())
def test_base_template_with_default(self):
config = _root({})
valid = config["foo"].get(confuse.Template("bar"))
assert valid == "bar"
class TypeTemplateTest(unittest.TestCase):
def test_correct_type(self):
config = _root({"foo": set()})
valid = config["foo"].get(confuse.TypeTemplate(set))
assert valid == set()
def test_incorrect_type(self):
config = _root({"foo": dict()})
with pytest.raises(confuse.ConfigTypeError):
config["foo"].get(confuse.TypeTemplate(set))
def test_missing_required_value(self):
config = _root({})
with pytest.raises(confuse.NotFoundError):
config["foo"].get(confuse.TypeTemplate(set))
def test_default_value(self):
config = _root({})
valid = config["foo"].get(confuse.TypeTemplate(set, {1, 2}))
assert valid == {1, 2}
class SequenceTest(unittest.TestCase):
def test_int_list(self):
config = _root({"foo": [1, 2, 3]})
valid = config["foo"].get(confuse.Sequence(int))
assert valid == [1, 2, 3]
def test_dict_list(self):
config = _root({"foo": [{"bar": 1, "baz": 2}, {"bar": 3, "baz": 4}]})
valid = config["foo"].get(confuse.Sequence({"bar": int, "baz": int}))
assert valid == [{"bar": 1, "baz": 2}, {"bar": 3, "baz": 4}]
def test_invalid_item(self):
config = _root({"foo": [{"bar": 1, "baz": 2}, {"bar": 3, "bak": 4}]})
with pytest.raises(confuse.NotFoundError):
config["foo"].get(confuse.Sequence({"bar": int, "baz": int}))
def test_wrong_type(self):
config = _root({"foo": {"one": 1, "two": 2, "three": 3}})
with pytest.raises(confuse.ConfigTypeError):
config["foo"].get(confuse.Sequence(int))
def test_missing(self):
config = _root({"foo": [1, 2, 3]})
valid = config["bar"].get(confuse.Sequence(int))
assert valid == []
class MappingValuesTest(unittest.TestCase):
def test_int_dict(self):
config = _root({"foo": {"one": 1, "two": 2, "three": 3}})
valid = config["foo"].get(confuse.MappingValues(int))
assert valid == {"one": 1, "two": 2, "three": 3}
def test_dict_dict(self):
config = _root(
{"foo": {"first": {"bar": 1, "baz": 2}, "second": {"bar": 3, "baz": 4}}}
)
valid: dict[str, dict[str, int]] = config["foo"].get(
confuse.MappingValues({"bar": int, "baz": int})
)
assert valid == {"first": {"bar": 1, "baz": 2}, "second": {"bar": 3, "baz": 4}}
def test_invalid_item(self):
config = _root(
{"foo": {"first": {"bar": 1, "baz": 2}, "second": {"bar": 3, "bak": 4}}}
)
with pytest.raises(confuse.NotFoundError):
config["foo"].get(confuse.MappingValues({"bar": int, "baz": int}))
def test_wrong_type(self):
config = _root({"foo": [1, 2, 3]})
with pytest.raises(confuse.ConfigTypeError):
config["foo"].get(confuse.MappingValues(int))
def test_missing(self):
config = _root({"foo": {"one": 1, "two": 2, "three": 3}})
valid = config["bar"].get(confuse.MappingValues(int))
assert valid == {}
class OptionalTest(unittest.TestCase):
def test_optional_string_valid_type(self):
config = _root({"foo": "bar"})
valid = config["foo"].get(confuse.Optional(confuse.String()))
assert valid == "bar"
def test_optional_string_invalid_type(self):
config = _root({"foo": 5})
with pytest.raises(confuse.ConfigTypeError):
config["foo"].get(confuse.Optional(confuse.String()))
def test_optional_string_null(self):
config = _root({"foo": None})
valid = config["foo"].get(confuse.Optional(confuse.String()))
assert valid is None
def test_optional_string_null_default_value(self):
config = _root({"foo": None})
valid = config["foo"].get(confuse.Optional(confuse.String(), "baz"))
assert valid == "baz"
def test_optional_string_null_string_provides_default(self):
config = _root({"foo": None})
valid = config["foo"].get(confuse.Optional(confuse.String("baz")))
assert valid == "baz"
def test_optional_string_null_string_default_override(self):
config = _root({"foo": None})
valid = config["foo"].get(
confuse.Optional(confuse.String("baz"), default="bar")
)
assert valid == "bar"
def test_optional_string_allow_missing_no_explicit_default(self):
config = _root({})
valid = config["foo"].get(confuse.Optional(confuse.String()))
assert valid is None
def test_optional_string_allow_missing_default_value(self):
config = _root({})
valid = config["foo"].get(confuse.Optional(confuse.String(), "baz"))
assert valid == "baz"
def test_optional_string_missing_not_allowed(self):
config = _root({})
with pytest.raises(confuse.NotFoundError):
config["foo"].get(confuse.Optional(confuse.String(), allow_missing=False))
def test_optional_string_null_missing_not_allowed(self):
config = _root({"foo": None})
valid = config["foo"].get(
confuse.Optional(confuse.String(), allow_missing=False)
)
assert valid is None
def test_optional_mapping_template_valid(self):
config = _root({"foo": {"bar": 5, "baz": "bak"}})
template = {"bar": confuse.Integer(), "baz": confuse.String()}
valid = config.get({"foo": confuse.Optional(template)})
assert valid["foo"]
assert valid["foo"]["bar"] == 5
assert valid["foo"]["baz"] == "bak"
def test_optional_mapping_template_invalid(self):
config = _root({"foo": {"bar": 5, "baz": 10}})
template = {"bar": confuse.Integer(), "baz": confuse.String()}
with pytest.raises(confuse.ConfigTypeError):
config.get({"foo": confuse.Optional(template)})
def test_optional_mapping_template_null(self):
config = _root({"foo": None})
template = {"bar": confuse.Integer(), "baz": confuse.String()}
valid = config.get({"foo": confuse.Optional(template)})
assert valid["foo"] is None
def test_optional_mapping_template_null_default_value(self):
config = _root({"foo": None})
template = {"bar": confuse.Integer(), "baz": confuse.String()}
valid = config.get({"foo": confuse.Optional(template, {})})
assert isinstance(valid["foo"], dict)
def test_optional_mapping_template_allow_missing_no_explicit_default(self):
config = _root({})
template = {"bar": confuse.Integer(), "baz": confuse.String()}
valid = config.get({"foo": confuse.Optional(template)})
assert valid["foo"] is None
def test_optional_mapping_template_allow_missing_default_value(self):
config = _root({})
template = {"bar": confuse.Integer(), "baz": confuse.String()}
valid = config.get({"foo": confuse.Optional(template, {})})
assert isinstance(valid["foo"], dict)
def test_optional_mapping_template_missing_not_allowed(self):
config = _root({})
template = {"bar": confuse.Integer(), "baz": confuse.String()}
with pytest.raises(confuse.NotFoundError):
config.get({"foo": confuse.Optional(template, allow_missing=False)})
def test_optional_mapping_template_null_missing_not_allowed(self):
config = _root({"foo": None})
template = {"bar": confuse.Integer(), "baz": confuse.String()}
valid = config.get({"foo": confuse.Optional(template, allow_missing=False)})
assert valid["foo"] is None
beetbox-confuse-cc1e751/test/test_validation.py 0000664 0000000 0000000 00000011105 15136363550 0021743 0 ustar 00root root 0000000 0000000 import enum
import os
import unittest
import pytest
import confuse
from . import _root
class TypeCheckTest(unittest.TestCase):
def test_str_type_correct(self):
config = _root({"foo": "bar"})
value = config["foo"].get(str)
assert value == "bar"
def test_str_type_incorrect(self):
config = _root({"foo": 2})
with pytest.raises(confuse.ConfigTypeError):
config["foo"].get(str)
def test_int_type_correct(self):
config = _root({"foo": 2})
value = config["foo"].get(int)
assert value == 2
def test_int_type_incorrect(self):
config = _root({"foo": "bar"})
with pytest.raises(confuse.ConfigTypeError):
config["foo"].get(int)
class BuiltInValidatorTest(unittest.TestCase):
def test_as_filename_with_non_file_source(self):
config = _root({"foo": "foo/bar"})
value = config["foo"].as_filename()
assert value == os.path.join(os.getcwd(), "foo", "bar")
def test_as_filename_with_file_source(self):
source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml")
config = _root(source)
config.config_dir = lambda: "/config/path" # type: ignore[attr-defined]
value = config["foo"].as_filename()
assert value == os.path.realpath("/config/path/foo/bar")
def test_as_filename_with_default_source(self):
source = confuse.ConfigSource(
{"foo": "foo/bar"}, filename="/baz/config.yaml", default=True
)
config = _root(source)
config.config_dir = lambda: "/config/path" # type: ignore[attr-defined]
value = config["foo"].as_filename()
assert value == os.path.realpath("/config/path/foo/bar")
def test_as_filename_wrong_type(self):
config = _root({"foo": None})
with pytest.raises(confuse.ConfigTypeError):
config["foo"].as_filename()
def test_as_path(self):
config = _root({"foo": "foo/bar"})
path_str = os.path.join(os.getcwd(), "foo", "bar")
try:
import pathlib
except ImportError:
with pytest.raises(ImportError):
value = config["foo"].as_path()
else:
value = config["foo"].as_path()
path = pathlib.Path(path_str)
assert value == path
def test_as_choice_correct(self):
config = _root({"foo": "bar"})
value = config["foo"].as_choice(["foo", "bar", "baz"])
assert value == "bar"
def test_as_choice_error(self):
config = _root({"foo": "bar"})
with pytest.raises(confuse.ConfigValueError):
config["foo"].as_choice(["foo", "baz"])
def test_as_choice_with_dict(self):
config = _root({"foo": "bar"})
res = config["foo"].as_choice(
{
"bar": "baz",
"x": "y",
}
)
assert res == "baz"
def test_as_choice_with_enum(self):
class Foobar(enum.Enum):
Foo = "bar"
config = _root({"foo": Foobar.Foo.value})
res = config["foo"].as_choice(Foobar)
assert res == Foobar.Foo
def test_as_choice_with_enum_error(self):
class Foobar(enum.Enum):
Foo = "bar"
config = _root({"foo": "foo"})
with pytest.raises(confuse.ConfigValueError):
config["foo"].as_choice(Foobar)
def test_as_number_float(self):
config = _root({"f": 1.0})
config["f"].as_number()
def test_as_number_int(self):
config = _root({"i": 2})
config["i"].as_number()
def test_as_number_string(self):
config = _root({"s": "a"})
with pytest.raises(confuse.ConfigTypeError):
config["s"].as_number()
def test_as_str_seq_str(self):
config = _root({"k": "a b c"})
assert config["k"].as_str_seq() == ["a", "b", "c"]
def test_as_str_seq_list(self):
config = _root({"k": ["a b", "c"]})
assert config["k"].as_str_seq() == ["a b", "c"]
def test_as_str(self):
config = _root({"s": "foo"})
config["s"].as_str()
def test_as_str_non_string(self):
config = _root({"f": 1.0})
with pytest.raises(confuse.ConfigTypeError):
config["f"].as_str()
def test_as_str_expanded(self):
config = _root({"s": "${CONFUSE_TEST_VAR}/bar"})
os.environ["CONFUSE_TEST_VAR"] = "foo"
assert config["s"].as_str_expanded() == "foo/bar"
def test_as_pairs(self):
config = _root({"k": [{"a": "A"}, "b", ["c", "C"]]})
assert [("a", "A"), ("b", None), ("c", "C")] == config["k"].as_pairs()
beetbox-confuse-cc1e751/test/test_views.py 0000664 0000000 0000000 00000022473 15136363550 0020760 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import unittest
from typing import TYPE_CHECKING
import pytest
import confuse
from . import _root
if TYPE_CHECKING:
from collections.abc import Iterator
from confuse import Subview
class SingleSourceTest(unittest.TestCase):
def test_dict_access(self):
config = _root({"foo": "bar"})
value = config["foo"].get()
assert value == "bar"
def test_list_access(self):
config = _root({"foo": ["bar", "baz"]})
value = config["foo"][1].get()
assert value == "baz"
def test_missing_key(self):
config = _root({"foo": "bar"})
with pytest.raises(confuse.NotFoundError):
config["baz"].get()
def test_missing_index(self):
config = _root({"l": ["foo", "bar"]})
with pytest.raises(confuse.NotFoundError):
config["l"][5].get()
def test_dict_iter(self):
config = _root({"foo": "bar", "baz": "qux"})
keys = [key for key in config]
assert set(keys) == {"foo", "baz"}
def test_list_iter(self):
config = _root({"l": ["foo", "bar"]})
# TODO(@snejus): we need to split Subview to SequenceView and MappingView in
# order to have automatic resolution here
_items: Iterator[Subview] = iter(config["l"])
items = [subview.get() for subview in _items]
assert items == ["foo", "bar"]
def test_int_iter(self):
config = _root({"n": 2})
with pytest.raises(confuse.ConfigTypeError):
[item for item in config["n"]]
def test_dict_keys(self):
config = _root({"foo": "bar", "baz": "qux"})
keys = config.keys()
assert set(keys) == {"foo", "baz"}
def test_dict_values(self):
config = _root({"foo": "bar", "baz": "qux"})
values = [value.get() for value in config.values()]
assert set(values) == {"bar", "qux"}
def test_dict_items(self):
config = _root({"foo": "bar", "baz": "qux"})
items = [(key, value.get()) for (key, value) in config.items()]
assert set(items) == {("foo", "bar"), ("baz", "qux")}
def test_list_keys_error(self):
config = _root({"l": ["foo", "bar"]})
with pytest.raises(confuse.ConfigTypeError):
config["l"].keys()
def test_list_sequence(self):
config = _root({"l": ["foo", "bar"]})
items = [item.get() for item in config["l"].sequence()]
assert items == ["foo", "bar"]
def test_dict_sequence_error(self):
config = _root({"foo": "bar", "baz": "qux"})
with pytest.raises(confuse.ConfigTypeError):
list(config.sequence())
def test_dict_contents(self):
config = _root({"foo": "bar", "baz": "qux"})
contents = config.all_contents()
assert set(contents) == {"foo", "baz"}
def test_list_contents(self):
config = _root({"l": ["foo", "bar"]})
contents = config["l"].all_contents()
assert list(contents) == ["foo", "bar"]
def test_int_contents(self):
config = _root({"n": 2})
with pytest.raises(confuse.ConfigTypeError):
list(config["n"].all_contents())
class ConverstionTest(unittest.TestCase):
def test_str_conversion_from_str(self):
config = _root({"foo": "bar"})
value = str(config["foo"])
assert value == "bar"
def test_str_conversion_from_int(self):
config = _root({"foo": 2})
value = str(config["foo"])
assert value == "2"
def test_bool_conversion_from_bool(self):
config = _root({"foo": True})
value = bool(config["foo"])
assert value
def test_bool_conversion_from_int(self):
config = _root({"foo": 0})
value = bool(config["foo"])
assert not value
class NameTest(unittest.TestCase):
def test_root_name(self):
config = _root()
assert config.name == "root"
def test_string_access_name(self):
config = _root()
name = config["foo"].name
assert name == "foo"
def test_int_access_name(self):
config = _root()
name = config[5].name
assert name == "#5"
def test_nested_access_name(self):
config = _root()
name = config[5]["foo"]["bar"][20].name
assert name == "#5.foo.bar#20"
class MultipleSourceTest(unittest.TestCase):
def test_dict_access_shadowed(self):
config = _root({"foo": "bar"}, {"foo": "baz"})
value = config["foo"].get()
assert value == "bar"
def test_dict_access_fall_through(self):
config = _root({"qux": "bar"}, {"foo": "baz"})
value = config["foo"].get()
assert value == "baz"
def test_dict_access_missing(self):
config = _root({"qux": "bar"}, {"foo": "baz"})
with pytest.raises(confuse.NotFoundError):
config["fred"].get()
def test_list_access_shadowed(self):
config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]})
value = config["l"][1].get()
assert value == "b"
def test_list_access_fall_through(self):
config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]})
value = config["l"][2].get()
assert value == "e"
def test_list_access_missing(self):
config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]})
with pytest.raises(confuse.NotFoundError):
config["l"][3].get()
def test_access_dict_replaced(self):
config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}})
value = config["foo"].get()
assert value == {"bar": "baz"}
def test_dict_keys_merged(self):
config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}})
keys = config["foo"].keys()
assert set(keys) == {"bar", "qux"}
def test_dict_keys_replaced(self):
config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}})
keys = config["foo"].keys()
assert list(keys) == ["bar"]
def test_dict_values_merged(self):
config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}})
values = [value.get() for value in config["foo"].values()]
assert set(values) == {"baz", "fred"}
def test_dict_values_replaced(self):
config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}})
values = [value.get() for value in config["foo"].values()]
assert list(values) == ["baz"]
def test_dict_items_merged(self):
config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}})
items = [(key, value.get()) for (key, value) in config["foo"].items()]
assert set(items) == {("bar", "baz"), ("qux", "fred")}
def test_dict_items_replaced(self):
config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}})
items = [(key, value.get()) for (key, value) in config["foo"].items()]
assert list(items) == [("bar", "baz")]
def test_list_sequence_shadowed(self):
config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]})
items = [item.get() for item in config["l"].sequence()]
assert items == ["a", "b"]
def test_list_sequence_shadowed_by_dict(self):
config = _root({"foo": {"bar": "baz"}}, {"foo": ["qux", "fred"]})
with pytest.raises(confuse.ConfigTypeError):
list(config["foo"].sequence())
def test_dict_contents_concatenated(self):
config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}})
contents = config["foo"].all_contents()
assert set(contents) == {"bar", "qux"}
def test_dict_contents_concatenated_not_replaced(self):
config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}})
contents = config["foo"].all_contents()
assert list(contents) == ["bar", "bar"]
def test_list_contents_concatenated(self):
config = _root({"foo": ["bar", "baz"]}, {"foo": ["qux", "fred"]})
contents = config["foo"].all_contents()
assert list(contents) == ["bar", "baz", "qux", "fred"]
def test_int_contents_error(self):
config = _root({"foo": ["bar", "baz"]}, {"foo": 5})
with pytest.raises(confuse.ConfigTypeError):
list(config["foo"].all_contents())
def test_list_and_dict_contents_concatenated(self):
config = _root({"foo": ["bar", "baz"]}, {"foo": {"qux": "fred"}})
contents = config["foo"].all_contents()
assert list(contents) == ["bar", "baz", "qux"]
def test_add_source(self):
config = _root({"foo": "bar"})
config.add({"baz": "qux"})
assert config["foo"].get() == "bar"
assert config["baz"].get() == "qux"
class SetTest(unittest.TestCase):
def test_set_missing_top_level_key(self):
config = _root({})
config["foo"] = "bar"
assert config["foo"].get() == "bar"
def test_override_top_level_key(self):
config = _root({"foo": "bar"})
config["foo"] = "baz"
assert config["foo"].get() == "baz"
def test_set_second_level_key(self):
config = _root({})
config["foo"]["bar"] = "baz"
assert config["foo"]["bar"].get() == "baz"
def test_override_second_level_key(self):
config = _root({"foo": {"bar": "qux"}})
config["foo"]["bar"] = "baz"
assert config["foo"]["bar"].get() == "baz"
def test_override_list_index(self):
config = _root({"foo": ["a", "b", "c"]})
config["foo"][1] = "bar"
assert config["foo"][1].get() == "bar"
beetbox-confuse-cc1e751/test/test_yaml.py 0000664 0000000 0000000 00000007324 15136363550 0020563 0 ustar 00root root 0000000 0000000 import unittest
from collections import OrderedDict
import pytest
import yaml
import confuse
from . import TempDir
def load(s):
return yaml.load(s, Loader=confuse.Loader)
class ParseTest(unittest.TestCase):
def test_dict_parsed_as_ordereddict(self):
v = load("a: b\nc: d")
assert isinstance(v, OrderedDict)
assert list(v) == ["a", "c"]
def test_string_beginning_with_percent(self):
v = load("foo: %bar")
assert v["foo"] == "%bar"
class FileParseTest(unittest.TestCase):
def _parse_contents(self, contents):
with TempDir() as temp:
path = temp.sub("test_config.yaml", contents)
return confuse.load_yaml(path)
def test_load_file(self):
v = self._parse_contents(b"foo: bar")
assert v["foo"] == "bar"
def test_syntax_error(self):
with pytest.raises(confuse.ConfigError, match=r"test_config\.yaml"):
self._parse_contents(b":")
def test_reload_conf(self):
with TempDir() as temp:
path = temp.sub("test_config.yaml", b"foo: bar")
config = confuse.Configuration("test", __name__)
config.set_file(filename=path)
assert config["foo"].get() == "bar"
temp.sub("test_config.yaml", b"foo: bar2\ntest: hello world")
config.reload()
assert config["foo"].get() == "bar2"
assert config["test"].get() == "hello world"
def test_tab_indentation_error(self):
with pytest.raises(confuse.ConfigError, match="found tab"):
self._parse_contents(b"foo:\n\tbar: baz")
class StringParseTest(unittest.TestCase):
def test_load_string(self):
v = confuse.load_yaml_string("foo: bar", "test")
assert v["foo"] == "bar"
def test_string_syntax_error(self):
with pytest.raises(confuse.ConfigError, match="test"):
confuse.load_yaml_string(":", "test")
def test_string_tab_indentation_error(self):
with pytest.raises(confuse.ConfigError, match="found tab"):
confuse.load_yaml_string("foo:\n\tbar: baz", "test")
class ParseAsScalarTest(unittest.TestCase):
def test_text_string(self):
v = confuse.yaml_util.parse_as_scalar("foo", confuse.Loader)
assert v == "foo"
def test_number_string_to_int(self):
v = confuse.yaml_util.parse_as_scalar("1", confuse.Loader)
assert isinstance(v, int)
assert v == 1
def test_number_string_to_float(self):
v = confuse.yaml_util.parse_as_scalar("1.0", confuse.Loader)
assert isinstance(v, float)
assert v == 1.0
def test_bool_string_to_bool(self):
v = confuse.yaml_util.parse_as_scalar("true", confuse.Loader)
assert v is True
def test_empty_string_to_none(self):
v = confuse.yaml_util.parse_as_scalar("", confuse.Loader)
assert v is None
def test_null_string_to_none(self):
v = confuse.yaml_util.parse_as_scalar("null", confuse.Loader)
assert v is None
def test_dict_string_unchanged(self):
v = confuse.yaml_util.parse_as_scalar('{"foo": "bar"}', confuse.Loader)
assert v == '{"foo": "bar"}'
def test_dict_unchanged(self):
v = confuse.yaml_util.parse_as_scalar({"foo": "bar"}, confuse.Loader)
assert v == {"foo": "bar"}
def test_list_string_unchanged(self):
v = confuse.yaml_util.parse_as_scalar('["foo", "bar"]', confuse.Loader)
assert v == '["foo", "bar"]'
def test_list_unchanged(self):
v = confuse.yaml_util.parse_as_scalar(["foo", "bar"], confuse.Loader)
assert v == ["foo", "bar"]
def test_invalid_yaml_string_unchanged(self):
v = confuse.yaml_util.parse_as_scalar("!", confuse.Loader)
assert v == "!"