Compare commits

..

10 Commits

22 changed files with 2410 additions and 1211 deletions

2
.gitignore vendored
View File

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

164
README.md
View File

@ -1,62 +1,73 @@
# Symconf # Symconf
`symconf` is a CLI tool for managing local application configuration. It implements a `symconf` is a CLI tool for managing local application configuration. It
general model that supports dynamically switching/reloading themes for any application, implements a general model that supports dynamically switching/reloading themes
and provides a basic means of templatizing your config files. for any application, and provides a basic means of templatizing your config
files.
## Simple example ## Simple example
Below is a simple example demonstrating two system-wide theme switches: Below is a simple example demonstrating two system-wide theme switches:
![Simple example](docs/_static/example.gif) ![Simple example](docs/_static/example.gif)
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
the second a dark [`monobiome`][1] variant. Each call (of the form `symconf config -m dark -s theme and the second a dark [`monobiome`][1] variant. Each call (of the form
<style>`) indicates a dark mode preference and a particular color palette that should be `symconf config -m dark -s style`) indicates a dark mode preference and a
used when populating config file templates. Specifically, in this example, invoking particular color palette that should be used when populating config file
`symconf` results in the following app-specific config updates: templates. Specifically, in this example, invoking `symconf` results in the
following app-specific config updates:
- **GTK**: reacts to the mode setting and sets `prefer-dark` system-wide, changing general - **GTK**: reacts to the mode setting and sets `prefer-dark` system-wide,
GTK-responsive applications like Nautilus and Firefox (and subsequently websites that changing general GTK-responsive applications like Nautilus and Firefox (and
are responsive to `prefers-color-scheme`) subsequently websites that 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
processes are sent a message to live-reload the new config file `kitty` 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
instances of `neovim` are sent a message to re-source this theme from the 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
colors, and `swaymsg reload` is called palette colors, and `swaymsg reload` is called
- **fzf**: a palette-dependent theme is re-generated and re-exported - **fzf**: a palette-dependent theme is re-generated and re-exported
- **rofi**: launcher text and highlight colors are set according to the mode and palette, - **rofi**: launcher text and highlight colors are set according to the mode
applying on next invocation and palette, applying on next invocation
This example highlights the generality of `symconf`, and so long as an app's config can be This example highlights the generality of `symconf`, and so long as an app's
reloaded dynamically, you can use a single `symconf` call to apply themes for an arbitrary config can be reloaded dynamically, you can use a single `symconf` call to
number of apps at once. apply themes for an arbitrary number of apps at once.
# Behavior # Behavior
`symconf` uses a simple operational model that symlinks centralized config files to their `symconf` uses a simple operational model that symlinks centralized config
expected locations across the system. This central config directory can then be version files to their expected locations across the system. This central config
controlled, and app config files can be updated in one place. directory can then be version controlled, and app config files can be updated
in one place.
App config files can either be concrete (fully-specified) or templates (to be populated by App config files can either be concrete (fully-specified) or templates (to be
values conditional on style, e.g., a palette). When `symconf` is executed with a populated by values conditional on style, e.g., a palette). When `symconf` is
particular mode preference (dark or light) and a style (any other indicator of thematic executed with a particular mode preference (dark or light) and a style (any
elements, often simply in the form of a palette like `solarized` or `gruvbox`), it other indicator of thematic elements, often simply in the form of a palette
searches for both concrete and template config files that match and symlinks them to like `solarized` or `gruvbox`), it searches for both concrete and template
registered locations. When necessary, `symconf` will also match and execute scripts to config files that match and symlinks them to registered locations. When
reload apps after updating their configuration. necessary, `symconf` will also match and execute scripts to 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
give `symconf` something to act on. See [Configuring](docs/reference/configuring) for files and give `symconf` something to act on. See
details. [Configuring](docs/reference/configuring.md) for 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
for managing Python packages meant to be used as CLI programs. With `pipx` on your system, well-suited for managing Python packages meant to be used as CLI programs. With
you can install with `uv` on your system, 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
@ -67,44 +78,60 @@ You can also install via `pip`, or clone and install locally.
# Usage # Usage
- `-h --help`: print help message - `-h --help`: print help message
- `-c --config-dir`: set the location of the `symconf` config directory - `-c --config-dir`: set the location of the `symconf` config directory
- `symconf config` is the subcommand used to match and set available config files for - `symconf config` is the subcommand used to match and set available config
registered applications files for registered applications
* `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to consider * `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to
all registered apps. consider all registered apps.
* `-m --mode`: preferred lightness mode/scheme, either `light`, `dark`, `any`, or * `-m --mode`: preferred lightness mode/scheme, either `light`, `dark`,
`none`. `any`, or `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
details in a config file to be matched. `any` or `none` are reserved keywords (see thematic details in a config file to be matched. `any` or `none` are
below). reserved keywords (see below).
* `-T --template-vars`: additional groups to use when populating templates, in the form * `-T --template-vars`: additional groups to use when populating templates,
`<group>=<value>`, where `<group>` is a template group with a folder in the form `<group>=<value>`, where `<group>` is a template group with a
`$CONFIG_HOME/groups/<group>/` and `<value>` should correspond to a TOML file in this folder `$CONFIG_HOME/groups/<group>/` and `<value>` should correspond to a
folder (i.e., `<value>.toml`). TOML file in this 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`,
value in `--template-vars` (and we refer to each of these variables as _factors_ that help or as a value in `--template-vars` (and we refer to each of these variables as
determine a config match): _factors_ that help determine a config match):
- `any` will match config files with _any_ value for this factor, preferring config files - `any` will match config files with _any_ value for this factor, preferring
with a value `none`, indicating no dependence on the factor. This is the default value config files with a value `none`, indicating no dependence on the factor.
when a factor is left unspecified. This is the default value when a factor is left unspecified.
- `none` will match `"none"` directly for a given factor (so no special behavior), but - `none` will match `"none"` directly for a given factor (so no special
used to indicate that a config file is independent of the factor. For instance, behavior), but used to indicate that a config file is independent of the
factor. For instance,
```sh ```sh
symconf config -m light -s none symconf config -m light -s none
``` ```
will match config files that capture the notion of a light mode, but do not depend on or will match config files that capture the notion of a light mode, but do not
provide further thematic components such as a color palette. depend on or provide further thematic components such as a color palette.
## Examples ## Examples
- Set a dark mode for all registered apps, matching any available style/palette component: - Set a dark mode for all registered apps, matching any available style/palette
component:
```sh ```sh
symconf config -m dark symconf config -m dark
``` ```
- Set `solarized` theme for `kitty` and match any available mode (light or dark): - Set `solarized` theme for `kitty` and match any available mode (light or
dark):
```sh ```sh
symconf config -s solarized -a kitty symconf config -s solarized -a kitty
@ -114,16 +141,17 @@ determine a config match):
```sh ```sh
symconf config -m dark -s gruvbox -apps="kitty,nvim" symconf config -m dark -s gruvbox -apps="kitty,nvim"
``` ```
- Set a dark `gruvbox` theme for all apps, and attempt to match other template elements: - Set a dark `gruvbox` theme for all apps, and attempt to match other template
elements:
```sh ```sh
symconf config -m dark -s gruvbox -T font=mono window=sharp symconf config -m dark -s gruvbox -T font=mono window=sharp
``` ```
which would attempt to find and load key-value pairs in the files which would attempt to find and load key-value pairs in the files
`$CONFIG_HOME/groups/font/mono.toml` and `$CONFIG_HOME/groups/window/sharp.toml` to be `$CONFIG_HOME/groups/font/mono.toml` and
used as values when filling templatized config files. `$CONFIG_HOME/groups/window/sharp.toml` to be used as values when filling
templatized config files.
[1]: https://github.com/ologio/monobiome [1]: https://github.com/ologio/monobiome

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

@ -1,31 +1,35 @@
# `symconf` package docs # `symconf` package
*General-purpose local application configuration manager*
{ref}`genindex` {ref}`genindex`
{ref}`modindex` {ref}`modindex`
{ref}`search`
```{eval-rst} ```{eval-rst}
.. autosummary:: .. autosummary::
:nosignatures: :nosignatures:
:recursive:
:caption: Modules
# list modules here for quick links symconf.config
symconf.reader
symconf.runner
symconf.matching
symconf.template
``` ```
```{toctree} ```{toctree}
:maxdepth: 3 :maxdepth: 1
:caption: Autoref
_autoref/symconf.rst
```
```{toctree}
:maxdepth: 3
:caption: Contents :caption: Contents
:hidden:
reference/configuring
reference/usage
reference/archive reference/archive
reference/documentation/index reference/configuring
reference/matching
reference/usage
``` ```
```{include} ../README.md ```{include} ../README.md
:relative-docs: docs/
:relative-images:
``` ```

View File

@ -1,80 +1,89 @@
# Archive # Archive
The `autoconf` project is an attempt at wrangling the complexity of configuring many The `autoconf` project is an attempt at wrangling the complexity of configuring
applications across one's Linux system. It provides a simple operational model for pulling many applications across one's Linux system. It provides a simple operational
many application config files into one place, as well as generating/setting color schemes model for pulling many application config files into one place, as well as
across apps. generating/setting color schemes across apps.
Quick terminology rundown for theme-related items: Quick terminology rundown for theme-related items:
- **Theme**: loose term referring generally to the overall aesthetic of a visual setting. - **Theme**: loose term referring generally to the overall aesthetic of a
Ignoring stylistic changes (only applicable to some apps; example here might be a visual setting. Ignoring stylistic changes (only applicable to some apps;
a particular setting of the `waybar` layout), a theme is often just the wrapper term for example here might be a a particular setting of the `waybar` layout), a theme
a choice of color _palette_ and _scheme_. For example, "tone4-light" could be a _theme_ is often just the wrapper term for a choice of color _palette_ and _scheme_.
setting for an app like `kitty`, referring to both a palette and scheme. For example, "tone4-light" could be a _theme_ setting for an app like
- **Palette**: a set of base colors used to style text or other aspects of an app's `kitty`, referring to both a palette and scheme.
displayed assets - **Palette**: a set of base colors used to style text or other aspects of an
app's displayed assets
- **Scheme**: an indication of lightness, typically either "light" or "dark. - **Scheme**: an indication of lightness, typically either "light" or "dark.
As far as managing settings across apps, there are current two useful classifications As far as managing settings across apps, there are current two useful
here: classifications here:
1. **Inseparable from theme**: some apps (e.g., `sway`, `waybar`) have color scheme 1. **Inseparable from theme**: some apps (e.g., `sway`, `waybar`) have color
components effectively built in to their canonical configuration file. This can make it scheme components effectively built in to their canonical configuration
hard to set themes dynamically, as it would likely require some involved file. This can make it hard to set themes dynamically, as it would likely
matching/substitution rules. This is not a level of complexity I'm willing to embrace, require some involved matching/substitution rules. This is not a level of
so we simply split the config files according to theme and/or scheme. complexity I'm willing to embrace, so we simply split the config files
2. **Can load an external theme file**: some apps (e.g., `kitty`) have a clear mechanism according to theme and/or scheme.
for loading themes. This typically implies some distinct color format, although usually 2. **Can load an external theme file**: some apps (e.g., `kitty`) have a clear
somewhat easy to generate (don't have to navigate non-color settings, for instance). mechanism for loading themes. This typically implies some distinct color
Such apps allow for an even less "invasive" config swapping process when setting a new format, although usually somewhat easy to generate (don't have to navigate
theme, as one can just swap out the external theme file. non-color settings, for instance). Such apps allow for an even less
"invasive" config swapping process when setting a new theme, as one can just
swap out the external theme file.
To be clear on operation implications here: apps of type (1) must have _manually To be clear on operation implications here: apps of type (1) must have
maintained_ config variations according the desired themes. General theme settings must _manually maintained_ config variations according the desired themes. General
follow the naming scheme `<app-name>-<palette>-<scheme>.<ext>`. For example, if I wanted to set theme settings must follow the naming scheme
`sway` to a light variation (which, at the time of writing, would just entail changing a `<app-name>-<palette>-<scheme>.<ext>`. For example, if I wanted to set `sway`
single background color), I must have explicitly created a `sway-tone4-light.conf` file to a light variation (which, at the time of writing, would just entail changing
that captures this setting. The canonical config file will then be symlinked to the a single background color), I must have explicitly created a
theme-specific file when the theme is set. (Note that the palette in this example is pretty much `sway-tone4-light.conf` file that captures this setting. The canonical config
irrelevant, but it needs to be present in order to match the overarching setting; here you file will then be symlinked to the theme-specific file when the theme is set.
can just think of the format being `<app-name>-<theme>.<ext>`, where `tone4-light` is the (Note that the palette in this example is pretty much irrelevant, but it needs
provided theme name.) to be present in order to match the overarching setting; here you can just
think of the format being `<app-name>-<theme>.<ext>`, where `tone4-light` is
the provided theme name.)
For apps of type (2), the canonical config file can remain untouched so long as it refers For apps of type (2), the canonical config file can remain untouched so long as
to a fixed, generic theme file. For example, with `kitty`, my config file can point to a it refers to a fixed, generic theme file. For example, with `kitty`, my config
`current-theme.conf` file, which will be symlinked to a specific theme file here in file can point to a `current-theme.conf` file, which will be symlinked to a
`autoconf` when a change is requested. This enables a couple of conveniences: specific theme file here in `autoconf` when a change is requested. This enables
a couple of conveniences:
- The true config directory on disk remains unpolluted with theme variants. - The true config directory on disk remains unpolluted with theme variants.
- If the set theme is regenerated, there is no intervention necessary to propagate its - If the set theme is regenerated, there is no intervention necessary to
changes to the target app. The symlinked file itself will be updated when the theme propagate its changes to the target app. The symlinked file itself will be
does, ensuring the latest theme version is always immediately available and pointed to updated when the theme does, ensuring the latest theme version is always
by the app. immediately available and pointed to by the app.
Keep in mind that some apps may fall into some grey area here, allowing some external Keep in mind that some apps may fall into some grey area here, allowing some
customization but locking down other settings internally. In such instances, there's no external customization but locking down other settings internally. In such
need to overcomplicate things; just stick to explicit config variants under the type (1) instances, there's no need to overcomplicate things; just stick to explicit
umbrella. Type (2) only works for generated themes anyhow; even if the target app can load config variants under the type (1) umbrella. Type (2) only works for generated
an external theme, type (1) should be used if preset themes are fixed. themes anyhow; even if the target app can load an external theme, type (1)
should be used if preset themes are fixed.
## Naming standards ## Naming standards
To keep things simple, we use a few fixed naming standards for setting app config files To keep things simple, we use a few fixed naming standards for setting app
and their themed counterparts. The app registry requires each theme-eligible app to config files and their themed counterparts. The app registry requires each
provide a config directory (`config_dir`), containing some canonical config file theme-eligible app to provide a config directory (`config_dir`), containing
(`config_file`) and to serve as a place for theme-specific config variations. The some canonical config file (`config_file`) and to serve as a place for
following naming schemes must be used in order for theme switching to behave theme-specific config variations. The following naming schemes must be used in
appropriately: order for theme switching to behave appropriately:
- When setting a theme for a particular app, the following variables will be available: - When setting a theme for a particular app, the following variables will be
available:
* `<app-name>` * `<app-name>`
* `<palette>` * `<palette>`
* `<scheme>` * `<scheme>`
- For apps with `external_theme = False`, config variants must named as - For apps with `external_theme = False`, config variants must named as
`<app-name>-<palette>-<scheme>.<ext>`, where `<ext>` is the app's default config file `<app-name>-<palette>-<scheme>.<ext>`, where `<ext>` is the app's default
extension. config file extension.
- For apps with `external_theme = True`, the file `<config-dir>/current-theme.conf` will - For apps with `external_theme = True`, the file
be used when symlinking the requested theme. The config file thus must point to this `<config-dir>/current-theme.conf` will be used when symlinking the requested
file in order to change with the set theme. theme. The config file thus must point to this file in order to change with
the set theme.
Additionally, the theme symlink will be created from the file Additionally, the theme symlink will be created from the file
@ -87,147 +96,162 @@ appropriately:
## Directory structure ## Directory structure
- `autoconf/`: main repo directory - `autoconf/`: main repo directory
* `config/`: app-specific configuration files. Each folder inside this directory is * `config/`: app-specific configuration files. Each folder inside this
app-specific, and the target of associated copy operations when a config sync is directory is app-specific, and the target of associated copy operations
performed. Nothing in this directory should pertain to any repo functionality; it when a config sync is performed. Nothing in this directory should pertain
should only contain config files that originated elsewhere on the system. to any repo functionality; it should only contain config files that
* `themes/`: app-independent theme data files. Each folder in this directory should originated elsewhere on the system.
correspond to a specific color palette and house any relevant color spec files * `themes/`: app-independent theme data files. Each folder in this
(currently likely be a `colors.json`). Also servers the output location for directory should correspond to a specific color palette and house any
generated theme files relevant color spec files (currently likely be a `colors.json`). Also
* `<palette>/colors.json`: JSON formatted color key-value pairings for palette servers the output location for generated theme files
colors. There's no standard here aside from the filename and format; downstream * `<palette>/colors.json`: JSON formatted color key-value pairings for
app-specific TOML templates can be dependent on any key naming scheme within the palette colors. There's no standard here aside from the filename and
JSON. format; downstream app-specific TOML templates can be dependent on
+ `<palette>/apps/<app-name>/templates/`: houses the TOML maps for the color any key naming scheme within the JSON.
palette `<palette>` under app `<app-name>`. Files `<fname>.toml` will be mapped to + `<palette>/apps/<app-name>/templates/`: houses the TOML maps for the
`<fname>.conf` in the theme output folder (below), so ensure the naming color palette `<palette>` under app `<app-name>`. Files
standards align with those outlined above. `<fname>.toml` will be mapped to `<fname>.conf` in the theme output
+ `<palette>/apps/<app-name>/generated/`: output directory for generated scheme folder (below), so ensure the naming standards align with those
variants. These are the symlink targets for dynamically set external themes. outlined above.
* `app_registry.toml`: global application "registry" used by sync and theme-setting + `<palette>/apps/<app-name>/generated/`: output directory for
scripts. This lets apps be dynamically added or removed from being eligible for generated scheme variants. These are the symlink targets for
config-related operations. dynamically set external themes.
* `app_registry.toml`: global application "registry" used by sync and
theme-setting scripts. This lets apps be dynamically added or removed
from being eligible for config-related operations.
## Scripts ## Scripts
`set_theme.py`: sets a theme across select apps. `set_theme.py`: sets a theme across select apps.
- Applies to specific app with `-a <app>` , or to all apps in the `app_registry.toml` with - Applies to specific app with `-a <app>` , or to all apps in the
`-a "*"`. `app_registry.toml` with `-a "*"`.
- Uses symlinks to set canonical config files to theme-based variations. Which files get - Uses symlinks to set canonical config files to theme-based variations. Which
set depends on the _app type_ (see above), which really just boils down to whether files get set depends on the _app type_ (see above), which really just boils
theming (1) can be specified with an external format, and (2) if it depends on down to whether theming (1) can be specified with an external format, and (2)
auto-generated theme files from within `autoconf`. if it depends on auto-generated theme files from within `autoconf`.
- Palette and scheme are specified as expected. They are used to infer proper paths - Palette and scheme are specified as expected. They are used to infer proper
according to naming and structure standards. paths according to naming and structure standards.
- Real config files will never be overwritten. To begin setting themes with the script, - Real config files will never be overwritten. To begin setting themes with the
you must delete the canonical config file expected by the app (and specified in the app script, you must delete the canonical config file expected by the app (and
registry) to allow the first symlink to be set. From there on out, symlinks will be specified in the app registry) to allow the first symlink to be set. From
automatically flushed. there on out, symlinks will be automatically flushed.
- A report will be provided on which apps were successfully set to the requested theme, - A report will be provided on which apps were successfully set to the
along with the file stems. A number of checks are in place for the existence of involved requested theme, along with the file stems. A number of checks are in place
files and directories. Overall, the risk of overwritting a real config file is low; we for the existence of involved files and directories. Overall, the risk of
only flush existing symlinks, and if the would-be target for the requested theme (be it overwritting a real config file is low; we only flush existing symlinks, and
from an auto-generated theme file, or from a manually manage config variant) doesn't if the would-be target for the requested theme (be it from an auto-generated
exist, that app's config will be completed skipped. Essentially, everything must be in theme file, or from a manually manage config variant) doesn't exist, that
app's config will be completed skipped. Essentially, everything must be in
perfect shape before the symlink trigger is officially pulled. perfect shape before the symlink trigger is officially pulled.
`gen_theme.py`: generates theme files for palettes by mapping their color
definitions through app-specific templates. These templates specific how to
relate an app's theme variables to the color names provided by the template.
`gen_theme.py`: generates theme files for palettes by mapping their color definitions - An app and palette are the two required parameters. If no template or output
through app-specific templates. These templates specific how to relate an app's theme paths are provided, they will be inferred according to the theme path
variables to the color names provided by the template. standards seen above.
- The `--template` argument can be a directory or a file, depending on what
theme files you'd like to render.
- The `--output` path, if specified, must be a directory. Generated theme files
take on a name with the same stem as their source template, but using the
`.conf` extension.
- The TOML templates should make config variable names to JSON dot-notation
accessors. If color definitions are nested, the dot notation should be
properly expanded by the script when mapping the colors to keyword values.
- There are a number of checks for existing paths, even those inferred (e.g.,
template and output) from the palette and app. If the appropriate setup
hasn't been followed, the script will fail. Make sure the `theme` folder in
question and it's nested `app` directory are correctly setup before running
the script. (Perhaps down the line there are some easy auto-setup steps to
take here, but I'm not making that jump now.)
- TODO: open up different app "writers," or make it easy to extend output
syntax based on the app in question. This would like be as simple as mapping
app names to line-generating functions, which accept the keyword and color
(among other items). This can be fleshed out as needed.
- An app and palette are the two required parameters. If no template or output paths are `sync.sh`: copies relevant configuration files from local paths into the
provided, they will be inferred according to the theme path standards seen above. `autoconf` subpath. Markdown files in the docs directory then reference the
- The `--template` argument can be a directory or a file, depending on what theme files local copies of these files, meaning the documentation updates dynamically when
you'd like to render. the configuration files do. That is, the (possibly extracted) config snippets
- The `--output` path, if specified, must be a directory. Generated theme files take on will change with the current state of my system config without any manual
a name with the same stem as their source template, but using the `.conf` extension. intervention of the documentation files.
- The TOML templates should make config variable names to JSON dot-notation accessors. If
color definitions are nested, the dot notation should be properly expanded by the script
when mapping the colors to keyword values.
- There are a number of checks for existing paths, even those inferred (e.g., template and
output) from the palette and app. If the appropriate setup hasn't been followed, the
script will fail. Make sure the `theme` folder in question and it's nested `app`
directory are correctly setup before running the script. (Perhaps down the line there
are some easy auto-setup steps to take here, but I'm not making that jump now.)
- TODO: open up different app "writers," or make it easy to extend output syntax based on
the app in question. This would like be as simple as mapping app names to
line-generating functions, which accept the keyword and color (among other items). This
can be fleshed out as needed.
`sync.sh`: copies relevant configuration files from local paths into the `autoconf`
subpath. Markdown files in the docs directory then reference the local copies of these
files, meaning the documentation updates dynamically when the configuration files do. That
is, the (possibly extracted) config snippets will change with the current state of my
system config without any manual intervention of the documentation files.
### Specific theme-setting example ### Specific theme-setting example
To make clear how the theme setting script works on my system, the following breaks down To make clear how the theme setting script works on my system, the following
exactly what steps are taken to exert as much scheme control as possible. Everything at this breaks down exactly what steps are taken to exert as much scheme control as
point is wrapped up in a single `make set-<palette>-<scheme>` call; suppose we're possible. Everything at this point is wrapped up in a single `make
currently running the dark scheme (see first image) and I run `make set-tone4-light`: set-<palette>-<scheme>` call; suppose we're currently running the dark scheme
(see first image) and I run `make set-tone4-light`:
![ ![
Starting point; have a GTK app (GNOME files), `kitty`, and Firefox (with the Starting point; have a GTK app (GNOME files), `kitty`, and Firefox (with the
system-dependent default theme set). In Firefox, I have open `localsys` with its system-dependent default theme set). In Firefox, I have open `localsys` with
scheme-mode to set to "auto," which should reflect the theme setting picked up by the its scheme-mode to set to "auto," which should reflect the theme setting
browser (and note the white tab icon). picked up by the browser (and note the white tab icon).
](_static/set-theme-1.png) ](_static/set-theme-1.png)
_(Starting point; have a GTK app (GNOME files), `kitty`, and Firefox (with the _(Starting point; have a GTK app (GNOME files), `kitty`, and Firefox (with the
system-dependent default theme set). In Firefox, I have open `localsys` with its system-dependent default theme set). In Firefox, I have open `localsys` with
scheme-mode to set to "auto," which should reflect the theme setting picked up by the its scheme-mode to set to "auto," which should reflect the theme setting picked
browser (and note the white tab icon).)_ up by the browser (and note the white tab icon).)_
1. `set_theme.py` is invoked. Global settings are applied first, based on my OS (`Linux`), 1. `set_theme.py` is invoked. Global settings are applied first, based on my OS
which calls (`Linux`), which calls
``` ```
gsettings set org.gnome.desktop.interface color-scheme 'prefer-light' gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
``` ```
controlling settings for GTK apps and other `desktop-portal`-aware programs. This controlling settings for GTK apps and other `desktop-portal`-aware programs.
yields the following: This yields the following:
![Portal-aware apps changed, config apps not yet set](_static/set-theme-2.png) ![Portal-aware apps changed, config apps not yet set](_static/set-theme-2.png)
_(Portal-aware apps changed, config apps not yet set. Scheme-aware sites are updated _(Portal-aware apps changed, config apps not yet set. Scheme-aware sites are
without page refresh.)_ updated without page refresh.)_
2. Specific application styles are set. For now the list is small, including `kitty`, 2. Specific application styles are set. For now the list is small, including
`waybar`, and `sway`. `kitty` is the only type (2) application here, whereas the other `kitty`, `waybar`, and `sway`. `kitty` is the only type (2) application
two are type (1). here, whereas the other two are type (1).
a. For the type (1) apps, the canonical config files as specified in the app registry a. For the type (1) apps, the canonical config files as specified in the app
are symlinked to their light variants. For `sway`, this is `~/.config/sway/config`, registry are symlinked to their light variants. For `sway`, this is
and if we look at the `file`: `~/.config/sway/config`, and if we look at the `file`:
```sh ```sh
.config/sway/config: symbolic link to ~/.config/sway/sway-tone4-light .config/sway/config: symbolic link to ~/.config/sway/sway-tone4-light
``` ```
b. For the type (2) apps, just the `current-theme.conf` file is symlinked to the b. For the type (2) apps, just the `current-theme.conf` file is symlinked to
relevant palette-scheme file. `kitty` is such an app, with a supported theme file the relevant palette-scheme file. `kitty` is such an app, with a
for `tone4`, and those files have been auto-generated via `gen_theme.py`. Looking at supported theme file for `tone4`, and those files have been
this file under the `kitty` config directory: auto-generated via `gen_theme.py`. Looking at this file under the `kitty`
config directory:
```sh ```sh
.config/kitty/current-theme.conf: symbolic link to ~/Documents/projects/autoconf/autoconf/themes/tone4/apps/kitty/generated/light.conf .config/kitty/current-theme.conf: symbolic link to
~/Documents/projects/autoconf/autoconf/themes/tone4/apps/kitty/generated/light.conf
``` ```
The `kitty.conf` file isn't changed, as all palette-related items are specified in The `kitty.conf` file isn't changed, as all palette-related items are
the theme file. (Note that the general notion of a "theme" could include changes to specified in the theme file. (Note that the general notion of a "theme"
other stylistic aspects, like the font family; this would likely require some hybrid could include changes to other stylistic aspects, like the font family;
type 1-2 approach not yet implemented). this would likely require some hybrid type 1-2 approach not yet
3. Live application instances are reloaded, according to the registered `refresh_cmd`s. implemented).
Here the apps with style/config files set in step (2) are reloaded to reflect those 3. Live application instances are reloaded, according to the registered
changes. Again, in this example, this is `kitty`, `sway`, and the `waybar`. `refresh_cmd`s. Here the apps with style/config files set in step (2) are
reloaded to reflect those changes. Again, in this example, this is `kitty`,
`sway`, and the `waybar`.
![Final light setting: portal-dependent apps _and_ config-based apps changed](_static/set-theme-3.png) ![
Final light setting: portal-dependent apps _and_ config-based apps changed
](_static/set-theme-3.png)
_(Final light setting: portal-dependent apps _and_ config-based apps changed)_ _(Final light setting: portal-dependent apps _and_ config-based apps
4. `set_theme.py` provides a report for the actions taken; in this case, the following was changed)_
printed: 4. `set_theme.py` provides a report for the actions taken; in this case, the
following was printed:
![`set_theme.py` output](_static/set-theme-4.png) ![`set_theme.py` output](_static/set-theme-4.png)
_(`set_theme.py` output)_ _(`set_theme.py` output)_

View File

@ -1,61 +1,64 @@
# Configuring # Configuring
`symconf` operates on a central directory that houses all of the config files you may wish `symconf` operates on a central directory that houses all of the config files
to apply. The default location for this directory is your `$XDG_CONFIG_HOME` (e.g., you may wish to apply. The default location for this directory is your
`~/.config/symconf/`), but it can be any location on your system so long as it's specified `$XDG_CONFIG_HOME` (e.g., `~/.config/symconf/`), but it can be any location on
(see more in Usage). your system so long as it's specified (see more in Usage).
`symconf` expects you to create two top-level components in your config directory: an `symconf` expects you to create two top-level components in your config
`apps/` directory and an `app_registry.toml` file. directory: an `apps/` directory and an `app_registry.toml` file.
**High-level view**: **High-level view**:
- `symconf` operates on a single directory that houses all your config files - `symconf` operates on a single directory that houses all your config files
- Config files in this directory must be placed under an `apps/<app-name>/` folder to be - Config files in this directory must be placed under an `apps/<app-name>/`
associated with the app `<app-name>` folder to be associated with the app `<app-name>`
- For apps to be visible, you need an `app_registry.toml` file that tells `symconf` where - For apps to be visible, you need an `app_registry.toml` file that tells
to symlink your files in `apps/` `symconf` where to symlink your files in `apps/`
## Apps directory ## Apps directory
An `apps/` directory should be created in your config home, with a subdirectory An `apps/` directory should be created in your config home, with a subdirectory
`apps/<app-name>/` for each app with config files that you'd like to be visible to `apps/<app-name>/` for each app with config files that you'd like to be visible
`symconf`. Note that simply populating an app's config folder here will do nothing on its to `symconf`. Note that simply populating an app's config folder here will do
own; the app must also have been registered (discussed in item #2) in order for these nothing on its own; the app must also have been registered (discussed in item
files to be used when `symconf` is invoked. (This just means you can populate your `apps/` #2) in order for these files to be used when `symconf` is invoked. (This just
folder safely without expecting any default behavior. More often than not you'll be means you can populate your `apps/` folder safely without expecting any default
expected to tell `symconf` exactly where your config files should end up, meaning you know behavior. More often than not you'll be expected to tell `symconf` exactly
exactly what it's doing.) where your config files should end up, meaning you know exactly what it's
doing.)
### User config ### User config
Inside your app-specific subdirectory, your managed config files should be placed in a Inside your app-specific subdirectory, your managed config files should be
`user/` subdirectory (distinguishing them from those generated by templates; see more placed in a `user/` subdirectory (distinguishing them from those generated by
in Themes). Your config files themselves are then expected to follow a specific naming templates; see more in Themes). Your config files themselves are then expected
scheme: to follow a specific naming scheme:
```sh ```sh
<palette>-<scheme>.<config-name> <palette>-<scheme>.<config-name>
``` ```
This ties your config file to a particular theme setting as needed, and `symconf` will This ties your config file to a particular theme setting as needed, and
apply it if it matches the theme setting you provide when invoked. The specific values are `symconf` will apply it if it matches the theme setting you provide when
as follows: invoked. The specific values are as follows:
- `scheme`: can be `light`, `dark`, or `none`. Indicates whether the config file should - `scheme`: can be `light`, `dark`, or `none`. Indicates whether the config
be applied specifically when requesting a light or dark mode. Use `none` to indicate file should be applied specifically when requesting a light or dark mode. Use
that the config file does not have settings specific to a light/dark mode. `none` to indicate that the config file does not have settings specific to a
- `palette`: a "palette name" of your choosing, or `none`. The palette name you use light/dark mode.
here may refer specifically to a color palette used by the config file, but can be - `palette`: a "palette name" of your choosing, or `none`. The palette name you
used generally to indicate any particular group of config settings (e.g., fonts, use here may refer specifically to a color palette used by the config file,
transparency, etc). Use `none` to indicate that the file does not correspond to any but can be used generally to indicate any particular group of config settings
particular style group. (e.g., fonts, transparency, etc). Use `none` to indicate that the file does
- `config-name`: the _name_ of the config file. This should correspond to _same path not correspond to any particular style group.
name_ that is expected by the app being configured. For example, if your app expects a - `config-name`: the _name_ of the config file. This should correspond to _same
config file at `a/b/c/d.conf`, "`d.conf`" is the path name. path name_ that is expected by the app being configured. For example, if your
app expects a config file at `a/b/c/d.conf`, "`d.conf`" is the path name.
When invoking `symconf` with specific scheme and palette settings (see more in Usage), When invoking `symconf` with specific scheme and palette settings (see more in
appropriate config files can be matched based on how you've named your files. Usage), appropriate config files can be matched based on how you've named your
files.
For example, suppose I want to set up a simple light/dark mode switch for the `kitty` For example, suppose I want to set up a simple light/dark mode switch for the
terminal emulator. The following tree demonstrates a valid setup: `kitty` terminal emulator. The following tree demonstrates a valid setup:
```sh ```sh
<config-home> <config-home>
@ -66,30 +69,31 @@ terminal emulator. The following tree demonstrates a valid setup:
└── none-dark.kitty.conf └── none-dark.kitty.conf
``` ```
where `none-light.kitty.conf` may set a light background and `none-dark.kitty.conf` a dark where `none-light.kitty.conf` may set a light background and
one. `none` is used for the `<palette>` part of the name to indicate the configuration does `none-dark.kitty.conf` a dark one. `none` is used for the `<palette>` part of
not pertain to any specific palette and can be matched even if one is not provided. With the name to indicate the configuration does not pertain to any specific palette
an appropriate `app_regsitry.toml` file (see below), invoking and can be matched even if one is not provided. With an appropriate
`app_regsitry.toml` file (see below), invoking
```sh ```sh
symconf --theme=light --apps=kitty symconf --theme=light --apps=kitty
``` ```
would symlink `$XDG_CONFIG_HOME/symconf/apps/kitty/user/none-light.kitty.conf` to would symlink `$XDG_CONFIG_HOME/symconf/apps/kitty/user/none-light.kitty.conf`
`~/.config/kitty/kitty.conf`. to `~/.config/kitty/kitty.conf`.
### Templatized config ### Templatized config
Note the potential inconvenience in needing to manage two separate config files in the Note the potential inconvenience in needing to manage two separate config files
above example, very likely with all but one line of difference. Templating enables in the above example, very likely with all but one line of difference.
populating config template files dynamically with theme-specific variables of your Templating enables populating config template files dynamically with
choosing. theme-specific variables of your choosing.
### Reload scripts ### Reload scripts
After symlinking a new set of config files, it is often necessary to reload the system or After symlinking a new set of config files, it is often necessary to reload the
relevant apps in order for the new config settings to apply. Within an app's subdirectory, system or relevant apps in order for the new config settings to apply. Within
a `call/` folder can be created to hold scripts that should apply based on certain schemes an app's subdirectory, a `call/` folder can be created to hold scripts that
or palettes (matching them in the same way as config files). For example, should apply based on certain schemes or palettes (matching them in the same
way as config files). For example,
```sh ```sh
<config-home> <config-home>
@ -99,22 +103,22 @@ or palettes (matching them in the same way as config files). For example,
└── none-none.sh └── none-none.sh
``` ```
`none-none.sh` might simply contain `kill -s USR1 $(pgrep kitty)`, which is a way to tell `none-none.sh` might simply contain `kill -s USR1 $(pgrep kitty)`, which is a
all running `kitty` instances to reload their config settings. Again, following the naming way to tell all running `kitty` instances to reload their config settings.
scheme for config files, a script named `none-none.sh` will apply under any scheme or Again, following the naming scheme for config files, a script named
palette specification. Thus, in our light/dark mode switch example, invoking `symconf `none-none.sh` will apply under any scheme or palette specification. Thus, in
--theme=light --apps=kitty` would: our light/dark mode switch example, invoking `symconf --theme=light
--apps=kitty` would:
1. Search and match the config name `none-light.kitty.conf` and symlink it to 1. Search and match the config name `none-light.kitty.conf` and symlink it to
`~/.config/kitty/kitty.conf`. `~/.config/kitty/kitty.conf`.
2. Search and match `none-none.sh` and execute it, applying the new light mode settings to 2. Search and match `none-none.sh` and execute it, applying the new light mode
all running `kitty` instances. settings to all running `kitty` instances.
## App registry ## App registry
An `app_registry.toml` file, used to specify the target locations for app-specific An `app_registry.toml` file, used to specify the target locations for
config files. To "register" an app, you simply need to add the following text block to app-specific config files. To "register" an app, you simply need to add the
the `app_registry.toml` file: following text block to the `app_registry.toml` file:
```toml ```toml
[app.<app-name>] [app.<app-name>]
@ -128,22 +132,22 @@ config_map = {
} }
``` ```
(Note that text in angle brackets refers to values that should be replaced.) This tells (Note that text in angle brackets refers to values that should be replaced.)
`symconf` how it should handle each app's config files. The `<app-name>` (e.g., This tells `symconf` how it should handle each app's config files. The
`kitty`) should correspond to a subdirectory under `apps/` (e.g., `apps/kitty/`) that `<app-name>` (e.g., `kitty`) should correspond to a subdirectory under `apps/`
holds your config files for that app. As shown, you then need to supply either of the (e.g., `apps/kitty/`) that holds your config files for that app. As shown, you
following options: then need to supply either of the following options:
- `config_dir`: specifies a single directory where all of the app's matching config - `config_dir`: specifies a single directory where all of the app's matching
files should be symlinked. In the `kitty` example, this might be `~/.config/kitty`. config files should be symlinked. In the `kitty` example, this might be
This is the simplest and most common option provided most apps expect all of their `~/.config/kitty`. This is the simplest and most common option provided most
config files to be in a single directory. apps expect all of their config files to be in a single directory.
- `config_map`: a dictionary mapping config file _path names_ to the _exact paths_ that - `config_map`: a dictionary mapping config file _path names_ to the _exact
should be created during the symlink process. This is typically needed when an app paths_ that should be created during the symlink process. This is typically
has many config files that need to be set in several disparate locations across your needed when an app has many config files that need to be set in several
system. In the `kitty` example, although not necessary (and in general you should disparate locations across your system. In the `kitty` example, although not
prefer to set `config_dir` when applicable), we could have the following necessary (and in general you should prefer to set `config_dir` when
`config_map`: applicable), we could have the following `config_map`:
```toml ```toml
[app.kitty] [app.kitty]
@ -152,8 +156,9 @@ following options:
} }
``` ```
This tells `symconf` to symlink the exact location `~/.config/kitty/kitty.conf` to This tells `symconf` to symlink the exact location
the matching `kitty.conf` under the `apps/kitty` directory. `~/.config/kitty/kitty.conf` to the matching `kitty.conf` under the
`apps/kitty` directory.
## Directory structure ## Directory structure
In total, the structure of your base config directory can look as follows: In total, the structure of your base config directory can look as follows:

View File

@ -1,6 +1,170 @@
# Usage # Usage
```{toctree} ```sh
:hidden: usage: symconf [-h] [-c CONFIG_DIR] [-v] {config,generate,install,update} ...
Manage application configuration with symlinks.
options:
-h, --help show this help message and exit
-c CONFIG_DIR, --config-dir CONFIG_DIR
Path to config directory
-v, --version Print symconf version
subcommand actions:
{config,generate,install,update}
``` ```
Additional argument details:
- `-h --help`: print help message
- `-c --config-dir`: the location of the `symconf` config directory. Assumes
`$XDG_CONFIG_HOME` (e.g., `~/.config/symconf/`) by default.
## `config` subcommand
The config subcommand applies symlinks for registered application routes that
meet specified constraints.
```sh
usage: symconf config [-h] [-s STYLE] [-m MODE] [-a APPS] [-T TEMPLATE_VARS [TEMPLATE_VARS ...]]
Set config files for registered applications.
options:
-h, --help show this help message and exit
-s STYLE, --style STYLE
Style indicator (often a color palette) capturing thematic details in a config file
-m MODE, --mode MODE Preferred lightness mode/scheme, either "light," "dark," "any," or "none."
-a APPS, --apps APPS Application target for theme. App must be present in the registry. Use "*" to apply to all registered apps
-T TEMPLATE_VARS [TEMPLATE_VARS ...], --template-vars TEMPLATE_VARS [TEMPLATE_VARS ...]
Groups to use when populating templates, in the form group=value
```
- `symconf config` is the subcommand used to match and set available config
files for registered applications
* `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to
consider all registered apps.
* `-m --mode`: preferred lightness mode/scheme, either `light`, `dark`,
`any`, or `none`.
* `-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 below).
* `-T --template-vars`: additional groups to use when populating templates,
in the form `<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 folder (i.e., `<value>.toml`).
## `generate` subcommand
The generate subcommand fills in templates with theme values matched under
provided constraints,
```sh
usage: symconf generate [-h] -o OUTPUT_DIR [-s STYLE] [-m MODE] [-a APPS] [-T TEMPLATE_VARS [TEMPLATE_VARS ...]]
Generate all template config files for specified apps
options:
-h, --help show this help message and exit
-o OUTPUT_DIR, --output-dir OUTPUT_DIR
Path to write generated template files
-s STYLE, --style STYLE
Style indicator (often a color palette) capturing thematic details in a config file
-m MODE, --mode MODE Preferred lightness mode/scheme, either "light," "dark," "any," or "none."
-a APPS, --apps APPS Application target for theme. App must be present in the registry. Use "*" to apply to all registered apps
-T TEMPLATE_VARS [TEMPLATE_VARS ...], --template-vars TEMPLATE_VARS [TEMPLATE_VARS ...]
Groups to use when populating templates, in the form group=value
```
- `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.
## `install` subcommand
The install subcommand runs the `install.sh` scripts for registered apps.
```sh
usage: symconf install [-h] [-a APPS]
Run install scripts for registered applications.
options:
-h, --help show this help message and exit
-a APPS, --apps APPS Application target for theme. App must be present in the registry. Use "*" to apply to all registered apps
```
- `symconf install`: runs install scripts for matching apps
* `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to
consider all registered apps.
## `update` subcommand
The update subcommand runs the `update.sh` scripts for registered apps. This
action expects the install process for each matched app to have been run
beforehand.
```sh
usage: symconf update [-h] [-a APPS]
Run update scripts for registered applications.
options:
-h, --help show this help message and exit
-a APPS, --apps APPS Application target for theme. App must be present in the registry. Use "*" to apply to 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.
## Matching factors
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 determine a config match):
- `any` will match config files with _any_ value for this factor, preferring
config files with a value `none`, indicating no dependence on the factor.
This is the default value when a factor is left unspecified.
- `none` will match `"none"` directly for a given factor (so no special
behavior), but used to indicate that a config file is independent of the
factor. For instance,
```sh
symconf config -m light -s none
```
will match config files that capture the notion of a light mode, but do not
depend on or provide further thematic components such as a color palette.
## Examples
- Set a dark mode for all registered apps, matching any available style/palette
component:
```sh
symconf config -m dark
```
- Set `solarized` theme for `kitty` and match any available mode (light or
dark):
```sh
symconf config -s solarized -a kitty
```
- Set a dark `gruvbox` theme for multiple apps (but not all):
```sh
symconf config -m dark -s gruvbox -apps="kitty,nvim"
```
- Set a dark `gruvbox` theme for all apps, and attempt to match other template
elements:
```sh
symconf config -m dark -s gruvbox -T font=mono window=sharp
```
which would attempt to find and load key-value pairs in the files
`$CONFIG_HOME/groups/font/mono.toml` and
`$CONFIG_HOME/groups/window/sharp.toml` to be used as values when filling
templatized config files.

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.3"
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",
"--apps",
required=False, required=False,
default="*", default="*",
type = lambda s: s.split(',') if s != '*' else s, type=lambda s: s.split(",") if s != "*" else s,
help = 'Application target for theme. App must be present in the registry. ' \ help=(
+ 'Use "*" to apply to all registered apps' "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",
"--apps",
required=False, required=False,
default = '*', default="*",
type = lambda s: s.split(',') if s != '*' else s, type=lambda s: s.split(",") if s != "*" else s,
help = 'Application target for theme. App must be present in the registry. ' \ help=(
+ 'Use "*" to apply to all registered apps' "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',
help = 'Style indicator (often a color palette) capturing thematic details in '
'a config file'
)
parser.add_argument(
'-m', '--mode',
required=False, required=False,
default="any", default="any",
help = 'Preferred lightness mode/scheme, either "light," "dark," "any," or "none."' help=(
"Style indicator (often a color palette) capturing "
"thematic details in a config file"
),
) )
parser.add_argument( parser.add_argument(
'-a', '--apps', "-m",
"--mode",
required=False,
default="any",
help=(
'Preferred lightness mode/scheme, either "light," "dark," '
'"any," or "none."'
),
)
parser.add_argument(
"-a",
"--apps",
required=False, required=False,
default="*", default="*",
type = lambda s: s.split(',') if s != '*' else s, type=lambda s: s.split(",") if s != "*" else s,
help = 'Application target for theme. App must be present in the registry. ' \ help=(
+ 'Use "*" to apply to all registered apps' "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",
"--template-vars",
required=False, required=False,
nargs='+', 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)
# central argparse entry point def add_generate_subparser(subparsers: ArgumentParser) -> None:
parser = argparse.ArgumentParser( def generate_apps(args: Namespace) -> None:
'symconf', cm = ConfigManager(args.config_dir)
description='Manage application configuration with symlinks.' 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( parser.add_argument(
'-c', '--config-dir', "-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
parser = ArgumentParser(
"symconf", description="Manage application configuration with symlinks."
)
parser.add_argument(
"-c",
"--config-dir",
default=util.xdg_config_path(), default=util.xdg_config_path(),
type=util.absolute_path, type=util.absolute_path,
help = 'Path to config directory' 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,56 +1,62 @@
''' """
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:
.. code-block:: sh
<style>-<scheme>.<config_pathname> <style>-<scheme>.<config_pathname>
- ``config_pathname``: refers to a concrete filename, typically that which is expected by - ``config_pathname``: refers to a concrete filename, typically that which is
the target app (e.g., ``kitty.conf``). In the context of ``config_map`` in the registry, expected by the target app (e.g., ``kitty.conf``). In the context of
however, it merely serves as an identifier, as it can be mapped onto any path. ``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 soft-gruvbox-dark.kitty.conf
```
gets mapped to gets mapped to
```sh .. code-block:: sh
style -> "soft-gruvbox" style -> "soft-gruvbox"
scheme -> "dark" scheme -> "dark"
pathname -> "kitty.conf" 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
@ -59,81 +65,87 @@ class Matcher:
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),
] ]
@ -144,45 +156,46 @@ class Matcher:
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 This method exists because we need a way to allow any of the combos in
the prefix order to match the candidate files. We don't know a priori
how good of a match will be available, so we consider each file for
each of the prefixes, and take the latest/best match for each unique
config pathname (allowing for a "soft" match).
When thinking about how best to structure this method, it initially felt like .. admonition:: Checking for matches
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 When thinking about how best to structure this method, it initially
efficient, but the loop and the simple conditionals is just much simpler to felt like indexing factors of the FileParts would make the most
follow. We're also talking about at most 10s of files, so it really doesn't sense, preventing the inner loop that needs to inspect each
matter. 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,14 +203,17 @@ 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)
@ -205,35 +221,36 @@ class Matcher:
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
@ -76,9 +87,10 @@ class DictReader:
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:
@ -89,6 +101,5 @@ class DictReader:
# 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
@ -10,10 +14,12 @@ 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,
@ -21,32 +27,66 @@ class Template:
) -> 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(
@ -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.3"
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" },
]