8 Commits
0.5.2 ... 0.8.2

19 changed files with 1895 additions and 893 deletions

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ __pycache__/
.ipynb_checkpoints/ .ipynb_checkpoints/
.pytest_cache/ .pytest_cache/
.python-version .python-version
.venv/
# vendor and build files # vendor and build files
dist/ dist/

View File

@@ -10,7 +10,7 @@ Below is a simple example demonstrating two system-wide theme switches:
This GIF shows two `symconf` calls, the first of which applies a `gruvbox` dark theme and This GIF shows two `symconf` calls, the first of which applies a `gruvbox` dark theme and
the second a dark [`monobiome`][1] variant. Each call (of the form `symconf config -m dark -s the second a dark [`monobiome`][1] variant. Each call (of the form `symconf config -m dark -s
<style>`) indicates a dark mode preference and a particular color palette that should be style`) indicates a dark mode preference and a particular color palette that should be
used when populating config file templates. Specifically, in this example, invoking used when populating config file templates. Specifically, in this example, invoking
`symconf` results in the following app-specific config updates: `symconf` results in the following app-specific config updates:
@@ -19,8 +19,9 @@ used when populating config file templates. Specifically, in this example, invok
are responsive to `prefers-color-scheme`) are responsive to `prefers-color-scheme`)
- **kitty**: theme template is re-generated using the specified palette, and `kitty` - **kitty**: theme template is re-generated using the specified palette, and `kitty`
processes are sent a message to live-reload the new config file processes are sent a message to live-reload the new config file
- **neovim**: a `vim` theme file is generated from the chosen palette, and running - **neovim**: a `vim` theme file (along with a statusline theme) is generated from the
instances of `neovim` are sent a message to re-source this theme chosen palette, and running instances of `neovim` are sent a message to re-source this
theme (via `nvim --remote-send`)
- **waybar**: bar styles are updated to match the mode setting - **waybar**: bar styles are updated to match the mode setting
- **sway**: the background color and window borders are dynamically set to base palette - **sway**: the background color and window borders are dynamically set to base palette
colors, and `swaymsg reload` is called colors, and `swaymsg reload` is called
@@ -46,18 +47,24 @@ registered locations. When necessary, `symconf` will also match and execute scri
reload apps after updating their configuration. reload apps after updating their configuration.
You can find more details on how `symconf`'s matching scheme works in You can find more details on how `symconf`'s matching scheme works in
[Matching](docs/reference/matching). [Matching](docs/reference/matching.md).
# Configuring # Configuring
Before using, you must first set up your config directory to house your config files and Before using, you must first set up your config directory to house your config files and
give `symconf` something to act on. See [Configuring](docs/reference/configuring) for give `symconf` something to act on. See [Configuring](docs/reference/configuring.md) for
details. details.
# Installation # Installation
The recommended way to install `symconf` is via `pipx`, which is particularly well-suited The recommended way to install `symconf` is via `pipx`, which is particularly well-suited
for managing Python packages meant to be used as CLI programs. With `pipx` on your system, for managing Python packages meant to be used as CLI programs. With `uv` on your system,
you can install with you can install with
```sh
uv tool install symconf
```
Alternatively, you can use `pipx` to similar effect:
```sh ```sh
pipx install symconf pipx install symconf
``` ```
@@ -73,13 +80,25 @@ You can also install via `pip`, or clone and install locally.
all registered apps. all registered apps.
* `-m --mode`: preferred lightness mode/scheme, either `light`, `dark`, `any`, or * `-m --mode`: preferred lightness mode/scheme, either `light`, `dark`, `any`, or
`none`. `none`.
* `-s --style`: style indicate, often the name of a color palette, capturing thematic * `-s --style`: style indicator, often the name of a color palette, capturing thematic
details in a config file to be matched. `any` or `none` are reserved keywords (see details in a config file to be matched. `any` or `none` are reserved keywords (see
below). below).
* `-T --template-vars`: additional groups to use when populating templates, in the form * `-T --template-vars`: additional groups to use when populating templates, in the form
`<group>=<value>`, where `<group>` is a template group with a folder `<group>=<value>`, where `<group>` is a template group with a folder
`$CONFIG_HOME/groups/<group>/` and `<value>` should correspond to a TOML file in this `$CONFIG_HOME/groups/<group>/` and `<value>` should correspond to a TOML file in this
folder (i.e., `<value>.toml`). folder (i.e., `<value>.toml`).
- `symconf generate` is a subcommand that can be used for batch generation of config
files. It accepts the same arguments as `symconf config`, but rather than selecting the
best match to be used for the system setting, all matching templates are generated.
There is one additional required argument:
* `-o --output-dir`: the directory under which generated config files should be written.
App-specific subdirectories are created to house config files for each provided app.
- `symconf install`: runs install scripts for matching apps that specify one
* `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to consider
all registered apps.
- `symconf update`: runs update scripts for matching apps that specify one
* `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to consider
all registered apps.
The keywords `any` and `none` can be used when specifying `--mode`, `--style`, or as a The keywords `any` and `none` can be used when specifying `--mode`, `--style`, or as a
value in `--template-vars` (and we refer to each of these variables as _factors_ that help value in `--template-vars` (and we refer to each of these variables as _factors_ that help

View File

@@ -3,43 +3,47 @@
# For the full list of built-in configuration values, see the documentation: # For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html # https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information ----------------------------------------------------- # -- Project information ------------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'symconf' project = "symconf"
copyright = '2024, Sam Griesemer' copyright = "2025, Sam Griesemer"
author = 'Sam Griesemer' author = "Sam Griesemer"
# -- General configuration --------------------------------------------------- # -- General configuration ----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [ extensions = [
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.autosummary", # enables a directive to be specified manually that gathers # enables a directive to be specified manually that gathers module/object
# module/object summary details in a table # summary details in a table
"sphinx.ext.viewcode", # allow viewing source in the HTML pages "sphinx.ext.autosummary",
"myst_parser", # only really applies to manual docs; docstrings still need RST-like # allow viewing source in the HTML pages
"sphinx.ext.napoleon", # enables Google-style docstring formats "sphinx.ext.viewcode",
"sphinx_autodoc_typehints", # external extension that allows arg types to be inferred by type hints # only really applies to manual docs; docstrings still need RST-like
"myst_parser",
# enables Google-style docstring formats
"sphinx.ext.napoleon",
# external extension that allows arg types to be inferred by type hints
"sphinx_autodoc_typehints",
] ]
autosummary_generate = True autosummary_generate = True
autosummary_imported_members = True autosummary_imported_members = True
# include __init__ definitions in autodoc # include __init__ definitions in autodoc
autodoc_default_options = { autodoc_default_options = {
'special-members': '__init__', "special-members": "__init__",
} }
templates_path = ['_templates'] templates_path = ["_templates"]
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output --------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'furo' html_theme = "furo"
html_static_path = ['_static'] html_static_path = ["_static"]
#html_sidebars = { # html_sidebars = {
# '**': ['/modules.html'], # '**': ['/modules.html'],
#} # }

View File

@@ -3,22 +3,29 @@
{ref}`modindex` {ref}`modindex`
{ref}`search` {ref}`search`
## Top-level module overview
```{eval-rst} ```{eval-rst}
.. autosummary:: .. autosummary::
:nosignatures: :nosignatures:
:recursive:
# list modules here for quick links symconf.config
symconf.template
symconf.matching
symconf.reader
symconf.runner
``` ```
## Auto-reference contents
```{toctree} ```{toctree}
:maxdepth: 3 :maxdepth: 3
:caption: Autoref
_autoref/symconf.rst _autoref/symconf.rst
``` ```
```{toctree} ```{toctree}
:maxdepth: 3 :maxdepth: 2
:caption: Contents :caption: Contents
reference/configuring reference/configuring
@@ -28,4 +35,7 @@ reference/documentation/index
``` ```
```{include} ../README.md ```{include} ../README.md
:relative-docs: docs/
:relative-images:
``` ```

0
example/README.md Normal file
View File

View File

@@ -1,27 +1,26 @@
[build-system] [build-system]
requires = ["setuptools", "wheel", "setuptools-git-versioning>=2.0,<3"] requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.setuptools-git-versioning]
enabled = true
[project] [project]
name = "symconf" name = "symconf"
version = "0.8.2"
description = "Local app configuration manager" description = "Local app configuration manager"
readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dynamic = ["version"]
#license = {file = "LICENSE"}
authors = [ authors = [
{ name="Sam Griesemer", email="samgriesemer+git@gmail.com" }, { name="Sam Griesemer", email="samgriesemer+git@gmail.com" },
] ]
keywords = ["config"]
readme = "README.md"
license = "MIT"
keywords = ["tempate-engine", "theme-switcher", "configuration-files"]
classifiers = [ classifiers = [
"Programming Language :: Python :: 3.12", "Programming Language :: Python",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
] ]
dependencies = [ dependencies = [
"pyxdg", "pyxdg",
@@ -32,15 +31,16 @@ dependencies = [
symconf = "symconf.__main__:main" symconf = "symconf.__main__:main"
[project.optional-dependencies] [project.optional-dependencies]
tests = ["pytest"] doc = [
docs = [
"sphinx", "sphinx",
"sphinx-togglebutton", "sphinx-togglebutton",
"sphinx-autodoc-typehints", "sphinx-autodoc-typehints",
"furo", "furo",
"myst-parser", "myst-parser",
] ]
build = ["build", "twine"] dev = [
"pytest"
]
[project.urls] [project.urls]
Homepage = "https://doc.olog.io/symconf" Homepage = "https://doc.olog.io/symconf"
@@ -48,6 +48,25 @@ Documentation = "https://doc.olog.io/symconf"
Repository = "https://git.olog.io/olog/symconf" Repository = "https://git.olog.io/olog/symconf"
Issues = "https://git.olog.io/olog/symconf/issues" Issues = "https://git.olog.io/olog/symconf/issues"
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["symconf*"] # pattern to match package names include = ["symconf*"] # pattern to match package names
[tool.ruff]
line-length = 79
[tool.ruff.lint]
select = ["ANN", "E", "F", "UP", "B", "SIM", "I", "C4", "PERF"]
[tool.ruff.lint.isort]
length-sort = true
order-by-type = false
force-sort-within-sections = false
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"]
"**/__init__.py" = ["F401"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true

View File

@@ -1,12 +1,10 @@
from symconf.runner import Runner from importlib.metadata import version
from symconf.reader import DictReader
from symconf import util, config, reader, matching, template
from symconf.config import ConfigManager from symconf.config import ConfigManager
from symconf.reader import DictReader
from symconf.runner import Runner
from symconf.matching import Matcher, FilePart from symconf.matching import Matcher, FilePart
from symconf.template import Template, FileTemplate, TOMLTemplate from symconf.template import Template, FileTemplate, TOMLTemplate
from symconf import config __version__ = version("symconf")
from symconf import matching
from symconf import reader
from symconf import template
from symconf import theme
from symconf import util

View File

@@ -1,117 +1,214 @@
import argparse from argparse import Namespace, ArgumentParser
from symconf import util from symconf import util, __version__
from symconf.config import ConfigManager from symconf.config import ConfigManager
def add_install_subparser(subparsers): def add_install_subparser(subparsers: ArgumentParser) -> None:
def install_apps(args): def install_apps(args: Namespace) -> None:
cm = ConfigManager(args.config_dir) cm = ConfigManager(args.config_dir)
cm.install_apps(apps=args.apps) cm.install_apps(apps=args.apps)
parser = subparsers.add_parser( parser = subparsers.add_parser(
'install', "install",
description='Run install scripts for registered applications.' description="Run install scripts for registered applications.",
) )
parser.add_argument( parser.add_argument(
'-a', '--apps', "-a",
required = False, "--apps",
default = "*", required=False,
type = lambda s: s.split(',') if s != '*' else s, default="*",
help = 'Application target for theme. App must be present in the registry. ' \ type=lambda s: s.split(",") if s != "*" else s,
+ 'Use "*" to apply to all registered apps' help=(
"Application target for theme. App must be present in the "
'registry. Use "*" to apply to all registered apps'
),
) )
parser.set_defaults(func=install_apps) parser.set_defaults(func=install_apps)
def add_update_subparser(subparsers):
def update_apps(args): def add_update_subparser(subparsers: ArgumentParser) -> None:
def update_apps(args: Namespace) -> None:
cm = ConfigManager(args.config_dir) cm = ConfigManager(args.config_dir)
cm.update_apps(apps=args.apps) cm.update_apps(apps=args.apps)
parser = subparsers.add_parser( parser = subparsers.add_parser(
'update', "update", description="Run update scripts for registered applications."
description='Run update scripts for registered applications.'
) )
parser.add_argument( parser.add_argument(
'-a', '--apps', "-a",
required = False, "--apps",
default = '*', required=False,
type = lambda s: s.split(',') if s != '*' else s, default="*",
help = 'Application target for theme. App must be present in the registry. ' \ type=lambda s: s.split(",") if s != "*" else s,
+ 'Use "*" to apply to all registered apps' help=(
"Application target for theme. App must be present in the "
'registry. Use "*" to apply to all registered apps'
),
) )
parser.set_defaults(func=update_apps) parser.set_defaults(func=update_apps)
def add_config_subparser(subparsers):
def configure_apps(args): def add_config_subparser(subparsers: ArgumentParser) -> None:
def configure_apps(args: Namespace) -> None:
cm = ConfigManager(args.config_dir) cm = ConfigManager(args.config_dir)
cm.configure_apps( cm.configure_apps(
apps=args.apps, apps=args.apps,
scheme=args.scheme, scheme=args.mode,
style=args.palette, style=args.style,
**args.template_vars,
) )
parser = subparsers.add_parser( parser = subparsers.add_parser(
'config', "config", description="Set config files for registered applications."
description='Set config files for registered applications.'
) )
parser.add_argument( parser.add_argument(
'-s', '--style', "-s",
required = False, "--style",
default = 'any', required=False,
help = 'Style indicator (often a color palette) capturing thematic details in ' default="any",
'a config file' help=(
"Style indicator (often a color palette) capturing "
"thematic details in a config file"
),
) )
parser.add_argument( parser.add_argument(
'-m', '--mode', "-m",
required = False, "--mode",
default = "any", required=False,
help = 'Preferred lightness mode/scheme, either "light," "dark," "any," or "none."' default="any",
help=(
'Preferred lightness mode/scheme, either "light," "dark," '
'"any," or "none."'
),
) )
parser.add_argument( parser.add_argument(
'-a', '--apps', "-a",
required = False, "--apps",
default = "*", required=False,
type = lambda s: s.split(',') if s != '*' else s, default="*",
help = 'Application target for theme. App must be present in the registry. ' \ type=lambda s: s.split(",") if s != "*" else s,
+ 'Use "*" to apply to all registered apps' help=(
"Application target for theme. App must be present in the "
'registry. Use "*" to apply to all registered apps'
),
) )
parser.add_argument( parser.add_argument(
'-T', '--template-vars', "-T",
required = False, "--template-vars",
nargs='+', required=False,
nargs="+",
default={},
action=util.KVPair, action=util.KVPair,
help='Groups to use when populating templates, in the form group=value' help=(
"Groups to use when populating templates, in the form group=value"
),
) )
parser.set_defaults(func=configure_apps) parser.set_defaults(func=configure_apps)
def add_generate_subparser(subparsers: ArgumentParser) -> None:
def generate_apps(args: Namespace) -> None:
cm = ConfigManager(args.config_dir)
cm.generate_app_templates(
gen_dir=args.output_dir,
apps=args.apps,
scheme=args.mode,
style=args.style,
**args.template_vars,
)
parser = subparsers.add_parser(
"generate",
description="Generate all template config files for specified apps",
)
parser.add_argument(
"-o",
"--output-dir",
required=True,
type=util.absolute_path,
help="Path to write generated template files",
)
parser.add_argument(
"-s",
"--style",
required=False,
default="any",
help=(
"Style indicator (often a color palette) capturing "
"thematic details in a config file"
),
)
parser.add_argument(
"-m",
"--mode",
required=False,
default="any",
help=(
'Preferred lightness mode/scheme, either "light," "dark," '
'"any," or "none."'
),
)
parser.add_argument(
"-a",
"--apps",
required=False,
default="*",
type=lambda s: s.split(",") if s != "*" else s,
help=(
"Application target for theme. App must be present in the "
'registry. Use "*" to apply to all registered apps'
),
)
parser.add_argument(
"-T",
"--template-vars",
required=False,
nargs="+",
default={},
action=util.KVPair,
help=(
"Groups to use when populating templates, in the form group=value"
),
)
parser.set_defaults(func=generate_apps)
# central argparse entry point # central argparse entry point
parser = argparse.ArgumentParser( parser = ArgumentParser(
'symconf', "symconf", description="Manage application configuration with symlinks."
description='Manage application configuration with symlinks.'
) )
parser.add_argument( parser.add_argument(
'-c', '--config-dir', "-c",
default = util.xdg_config_path(), "--config-dir",
type = util.absolute_path, default=util.xdg_config_path(),
help = 'Path to config directory' type=util.absolute_path,
help="Path to config directory",
)
parser.add_argument(
"-v",
"--version",
action="version",
version=__version__,
help="Print symconf version",
) )
# add subparsers # add subparsers
subparsers = parser.add_subparsers(title='subcommand actions') subparsers = parser.add_subparsers(title="subcommand actions")
add_config_subparser(subparsers)
add_generate_subparser(subparsers)
add_install_subparser(subparsers) add_install_subparser(subparsers)
add_update_subparser(subparsers) add_update_subparser(subparsers)
add_config_subparser(subparsers)
def main(): def main() -> None:
args = parser.parse_args() args = parser.parse_args()
if 'func' in args: if "func" in args:
args.func(args) args.func(args)
else: else:
parser.print_help() parser.print_help()
if __name__ == '__main__':
if __name__ == "__main__":
main() main()

File diff suppressed because it is too large Load Diff

View File

@@ -1,188 +1,201 @@
''' """
Top-level definitions Generic combinatorial name-matching subsystem
Config files are expected to have names matching the following spec: Config files are expected to have names matching the following spec:
<style>-<scheme>.<config_pathname> .. code-block:: sh
- ``config_pathname``: refers to a concrete filename, typically that which is expected by <style>-<scheme>.<config_pathname>
the target app (e.g., ``kitty.conf``). In the context of ``config_map`` in the registry,
however, it merely serves as an identifier, as it can be mapped onto any path. - ``config_pathname``: refers to a concrete filename, typically that which is
expected by the target app (e.g., ``kitty.conf``). In the context of
``config_map`` in the registry, however, it merely serves as an identifier,
as it can be mapped onto any path.
- ``scheme``: indicates the lightness mode ("light" or "dark") - ``scheme``: indicates the lightness mode ("light" or "dark")
- ``style``: general identifier capturing the stylizations applied to the config file. - ``style``: general identifier capturing the stylizations applied to the
This is typically of the form ``<variant>-<palette>``, i.e., including a reference to a config file. This is typically of the form ``<variant>-<palette>``, i.e.,
particular color palette. including a reference to a particular color palette.
For example For example
```sh .. code-block:: sh
soft-gruvbox-dark.kitty.conf
```
gets mapped to soft-gruvbox-dark.kitty.conf
gets mapped to
.. code-block:: sh
style -> "soft-gruvbox"
scheme -> "dark"
pathname -> "kitty.conf"
"""
```sh
style -> "soft-gruvbox"
scheme -> "dark"
pathname -> "kitty.conf"
```
'''
from pathlib import Path from pathlib import Path
from symconf import util from symconf import util
class FilePart: class FilePart:
def __init__(self, path: str | Path): def __init__(self, path: str | Path) -> None:
self.path = util.absolute_path(path) self.path = util.absolute_path(path)
self.pathname = self.path.name self.pathname = self.path.name
parts = str(self.pathname).split('.') parts = str(self.pathname).split(".")
if len(parts) < 2: if len(parts) < 2:
raise ValueError(f'Filename "{pathname}" incorrectly formatted, ignoring') raise ValueError(
f'Filename "{self.pathname}" incorrectly formatted, ignoring'
)
self.theme = parts[0] self.theme = parts[0]
self.conf = '.'.join(parts[1:]) self.conf = ".".join(parts[1:])
theme_split = self.theme.split('-') theme_split = self.theme.split("-")
self.scheme = theme_split[-1] self.scheme = theme_split[-1]
self.style = '-'.join(theme_split[:-1]) self.style = "-".join(theme_split[:-1])
self.index = -1 self.index = -1
def set_index(self, idx: int): def set_index(self, idx: int) -> None:
self.index = idx self.index = idx
class Matcher: class Matcher:
def get_file_parts( def get_file_parts(
self, self,
paths: list[str | Path], paths: list[str | Path],
) -> list[FilePart]: ) -> list[FilePart]:
''' """
Split pathnames into parts for matching. Split pathnames into parts for matching.
Pathnames should be of the format Pathnames should be of the format
```sh .. code-block:: sh
<style>-<scheme>.<config_pathname>
``` <style>-<scheme>.<config_pathname>
where ``style`` is typically itself of the form
``<variant>-<palette>``.
"""
where ``style`` is typically itself of the form ``<variant>-<palette>``.
'''
file_parts = [] file_parts = []
for path in paths: for path in paths:
try: try:
config_file = FilePart(path) config_file = FilePart(path)
file_parts.append(config_file) file_parts.append(config_file)
except ValueError as e: except ValueError:
print(f'Filename "{pathname}" incorrectly formatted, ignoring') print(f'Filename "{path}" incorrectly formatted, ignoring')
return file_parts return file_parts
def prefix_order( def prefix_order(
self, self,
scheme, scheme: str,
style, style: str,
strict=False, strict: bool = False,
) -> list[tuple[str, str]]: ) -> list[tuple[str, str]]:
''' """
Determine the order of concrete config pathname parts to match, given the Determine the order of concrete config pathname parts to match, given
``scheme`` and ``style`` inputs. the ``scheme`` and ``style`` inputs.
There is a unique preferred match order when ``style``, ``scheme``, both, or none There is a unique preferred match order when ``style``, ``scheme``,
are ``any``. In general, when ``any`` is provided for a given factor, it is both, or none are ``any``. In general, when ``any`` is provided for a
best matched by a config file that expresses indifference under that factor. given factor, it is best matched by a config file that expresses
''' indifference under that factor.
# explicit cases are the most easily managed here, even if a little redundant """
# explicit cases are the most easily managed here, even if a little
# redundant
if strict: if strict:
theme_order = [ theme_order = [
(style, scheme), (style, scheme),
] ]
else: else:
# inverse order of match relaxation; intention being to overwrite with # inverse order of match relaxation; intention being to overwrite
# results from increasingly relevant groups given the conditions # with results from increasingly relevant groups given the
if style == 'any' and scheme == 'any': # conditions
if style == "any" and scheme == "any":
# prefer both be "none", with preference for specific scheme # prefer both be "none", with preference for specific scheme
theme_order = [ theme_order = [
(style , scheme), (style, scheme),
(style , 'none'), (style, "none"),
('none' , scheme), ("none", scheme),
('none' , 'none'), ("none", "none"),
] ]
elif style == 'any': elif style == "any":
# prefer style to be "none", then specific, then relax specific scheme # prefer style to be "none", then specific, then relax specific
# to "none" # scheme to "none"
theme_order = [ theme_order = [
(style , 'none'), (style, "none"),
('none' , 'none'), ("none", "none"),
(style , scheme), (style, scheme),
('none' , scheme), ("none", scheme),
] ]
elif scheme == 'any': elif scheme == "any":
# prefer scheme to be "none", then specific, then relax specific style # prefer scheme to be "none", then specific, then relax
# to "none" # specific style to "none"
theme_order = [ theme_order = [
('none' , scheme), ("none", scheme),
('none' , 'none'), ("none", "none"),
(style , scheme), (style, scheme),
(style , 'none'), (style, "none"),
] ]
else: else:
# neither component is any; prefer most specific # neither component is any; prefer most specific
theme_order = [ theme_order = [
('none' , 'none'), ("none", "none"),
('none' , scheme), ("none", scheme),
(style , 'none'), (style, "none"),
(style , scheme), (style, scheme),
] ]
return theme_order return theme_order
def match_paths( def match_paths(
self, self,
paths: list[str | Path], paths: list[str | Path],
prefix_order: list[tuple[str, str]], prefix_order: list[tuple[str, str]],
) -> list[FilePart]: ) -> list[FilePart]:
''' """
Find and return FilePart matches according to the provided prefix order. Find and return FilePart matches according to the provided prefix
order.
The prefix order specifies all valid style-scheme combos that can be considered as The prefix order specifies all valid style-scheme combos that can be
"consistent" with some user input (and is computed external to this method). For considered as "consistent" with some user input (and is computed
example, it could be external to this method). For example, it could be
```py .. code-block:: python
[
('none', 'none')
('none', 'dark')
]
```
indicating that either ``none-none.<config>`` or ``none-dark.<config>`` would be [("none", "none")("none", "dark")]
considered matching pathnames, with the latter being preferred.
This method exists because we need a way to allow any of the combos in the prefix indicating that either ``none-none.<config>`` or ``none-dark.<config>``
order to match the candidate files. We don't know a priori how good of a match would be considered matching pathnames, with the latter being
will be available, so we consider each file for each of the prefixes, and take the preferred.
latest/best match for each unique config pathname (allowing for a "soft" match).
.. admonition: Checking for matches
When thinking about how best to structure this method, it initially felt like This method exists because we need a way to allow any of the combos in
indexing factors of the FileParts would make the most sense, preventing the the prefix order to match the candidate files. We don't know a priori
inner loop that needs to inspect each FilePart for each element of the prefix how good of a match will be available, so we consider each file for
order. But indexing the file parts and checking against prefixes isn't so each of the prefixes, and take the latest/best match for each unique
straightforward, as we'd still need to check matches by factor. For instance, config pathname (allowing for a "soft" match).
if we index by style-scheme, either are allowed to be "any," so we'd need to
check for the 4 valid combos and join the matching lists. If we index by both
factors individually, we may have several files associated with a given key,
and then need to coordinate the checks across both to ensure they belong to
the same file.
In any case, you should be able to do this in a way that's a bit more .. admonition:: Checking for matches
efficient, but the loop and the simple conditionals is just much simpler to
follow. We're also talking about at most 10s of files, so it really doesn't When thinking about how best to structure this method, it initially
matter. felt like indexing factors of the FileParts would make the most
sense, preventing the inner loop that needs to inspect each
FilePart for each element of the prefix order. But indexing the
file parts and checking against prefixes isn't so straightforward,
as we'd still need to check matches by factor. For instance, if we
index by style-scheme, either are allowed to be "any," so we'd need
to check for the 4 valid combos and join the matching lists. If we
index by both factors individually, we may have several files
associated with a given key, and then need to coordinate the checks
across both to ensure they belong to the same file.
In any case, you should be able to do this in a way that's a bit
more efficient, but the loop and the simple conditionals is just
much simpler to follow. We're also talking about at most 10s of
files, so it really doesn't matter.
Parameters: Parameters:
pathnames: pathnames:
@@ -190,50 +203,54 @@ class Matcher:
style: style:
prefix_order: prefix_order:
strict: strict:
''' """
file_parts = self.get_file_parts(paths) file_parts = self.get_file_parts(paths)
ordered_matches = [] ordered_matches = []
for i, (style_prefix, scheme_prefix) in enumerate(prefix_order): for i, (style_prefix, scheme_prefix) in enumerate(prefix_order):
for fp in file_parts: for fp in file_parts:
style_match = style_prefix == fp.style or style_prefix == 'any' style_match = style_prefix == fp.style or style_prefix == "any"
scheme_match = scheme_prefix == fp.scheme or scheme_prefix == 'any' scheme_match = (
scheme_prefix == fp.scheme or scheme_prefix == "any"
)
if style_match and scheme_match: if style_match and scheme_match:
fp.set_index(i+1) fp.set_index(i + 1)
ordered_matches.append(fp) ordered_matches.append(fp)
return ordered_matches return ordered_matches
def relaxed_match( def relaxed_match(self, match_list: list[FilePart]) -> list[FilePart]:
self, """
match_list: list[FilePart]
) -> list[FilePart]:
'''
Isolate the best match in a match list and find its relaxed variants. Isolate the best match in a match list and find its relaxed variants.
This method allows us to use the ``match_paths()`` method for matching templates This method allows us to use the ``match_paths()`` method for matching
rather than direct user config files. In the latter case, we want to symlink the templates rather than direct user config files. In the latter case, we
single best config file match for each stem, across all stems with matching want to symlink the single best config file match for each stem, across
prefixes (e.g., ``none-dark.config.a`` and ``solarized-dark.config.b`` have two all stems with matching prefixes (e.g., ``none-dark.config.a`` and
separate stems with prefixes that could match ``scheme=dark, style=any`` query). ``solarized-dark.config.b`` have two separate stems with prefixes that
We can find these files by just indexing the ``match_path`` outputs (i.e., all could match ``scheme=dark, style=any`` query). We can find these files
matches) by config pathname and taking the one that appears latest (under the by just indexing the ``match_path`` outputs (i.e., all matches) by
config pathname and taking the one that appears latest (under the
prefix order) for each unique value. prefix order) for each unique value.
In the template matching case, we want only a single best file match, period In the template matching case, we want only a single best file match,
(there's really no notion of "config stems," it's just the prefixes). Once that period (there's really no notion of "config stems," it's just the
match has been found, we can then "relax" either the scheme or style (or both) to prefixes). Once that match has been found, we can then "relax" either
``none``, and if the corresponding files exist, we use those as parts of the the scheme or style (or both) to ``none``, and if the corresponding
template keys. For example, if we match ``solarized-dark.toml``, we would also files exist, we use those as parts of the template keys. For example,
consider the values in ``none-dark.toml`` if available. The TOML values that are if we match ``solarized-dark.toml``, we would also consider the values
defined in the most specific (i.e., better under the prefix order) match are in ``none-dark.toml`` if available. The TOML values that are defined in
loaded "on top of" those less specific matches, overwriting keys when there's a the most specific (i.e., better under the prefix order) match are
conflict. ``none-dark.toml``, for instance, might define a general dark scheme loaded "on top of" those less specific matches, overwriting keys when
background color, but a more specific definition in ``solarized-dark.toml`` would there's a conflict. ``none-dark.toml``, for instance, might define a
take precedent. These TOML files would be stacked before using the resulting general dark scheme background color, but a more specific definition in
dictionary to populate config templates. ``solarized-dark.toml`` would take precedent. These TOML files would be
''' stacked before using the resulting dictionary to populate config
templates.
"""
if not match_list: if not match_list:
return [] return []
@@ -241,11 +258,10 @@ class Matcher:
match = match_list[-1] match = match_list[-1]
for fp in match_list: for fp in match_list:
style_match = fp.style == match.style or fp.style == 'none' style_match = fp.style == match.style or fp.style == "none"
scheme_match = fp.scheme == match.scheme or fp.scheme == 'none' scheme_match = fp.scheme == match.scheme or fp.scheme == "none"
if style_match and scheme_match: if style_match and scheme_match:
relaxed_map[fp.pathname] = fp relaxed_map[fp.pathname] = fp
return list(relaxed_map.values()) return list(relaxed_map.values())

View File

@@ -1,35 +1,44 @@
"""
Simplified management for nested dictionaries
"""
import copy import copy
import pprint import pprint
import tomllib
import hashlib import hashlib
import logging
import tomllib
from typing import Any from typing import Any
from pathlib import Path from pathlib import Path
from symconf.util import deep_update from symconf.util import deep_update
logger = logging.getLogger(__name__)
class DictReader: class DictReader:
def __init__(self, toml_path=None): def __init__(self, toml_path: str | None = None) -> None:
self._config = {} self._config = {}
self.toml_path = toml_path self.toml_path = toml_path
if toml_path is not None: if toml_path is not None:
self._config = self._load_toml(toml_path) self._config = self._load_toml(toml_path)
def __str__(self): def __str__(self) -> str:
return pprint.pformat(self._config, indent=4) return pprint.pformat(self._config, indent=4)
@staticmethod @staticmethod
def _load_toml(toml_path) -> dict[str, Any]: def _load_toml(toml_path: str) -> dict[str, Any]:
return tomllib.loads(Path(toml_path).read_text()) return tomllib.loads(Path(toml_path).read_text())
@classmethod @classmethod
def from_dict(cls, config_dict): def from_dict(cls, config_dict: dict) -> "DictReader":
new_instance = cls() new_instance = cls()
new_instance._config = copy.deepcopy(config_dict) new_instance._config = copy.deepcopy(config_dict)
return new_instance return new_instance
def update(self, config, in_place=False): def update(
self, config: "DictReader", in_place: bool = False
) -> "DictReader":
new_config = deep_update(self._config, config._config) new_config = deep_update(self._config, config._config)
if in_place: if in_place:
@@ -38,13 +47,14 @@ class DictReader:
return self.from_dict(new_config) return self.from_dict(new_config)
def copy(self): def copy(self) -> "DictReader":
return self.from_dict(copy.deepcopy(self._config)) return self.from_dict(copy.deepcopy(self._config))
def get_subconfig(self, key): pass def get_subconfig(self, key: str) -> "DictReader":
pass
def get(self, key, default=None): def get(self, key: str, default: str | None = None) -> str:
keys = key.split('.') keys = key.split(".")
subconfig = self._config subconfig = self._config
for subkey in keys[:-1]: for subkey in keys[:-1]:
@@ -55,8 +65,8 @@ class DictReader:
return subconfig.get(keys[-1], default) return subconfig.get(keys[-1], default)
def set(self, key, value): def set(self, key: str, value: str) -> bool:
keys = key.split('.') keys = key.split(".")
subconfig = self._config subconfig = self._config
for subkey in keys[:-1]: for subkey in keys[:-1]:
@@ -65,7 +75,8 @@ class DictReader:
if type(subconfig) is not dict: if type(subconfig) is not dict:
logger.debug( logger.debug(
'Attempting to set nested key with an existing non-dict parent' "Attempting to set nested key with an "
"existing non-dict parent"
) )
return False return False
@@ -75,20 +86,20 @@ class DictReader:
subconfig[subkey] = subdict subconfig[subkey] = subdict
subconfig = subdict subconfig = subdict
subconfig.update({ keys[-1]: value }) subconfig.update({keys[-1]: value})
return True return True
def generate_hash(self, exclude_keys=None): def generate_hash(self, exclude_keys: list[str] | None = None) -> str:
inst_copy = self.copy() inst_copy = self.copy()
if exclude_keys is not None: if exclude_keys is not None:
for key in exclude_keys: for key in exclude_keys:
inst_copy.set(key, None) inst_copy.set(key, None)
items = inst_copy._config.items() items = inst_copy._config.items()
# create hash from config options # create hash from config options
config_str = str(sorted(items)) config_str = str(sorted(items))
return hashlib.md5(config_str.encode()).hexdigest() return hashlib.md5(config_str.encode()).hexdigest()

View File

@@ -1,50 +1,53 @@
"""
Handle job/script execution
"""
import stat import stat
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from colorama import Fore, Back, Style from colorama import Fore, Style
from symconf.util import printc, color_text from symconf.util import color_text
class Runner: class Runner:
def run_script( def run_script(
self, self,
script: str | Path, script: str | Path,
): ) -> str | None:
script_path = Path(script) script_path = Path(script)
if script_path.stat().st_mode & stat.S_IXUSR == 0: if script_path.stat().st_mode & stat.S_IXUSR == 0:
print( print(
color_text("", Fore.BLUE), color_text("", Fore.BLUE),
color_text( color_text(
f' > script "{script_path.name}" missing execute permissions, skipping', f' > script "{script_path.name}" missing '
Fore.RED + Style.DIM "execute permissions, skipping",
) Fore.RED + Style.DIM,
),
) )
return return
print( print(
color_text("", Fore.BLUE), color_text("", Fore.BLUE),
color_text( color_text(f' > running script "{script_path.name}"', Fore.BLUE),
f' > running script "{script_path.name}"',
Fore.BLUE
)
) )
output = subprocess.check_output(str(script_path), shell=True) output = subprocess.check_output(str(script_path), shell=True)
if output: if output:
fmt_output = output.decode().strip().replace( fmt_output = (
'\n', output.decode()
f'\n{Fore.BLUE}{Style.NORMAL}{Style.DIM} ' .strip()
.replace("\n", f"\n{Fore.BLUE}{Style.NORMAL}{Style.DIM} ")
) )
print( print(
color_text("", Fore.BLUE), color_text("", Fore.BLUE),
color_text( color_text(
f' captured script output "{fmt_output}"', f' captured script output "{fmt_output}"',
Fore.BLUE + Style.DIM Fore.BLUE + Style.DIM,
) ),
) )
return output return output
@@ -52,7 +55,7 @@ class Runner:
def run_many( def run_many(
self, self,
script_list: list[str | Path], script_list: list[str | Path],
): ) -> list[str | None]:
outputs = [] outputs = []
for script in script_list: for script in script_list:
output = self.run_script(script) output = self.run_script(script)

View File

@@ -1,3 +1,7 @@
"""
Support for basic config templates
"""
import re import re
import tomllib import tomllib
from pathlib import Path from pathlib import Path
@@ -9,49 +13,85 @@ from symconf.reader import DictReader
class Template: class Template:
def __init__( def __init__(
self, self,
template_str : str, template_str: str,
pattern : str = r'f{{(\S+?)}}', key_pattern: str = r"f{{(\S+?)}}",
): exe_pattern: str = r"x{{((?:(?!x{{).)*)}}",
) -> None:
self.template_str = template_str self.template_str = template_str
self.pattern = pattern self.key_pattern = key_pattern
self.exe_pattern = exe_pattern
def fill( def fill(
self, self,
template_dict : dict, template_dict: dict,
) -> str: ) -> str:
dr = DictReader.from_dict(template_dict) dr = DictReader.from_dict(template_dict)
return re.sub( exe_filled = re.sub(
self.pattern, self.exe_pattern,
lambda m: str(dr.get(m.group(1))), lambda m: self._exe_fill(m, dr),
self.template_str self.template_str,
) )
key_filled = re.sub(
self.key_pattern, lambda m: self._key_fill(m, dr), exe_filled
)
return key_filled
def _key_fill(
self,
match: re.Match,
dict_reader: DictReader,
) -> str:
key = match.group(1)
return str(dict_reader.get(key))
def _exe_fill(
self,
match: re.Match,
dict_reader: DictReader,
) -> str:
key_fill = re.sub(
self.key_pattern,
lambda m: f'"{self._key_fill(m, dict_reader)}"',
match.group(1),
)
return str(eval(key_fill))
class FileTemplate(Template): class FileTemplate(Template):
def __init__( def __init__(
self, self,
path : Path, path: Path,
pattern : str = r'f{{(\S+)}}', key_pattern: str = r"f{{(\S+?)}}",
): exe_pattern: str = r"x{{((?:(?!x{{).)*)}}",
) -> None:
super().__init__( super().__init__(
path.open('r').read(), path.open("r").read(),
pattern=pattern key_pattern=key_pattern,
exe_pattern=exe_pattern,
) )
class TOMLTemplate(FileTemplate): class TOMLTemplate(FileTemplate):
def __init__( def __init__(
self, self,
toml_path : Path, toml_path: Path,
pattern : str = r'f{{(\S+)}}', key_pattern: str = r"f{{(\S+?)}}",
): exe_pattern: str = r"x{{((?:(?!x{{).)*)}}",
) -> None:
super().__init__( super().__init__(
toml_path, toml_path,
pattern=pattern key_pattern=key_pattern,
exe_pattern=exe_pattern,
) )
def fill( def fill(
self, self,
template_dict : dict, template_dict: dict,
) -> str: ) -> str:
filled_template = super().fill(template_dict) filled_template = super().fill(template_dict)
toml_dict = tomllib.loads(filled_template) toml_dict = tomllib.loads(filled_template)
@@ -59,12 +99,10 @@ class TOMLTemplate(FileTemplate):
return toml_dict return toml_dict
@staticmethod @staticmethod
def stack_toml( def stack_toml(path_list: list[Path]) -> dict:
path_list: list[Path]
) -> dict:
stacked_dict = {} stacked_dict = {}
for toml_path in path_list: for toml_path in path_list:
updated_map = tomllib.load(toml_path.open('rb')) updated_map = tomllib.load(toml_path.open("rb"))
stacked_dict = util.deep_update(stacked_dict, updated_map) stacked_dict = util.deep_update(stacked_dict, updated_map)
return stacked_dict return stacked_dict

View File

@@ -1,74 +0,0 @@
import argparse
import inspect
import json
import tomllib as toml
from pathlib import Path
# separation sequences to use base on app
app_sep_map = {
'kitty': ' ',
}
def generate_theme_files():
basepath = get_running_path()
# set arg conditional variables
palette_path = Path(basepath, 'themes', args.palette)
colors_path = Path(palette_path, 'colors.json')
theme_app = args.app
template_path = None
output_path = None
if args.template is None:
template_path = Path(palette_path, 'apps', theme_app, 'templates')
else:
template_path = Path(args.template).resolve()
if args.output is None:
output_path = Path(palette_path, 'apps', theme_app, 'generated')
else:
output_path = Path(args.output).resolve()
# check paths
if not colors_path.exists():
print(f'Resolved colors path [{colors_path}] doesn\'t exist, exiting')
return
if not template_path.exists():
print(f'Template path [{template_path}] doesn\'t exist, exiting')
return
if not output_path.exists() or not output_path.is_dir():
print(f'Output path [{output_path}] doesn\'t exist or not a directory, exiting')
return
print(f'Using palette colors [{colors_path}]')
print(f'-> with templates in [{template_path}]')
print(f'-> to output path [{output_path}]\n')
# load external files (JSON, TOML)
colors_json = json.load(colors_path.open())
# get all matching TOML files
template_list = [template_path]
if template_path.is_dir():
template_list = template_path.rglob('*.toml')
for template_path in template_list:
template_toml = toml.load(template_path.open('rb'))
# lookup app-specific config separator
config_sep = app_sep_map.get(theme_app, ' ')
output_lines = []
for config_key, color_key in template_toml.items():
color_value = colors_json
for _key in color_key.split('.'):
color_value = color_value.get(_key, {})
output_lines.append(f'{config_key}{config_sep}{color_value}')
output_file = Path(output_path, template_path.stem).with_suffix('.conf')
output_file.write_text('\n'.join(output_lines))
print(f'[{len(output_lines)}] lines written to [{output_file}] for app [{theme_app}]')

View File

@@ -1,33 +1,35 @@
import re import re
import argparse
from pathlib import Path from pathlib import Path
from argparse import Action, Namespace, ArgumentParser
from xdg import BaseDirectory from xdg import BaseDirectory
from colorama import Fore, Back, Style from colorama import Back, Fore, Style
from colorama.ansi import AnsiFore, AnsiBack, AnsiStyle from colorama.ansi import AnsiCodes
def color_text(text, *colorama_args): def color_text(text: str, *colorama_args: AnsiCodes) -> str:
''' """
Colorama text helper function Colorama text helper function
Note: we attempt to preserve expected nested behavior by only resetting the groups Note: we attempt to preserve expected nested behavior by only resetting the
(Fore, Back, Style) affected the styles passed in. This works when an outer call is groups (Fore, Back, Style) affected the styles passed in. This works when
changing styles in one group, and an inner call is changing styles in another, but an outer call is changing styles in one group, and an inner call is
_not_ when affected groups overlap. changing styles in another, but *not* when affected groups overlap.
For example, if an outer call is setting the foreground color (e.g.,
``Fore.GREEN``), nested calls on the text being passed into the function
can modify and reset the background or style with affecting the foreground.
The primary use case here is styling a group of text a single color, but
applying ``BRIGHT`` or ``DIM`` styles only to some text elements within. If
we didn't reset by group, the outer coloration request will be "canceled
out" as soon as the first inner call is made (since the unconditional
behavior just employs ``Style.RESET_ALL``).
"""
For example, if an outer call is setting the foreground color (e.g., ``Fore.GREEN``),
nested calls on the text being passed into the function can modify and reset the
background or style with affecting the foreground. The primary use case here is
styling a group of text a single color, but applying ``BRIGHT`` or ``DIM`` styles only
to some text elements within. If we didn't reset by group, the outer coloration
request will be "canceled out" as soon as the first inner call is made (since the
unconditional behavior just employs ``Style.RESET_ALL``).
'''
# reverse map colorama Ansi codes # reverse map colorama Ansi codes
resets = [] resets = []
for carg in colorama_args: for carg in colorama_args:
match = re.match(r'.*\[(\d+)m', carg) match = re.match(r".*\[(\d+)m", carg)
if match: if match:
intv = int(match.group(1)) intv = int(match.group(1))
if (intv >= 30 and intv <= 39) or (intv >= 90 and intv <= 97): if (intv >= 30 and intv <= 39) or (intv >= 90 and intv <= 97):
@@ -39,42 +41,62 @@ def color_text(text, *colorama_args):
return f"{''.join(colorama_args)}{text}{''.join(resets)}" return f"{''.join(colorama_args)}{text}{''.join(resets)}"
def printc(text, *colorama_args):
def printc(text: str, *colorama_args: AnsiCodes) -> None:
print(color_text(text, *colorama_args)) print(color_text(text, *colorama_args))
def absolute_path(path: str | Path) -> Path: def absolute_path(path: str | Path) -> Path:
return Path(path).expanduser().absolute() return Path(path).expanduser().absolute()
def xdg_config_path():
return Path(BaseDirectory.save_config_path('symconf')) def xdg_config_path() -> Path:
return Path(BaseDirectory.save_config_path("symconf"))
def to_tilde_path(path: Path) -> Path: def to_tilde_path(path: Path) -> Path:
''' """
Abbreviate an absolute path by replacing HOME with "~", if applicable. Abbreviate an absolute path by replacing HOME with "~", if applicable.
''' """
try: try:
return Path(f"~/{path.relative_to(Path.home())}") return Path(f"~/{path.relative_to(Path.home())}")
except ValueError: except ValueError:
return path return path
def deep_update(mapping: dict, *updating_mappings: dict) -> dict: def deep_update(mapping: dict, *updating_mappings: dict) -> dict:
'''Code adapted from pydantic''' """Code adapted from pydantic"""
updated_mapping = mapping.copy() updated_mapping = mapping.copy()
for updating_mapping in updating_mappings: for updating_mapping in updating_mappings:
for k, v in updating_mapping.items(): for k, v in updating_mapping.items():
if k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict): if (
k in updated_mapping
and isinstance(updated_mapping[k], dict)
and isinstance(v, dict)
):
updated_mapping[k] = deep_update(updated_mapping[k], v) updated_mapping[k] = deep_update(updated_mapping[k], v)
else: else:
updated_mapping[k] = v updated_mapping[k] = v
return updated_mapping return updated_mapping
class KVPair(argparse.Action): class KVPair(Action):
def __call__(self, parser, namespace, values, option_string=None): def __call__(
self,
parser: ArgumentParser,
namespace: Namespace,
values: list[str],
option_string: str | None = None,
) -> None:
kv_dict = getattr(namespace, self.dest, {}) kv_dict = getattr(namespace, self.dest, {})
if kv_dict is None: if kv_dict is None:
kv_dict = {} kv_dict = {}
for value in values: for value in values:
key, val = value.split('=', 1) key, val = value.split("=", 1)
kv_dict[key] = val kv_dict[key] = val
setattr(namespace, self.dest, kv_dict) setattr(namespace, self.dest, kv_dict)

View File

@@ -1,11 +0,0 @@
def test_imports():
from symconf.runner import Runner
from symconf.reader import DictReader
from symconf.config import ConfigManager
from symconf.matching import Matcher, FilePart
from symconf.template import Template, FileTemplate, TOMLTemplate
from symconf import config
from symconf import reader
from symconf import theme
from symconf import util

View File

@@ -2,64 +2,64 @@ from pathlib import Path
from symconf import ConfigManager from symconf import ConfigManager
config_dir = Path(__file__, "..", "test-config-dir/").resolve()
config_dir = Path(
__file__, '..', 'test-config-dir/'
).resolve()
cm = ConfigManager(config_dir) cm = ConfigManager(config_dir)
def test_matching_configs_exact(): def test_matching_configs_exact() -> None:
''' """
Test matching exact style and scheme. Given strict mode not set (allowing relaxation Test matching exact style and scheme. Given strict mode not set (allowing
to "none"), the order of matching should be relaxation to "none"), the order of matching should be
1. (none, none) :: none-none.aaa 1. (none, none) :: none-none.aaa
2. (none, scheme) :: none-light.aaa 2. (none, scheme) :: none-light.aaa
3. (style, none) :: test-none.aaa 3. (style, none) :: test-none.aaa
4. (style, scheme) :: test-light.ccc 4. (style, scheme) :: test-light.ccc
Yielding "test-light.aaa", "test-light.ccc" (unique only on config pathname). Yielding "test-light.aaa", "test-light.ccc" (unique only on config
''' pathname).
"""
any_light = cm.get_matching_configs( any_light = cm.get_matching_configs(
'test', "test",
style='test', style="test",
scheme='light', scheme="light",
) )
print(any_light) print(any_light)
assert len(any_light) == 2 assert len(any_light) == 2
assert any_light['aaa'].pathname == 'test-none.aaa' assert any_light["aaa"].pathname == "test-none.aaa"
assert any_light['ccc'].pathname == 'test-light.ccc' assert any_light["ccc"].pathname == "test-light.ccc"
def test_matching_configs_any_style():
''' def test_matching_configs_any_style() -> None:
Test matching "any" style and exact scheme. Given strict mode not set (allowing """
relaxation to "none"), the order of matching should be Test matching "any" style and exact scheme. Given strict mode not set
(allowing relaxation to "none"), the order of matching should be
1. (style, none) :: none-none.aaa, test-none.aaa 1. (style, none) :: none-none.aaa, test-none.aaa
2. (none, none) :: none-none.aaa 2. (none, none) :: none-none.aaa
3. (style, scheme) :: test-dark.bbb 3. (style, scheme) :: test-dark.bbb
4. (none, scheme) :: (nothing) 4. (none, scheme) :: (nothing)
Yielding "none-none.aaa" (should always overwrite "test-none.aaa" due to "any"'s Yielding "none-none.aaa" (should always overwrite "test-none.aaa" due to
preference for non-specific matches, i.e., "none"s), "test-none.ddd", "test-dark.bbb" "any"'s preference for non-specific matches, i.e., "none"s),
(unique only on config pathname). "test-none.ddd", "test-dark.bbb" (unique only on config pathname).
''' """
any_dark = cm.get_matching_configs( any_dark = cm.get_matching_configs(
'test', "test",
style='any', style="any",
scheme='dark', scheme="dark",
) )
assert len(any_dark) == 2 assert len(any_dark) == 2
assert any_dark['aaa'].pathname == 'none-none.aaa' assert any_dark["aaa"].pathname == "none-none.aaa"
assert any_dark['bbb'].pathname == 'test-dark.bbb' assert any_dark["bbb"].pathname == "test-dark.bbb"
def test_matching_configs_any_scheme():
''' def test_matching_configs_any_scheme() -> None:
Test matching exact style and "any" scheme. Given strict mode not set (allowing """
relaxation to "none"), the order of matching should be Test matching exact style and "any" scheme. Given strict mode not set
(allowing relaxation to "none"), the order of matching should be
1. (none, scheme) :: none-light.aaa & none-none.aaa 1. (none, scheme) :: none-light.aaa & none-none.aaa
2. (none, none) :: none-none.aaa 2. (none, none) :: none-none.aaa
@@ -67,54 +67,64 @@ def test_matching_configs_any_scheme():
4. (style, none) :: test-none.aaa 4. (style, none) :: test-none.aaa
Yielding "test-none.aaa", "test-light.ccc", "test-dark.bbb" Yielding "test-none.aaa", "test-light.ccc", "test-dark.bbb"
''' """
test_any = cm.get_matching_configs( test_any = cm.get_matching_configs(
'test', "test",
style='test', style="test",
scheme='any', scheme="any",
) )
assert len(test_any) == 3 assert len(test_any) == 3
assert test_any['aaa'].pathname == 'test-none.aaa' assert test_any["aaa"].pathname == "test-none.aaa"
assert test_any['bbb'].pathname == 'test-dark.bbb' assert test_any["bbb"].pathname == "test-dark.bbb"
assert test_any['ccc'].pathname == 'test-light.ccc' assert test_any["ccc"].pathname == "test-light.ccc"
def test_matching_scripts():
''' def test_matching_scripts() -> None:
Test matching exact style and scheme. Given strict mode not set (allowing relaxation """
to "none"), the order of matching should be Test matching exact style and scheme. Given strict mode not set (allowing
relaxation to "none"), the order of matching should be
1. (none, none) :: none-none.sh 1. (none, none) :: none-none.sh
2. (none, scheme) :: none-light.sh 2. (none, scheme) :: none-light.sh
3. (style, none) :: test-none.sh 3. (style, none) :: test-none.sh
4. (style, scheme) :: (nothing) 4. (style, scheme) :: (nothing)
Yielding (ordered by dec specificity) "test-none.sh" as primary match, then relaxation Yielding (ordered by dec specificity) "test-none.sh" as primary match, then
match "none-none.sh". relaxation match "none-none.sh".
''' """
test_any = cm.get_matching_scripts( test_any = cm.get_matching_scripts(
'test', "test",
style='test', style="test",
scheme='any', scheme="any",
) )
assert len(test_any) == 2 assert len(test_any) == 2
assert list(map(lambda p:p.pathname, test_any)) == ['test-none.sh', 'none-none.sh'] assert [p.pathname for p in test_any] == [
"test-none.sh",
"none-none.sh",
]
any_light = cm.get_matching_scripts( any_light = cm.get_matching_scripts(
'test', "test",
style='any', style="any",
scheme='light', scheme="light",
) )
assert len(any_light) == 2 assert len(any_light) == 2
assert list(map(lambda p:p.pathname, any_light)) == ['none-light.sh', 'none-none.sh'] assert [p.pathname for p in any_light] == [
"none-light.sh",
"none-none.sh",
]
any_dark = cm.get_matching_scripts( any_dark = cm.get_matching_scripts(
'test', "test",
style='any', style="any",
scheme='dark', scheme="dark",
) )
assert len(any_dark) == 2 assert len(any_dark) == 2
assert list(map(lambda p:p.pathname, any_dark)) == ['test-none.sh', 'none-none.sh'] assert [p.pathname for p in any_dark] == [
"test-none.sh",
"none-none.sh",
]

View File

@@ -2,30 +2,47 @@ from pathlib import Path
from symconf import Template, TOMLTemplate from symconf import Template, TOMLTemplate
def test_template_fill():
def test_template_fill() -> None:
# test simple replacment # test simple replacment
assert Template('f{{a}} - f{{b}}').fill({ assert (
'a': 1, Template("f{{a}} - f{{b}}").fill(
'b': 2, {
}) == '1 - 2' "a": 1,
"b": 2,
}
)
== "1 - 2"
)
# test nested brackets (using default pattern) # test nested brackets (using default pattern)
assert Template('{{ f{{a}} - f{{b}} }}').fill({ assert (
'a': 1, Template("{{ f{{a}} - f{{b}} }}").fill(
'b': 2, {
}) == '{{ 1 - 2 }}' "a": 1,
"b": 2,
}
)
== "{{ 1 - 2 }}"
)
# test tight nested brackets (requires greedy quantifier) # test tight nested brackets (requires greedy quantifier)
assert Template('{{f{{a}} - f{{b}}}}').fill({ assert (
'a': 1, Template("{{f{{a}} - f{{b}}}}").fill(
'b': 2, {
}) == '{{1 - 2}}' "a": 1,
"b": 2,
}
)
== "{{1 - 2}}"
)
def test_toml_template_fill():
def test_toml_template_fill() -> None:
test_group_dir = Path( test_group_dir = Path(
__file__, '..', 'test-config-dir/groups/test/' __file__, "..", "test-config-dir/groups/test/"
).resolve() ).resolve()
stacked_dict = TOMLTemplate.stack_toml(test_group_dir.iterdir()) stacked_dict = TOMLTemplate.stack_toml(test_group_dir.iterdir())
assert stacked_dict == {'base':'aaa','concrete':'zzz'} assert stacked_dict == {"base": "aaa", "concrete": "zzz"}

589
uv.lock generated Normal file
View File

@@ -0,0 +1,589 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "accessible-pygments"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" },
]
[[package]]
name = "alabaster"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" },
]
[[package]]
name = "babel"
version = "2.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
]
[[package]]
name = "beautifulsoup4"
version = "4.13.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" },
]
[[package]]
name = "certifi"
version = "2025.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" },
{ url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" },
{ url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" },
{ url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" },
{ url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" },
{ url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" },
{ url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" },
{ url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" },
{ url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" },
{ url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" },
{ url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" },
{ url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" },
{ url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" },
{ url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" },
{ url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" },
{ url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" },
{ url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" },
{ url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" },
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "docutils"
version = "0.21.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" },
]
[[package]]
name = "furo"
version = "2025.9.25"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "accessible-pygments" },
{ name = "beautifulsoup4" },
{ name = "pygments" },
{ name = "sphinx" },
{ name = "sphinx-basic-ng" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4e/29/ff3b83a1ffce74676043ab3e7540d398e0b1ce7660917a00d7c4958b93da/furo-2025.9.25.tar.gz", hash = "sha256:3eac05582768fdbbc2bdfa1cdbcdd5d33cfc8b4bd2051729ff4e026a1d7e0a98", size = 1662007, upload-time = "2025-09-25T21:37:19.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/69/964b55f389c289e16ba2a5dfe587c3c462aac09e24123f09ddf703889584/furo-2025.9.25-py3-none-any.whl", hash = "sha256:2937f68e823b8e37b410c972c371bc2b1d88026709534927158e0cb3fac95afe", size = 340409, upload-time = "2025-09-25T21:37:17.244Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "imagesize"
version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
]
[[package]]
name = "mdit-py-plugins"
version = "0.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "myst-parser"
version = "4.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docutils" },
{ name = "jinja2" },
{ name = "markdown-it-py" },
{ name = "mdit-py-plugins" },
{ name = "pyyaml" },
{ name = "sphinx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "pyxdg"
version = "0.28"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/25/7998cd2dec731acbd438fbf91bc619603fc5188de0a9a17699a781840452/pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4", size = 77776, upload-time = "2022-06-05T11:35:01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/8d/cf41b66a8110670e3ad03dab9b759704eeed07fa96e90fdc0357b2ba70e2/pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab", size = 49520, upload-time = "2022-06-05T11:34:58.832Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "roman-numerals-py"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" },
]
[[package]]
name = "setuptools"
version = "80.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
]
[[package]]
name = "snowballstemmer"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" },
]
[[package]]
name = "soupsieve"
version = "2.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
]
[[package]]
name = "sphinx"
version = "8.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "alabaster" },
{ name = "babel" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "docutils" },
{ name = "imagesize" },
{ name = "jinja2" },
{ name = "packaging" },
{ name = "pygments" },
{ name = "requests" },
{ name = "roman-numerals-py" },
{ name = "snowballstemmer" },
{ name = "sphinxcontrib-applehelp" },
{ name = "sphinxcontrib-devhelp" },
{ name = "sphinxcontrib-htmlhelp" },
{ name = "sphinxcontrib-jsmath" },
{ name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" },
]
[[package]]
name = "sphinx-autodoc-typehints"
version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/93/68/a388a9b8f066cd865d9daa65af589d097efbfab9a8c302d2cb2daa43b52e/sphinx_autodoc_typehints-3.2.0.tar.gz", hash = "sha256:107ac98bc8b4837202c88c0736d59d6da44076e65a0d7d7d543a78631f662a9b", size = 36724, upload-time = "2025-04-25T16:53:25.872Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/c7/8aab362e86cbf887e58be749a78d20ad743e1eb2c73c2b13d4761f39a104/sphinx_autodoc_typehints-3.2.0-py3-none-any.whl", hash = "sha256:884b39be23b1d884dcc825d4680c9c6357a476936e3b381a67ae80091984eb49", size = 20563, upload-time = "2025-04-25T16:53:24.492Z" },
]
[[package]]
name = "sphinx-basic-ng"
version = "1.0.0b2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" },
]
[[package]]
name = "sphinx-togglebutton"
version = "0.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docutils" },
{ name = "setuptools" },
{ name = "sphinx" },
{ name = "wheel" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f0/df/d151dfbbe588116e450ca7e898750cb218dca6b2e557ced8de6f9bd7242b/sphinx-togglebutton-0.3.2.tar.gz", hash = "sha256:ab0c8b366427b01e4c89802d5d078472c427fa6e9d12d521c34fa0442559dc7a", size = 8324, upload-time = "2022-07-15T12:08:50.286Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/18/267ce39f29d26cdc7177231428ba823fe5ca94db8c56d1bed69033b364c8/sphinx_togglebutton-0.3.2-py3-none-any.whl", hash = "sha256:9647ba7874b7d1e2d43413d8497153a85edc6ac95a3fea9a75ef9c1e08aaae2b", size = 8249, upload-time = "2022-07-15T12:08:48.8Z" },
]
[[package]]
name = "sphinxcontrib-applehelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" },
]
[[package]]
name = "sphinxcontrib-devhelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" },
]
[[package]]
name = "sphinxcontrib-htmlhelp"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" },
]
[[package]]
name = "sphinxcontrib-jsmath"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" },
]
[[package]]
name = "sphinxcontrib-qthelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" },
]
[[package]]
name = "sphinxcontrib-serializinghtml"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
]
[[package]]
name = "symconf"
version = "0.8.2"
source = { editable = "." }
dependencies = [
{ name = "colorama" },
{ name = "pyxdg" },
]
[package.optional-dependencies]
dev = [
{ name = "pytest" },
]
doc = [
{ name = "furo" },
{ name = "myst-parser" },
{ name = "sphinx" },
{ name = "sphinx-autodoc-typehints" },
{ name = "sphinx-togglebutton" },
]
[package.metadata]
requires-dist = [
{ name = "colorama" },
{ name = "furo", marker = "extra == 'doc'" },
{ name = "myst-parser", marker = "extra == 'doc'" },
{ name = "pytest", marker = "extra == 'dev'" },
{ name = "pyxdg" },
{ name = "sphinx", marker = "extra == 'doc'" },
{ name = "sphinx-autodoc-typehints", marker = "extra == 'doc'" },
{ name = "sphinx-togglebutton", marker = "extra == 'doc'" },
]
provides-extras = ["doc", "dev"]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[package]]
name = "wheel"
version = "0.45.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" },
]