├── tests
├── __init__.py
├── data
│ ├── config
│ │ ├── data.toml
│ │ └── data.yaml
│ ├── input2
│ │ ├── ui-lovelace.yaml
│ │ ├── not-empty.yaml.jinja
│ │ ├── include.yaml.partial
│ │ ├── empty.yaml.jinja
│ │ └── views
│ │ │ └── home.yaml.jinja
│ ├── file-specific
│ │ ├── data2.yaml
│ │ └── data1.yaml
│ ├── output
│ │ ├── not-empty.yaml
│ │ ├── extra-file.yaml
│ │ ├── ui-lovelace.yaml
│ │ ├── file-specific.yaml
│ │ └── views
│ │ │ └── home.yaml
│ ├── input1
│ │ ├── secret.yaml
│ │ ├── empty-folder
│ │ │ └── empty-folder.yaml.jinja
│ │ ├── extra-file.yaml
│ │ ├── not-empty.yaml.jinja
│ │ ├── ui-lovelace.yaml.jinja
│ │ └── file-specific.yaml.jinja
│ ├── makejinja.toml
│ ├── plugin.py
│ └── README.md
└── test_makejinja.py
├── src
└── makejinja
│ ├── py.typed
│ ├── __main__.py
│ ├── __init__.py
│ ├── cli.py
│ ├── plugin.py
│ ├── app.py
│ └── config.py
├── assets
├── logo.png
└── demo.scenario
├── renovate.json
├── .github
├── workflows
│ ├── check.yml
│ ├── autofix.yml
│ └── release.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── LICENSE
├── pyproject.toml
├── default.nix
├── flake.nix
├── README.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── flake.lock
├── CHANGELOG.md
└── uv.lock
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/makejinja/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/data/config/data.toml:
--------------------------------------------------------------------------------
1 | toml-value = "Hello world"
2 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mirkolenz/makejinja/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/src/makejinja/__main__.py:
--------------------------------------------------------------------------------
1 | from .cli import makejinja_cli
2 |
3 | makejinja_cli()
4 |
--------------------------------------------------------------------------------
/tests/data/input2/ui-lovelace.yaml:
--------------------------------------------------------------------------------
1 | title: Overview
2 | views: !include_dir_merge_list views
3 |
--------------------------------------------------------------------------------
/tests/data/file-specific/data2.yaml:
--------------------------------------------------------------------------------
1 | specific_var2: "Value from data2"
2 | override_var: "Overridden value"
3 |
--------------------------------------------------------------------------------
/tests/data/input2/not-empty.yaml.jinja:
--------------------------------------------------------------------------------
1 | <# This file is overridden in input_2 to test jinja file overrides #>
2 |
--------------------------------------------------------------------------------
/tests/data/input2/include.yaml.partial:
--------------------------------------------------------------------------------
1 | # Partial template: Included by ui-lovelace.yaml.jinja
2 | icon: mdi:home
3 |
--------------------------------------------------------------------------------
/tests/data/output/not-empty.yaml:
--------------------------------------------------------------------------------
1 | # Test file: Demonstrates custom Jinja delimiters
2 | # This file is NOT EMPTY
3 |
--------------------------------------------------------------------------------
/tests/data/file-specific/data1.yaml:
--------------------------------------------------------------------------------
1 | specific_var1: "Value from data1"
2 | specific_list:
3 | - item1
4 | - item2
5 |
--------------------------------------------------------------------------------
/tests/data/input1/secret.yaml:
--------------------------------------------------------------------------------
1 | # Test file: Should be excluded by plugin path filter (contains 'secret' in filename)
2 | {}
3 |
--------------------------------------------------------------------------------
/tests/data/input1/empty-folder/empty-folder.yaml.jinja:
--------------------------------------------------------------------------------
1 | <# This file is here to test the --skip-empty feature for directories #>
2 |
--------------------------------------------------------------------------------
/tests/data/input1/extra-file.yaml:
--------------------------------------------------------------------------------
1 | title: Overview
2 | views: !include_dir_merge_list views
3 | <% include 'include.yaml.partial' %>
4 |
--------------------------------------------------------------------------------
/tests/data/input1/not-empty.yaml.jinja:
--------------------------------------------------------------------------------
1 | # Test file: Demonstrates custom Jinja delimiters
2 | # This file is << 'not empty' | upper >>
3 |
--------------------------------------------------------------------------------
/tests/data/input2/empty.yaml.jinja:
--------------------------------------------------------------------------------
1 | <# Test file: Demonstrates empty template handling (comment-only, should not generate output) #>
2 |
--------------------------------------------------------------------------------
/tests/data/output/extra-file.yaml:
--------------------------------------------------------------------------------
1 | title: Overview
2 | views: !include_dir_merge_list views
3 | <% include 'include.yaml.partial' %>
4 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["github>mirkolenz/renovate-preset"],
4 | "lockFileMaintenance": {
5 | "enabled": true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tests/data/input1/ui-lovelace.yaml.jinja:
--------------------------------------------------------------------------------
1 | # Test file: Demonstrates partial template inclusion and YAML includes
2 | title: Overview
3 | views: !include_dir_merge_list views
4 | <% include 'include.yaml.partial' %>
5 |
--------------------------------------------------------------------------------
/tests/data/output/ui-lovelace.yaml:
--------------------------------------------------------------------------------
1 | # Test file: Demonstrates partial template inclusion and YAML includes
2 | title: Overview
3 | views: !include_dir_merge_list views
4 | # Partial template: Included by ui-lovelace.yaml.jinja
5 | icon: mdi:home
6 |
--------------------------------------------------------------------------------
/tests/data/output/file-specific.yaml:
--------------------------------------------------------------------------------
1 | # Test file: Demonstrates file-specific data loading (data1.yaml, data2.yaml)
2 | specific_var1: Value from data1
3 | specific_var2: Value from data2
4 | specific_list:
5 | - item1
6 | - item2
7 | override_var: Overridden value
8 |
--------------------------------------------------------------------------------
/assets/demo.scenario:
--------------------------------------------------------------------------------
1 | $ cat demo.txt.jinja
2 | {% for word in ["lorem", "ipsum", "dolor"] %}
3 | Word {{loop.index}}: {{word | capitalize}}
4 | {% endfor %}
5 |
6 | $ makejinja -i demo.txt.jinja -o .
7 | Render file 'demo.txt.jinja' -> 'demo.txt'
8 |
9 | $ cat demo.txt
10 | Word 1: Lorem
11 | Word 2: Ipsum
12 | Word 3: Dolor
13 |
--------------------------------------------------------------------------------
/tests/data/input1/file-specific.yaml.jinja:
--------------------------------------------------------------------------------
1 | # Test file: Demonstrates file-specific data loading (data1.yaml, data2.yaml)
2 | specific_var1: << specific_var1 >>
3 | specific_var2: << specific_var2 | default('Not available') >>
4 | specific_list:
5 | <% for item in specific_list %>
6 | - << item >>
7 | <% endfor %>
8 | override_var: << override_var | default('Default value') >>
--------------------------------------------------------------------------------
/tests/data/makejinja.toml:
--------------------------------------------------------------------------------
1 | [makejinja]
2 | inputs = ["./input1", "./input2"]
3 | output = "./output"
4 | data = ["./config"]
5 | plugins = ["plugin:Plugin"]
6 | exclude_patterns = ["*.partial"]
7 | data_vars = { "areas.kitchen.name.en" = "Cuisine" }
8 |
9 | [makejinja.file_data]
10 | "file-specific.yaml.jinja" = ["./file-specific/data1.yaml", "./file-specific/data2.yaml"]
11 |
12 | [makejinja.delimiter]
13 | block_start = "<%"
14 | block_end = "%>"
15 | comment_start = "<#"
16 | comment_end = "#>"
17 | variable_start = "<<"
18 | variable_end = ">>"
19 |
--------------------------------------------------------------------------------
/src/makejinja/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | **[🌟 GitHub Project 🌟](https://github.com/mirkolenz/makejinja)**
3 |
4 | 
5 |
6 | .. include:: ../../README.md
7 | :start-after:
8 |
9 | ## Usage as a Library
10 |
11 | While mainly intended to be used as a command line tool, makejinja can also be from Python directly.
12 | """
13 |
14 | from . import cli, config, plugin
15 | from .app import makejinja
16 |
17 | loader = plugin
18 |
19 | __all__ = ["makejinja", "config", "plugin", "loader", "cli"]
20 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: check
2 | on:
3 | pull_request:
4 | workflow_call:
5 | jobs:
6 | check:
7 | runs-on: ubuntu-latest
8 | permissions:
9 | contents: read
10 | steps:
11 | - uses: actions/checkout@v6
12 | - uses: DeterminateSystems/nix-installer-action@v21
13 | with:
14 | extra-conf: |
15 | accept-flake-config = true
16 | - uses: cachix/cachix-action@v16
17 | with:
18 | name: mirkolenz
19 | authToken: ${{ secrets.CACHIX_TOKEN }}
20 | - run: nix flake check --show-trace
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: enhancement
6 | assignees: mirkolenz
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/workflows/autofix.yml:
--------------------------------------------------------------------------------
1 | name: autofix.ci
2 | on:
3 | pull_request:
4 | push:
5 | branches: [main, beta]
6 | jobs:
7 | autofix:
8 | if: ${{ github.repository_owner == 'mirkolenz' }}
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: read
12 | steps:
13 | - uses: actions/checkout@v6
14 | - uses: DeterminateSystems/nix-installer-action@v21
15 | with:
16 | extra-conf: |
17 | accept-flake-config = true
18 | - uses: cachix/cachix-action@v16
19 | with:
20 | name: mirkolenz
21 | authToken: ${{ secrets.CACHIX_TOKEN }}
22 | - run: nix fmt
23 | - uses: autofix-ci/action@v1.3.2
24 | with:
25 | commit-message: "chore: reformat code"
26 |
--------------------------------------------------------------------------------
/tests/data/plugin.py:
--------------------------------------------------------------------------------
1 | from collections import abc
2 | from pathlib import Path
3 | from urllib.parse import quote
4 |
5 | import makejinja
6 |
7 |
8 | def hassurl(value: str) -> str:
9 | return quote(value).replace("_", "-")
10 |
11 |
12 | def getlang(value: str | abc.Mapping[str, str], lang: str, default_lang: str = "en"):
13 | if isinstance(value, str):
14 | return value
15 | else:
16 | return value.get(lang, value.get(default_lang, ""))
17 |
18 |
19 | class Plugin(makejinja.plugin.Plugin):
20 | def filters(self) -> makejinja.plugin.Filters:
21 | return [hassurl]
22 |
23 | def functions(self) -> makejinja.plugin.Functions:
24 | return [getlang]
25 |
26 | def path_filters(self) -> makejinja.plugin.PathFilters:
27 | return [self._remove_secrets]
28 |
29 | def _remove_secrets(self, path: Path) -> bool:
30 | if "secret" in path.stem:
31 | return False
32 |
33 | return True
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: bug
6 | assignees: mirkolenz
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 |
28 | - OS: [e.g. iOS]
29 | - Browser [e.g. chrome, safari]
30 | - Version [e.g. 22]
31 |
32 | **Smartphone (please complete the following information):**
33 |
34 | - Device: [e.g. iPhone6]
35 | - OS: [e.g. iOS8.1]
36 | - Browser [e.g. stock browser, safari]
37 | - Version [e.g. 22]
38 |
39 | **Additional context**
40 | Add any other context about the problem here.
41 |
--------------------------------------------------------------------------------
/tests/data/README.md:
--------------------------------------------------------------------------------
1 | # Dashboard Example for Home Assistant
2 |
3 | This directory contains a fully working example for automatically generating a dashboard for Home Assistant.
4 | Assuming you run `makejinja` inside this directory (`tests/data`), it can be run without any arguments as it will load its options from the `makejinja.toml` file.
5 |
6 | **Note:**
7 | We adjust the default Jinja template delimiters so that there are no collisions with the Home Assistant template syntax.
8 | This way, you can even automatically generate correct templates for sensors and other use cases.
9 |
10 | The following files/directories are relevant:
11 |
12 | - `makejinja.toml`: Config file for makejinja invocation.
13 | - `input`: Regular `yaml` config files together with `yaml.jinja` config templates (these are rendered by makejinja).
14 | - `output`: Resulting directory tree after running makejinja with the command shown above.
15 | - `config`: Directory containing a `yaml` file with variables used in our Jinja templates.
16 | - `plugin.py`: Class with custom Jinja filters and global functions to use in our templates.
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Mirko Lenz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/makejinja/cli.py:
--------------------------------------------------------------------------------
1 | """
2 | .. include:: ../../manpage.md
3 | """
4 |
5 | from pathlib import Path
6 |
7 | import rich_click as click
8 | import typed_settings as ts
9 |
10 | from makejinja.config import OPTION_GROUPS, Config
11 |
12 | from .app import makejinja
13 |
14 | __all__: list[str] = []
15 |
16 | click.rich_click.USE_MARKDOWN = True
17 | click.rich_click.OPTION_GROUPS = OPTION_GROUPS
18 |
19 | _ts_loaders = ts.default_loaders(
20 | appname="makejinja", config_files=(Path("makejinja.toml"),)
21 | )
22 |
23 |
24 | @click.command("makejinja", context_settings={"help_option_names": ("--help", "-h")})
25 | @click.version_option(None, "--version", "-v")
26 | @ts.click_options(Config, _ts_loaders)
27 | def makejinja_cli(config: Config):
28 | """makejinja can be used to automatically generate files from [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
29 |
30 | Instead of passing CLI options, you can also write them to a file called `makejinja.toml` in your working directory.
31 | **Note**: In this file, options may be named differently.
32 | Please refer to the file [`makejinja/config.py`](https://github.com/mirkolenz/makejinja/blob/main/src/makejinja/config.py) to see their actual names.
33 | You will also find an example here: [`makejinja/tests/data/makejinja.toml`](https://github.com/mirkolenz/makejinja/blob/main/tests/data/makejinja.toml).
34 | To override its location, you can set the environment variable `MAKEJINJA_SETTINGS` to the path of your config file.
35 | """
36 |
37 | makejinja(config)
38 |
39 |
40 | if __name__ == "__main__":
41 | makejinja_cli()
42 |
--------------------------------------------------------------------------------
/src/makejinja/plugin.py:
--------------------------------------------------------------------------------
1 | from collections import abc
2 | from pathlib import Path
3 | from typing import Any, Protocol
4 |
5 | from jinja2 import Environment
6 | from jinja2.ext import Extension
7 |
8 | from makejinja.config import Config
9 |
10 | __all__ = ["Plugin"]
11 |
12 | Extensions = abc.Sequence[type[Extension]]
13 | Filter = abc.Callable[[Any], Any]
14 | Filters = abc.Sequence[Filter]
15 | Function = abc.Callable[..., Any]
16 | Functions = abc.Sequence[Function]
17 | Test = abc.Callable[..., Any]
18 | Tests = abc.Sequence[Test]
19 | Policies = abc.Mapping[str, Any]
20 | MutableData = abc.MutableMapping[str, Any]
21 | Data = abc.Mapping[str, Any]
22 | PathFilter = abc.Callable[[Path], bool]
23 | PathFilters = abc.Sequence[PathFilter]
24 |
25 |
26 | class Plugin(Protocol):
27 | """Extend the functionality of makejinja with a plugin implementing a subset of this protocol."""
28 |
29 | def __init__(self, *, env: Environment, data: Data, config: Config) -> None:
30 | pass
31 |
32 | def functions(self) -> Functions:
33 | return []
34 |
35 | def data(self) -> Data:
36 | return {}
37 |
38 | def filters(self) -> Filters:
39 | return []
40 |
41 | def tests(self) -> Tests:
42 | return []
43 |
44 | def policies(self) -> Policies:
45 | return {}
46 |
47 | def extensions(self) -> Extensions:
48 | return []
49 |
50 | def path_filters(self) -> PathFilters:
51 | return []
52 |
53 | # Deprecated: Use functions() and data() instead
54 | def globals(self) -> Functions:
55 | return []
56 |
57 |
58 | AbstractLoader = Plugin
59 |
--------------------------------------------------------------------------------
/tests/data/config/data.yaml:
--------------------------------------------------------------------------------
1 | areas:
2 | living_room:
3 | name:
4 | en: Living room
5 | de: Wohnzimmer
6 | icon: mdi:sofa
7 | entities:
8 | climate:
9 | - entity: climate.living_room
10 | name: Thermostat
11 | cover:
12 | - entity: cover.living_room_door
13 | name:
14 | en: Cover door
15 | de: Rollo Tür
16 | - entity: cover.living_room_window
17 | name:
18 | en: Cover window
19 | de: Rollo Fenster
20 | light:
21 | - entity: light.living_room_ceiling
22 | name:
23 | en: Ceiling
24 | de: Decke
25 | - entity: light.living_room_spots
26 | name: Spots
27 | binary_sensor:
28 | - entity: binary_sensor.living_room_door_contact
29 | name:
30 | en: Door
31 | de: Tür
32 | - entity: binary_sensor.living_room_window_contact
33 | name:
34 | en: Window
35 | de: Fenster
36 | media_player:
37 | - entity: media_player.living_room_homepod
38 | name: HomePod
39 | - entity: media_player.living_room_apple_tv
40 | name: Apple TV
41 | kitchen:
42 | name:
43 | en: Kitchen
44 | de: Küche
45 | icon: mdi:chef-hat
46 | entities:
47 | climate:
48 | - entity: climate.kitchen
49 | name: Thermostat
50 | - entity: cover.kitchen_window
51 | name:
52 | en: Cover window
53 | de: Rollo Fenster
54 | light:
55 | - entity: light.kitchen_spots
56 | name: Spots
57 | binary_sensor:
58 | - entity: binary_sensor.kitchen_window_contact
59 | name:
60 | en: Window
61 | de: Fenster
62 | media_player:
63 | - entity: media_player.kitchen_homepod
64 | name: HomePod
65 |
--------------------------------------------------------------------------------
/tests/data/input2/views/home.yaml.jinja:
--------------------------------------------------------------------------------
1 | # Test file: Demonstrates nested directory processing, custom functions (getlang), and filters (hassurl)
2 | <% set lang = "en" %>
3 | <% for area_id, area in areas.items() %>
4 | - title: << getlang(area.name, lang) >>
5 | path: << area_id | hassurl >>
6 | icon: << area.icon >>
7 | cards:
8 | - type: grid
9 | square: false
10 | columns: 2
11 | cards:
12 | <% for item in area.entities.light %>
13 | - type: tile
14 | entity: << item.entity >>
15 | name: << getlang(item.name, lang) >>
16 | features:
17 | - type: light-brightness
18 | <% endfor %>
19 | <% for item in area.entities.cover %>
20 | - type: tile
21 | entity: << item.entity >>
22 | name: << getlang(item.name, lang) >>
23 | features:
24 | - type: cover-open-close
25 | <% endfor %>
26 | <% for item in area.entities.climate %>
27 | - type: tile
28 | entity: << item.entity >>
29 | name: << getlang(item.name, lang) >>
30 | <% endfor %>
31 | <% for item in area.entities.switch %>
32 | - type: tile
33 | entity: << item.entity >>
34 | name: << getlang(item.name, lang) >>
35 | <% endfor %>
36 | <% for item in area.entities.sensor %>
37 | - type: tile
38 | entity: << item.entity >>
39 | name: << getlang(item.name, lang) >>
40 | <% endfor %>
41 | <% for item in area.entities.binary_sensor %>
42 | - type: tile
43 | entity: << item.entity >>
44 | name: << getlang(item.name, lang) >>
45 | <% endfor %>
46 | <% for item in area.entities.media_player %>
47 | - type: tile
48 | entity: << item.entity >>
49 | name: << getlang(item.name, lang) >>
50 | tap_action:
51 | action: more-info
52 | <% endfor %>
53 | <% for item in area.entities.climate %>
54 | - type: history-graph
55 | title: << getlang(item.name, lang) >> history
56 | show_names: false
57 | entities:
58 | - << item.entity >>
59 | <% endfor %>
60 | <% endfor %>
61 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "makejinja"
3 | version = "2.8.2"
4 | description = "Generate entire directory structures using Jinja templates with support for external data and custom plugins"
5 | authors = [{ name = "Mirko Lenz", email = "mirko@mirkolenz.com" }]
6 | readme = "README.md"
7 | keywords = [
8 | "jinja2",
9 | "home-assistant",
10 | "hassio",
11 | "dashboard",
12 | "lovelace",
13 | "template",
14 | "generator",
15 | "cli",
16 | "tool",
17 | "library",
18 | ]
19 | classifiers = [
20 | "Development Status :: 5 - Production/Stable",
21 | "Environment :: Console",
22 | "Framework :: Pytest",
23 | "Intended Audience :: Developers",
24 | "Intended Audience :: End Users/Desktop",
25 | "Intended Audience :: System Administrators",
26 | "License :: OSI Approved :: MIT License",
27 | "Natural Language :: English",
28 | "Operating System :: OS Independent",
29 | "Programming Language :: Python :: 3.12",
30 | "Programming Language :: Python :: 3.13",
31 | "Programming Language :: Python :: 3.14",
32 | "Programming Language :: Python :: 3",
33 | "Topic :: File Formats",
34 | "Topic :: Home Automation",
35 | "Topic :: Software Development :: Code Generators",
36 | "Topic :: Software Development :: Libraries :: Python Modules",
37 | "Topic :: System :: Software Distribution",
38 | "Topic :: System :: Systems Administration",
39 | "Topic :: Text Processing :: Markup",
40 | "Topic :: Utilities",
41 | "Typing :: Typed",
42 | ]
43 | requires-python = ">=3.12,<4"
44 | dependencies = [
45 | "frozendict>=2,<3",
46 | "jinja2>=3,<4",
47 | "pyyaml>=6,<7",
48 | "rich-click>=1,<2",
49 | "typed-settings[attrs,cattrs,click]>=23,<26",
50 | ]
51 |
52 | [project.urls]
53 | Repository = "https://github.com/mirkolenz/makejinja"
54 | Homepage = "https://mirkolenz.github.io/makejinja/"
55 | Documentation = "https://mirkolenz.github.io/makejinja/makejinja/cli.html"
56 | Issues = "https://github.com/mirkolenz/makejinja/issues"
57 | Changelog = "https://github.com/mirkolenz/makejinja/releases"
58 |
59 | [project.scripts]
60 | makejinja = "makejinja.cli:makejinja_cli"
61 |
62 | [dependency-groups]
63 | test = ["pytest>=9,<10", "pytest-cov>=7,<8"]
64 | docs = ["pdoc>=16,<17"]
65 |
66 | [build-system]
67 | requires = ["uv-build>=0.9,<1"]
68 | build-backend = "uv_build"
69 |
70 | [tool.uv]
71 | default-groups = ["test", "docs"]
72 |
73 | [tool.pytest]
74 | addopts = ["--cov=makejinja", "--cov-report=term-missing"]
75 |
76 | [tool.ruff.lint.pydocstyle]
77 | convention = "google"
78 |
--------------------------------------------------------------------------------
/tests/data/output/views/home.yaml:
--------------------------------------------------------------------------------
1 | # Test file: Demonstrates nested directory processing, custom functions (getlang), and filters (hassurl)
2 | - title: Living room
3 | path: living-room
4 | icon: mdi:sofa
5 | cards:
6 | - type: grid
7 | square: false
8 | columns: 2
9 | cards:
10 | - type: tile
11 | entity: light.living_room_ceiling
12 | name: Ceiling
13 | features:
14 | - type: light-brightness
15 | - type: tile
16 | entity: light.living_room_spots
17 | name: Spots
18 | features:
19 | - type: light-brightness
20 | - type: tile
21 | entity: cover.living_room_door
22 | name: Cover door
23 | features:
24 | - type: cover-open-close
25 | - type: tile
26 | entity: cover.living_room_window
27 | name: Cover window
28 | features:
29 | - type: cover-open-close
30 | - type: tile
31 | entity: climate.living_room
32 | name: Thermostat
33 | - type: tile
34 | entity: binary_sensor.living_room_door_contact
35 | name: Door
36 | - type: tile
37 | entity: binary_sensor.living_room_window_contact
38 | name: Window
39 | - type: tile
40 | entity: media_player.living_room_homepod
41 | name: HomePod
42 | tap_action:
43 | action: more-info
44 | - type: tile
45 | entity: media_player.living_room_apple_tv
46 | name: Apple TV
47 | tap_action:
48 | action: more-info
49 | - type: history-graph
50 | title: Thermostat history
51 | show_names: false
52 | entities:
53 | - climate.living_room
54 | - title: Cuisine
55 | path: kitchen
56 | icon: mdi:chef-hat
57 | cards:
58 | - type: grid
59 | square: false
60 | columns: 2
61 | cards:
62 | - type: tile
63 | entity: light.kitchen_spots
64 | name: Spots
65 | features:
66 | - type: light-brightness
67 | - type: tile
68 | entity: climate.kitchen
69 | name: Thermostat
70 | - type: tile
71 | entity: cover.kitchen_window
72 | name: Cover window
73 | - type: tile
74 | entity: binary_sensor.kitchen_window_contact
75 | name: Window
76 | - type: tile
77 | entity: media_player.kitchen_homepod
78 | name: HomePod
79 | tap_action:
80 | action: more-info
81 | - type: history-graph
82 | title: Thermostat history
83 | show_names: false
84 | entities:
85 | - climate.kitchen
86 | - type: history-graph
87 | title: Cover window history
88 | show_names: false
89 | entities:
90 | - cover.kitchen_window
91 |
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | {
2 | lib,
3 | stdenv,
4 | callPackage,
5 | fetchFromGitHub,
6 | python3,
7 | jetbrains-mono,
8 | asciinema-scenario,
9 | asciinema-agg,
10 | uv2nix,
11 | pyproject-nix,
12 | pyproject-build-systems,
13 | }:
14 | let
15 | pdocRepo = fetchFromGitHub {
16 | owner = "mitmproxy";
17 | repo = "pdoc";
18 | tag = "v16.0.0";
19 | hash = "sha256-9amp6CWYIcniVfdlmPKYuRFR7B5JJtuMlOoDxpfvvJA=";
20 | };
21 | workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
22 | pyprojectOverlay = workspace.mkPyprojectOverlay {
23 | sourcePreference = "wheel";
24 | };
25 | packageOverlay = final: prev: {
26 | makejinja = prev.makejinja.overrideAttrs (old: {
27 | meta = (old.meta or { }) // {
28 | mainProgram = "makejinja";
29 | maintainers = with lib.maintainers; [ mirkolenz ];
30 | license = lib.licenses.mit;
31 | homepage = "https://github.com/mirkolenz/makejinja";
32 | description = "Generate entire directory structures using Jinja templates with support for external data and custom plugins.";
33 | platforms = with lib.platforms; darwin ++ linux;
34 | };
35 | passthru = lib.recursiveUpdate (old.passthru or { }) {
36 | tests.pytest = stdenv.mkDerivation {
37 | name = "${final.makejinja.name}-pytest";
38 | inherit (final.makejinja) src;
39 | nativeBuildInputs = [
40 | (final.mkVirtualEnv "makejinja-test-env" {
41 | makejinja = [ "test" ];
42 | })
43 | ];
44 | dontConfigure = true;
45 | buildPhase = ''
46 | runHook preBuild
47 | pytest --cov-report=html
48 | runHook postBuild
49 | '';
50 | installPhase = ''
51 | runHook preInstall
52 | mv htmlcov $out
53 | runHook postInstall
54 | '';
55 | };
56 | docs = stdenv.mkDerivation {
57 | name = "${final.makejinja.name}-docs";
58 | inherit (final.makejinja) src;
59 | nativeBuildInputs = [
60 | (final.mkVirtualEnv "makejinja-docs-env" {
61 | makejinja = [ "docs" ];
62 | })
63 | asciinema-scenario
64 | asciinema-agg
65 | ];
66 | dontConfigure = true;
67 | buildPhase = ''
68 | runHook preBuild
69 |
70 | {
71 | echo '```txt'
72 | COLUMNS=120 makejinja --help
73 | echo '```'
74 | } > ./manpage.md
75 |
76 | asciinema-scenario ./assets/demo.scenario > ./assets/demo.cast
77 | agg \
78 | --font-dir "${jetbrains-mono}/share/fonts/truetype" \
79 | --font-family "JetBrains Mono" \
80 | --theme monokai \
81 | ./assets/demo.cast ./assets/demo.gif
82 |
83 | pdoc \
84 | -d google \
85 | -t ${pdocRepo}/examples/dark-mode \
86 | --math \
87 | --logo https://raw.githubusercontent.com/mirkolenz/makejinja/main/assets/logo.png \
88 | -o "$out" \
89 | ./src/makejinja
90 |
91 | runHook postBuild
92 | '';
93 | installPhase = ''
94 | runHook preInstall
95 |
96 | mkdir -p "$out/assets"
97 | cp -rf ./assets/{*.png,*.gif} "$out/assets/"
98 |
99 | runHook postInstall
100 | '';
101 | };
102 | };
103 | });
104 | };
105 | baseSet = callPackage pyproject-nix.build.packages {
106 | python = python3;
107 | };
108 | in
109 | {
110 | inherit workspace;
111 | inherit (callPackage pyproject-nix.build.util { }) mkApplication;
112 | pythonSet = baseSet.overrideScope (
113 | lib.composeManyExtensions [
114 | pyproject-build-systems.overlays.wheel
115 | pyprojectOverlay
116 | packageOverlay
117 | ]
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | push:
4 | branches: [main, beta]
5 | jobs:
6 | check:
7 | uses: ./.github/workflows/check.yml
8 | release:
9 | if: ${{ github.repository_owner == 'mirkolenz' }}
10 | runs-on: ubuntu-latest
11 | needs: check
12 | environment:
13 | name: release
14 | url: https://github.com/mirkolenz/makejinja/releases/tag/${{ steps.semanticrelease.outputs.git-tag }}
15 | permissions:
16 | contents: write
17 | outputs:
18 | version: ${{ steps.semanticrelease.outputs.version }}
19 | released: ${{ steps.semanticrelease.outputs.released }}
20 | git-head: ${{ steps.semanticrelease.outputs.git-head }}
21 | steps:
22 | - uses: actions/checkout@v6
23 | - uses: DeterminateSystems/nix-installer-action@v21
24 | with:
25 | extra-conf: |
26 | accept-flake-config = true
27 | - uses: cachix/cachix-action@v16
28 | with:
29 | name: mirkolenz
30 | authToken: ${{ secrets.CACHIX_TOKEN }}
31 | - run: nix profile install .#release-env
32 | - uses: cihelper/action-semanticrelease-uv@v1
33 | id: semanticrelease
34 | with:
35 | uv-publish: false
36 | - uses: actions/upload-artifact@v6
37 | if: ${{ steps.semanticrelease.outputs.released == 'true' }}
38 | with:
39 | name: uv-build
40 | path: ./dist
41 | deploy-docker:
42 | runs-on: ubuntu-latest
43 | needs: release
44 | if: ${{ needs.release.outputs.released == 'true' }}
45 | permissions:
46 | contents: read
47 | packages: write
48 | environment:
49 | name: release
50 | url: https://ghcr.io/mirkolenz/makejinja
51 | steps:
52 | - uses: actions/checkout@v6
53 | with:
54 | ref: ${{ needs.release.outputs.git-head }}
55 | - uses: docker/setup-qemu-action@v3
56 | with:
57 | platforms: arm64
58 | - uses: DeterminateSystems/nix-installer-action@v21
59 | with:
60 | extra-conf: |
61 | extra-platforms = aarch64-linux
62 | accept-flake-config = true
63 | - uses: cachix/cachix-action@v16
64 | with:
65 | name: mirkolenz
66 | authToken: ${{ secrets.CACHIX_TOKEN }}
67 | - run: nix run .#docker-manifest --impure
68 | env:
69 | VERSION: ${{ needs.release.outputs.version }}
70 | GH_TOKEN: ${{ github.token }}
71 | deploy-pypi:
72 | runs-on: ubuntu-latest
73 | needs: release
74 | if: ${{ needs.release.outputs.released == 'true' }}
75 | permissions:
76 | id-token: write
77 | environment:
78 | name: release
79 | url: https://pypi.org/project/makejinja/${{needs.release.outputs.version}}/
80 | steps:
81 | - uses: actions/download-artifact@v7
82 | with:
83 | name: uv-build
84 | path: ./dist
85 | - uses: pypa/gh-action-pypi-publish@release/v1
86 | build-docs:
87 | runs-on: ubuntu-latest
88 | needs: release
89 | if: ${{ needs.release.outputs.released == 'true' }}
90 | permissions:
91 | contents: read
92 | pages: read
93 | environment: github-pages
94 | steps:
95 | - uses: actions/checkout@v6
96 | with:
97 | ref: ${{ needs.release.outputs.git-head }}
98 | - uses: actions/configure-pages@v5
99 | - uses: DeterminateSystems/nix-installer-action@v21
100 | with:
101 | extra-conf: |
102 | accept-flake-config = true
103 | - uses: cachix/cachix-action@v16
104 | with:
105 | name: mirkolenz
106 | authToken: ${{ secrets.CACHIX_TOKEN }}
107 | - run: nix build .#docs
108 | - uses: actions/upload-pages-artifact@v4
109 | with:
110 | path: ./result
111 | deploy-docs:
112 | runs-on: ubuntu-latest
113 | needs: build-docs
114 | environment:
115 | name: github-pages
116 | url: ${{ steps.deploy.outputs.page_url }}
117 | permissions:
118 | pages: write
119 | id-token: write
120 | steps:
121 | - uses: actions/deploy-pages@v4
122 | id: deploy
123 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | inputs = {
3 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
4 | flake-parts.url = "github:hercules-ci/flake-parts";
5 | systems.url = "github:nix-systems/default";
6 | flocken = {
7 | url = "github:mirkolenz/flocken/v2";
8 | inputs.nixpkgs.follows = "nixpkgs";
9 | };
10 | treefmt-nix = {
11 | url = "github:numtide/treefmt-nix";
12 | inputs.nixpkgs.follows = "nixpkgs";
13 | };
14 | pyproject-nix = {
15 | url = "github:pyproject-nix/pyproject.nix";
16 | inputs.nixpkgs.follows = "nixpkgs";
17 | };
18 | uv2nix = {
19 | url = "github:pyproject-nix/uv2nix";
20 | inputs.pyproject-nix.follows = "pyproject-nix";
21 | inputs.nixpkgs.follows = "nixpkgs";
22 | };
23 | pyproject-build-systems = {
24 | url = "github:pyproject-nix/build-system-pkgs";
25 | inputs.pyproject-nix.follows = "pyproject-nix";
26 | inputs.uv2nix.follows = "uv2nix";
27 | inputs.nixpkgs.follows = "nixpkgs";
28 | };
29 | };
30 | nixConfig = {
31 | extra-substituters = [
32 | "https://mirkolenz.cachix.org"
33 | "https://pyproject-nix.cachix.org"
34 | ];
35 | extra-trusted-public-keys = [
36 | "mirkolenz.cachix.org-1:R0dgCJ93t33K/gncNbKgUdJzwgsYVXeExRsZNz5jpho="
37 | "pyproject-nix.cachix.org-1:UNzugsOlQIu2iOz0VyZNBQm2JSrL/kwxeCcFGw+jMe0="
38 | ];
39 | };
40 | outputs =
41 | inputs@{
42 | self,
43 | flake-parts,
44 | systems,
45 | flocken,
46 | ...
47 | }:
48 | flake-parts.lib.mkFlake { inherit inputs; } {
49 | systems = import systems;
50 | imports = [
51 | inputs.flake-parts.flakeModules.easyOverlay
52 | inputs.treefmt-nix.flakeModule
53 | ];
54 | perSystem =
55 | {
56 | pkgs,
57 | system,
58 | lib,
59 | config,
60 | ...
61 | }:
62 | let
63 | inherit
64 | (pkgs.callPackage ./default.nix {
65 | inherit (inputs) uv2nix pyproject-nix pyproject-build-systems;
66 | })
67 | pythonSet
68 | workspace
69 | mkApplication
70 | ;
71 | in
72 | {
73 | overlayAttrs = {
74 | inherit (config.packages) makejinja;
75 | };
76 | checks = pythonSet.makejinja.passthru.tests // {
77 | inherit (pythonSet.makejinja.passthru) docs;
78 | };
79 | treefmt = {
80 | projectRootFile = "flake.nix";
81 | programs = {
82 | ruff-check.enable = true;
83 | ruff-format.enable = true;
84 | nixfmt.enable = true;
85 | };
86 | };
87 | packages = {
88 | inherit (pythonSet.makejinja.passthru) docs;
89 | default = config.packages.makejinja;
90 | makejinja = mkApplication {
91 | venv = pythonSet.mkVirtualEnv "makejinja-env" workspace.deps.optionals;
92 | package = pythonSet.makejinja;
93 | };
94 | docker = pkgs.dockerTools.streamLayeredImage {
95 | name = "makejinja";
96 | tag = "latest";
97 | created = "now";
98 | config.Entrypoint = [ (lib.getExe config.packages.makejinja) ];
99 | };
100 | release-env = pkgs.buildEnv {
101 | name = "release-env";
102 | paths = with pkgs; [
103 | uv
104 | python3
105 | ];
106 | };
107 | };
108 | legacyPackages.docker-manifest = flocken.legacyPackages.${system}.mkDockerManifest {
109 | github = {
110 | enable = true;
111 | token = "$GH_TOKEN";
112 | };
113 | version = builtins.getEnv "VERSION";
114 | imageStreams = with self.packages; [
115 | x86_64-linux.docker
116 | aarch64-linux.docker
117 | ];
118 | };
119 | devShells.default = pkgs.mkShell {
120 | packages = with pkgs; [
121 | uv
122 | config.treefmt.build.wrapper
123 | ];
124 | UV_PYTHON = lib.getExe pkgs.python3;
125 | shellHook = ''
126 | uv sync --all-extras --locked
127 | '';
128 | };
129 | };
130 | };
131 | }
132 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
makejinja
3 |
4 |
5 |
6 |
7 |
8 |
9 | PyPI |
10 | Docker |
11 | Docs |
12 | Example |
13 | Jinja reference
14 |
15 |
16 |
17 | Generate entire directory structures using Jinja templates with support for external data and custom plugins.
18 |
19 |
20 |
21 |
22 |
23 |
24 | ---
25 |
26 |
27 |
28 | makejinja can be used to automatically generate files from [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates).
29 | It is conceptually similar to [Ansible templates](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/template_module.html) since both are built on top of Jinja.
30 | However, makejinja is a standalone tool that can be used without Ansible and offers some advanced features like custom plugins.
31 |
32 | A popular use case is generating config files for [Home Assistant](https://www.home-assistant.io/):
33 | Using the same Jinja language that Home Assistant's built-in templates use, you can greatly simplify your configuration management.
34 | makejinja's custom delimiter support prevents conflicts with Home Assistant's own template syntax, while file-specific data loading enables modular dashboard and automation generation.
35 | Our comprehensive [Home Assistant example](https://github.com/mirkolenz/makejinja/tree/main/tests/data) demonstrates dashboard generation with custom plugins, multiple data sources, and advanced template organization.
36 |
37 | ## Key Features
38 |
39 | - **Multi-Source Data Integration**: Load variables from YAML, TOML, and Python files, with support for file-specific data sources and runtime variable injection.
40 | - **Custom Template Delimiters**: Configure Jinja delimiters (e.g., `<% %>` instead of `{{ }}`) to avoid conflicts with target file formats like Home Assistant, Kubernetes, or Terraform.
41 | - **Flexible Directory Processing**: Process multiple input directories with complex nested structures, preserving hierarchy while applying powerful template transformations.
42 | - **Extensible Plugin System**: Create custom [plugins](https://mirkolenz.github.io/makejinja/makejinja/plugin.html#Plugin) with filters, functions, and path filtering logic for specialized requirements.
43 | - **Production-Ready**: Comprehensive CLI interface, configuration file support, and Python library API for seamless workflow integration.
44 |
45 | ## Use Cases
46 |
47 | - **Configuration Management**: Generate environment-specific configs (dev/staging/prod) from shared templates with different data sources.
48 | - **Home Assistant Dashboards**: Create complex dashboards and automations using Jinja syntax without conflicts. See our [complete example](https://github.com/mirkolenz/makejinja/tree/main/tests/data).
49 | - **Infrastructure as Code**: Generate Kubernetes manifests, Terraform modules, or Docker Compose files with consistent patterns across environments.
50 | - **Web Development**: Generate HTML pages, CSS files, or JavaScript configs from data sources for static sites or multi-tenant applications.
51 | - **Database Schemas**: Create SQL migration scripts, database configurations, or ORM models based on structured schema definitions.
52 | - **Network Configuration**: Generate router configs, firewall rules, or network device settings from centralized network topology data.
53 | - **Monitoring & Alerting**: Create Grafana dashboards, Prometheus rules, or alerting configurations from service inventories.
54 | - **Documentation & CI/CD**: Create project docs, API specifications, or pipeline definitions from structured data sources.
55 |
56 | ## Installation
57 |
58 | The tool is written in Python and can be installed via uv, nix, and docker.
59 | It can be used as a CLI tool or as a Python library.
60 |
61 | ### UV
62 |
63 | makejinja is available on [PyPI](https://pypi.org/project/makejinja/) and can be installed via `uv`:
64 |
65 | ```shell
66 | uv tool install makejinja
67 | makejinja -i ./input -o ./output
68 | ```
69 |
70 | ### Nix
71 |
72 | makejinja is packaged in nixpkgs.
73 | To use the most recent version, you can run it via `nix run`:
74 |
75 | ```shell
76 | nix run github:mirkolenz/makejinja -- -i ./input -o ./output
77 | ```
78 |
79 | Alternatively, you can add this repository as an input to your flake and use `makejinja.packages.${system}.default`.
80 |
81 | ### Docker
82 |
83 | We automatically publish an image to the [GitHub Container Registry](https://ghcr.io/mirkolenz/makejinja).
84 | To use it, mount a directory to the container and pass the options as the command:
85 |
86 | ```shell
87 | docker run --rm -v $(pwd)/data:/data ghcr.io/mirkolenz/makejinja:latest -i /data/input -o /data/output
88 | ```
89 |
90 | ## Usage in Terminal / Command Line
91 |
92 | In its default configuration, makejinja searches the input directory recursively for files ending in `.jinja`.
93 | It then renders these files and writes them to the output directory, preserving the directory structure.
94 | Our [documentation](https://mirkolenz.github.io/makejinja/makejinja/cli.html) contains a detailed description of all options and can also be accessed via `makejinja --help`.
95 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
2 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,python
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,python
4 |
5 | ### macOS ###
6 | # General
7 | .DS_Store
8 | .AppleDouble
9 | .LSOverride
10 |
11 | # Icon must end with two \r
12 | Icon
13 |
14 | # Thumbnails
15 | ._*
16 |
17 | # Files that might appear in the root of a volume
18 | .DocumentRevisions-V100
19 | .fseventsd
20 | .Spotlight-V100
21 | .TemporaryItems
22 | .Trashes
23 | .VolumeIcon.icns
24 | .com.apple.timemachine.donotpresent
25 |
26 | # Directories potentially created on remote AFP share
27 | .AppleDB
28 | .AppleDesktop
29 | Network Trash Folder
30 | Temporary Items
31 | .apdisk
32 |
33 | ### macOS Patch ###
34 | # iCloud generated files
35 | *.icloud
36 |
37 | ### Python ###
38 | # Byte-compiled / optimized / DLL files
39 | __pycache__/
40 | *.py[cod]
41 | *$py.class
42 |
43 | # C extensions
44 | *.so
45 |
46 | # Distribution / packaging
47 | .Python
48 | build/
49 | develop-eggs/
50 | dist/
51 | downloads/
52 | eggs/
53 | .eggs/
54 | lib/
55 | lib64/
56 | parts/
57 | sdist/
58 | var/
59 | wheels/
60 | share/python-wheels/
61 | *.egg-info/
62 | .installed.cfg
63 | *.egg
64 | MANIFEST
65 |
66 | # PyInstaller
67 | # Usually these files are written by a python script from a template
68 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
69 | *.manifest
70 | *.spec
71 |
72 | # Installer logs
73 | pip-log.txt
74 | pip-delete-this-directory.txt
75 |
76 | # Unit test / coverage reports
77 | htmlcov/
78 | .tox/
79 | .nox/
80 | .coverage
81 | .coverage.*
82 | .cache
83 | nosetests.xml
84 | coverage.xml
85 | *.cover
86 | *.py,cover
87 | .hypothesis/
88 | .pytest_cache/
89 | cover/
90 |
91 | # Translations
92 | *.mo
93 | *.pot
94 |
95 | # Django stuff:
96 | *.log
97 | local_settings.py
98 | db.sqlite3
99 | db.sqlite3-journal
100 |
101 | # Flask stuff:
102 | instance/
103 | .webassets-cache
104 |
105 | # Scrapy stuff:
106 | .scrapy
107 |
108 | # Sphinx documentation
109 | docs/_build/
110 |
111 | # PyBuilder
112 | .pybuilder/
113 | target/
114 |
115 | # Jupyter Notebook
116 | .ipynb_checkpoints
117 |
118 | # IPython
119 | profile_default/
120 | ipython_config.py
121 |
122 | # pyenv
123 | # For a library or package, you might want to ignore these files since the code is
124 | # intended to run in multiple environments; otherwise, check them in:
125 | # .python-version
126 |
127 | # pipenv
128 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
129 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
130 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
131 | # install all needed dependencies.
132 | #Pipfile.lock
133 |
134 | # poetry
135 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
136 | # This is especially recommended for binary packages to ensure reproducibility, and is more
137 | # commonly ignored for libraries.
138 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
139 | #poetry.lock
140 |
141 | # pdm
142 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
143 | #pdm.lock
144 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
145 | # in version control.
146 | # https://pdm.fming.dev/#use-with-ide
147 | .pdm.toml
148 |
149 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
150 | __pypackages__/
151 |
152 | # Celery stuff
153 | celerybeat-schedule
154 | celerybeat.pid
155 |
156 | # SageMath parsed files
157 | *.sage.py
158 |
159 | # Environments
160 | .env
161 | .venv
162 | env/
163 | venv/
164 | ENV/
165 | env.bak/
166 | venv.bak/
167 |
168 | # Spyder project settings
169 | .spyderproject
170 | .spyproject
171 |
172 | # Rope project settings
173 | .ropeproject
174 |
175 | # mkdocs documentation
176 | /site
177 |
178 | # mypy
179 | .mypy_cache/
180 | .dmypy.json
181 | dmypy.json
182 |
183 | # Pyre type checker
184 | .pyre/
185 |
186 | # pytype static type analyzer
187 | .pytype/
188 |
189 | # Cython debug symbols
190 | cython_debug/
191 |
192 | # PyCharm
193 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
194 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
195 | # and can be added to the global gitignore or merged into this file. For a more nuclear
196 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
197 | #.idea/
198 |
199 | ### Python Patch ###
200 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
201 | poetry.toml
202 |
203 | ### VisualStudioCode ###
204 | .vscode/*
205 | !.vscode/settings.json
206 | !.vscode/tasks.json
207 | !.vscode/launch.json
208 | !.vscode/extensions.json
209 | !.vscode/*.code-snippets
210 |
211 | # Local History for Visual Studio Code
212 | .history/
213 |
214 | # Built Visual Studio Code Extensions
215 | *.vsix
216 |
217 | ### VisualStudioCode Patch ###
218 | # Ignore all local history of files
219 | .history
220 | .ionide
221 |
222 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,python
223 |
224 | # Custom rules (everything added below won't be overridden by 'Generate .gitignore File' if you use 'Update' option)
225 |
226 | /.pre-commit-config.yaml
227 | .vscode/
228 | /result
229 | /assets/*.gif
230 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | mirko@mirkolenz.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/tests/test_makejinja.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from pathlib import Path
3 |
4 | import pytest
5 | from click.testing import CliRunner
6 |
7 |
8 | @dataclass(slots=True, frozen=True)
9 | class MakejinjaPaths:
10 | """Paths used in makejinja test execution.
11 |
12 | Attributes:
13 | input: Path to input template directories
14 | baseline: Path to expected output files
15 | output: Path to actual generated output files
16 | """
17 |
18 | input: Path
19 | baseline: Path
20 | output: Path
21 |
22 | def __repr__(self) -> str:
23 | return "makejinja"
24 |
25 |
26 | @pytest.fixture(scope="session")
27 | def test_run(tmp_path_factory: pytest.TempPathFactory) -> MakejinjaPaths:
28 | """Execute makejinja on test data and return paths to input, expected, and actual output."""
29 | assert __package__ is not None
30 | data_path = Path(__package__, "data")
31 | input_path = data_path / "input"
32 | baseline_path = data_path / "output"
33 | output_path = tmp_path_factory.mktemp("data")
34 |
35 | with pytest.MonkeyPatch.context() as m:
36 | m.chdir(data_path)
37 | runner = CliRunner()
38 |
39 | # Need to import it AFTER chdir
40 | from makejinja.cli import makejinja_cli
41 |
42 | runner.invoke(
43 | makejinja_cli,
44 | [
45 | # Override it here to use our tmp_path
46 | "--output",
47 | str(output_path),
48 | ],
49 | catch_exceptions=False,
50 | color=True,
51 | )
52 |
53 | return MakejinjaPaths(input_path, baseline_path, output_path)
54 |
55 |
56 | def _dir_content(path: Path) -> set[Path]:
57 | return {item.relative_to(path) for item in path.rglob("*")}
58 |
59 |
60 | def test_output_structure(test_run: MakejinjaPaths):
61 | """Test that makejinja generates the expected directory structure."""
62 | expected_files = _dir_content(test_run.baseline)
63 | actual_files = _dir_content(test_run.output)
64 |
65 | if expected_files != actual_files:
66 | missing = sorted(expected_files - actual_files)
67 | extra = sorted(actual_files - expected_files)
68 | pytest.fail(f"Directory structure mismatch. Missing: {missing}, Extra: {extra}")
69 |
70 |
71 | def test_template_rendering(test_run: MakejinjaPaths):
72 | """Test that makejinja renders all templates with correct content."""
73 | paths = _dir_content(test_run.baseline)
74 |
75 | for item in paths:
76 | baseline_path = test_run.baseline / item
77 | output_path = test_run.output / item
78 |
79 | if baseline_path.is_file() and output_path.is_file():
80 | expected_content = baseline_path.read_text()
81 | actual_content = output_path.read_text()
82 |
83 | if expected_content.strip() != actual_content.strip():
84 | pytest.fail(
85 | f"Content mismatch in {item}: expected '{expected_content.strip()}', got '{actual_content.strip()}'"
86 | )
87 |
88 |
89 | def test_custom_delimiters(test_run: MakejinjaPaths):
90 | """Test that custom Jinja delimiters (<< >>, <% %>) work correctly."""
91 | not_empty_file = test_run.output / "not-empty.yaml"
92 | content = not_empty_file.read_text()
93 |
94 | assert "NOT EMPTY" in content, (
95 | f"Custom delimiters not working. Expected 'NOT EMPTY' in {not_empty_file}"
96 | )
97 |
98 |
99 | def test_file_specific_data(test_run: MakejinjaPaths):
100 | """Test that file-specific data loading works correctly."""
101 | file_specific_output = test_run.output / "file-specific.yaml"
102 | content = file_specific_output.read_text()
103 |
104 | # Should contain data from data1.yaml
105 | assert "Value from data1" in content, (
106 | f"File-specific data not loaded. Expected 'Value from data1' in {file_specific_output}"
107 | )
108 | assert "item1" in content and "item2" in content, (
109 | f"File-specific list data not loaded correctly in {file_specific_output}"
110 | )
111 |
112 |
113 | def test_plugin_features(test_run: MakejinjaPaths):
114 | """Test that custom plugin filters and functions work."""
115 | # Test that secret files are excluded by plugin path filter
116 | secret_file = test_run.output / "secret.yaml"
117 | assert not secret_file.exists(), (
118 | f"Secret file should be excluded by plugin but exists: {secret_file}"
119 | )
120 |
121 |
122 | def test_partial_inclusion(test_run: MakejinjaPaths):
123 | """Test that partial template inclusion works."""
124 | ui_lovelace_file = test_run.output / "ui-lovelace.yaml"
125 | content = ui_lovelace_file.read_text()
126 |
127 | # Should contain content from include.yaml.partial
128 | assert "icon: mdi:home" in content, (
129 | f"Partial inclusion not working. Expected 'icon: mdi:home' in {ui_lovelace_file}"
130 | )
131 |
132 |
133 | def test_empty_template_handling(test_run: MakejinjaPaths):
134 | """Test that empty templates are handled correctly."""
135 | # The empty.yaml.jinja should not produce output (contains only comments)
136 | empty_file = test_run.output / "empty.yaml"
137 | assert not empty_file.exists(), (
138 | f"Empty template should not generate output file but {empty_file} exists"
139 | )
140 |
141 |
142 | def test_nested_directory_processing(test_run: MakejinjaPaths):
143 | """Test that nested directories are processed correctly."""
144 | nested_file = test_run.output / "views" / "home.yaml"
145 | assert nested_file.exists(), (
146 | f"Nested directory processing failed. Expected {nested_file} to exist"
147 | )
148 |
149 | content = nested_file.read_text()
150 | assert len(content.strip()) > 0, (
151 | f"Nested template should have content but {nested_file} is empty"
152 | )
153 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-parts": {
4 | "inputs": {
5 | "nixpkgs-lib": "nixpkgs-lib"
6 | },
7 | "locked": {
8 | "lastModified": 1765835352,
9 | "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
10 | "owner": "hercules-ci",
11 | "repo": "flake-parts",
12 | "rev": "a34fae9c08a15ad73f295041fec82323541400a9",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "hercules-ci",
17 | "repo": "flake-parts",
18 | "type": "github"
19 | }
20 | },
21 | "flake-parts_2": {
22 | "inputs": {
23 | "nixpkgs-lib": "nixpkgs-lib_2"
24 | },
25 | "locked": {
26 | "lastModified": 1736143030,
27 | "narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=",
28 | "owner": "hercules-ci",
29 | "repo": "flake-parts",
30 | "rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de",
31 | "type": "github"
32 | },
33 | "original": {
34 | "owner": "hercules-ci",
35 | "repo": "flake-parts",
36 | "type": "github"
37 | }
38 | },
39 | "flocken": {
40 | "inputs": {
41 | "flake-parts": "flake-parts_2",
42 | "nixpkgs": [
43 | "nixpkgs"
44 | ],
45 | "systems": "systems"
46 | },
47 | "locked": {
48 | "lastModified": 1737581094,
49 | "narHash": "sha256-MSjyNy4zENfngnSdXQ6ef/wwACB0jfDyhy0qkI67F9A=",
50 | "owner": "mirkolenz",
51 | "repo": "flocken",
52 | "rev": "97921a2650cb3de20c2a5ee591b00a6d5099fc40",
53 | "type": "github"
54 | },
55 | "original": {
56 | "owner": "mirkolenz",
57 | "ref": "v2",
58 | "repo": "flocken",
59 | "type": "github"
60 | }
61 | },
62 | "nixpkgs": {
63 | "locked": {
64 | "lastModified": 1766125104,
65 | "narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=",
66 | "owner": "nixos",
67 | "repo": "nixpkgs",
68 | "rev": "7d853e518814cca2a657b72eeba67ae20ebf7059",
69 | "type": "github"
70 | },
71 | "original": {
72 | "owner": "nixos",
73 | "ref": "nixpkgs-unstable",
74 | "repo": "nixpkgs",
75 | "type": "github"
76 | }
77 | },
78 | "nixpkgs-lib": {
79 | "locked": {
80 | "lastModified": 1765674936,
81 | "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
82 | "owner": "nix-community",
83 | "repo": "nixpkgs.lib",
84 | "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
85 | "type": "github"
86 | },
87 | "original": {
88 | "owner": "nix-community",
89 | "repo": "nixpkgs.lib",
90 | "type": "github"
91 | }
92 | },
93 | "nixpkgs-lib_2": {
94 | "locked": {
95 | "lastModified": 1735774519,
96 | "narHash": "sha256-CewEm1o2eVAnoqb6Ml+Qi9Gg/EfNAxbRx1lANGVyoLI=",
97 | "type": "tarball",
98 | "url": "https://github.com/NixOS/nixpkgs/archive/e9b51731911566bbf7e4895475a87fe06961de0b.tar.gz"
99 | },
100 | "original": {
101 | "type": "tarball",
102 | "url": "https://github.com/NixOS/nixpkgs/archive/e9b51731911566bbf7e4895475a87fe06961de0b.tar.gz"
103 | }
104 | },
105 | "pyproject-build-systems": {
106 | "inputs": {
107 | "nixpkgs": [
108 | "nixpkgs"
109 | ],
110 | "pyproject-nix": [
111 | "pyproject-nix"
112 | ],
113 | "uv2nix": [
114 | "uv2nix"
115 | ]
116 | },
117 | "locked": {
118 | "lastModified": 1763662255,
119 | "narHash": "sha256-4bocaOyLa3AfiS8KrWjZQYu+IAta05u3gYZzZ6zXbT0=",
120 | "owner": "pyproject-nix",
121 | "repo": "build-system-pkgs",
122 | "rev": "042904167604c681a090c07eb6967b4dd4dae88c",
123 | "type": "github"
124 | },
125 | "original": {
126 | "owner": "pyproject-nix",
127 | "repo": "build-system-pkgs",
128 | "type": "github"
129 | }
130 | },
131 | "pyproject-nix": {
132 | "inputs": {
133 | "nixpkgs": [
134 | "nixpkgs"
135 | ]
136 | },
137 | "locked": {
138 | "lastModified": 1764134915,
139 | "narHash": "sha256-xaKvtPx6YAnA3HQVp5LwyYG1MaN4LLehpQI8xEdBvBY=",
140 | "owner": "pyproject-nix",
141 | "repo": "pyproject.nix",
142 | "rev": "2c8df1383b32e5443c921f61224b198a2282a657",
143 | "type": "github"
144 | },
145 | "original": {
146 | "owner": "pyproject-nix",
147 | "repo": "pyproject.nix",
148 | "type": "github"
149 | }
150 | },
151 | "root": {
152 | "inputs": {
153 | "flake-parts": "flake-parts",
154 | "flocken": "flocken",
155 | "nixpkgs": "nixpkgs",
156 | "pyproject-build-systems": "pyproject-build-systems",
157 | "pyproject-nix": "pyproject-nix",
158 | "systems": "systems_2",
159 | "treefmt-nix": "treefmt-nix",
160 | "uv2nix": "uv2nix"
161 | }
162 | },
163 | "systems": {
164 | "locked": {
165 | "lastModified": 1681028828,
166 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
167 | "owner": "nix-systems",
168 | "repo": "default",
169 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
170 | "type": "github"
171 | },
172 | "original": {
173 | "owner": "nix-systems",
174 | "repo": "default",
175 | "type": "github"
176 | }
177 | },
178 | "systems_2": {
179 | "locked": {
180 | "lastModified": 1681028828,
181 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
182 | "owner": "nix-systems",
183 | "repo": "default",
184 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
185 | "type": "github"
186 | },
187 | "original": {
188 | "owner": "nix-systems",
189 | "repo": "default",
190 | "type": "github"
191 | }
192 | },
193 | "treefmt-nix": {
194 | "inputs": {
195 | "nixpkgs": [
196 | "nixpkgs"
197 | ]
198 | },
199 | "locked": {
200 | "lastModified": 1766000401,
201 | "narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=",
202 | "owner": "numtide",
203 | "repo": "treefmt-nix",
204 | "rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd",
205 | "type": "github"
206 | },
207 | "original": {
208 | "owner": "numtide",
209 | "repo": "treefmt-nix",
210 | "type": "github"
211 | }
212 | },
213 | "uv2nix": {
214 | "inputs": {
215 | "nixpkgs": [
216 | "nixpkgs"
217 | ],
218 | "pyproject-nix": [
219 | "pyproject-nix"
220 | ]
221 | },
222 | "locked": {
223 | "lastModified": 1766021660,
224 | "narHash": "sha256-UUfz7qWB1Rb2KjGVCimt//Jncv3TgJwffPqbzqpkmgY=",
225 | "owner": "pyproject-nix",
226 | "repo": "uv2nix",
227 | "rev": "19fa99be3409f55ec05e823c66c9769df7a8dd17",
228 | "type": "github"
229 | },
230 | "original": {
231 | "owner": "pyproject-nix",
232 | "repo": "uv2nix",
233 | "type": "github"
234 | }
235 | }
236 | },
237 | "root": "root",
238 | "version": 7
239 | }
240 |
--------------------------------------------------------------------------------
/src/makejinja/app.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | import json
3 | import os
4 | import shutil
5 | import subprocess
6 | import sys
7 | import tomllib
8 | from collections import abc
9 | from inspect import signature
10 | from pathlib import Path
11 | from typing import Any
12 |
13 | import rich_click as click
14 | import yaml
15 | from jinja2 import BaseLoader, ChoiceLoader, DictLoader, Environment, FileSystemLoader
16 | from jinja2.environment import load_extensions
17 | from jinja2.utils import import_string
18 |
19 | from makejinja.config import Config
20 | from makejinja.plugin import Data, MutableData, PathFilter, Plugin
21 |
22 | __all__ = ["makejinja"]
23 |
24 | STDOUT_PATH = Path("/dev/stdout").resolve()
25 | STDIN_PATH = Path("/dev/stdin").resolve()
26 |
27 |
28 | def log(message: str, config: Config) -> None:
29 | if not config.quiet:
30 | click.echo(message, err=True)
31 |
32 |
33 | def exec(cmd: str) -> None:
34 | subprocess.run(cmd, shell=True, check=True)
35 |
36 |
37 | def makejinja(config: Config) -> None:
38 | """makejinja can be used to automatically generate files from [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/)."""
39 |
40 | for cmd in config.exec_pre:
41 | exec(cmd)
42 |
43 | for path in config.import_paths:
44 | sys.path.append(str(path.resolve()))
45 |
46 | data = load_data(config)
47 |
48 | if config.output.is_dir() and config.clean:
49 | log(f"Remove output '{config.output}'", config)
50 |
51 | shutil.rmtree(config.output)
52 |
53 | if not single_input_output_file(config):
54 | config.output.mkdir(exist_ok=True, parents=True)
55 |
56 | env = init_jinja_env(config, data)
57 | plugins: list[Plugin] = []
58 |
59 | for plugin_name in itertools.chain(config.plugins, config.loaders):
60 | plugins.append(load_plugin(plugin_name, env, data, config))
61 |
62 | plugin_path_filters: list[PathFilter] = []
63 |
64 | for plugin in plugins:
65 | if hasattr(plugin, "path_filters"):
66 | plugin_path_filters.extend(plugin.path_filters())
67 |
68 | # Save rendered files to avoid duplicate work
69 | # Even if two files are in two separate dirs, they will have the same template name (i.e., relative path)
70 | # and thus only the first one will be rendered every time
71 | # Key: output_path, Value: input_path
72 | rendered_files: dict[Path, Path] = {}
73 |
74 | # Save rendered dirs to later copy metadata
75 | # Key: output_path, Value: input_path
76 | rendered_dirs: dict[Path, Path] = {}
77 |
78 | for user_input_path in config.inputs:
79 | if user_input_path.is_file() or user_input_path == STDIN_PATH:
80 | handle_input_file(user_input_path, config, env, rendered_files)
81 | elif user_input_path.is_dir():
82 | handle_input_dir(
83 | user_input_path,
84 | config,
85 | env,
86 | rendered_files,
87 | rendered_dirs,
88 | plugin_path_filters,
89 | )
90 |
91 | postprocess_rendered_dirs(config, rendered_dirs)
92 |
93 | for cmd in config.exec_post:
94 | exec(cmd)
95 |
96 |
97 | def postprocess_rendered_dirs(
98 | config: Config,
99 | rendered_dirs: abc.Mapping[Path, Path],
100 | ) -> None:
101 | # Start with the deepest directory and work our way up, otherwise the statistics could be modified after copying
102 | for output_path, input_path in sorted(
103 | rendered_dirs.items(), key=lambda x: x[0], reverse=True
104 | ):
105 | if not config.keep_empty and not any(output_path.iterdir()):
106 | log(f"Remove empty dir '{output_path}'", config)
107 | shutil.rmtree(output_path)
108 |
109 | elif config.copy_metadata:
110 | log(f"Copy dir metadata '{input_path}' -> '{output_path}'", config)
111 | shutil.copystat(input_path, output_path)
112 |
113 |
114 | def single_input_output_file(config: Config) -> bool:
115 | """Check if the user provided a single input and a single output"""
116 | return (
117 | len(config.inputs) <= 1
118 | and not any(path.is_dir() for path in config.inputs)
119 | and (
120 | config.output == STDOUT_PATH
121 | or config.output.suffix != ""
122 | or config.output.is_file()
123 | )
124 | and not config.output.is_dir()
125 | )
126 |
127 |
128 | def handle_input_file(
129 | input_path: Path,
130 | config: Config,
131 | env: Environment,
132 | rendered_files: abc.MutableMapping[Path, Path],
133 | ) -> None:
134 | relative_path = Path(input_path.name)
135 | output_path = generate_output_path(config, relative_path)
136 |
137 | if output_path not in rendered_files:
138 | render_file(
139 | input_path,
140 | str(relative_path),
141 | output_path,
142 | config,
143 | env,
144 | enforce_jinja_suffix=False,
145 | )
146 |
147 | rendered_files[output_path] = input_path
148 |
149 |
150 | def handle_input_dir(
151 | user_input_path: Path,
152 | config: Config,
153 | env: Environment,
154 | rendered_files: abc.MutableMapping[Path, Path],
155 | rendered_dirs: abc.MutableMapping[Path, Path],
156 | plugin_path_filters: abc.Sequence[abc.Callable[[Path], bool]],
157 | ) -> None:
158 | input_paths = (
159 | input_path
160 | for include_pattern in config.include_patterns
161 | for input_path in sorted(user_input_path.glob(include_pattern))
162 | )
163 | # If the user provided a Jinja suffix, enforce it
164 | enforce_jinja_suffix = bool(config.jinja_suffix)
165 |
166 | for input_path in input_paths:
167 | relative_path = input_path.relative_to(user_input_path)
168 | output_path = generate_output_path(config, relative_path)
169 |
170 | exclude_pattern_match = any(
171 | input_path.match(x) for x in config.exclude_patterns
172 | )
173 | path_filter_match = any(
174 | not path_filter(input_path) for path_filter in plugin_path_filters
175 | )
176 | if exclude_pattern_match or path_filter_match:
177 | log(f"Skip excluded path '{input_path}'", config)
178 |
179 | elif input_path.is_file() and output_path not in rendered_files:
180 | render_file(
181 | input_path,
182 | str(relative_path),
183 | output_path,
184 | config,
185 | env,
186 | enforce_jinja_suffix,
187 | )
188 | rendered_files[output_path] = input_path
189 |
190 | elif input_path.is_dir() and output_path not in rendered_dirs:
191 | render_dir(input_path, output_path, config)
192 | rendered_dirs[output_path] = input_path
193 |
194 |
195 | def generate_output_path(config: Config, relative_path: Path) -> Path:
196 | if single_input_output_file(config):
197 | return config.output
198 |
199 | output_file = config.output / relative_path
200 |
201 | if relative_path.suffix == config.jinja_suffix and not config.keep_jinja_suffix:
202 | output_file = output_file.with_suffix("")
203 |
204 | return output_file
205 |
206 |
207 | def init_jinja_env(
208 | config: Config,
209 | data: Data,
210 | ) -> Environment:
211 | file_loader = DictLoader(
212 | {
213 | path.name: path.read_text()
214 | for path in config.inputs
215 | if path.is_file() or path == STDIN_PATH
216 | }
217 | )
218 | dir_loader = FileSystemLoader([path for path in config.inputs if path.is_dir()])
219 | loaders: list[BaseLoader] = [file_loader, dir_loader]
220 |
221 | env = Environment(
222 | loader=ChoiceLoader(loaders),
223 | extensions=config.extensions,
224 | block_start_string=config.delimiter.block_start,
225 | block_end_string=config.delimiter.block_end,
226 | variable_start_string=config.delimiter.variable_start,
227 | variable_end_string=config.delimiter.variable_end,
228 | comment_start_string=config.delimiter.comment_start,
229 | comment_end_string=config.delimiter.comment_end,
230 | line_statement_prefix=config.prefix.line_statement,
231 | line_comment_prefix=config.prefix.line_comment,
232 | trim_blocks=config.whitespace.trim_blocks,
233 | lstrip_blocks=config.whitespace.lstrip_blocks,
234 | newline_sequence=config.whitespace.newline_sequence,
235 | keep_trailing_newline=config.whitespace.keep_trailing_newline,
236 | optimized=config.internal.optimized,
237 | undefined=config.undefined.value,
238 | finalize=None,
239 | autoescape=config.internal.autoescape,
240 | cache_size=config.internal.cache_size,
241 | auto_reload=config.internal.auto_reload,
242 | bytecode_cache=None,
243 | enable_async=config.internal.enable_async,
244 | )
245 |
246 | env.globals.update(data)
247 | env.globals["env"] = os.environ
248 |
249 | return env
250 |
251 |
252 | def from_yaml(path: Path) -> dict[str, Any]:
253 | data = {}
254 |
255 | with path.open("rb") as fp:
256 | for doc in yaml.safe_load_all(fp):
257 | if isinstance(doc, abc.Mapping):
258 | data |= doc
259 | else:
260 | raise TypeError(
261 | f"Expected YAML documents in '{path}' to be mappings but found {type(doc).__name__}"
262 | )
263 |
264 | return data
265 |
266 |
267 | def from_toml(path: Path) -> dict[str, Any]:
268 | with path.open("rb") as fp:
269 | data = tomllib.load(fp)
270 |
271 | if isinstance(data, abc.Mapping):
272 | return dict(data)
273 |
274 | raise TypeError(
275 | f"Expected TOML documents in '{path}' to be mappings but found {type(data).__name__}"
276 | )
277 |
278 |
279 | def from_json(path: Path) -> dict[str, Any]:
280 | with path.open("rb") as fp:
281 | data = json.load(fp)
282 |
283 | if isinstance(data, abc.Mapping):
284 | return data
285 |
286 | raise TypeError(
287 | f"Expected JSON documents in '{path}' to be mappings but found {type(data).__name__}"
288 | )
289 |
290 |
291 | DATA_LOADERS: dict[str, abc.Callable[[Path], dict[str, Any]]] = {
292 | ".yaml": from_yaml,
293 | ".yml": from_yaml,
294 | ".toml": from_toml,
295 | ".json": from_json,
296 | }
297 |
298 |
299 | def collect_files(paths: abc.Iterable[Path], pattern: str = "**/*") -> list[Path]:
300 | files = []
301 |
302 | for path in paths:
303 | if path.is_dir():
304 | files.extend(
305 | file
306 | for file in sorted(path.glob(pattern))
307 | if not file.name.startswith(".") and file.is_file()
308 | )
309 | elif path.is_file():
310 | files.append(path)
311 |
312 | return files
313 |
314 |
315 | def dict_nested_set(data: MutableData, dotted_key: str, value: Any) -> None:
316 | """Given `foo`, 'key1.key2.key3', 'something', set foo['key1']['key2']['key3'] = 'something'
317 |
318 | Source: https://stackoverflow.com/a/57561744
319 | """
320 |
321 | # Start off pointing at the original dictionary that was passed in.
322 | here = data
323 |
324 | # Turn the string of key names into a list of strings.
325 | keys = dotted_key.split(".")
326 |
327 | # For every key *before* the last one, we concentrate on navigating through the dictionary.
328 | for key in keys[:-1]:
329 | # Try to find here[key]. If it doesn't exist, create it with an empty dictionary. Then,
330 | # update our `here` pointer to refer to the thing we just found (or created).
331 | here = here.setdefault(key, {})
332 |
333 | # Finally, set the final key to the given value
334 | here[keys[-1]] = value
335 |
336 |
337 | def load_data(config: Config) -> dict[str, Any]:
338 | data: dict[str, Any] = {}
339 |
340 | for path in collect_files(config.data):
341 | if loader := DATA_LOADERS.get(path.suffix):
342 | log(f"Load data '{path}'", config)
343 |
344 | data |= loader(path)
345 | else:
346 | log(f"Skip unsupported data '{path}'", config)
347 |
348 | for key, value in config.data_vars.items():
349 | dict_nested_set(data, key, value)
350 |
351 | return data
352 |
353 |
354 | def load_file_data(template_name: str, config: Config) -> dict[str, Any]:
355 | file_data: dict[str, Any] = {}
356 |
357 | if data_paths := config.file_data.get(template_name):
358 | for data_path in data_paths:
359 | if data_path.exists() and (loader := DATA_LOADERS.get(data_path.suffix)):
360 | log(
361 | f"Load file-specific data '{data_path}' for template '{template_name}'",
362 | config,
363 | )
364 | file_data |= loader(data_path)
365 | else:
366 | log(
367 | f"Skip missing or unsupported file-specific data '{data_path}'",
368 | config,
369 | )
370 |
371 | return file_data
372 |
373 |
374 | def load_plugin(
375 | plugin_name: str, env: Environment, data: Data, config: Config
376 | ) -> Plugin:
377 | cls: type[Plugin] = import_string(plugin_name)
378 | sig_params = signature(cls).parameters
379 | params: dict[str, Any] = {}
380 |
381 | if sig_params.get("env"):
382 | params["env"] = env
383 | if sig_params.get("environment"):
384 | params["environment"] = env
385 | if sig_params.get("data"):
386 | params["data"] = data
387 | if sig_params.get("config"):
388 | params["config"] = config
389 |
390 | plugin = cls(**params)
391 |
392 | if hasattr(plugin, "globals"):
393 | env.globals.update({func.__name__: func for func in plugin.globals()})
394 |
395 | if hasattr(plugin, "functions"):
396 | env.globals.update({func.__name__: func for func in plugin.functions()})
397 |
398 | if hasattr(plugin, "data"):
399 | env.globals.update(plugin.data())
400 |
401 | if hasattr(plugin, "extensions"):
402 | load_extensions(env, plugin.extensions())
403 |
404 | if hasattr(plugin, "filters"):
405 | env.filters.update({func.__name__: func for func in plugin.filters()})
406 |
407 | if hasattr(plugin, "tests"):
408 | env.tests.update({func.__name__: func for func in plugin.tests()})
409 |
410 | if hasattr(plugin, "policies"):
411 | env.policies.update(plugin.policies())
412 |
413 | return plugin
414 |
415 |
416 | def render_dir(input: Path, output: Path, config: Config) -> None:
417 | if output.exists() and not config.force:
418 | log(f"Skip existing dir '{output}'", config)
419 | else:
420 | log(f"Create dir '{input}' -> '{output}'", config)
421 |
422 | output.mkdir(exist_ok=True)
423 |
424 |
425 | def render_file(
426 | input: Path,
427 | template_name: str,
428 | output: Path,
429 | config: Config,
430 | env: Environment,
431 | enforce_jinja_suffix: bool,
432 | ) -> None:
433 | if output.exists() and not config.force and output != STDOUT_PATH:
434 | log(f"Skip existing file '{output}'", config)
435 |
436 | elif input.suffix == config.jinja_suffix or not enforce_jinja_suffix:
437 | template = env.get_template(template_name)
438 | file_data = load_file_data(template_name, config)
439 | rendered = template.render(file_data)
440 |
441 | # Write the rendered template if it has content
442 | # Prevents empty macro definitions
443 | if rendered.strip() == "" and not config.keep_empty:
444 | log(f"Skip empty file '{input}'", config)
445 | else:
446 | log(f"Render file '{input}' -> '{output}'", config)
447 |
448 | with output.open("w") as fp:
449 | fp.write(rendered)
450 |
451 | if config.copy_metadata:
452 | shutil.copystat(input, output)
453 |
454 | else:
455 | log(f"Copy file '{input}' -> '{output}'", config)
456 |
457 | shutil.copy2(input, output)
458 |
--------------------------------------------------------------------------------
/src/makejinja/config.py:
--------------------------------------------------------------------------------
1 | from collections import abc
2 | from enum import Enum
3 | from pathlib import Path
4 |
5 | import rich_click as click
6 | import typed_settings as ts
7 | from frozendict import frozendict
8 | from jinja2 import (
9 | ChainableUndefined,
10 | DebugUndefined,
11 | StrictUndefined,
12 | )
13 | from jinja2 import Undefined as DefaultUndefined
14 | from jinja2.defaults import (
15 | BLOCK_END_STRING,
16 | BLOCK_START_STRING,
17 | COMMENT_END_STRING,
18 | COMMENT_START_STRING,
19 | LINE_COMMENT_PREFIX,
20 | LINE_STATEMENT_PREFIX,
21 | NEWLINE_SEQUENCE,
22 | VARIABLE_END_STRING,
23 | VARIABLE_START_STRING,
24 | )
25 | from rich_click.utils import OptionGroupDict
26 |
27 | __all__ = ["Config", "Delimiter", "Internal", "Prefix", "Whitespace", "Undefined"]
28 |
29 |
30 | class Undefined(Enum):
31 | """How to handle undefined variables."""
32 |
33 | default = DefaultUndefined
34 | chainable = ChainableUndefined
35 | debug = DebugUndefined
36 | strict = StrictUndefined
37 |
38 |
39 | def _exclude_patterns_validator(instance, attribute, value) -> None:
40 | if any("**" in pattern for pattern in value):
41 | # todo: for next major release, raise ValueError instead of printing a warning
42 | click.echo(
43 | "The recursive wildcard `**` is not supported by `exclude_patterns` (it acts like non-recursive `*`).",
44 | err=True,
45 | )
46 |
47 |
48 | @ts.settings(frozen=True)
49 | class Delimiter:
50 | block_start: str = ts.option(
51 | default=BLOCK_START_STRING, help="The string marking the beginning of a block."
52 | )
53 | block_end: str = ts.option(
54 | default=BLOCK_END_STRING, help="The string marking the end of a block."
55 | )
56 | variable_start: str = ts.option(
57 | default=VARIABLE_START_STRING,
58 | help="The string marking the beginning of a print statement.",
59 | )
60 | variable_end: str = ts.option(
61 | default=VARIABLE_END_STRING,
62 | help="The string marking the end of a print statement.",
63 | )
64 | comment_start: str = ts.option(
65 | default=COMMENT_START_STRING,
66 | help="The string marking the beginning of a comment.",
67 | )
68 | comment_end: str = ts.option(
69 | default=COMMENT_END_STRING, help="The string marking the end of a comment."
70 | )
71 |
72 |
73 | @ts.settings(frozen=True)
74 | class Prefix:
75 | line_statement: str | None = ts.option(
76 | default=LINE_STATEMENT_PREFIX,
77 | help=(
78 | "If given and a string, this will be used as prefix for line based"
79 | " statements."
80 | ),
81 | )
82 | line_comment: str | None = ts.option(
83 | default=LINE_COMMENT_PREFIX,
84 | help=(
85 | "If given and a string, this will be used as prefix for line based"
86 | " comments."
87 | ),
88 | )
89 |
90 |
91 | @ts.settings(frozen=True)
92 | class Internal:
93 | optimized: bool = ts.option(
94 | default=True,
95 | click={"hidden": True},
96 | help=(
97 | "Should the"
98 | " [optimizer](https://github.com/Pfern/jinja2/blob/master/jinja2/optimizer.py)"
99 | " be enabled?"
100 | ),
101 | )
102 | autoescape: bool = ts.option(
103 | default=False,
104 | click={"param_decls": "--internal-autoescape", "hidden": True},
105 | help="""
106 | If set to `True` the XML/HTML autoescaping feature is enabled by default.
107 | For more details about autoescaping see `markupsafe.Markup`.
108 | """,
109 | )
110 | cache_size: int = ts.option(
111 | default=0,
112 | click={"hidden": True},
113 | help="""
114 | The size of the cache.
115 | If the cache size is set to a positive number like `400`,
116 | it means that if more than 400 templates are loaded the loader will clean out the least recently used template.
117 | If the cache size is set to `0`, templates are recompiled all the time.
118 | If the cache size is `-1` the cache will not be cleaned.
119 | """,
120 | )
121 | auto_reload: bool = ts.option(
122 | default=False,
123 | click={"hidden": True},
124 | help="""
125 | Some loaders load templates from locations where the template sources may change (ie: file system or database).
126 | If `auto_reload` is set to `True`, every time a template is requested, the loader checks if the source changed and if yes,
127 | it will reload the template. For higher performance it's possible to disable that.
128 | """,
129 | )
130 | enable_async: bool = ts.option(
131 | default=False,
132 | click={"param_decls": "--internal-enable-async", "hidden": True},
133 | help="""
134 | If set to true this enables async template execution which allows using async functions and generators.
135 | """,
136 | )
137 |
138 |
139 | @ts.settings(frozen=True)
140 | class Whitespace:
141 | trim_blocks: bool = ts.option(
142 | default=True,
143 | click={"param_decls": "--trim-blocks/--no-trim-blocks"},
144 | help="""
145 | If this is set to `True`, the first newline after a block is removed (block, not variable tag!).
146 | """,
147 | )
148 | lstrip_blocks: bool = ts.option(
149 | default=True,
150 | click={"param_decls": "--lstrip-blocks/--no-lstrip-blocks"},
151 | help="""
152 | If this is set to `True`, leading spaces and tabs are stripped from the start of a line to a block.
153 | """,
154 | )
155 | newline_sequence: str = ts.option(
156 | default=NEWLINE_SEQUENCE,
157 | click={"param_decls": "--newline-sequence"},
158 | help="""
159 | The sequence that starts a newline.
160 | The default is tailored for UNIX-like systems (Linux/macOS).
161 | """,
162 | )
163 | keep_trailing_newline: bool = ts.option(
164 | default=True,
165 | click={"param_decls": "--keep-trailing-newline/--strip-trailing-newline"},
166 | help="""
167 | Preserve the trailing newline when rendering templates.
168 | The default is `False`, which causes a single newline, if present, to be stripped from the end of the template.
169 | """,
170 | )
171 |
172 |
173 | @ts.settings(frozen=True)
174 | class Config:
175 | inputs: tuple[Path, ...] = ts.option(
176 | click={
177 | "type": click.Path(exists=True, path_type=Path),
178 | "param_decls": ("--input", "-i"),
179 | },
180 | help="""
181 | Path to a directory containing template files or a single template file.
182 | It is passed to Jinja's [FileSystemLoader](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.FileSystemLoader) when creating the environment.
183 | **Note:** This option may be passed multiple times to pass a list of values.
184 | If a template exists in multiple inputs, the first matching template in the provided order will be used.
185 | """,
186 | )
187 | output: Path = ts.option(
188 | click={
189 | "type": click.Path(path_type=Path),
190 | "param_decls": ("--output", "-o"),
191 | },
192 | help="""
193 | Path to a directory where the rendered templates are stored.
194 | makejinja preserves the relative paths in the process, meaning that you can even use it on nested directories.
195 | """,
196 | )
197 | include_patterns: tuple[str, ...] = ts.option(
198 | default=("**/*",),
199 | click={"param_decls": ("--include-pattern", "--include", "-I")},
200 | help="""
201 | Glob patterns to search for files in `inputs`.
202 | Accepts all pattern supported by [`fnmatch`](https://docs.python.org/3/library/fnmatch.html#module-fnmatch).
203 | If a file is matched by this pattern and does not end with the specified `jinja-suffix`, it is copied over to `output`.
204 | Multiple can be provided.
205 | **Note:** Do not add a special suffix used by your template files here, instead use the `jinja-suffix` option.
206 | """,
207 | )
208 | exclude_patterns: tuple[str, ...] = ts.option(
209 | default=tuple(),
210 | click={"param_decls": ("--exclude-pattern", "--exclude", "-E")},
211 | validator=_exclude_patterns_validator,
212 | help="""
213 | Glob patterns pattern to exclude files matched.
214 | Applied against files discovered through `include_patterns` via `Path.match`.
215 | **Note:** The recursive wildcard `**` is not supported (it acts like non-recursive `*`).
216 | Multiple can be provided.
217 | """,
218 | )
219 | jinja_suffix: str | None = ts.option(
220 | default=".jinja",
221 | help="""
222 | File ending of Jinja template files.
223 | All files with this suffix in `inputs` matched by `pattern` are passed to the Jinja renderer.
224 | This suffix is not enforced for individual files passed to `inputs`.
225 | **Note:** Should be provided *with* the leading dot.
226 | If empty, all files are considered to be Jinja templates.
227 | """,
228 | )
229 | keep_jinja_suffix: bool = ts.option(
230 | default=False,
231 | click={"param_decls": "--keep-jinja-suffix"},
232 | help="""
233 | Decide whether the specified `jinja-suffix` is removed from the file name after rendering.
234 | """,
235 | )
236 | keep_empty: bool = ts.option(
237 | default=False,
238 | click={"param_decls": "--keep-empty"},
239 | help="""
240 | Some Jinja template files may be empty after rendering (e.g., if they only contain macros that are imported by other templates).
241 | By default, we do not copy such empty files.
242 | If there is a need to have them available anyway, you can adjust that.
243 | """,
244 | )
245 | copy_metadata: bool = ts.option(
246 | default=False,
247 | click={"param_decls": ("--copy-metadata", "-m")},
248 | help="""
249 | Copy the file metadata (e.g., created/modified/permissions) from the input file using `shutil.copystat`
250 | """,
251 | )
252 | data: tuple[Path, ...] = ts.option(
253 | default=tuple(),
254 | click={
255 | "type": click.Path(exists=True, path_type=Path),
256 | "param_decls": ("--data", "-d"),
257 | },
258 | help="""
259 | Load variables from yaml/yml/toml/json files for use in your Jinja templates.
260 | The definitions are passed to Jinja as globals.
261 | Can either be a file or a directory containing files.
262 | **Note:** This option may be passed multiple times to pass a list of values.
263 | If multiple files are supplied, beware that previous declarations will be overwritten by newer ones.
264 | """,
265 | )
266 | data_vars: abc.Mapping[str, str] = ts.option(
267 | default=frozendict(),
268 | click={
269 | "param_decls": ("--data-var", "-D"),
270 | "help": """
271 | Load variables from the command line for use in your Jinja templates.
272 | The definitions are applied after loading the data from files.
273 | When using dotted keys (e.g., `foo.bar=42`), the value is converted to a nested dictionary.
274 | Consequently, you can override values loaded from files.
275 | **Note:** This option may be passed multiple times.
276 | """,
277 | },
278 | )
279 | file_data: abc.Mapping[str, tuple[Path, ...]] = ts.option(
280 | default=frozendict(),
281 | click={
282 | "param_decls": ("--file-data",),
283 | "help": """
284 | Load file-specific data for individual templates.
285 | Format: template_file=data_file1,data_file2,...
286 | Example: --file-data "home.yaml.jinja=home_data.yaml,common.yaml"
287 | The data from these files will be available only when rendering the specified template.
288 | **Note:** This option may be passed multiple times.
289 | """,
290 | },
291 | )
292 | loaders: tuple[str, ...] = ts.option(
293 | default=tuple(),
294 | click={
295 | "param_decls": ("--loader", "-l"),
296 | "hidden": True,
297 | },
298 | help="Deprecated, use `--plugin` instead.",
299 | )
300 | plugins: tuple[str, ...] = ts.option(
301 | default=tuple(),
302 | click={
303 | "param_decls": ("--plugin", "-p"),
304 | },
305 | help="""
306 | Use custom Python code to adjust the used Jinja environment to your needs.
307 | The specified Python file should export a **class** containing a subset of the following functions:
308 | `filters`, `globals`, `data`, and `extensions`.
309 | In addition, you may add an `__init__` function that receives two positional arguments:
310 | the created Jinja environment and the data parsed from the files supplied to makejinja's `data` option.
311 | This allows you to apply arbitrary logic to makejinja.
312 | An import path can be specified either in dotted notation (`your.custom.Plugin`)
313 | or with a colon as object delimiter (`your.custom:Plugin`).
314 | **Note:** This option may be passed multiple times to pass a list of values.
315 | """,
316 | )
317 | import_paths: tuple[Path, ...] = ts.option(
318 | default=(Path("."),),
319 | click={
320 | "type": click.Path(exists=True, file_okay=False, path_type=Path),
321 | "param_decls": "--import-path",
322 | "show_default": "current working directory",
323 | },
324 | help="""
325 | In order to load plugins or Jinja extensions, the PYTHONPATH variable needs to be patched.
326 | The default value works for most use cases, but you may load other paths as well.
327 | """,
328 | )
329 | extensions: tuple[str, ...] = ts.option(
330 | default=tuple(),
331 | click={"param_decls": ("--extension", "-e")},
332 | help="""
333 | List of Jinja extensions to use as strings of import paths.
334 | An overview of the built-in ones can be found on the [project website](https://jinja.palletsprojects.com/en/3.1.x/extensions/).
335 | **Note:** This option may be passed multiple times to pass a list of values.
336 | """,
337 | )
338 | undefined: Undefined = ts.option(
339 | default=Undefined.default,
340 | help=(
341 | """
342 | Whenever the template engine is unable to look up a name or access an attribute one of those objects is created and returned.
343 | Some operations on undefined values are then allowed, others fail.
344 | The closest to regular Python behavior is `strict` which disallows all operations beside testing if it is an undefined object.
345 | """
346 | ),
347 | )
348 | exec_pre: tuple[str, ...] = ts.option(
349 | default=tuple(),
350 | help="""
351 | Shell commands to execute before rendering.
352 | """,
353 | )
354 | exec_post: tuple[str, ...] = ts.option(
355 | default=tuple(),
356 | help="""
357 | Shell commands to execute after rendering.
358 | """,
359 | )
360 | clean: bool = ts.option(
361 | default=False,
362 | click={"param_decls": ("--clean", "-c")},
363 | help="""
364 | Whether to remove the output directory if it exists.
365 | """,
366 | )
367 | force: bool = ts.option(
368 | default=False,
369 | click={"param_decls": ("--force", "-f")},
370 | help="""
371 | Whether to overwrite existing files in the output directory.
372 | """,
373 | )
374 | quiet: bool = ts.option(
375 | default=False,
376 | click={"param_decls": ("--quiet", "-q")},
377 | help="""
378 | Print no information about the rendering process.
379 | """,
380 | )
381 | delimiter: Delimiter = Delimiter()
382 | prefix: Prefix = Prefix()
383 | whitespace: Whitespace = Whitespace()
384 | internal: Internal = Internal()
385 |
386 |
387 | OPTION_GROUPS: dict[str, list[OptionGroupDict]] = {
388 | "makejinja": [
389 | {
390 | "name": "Input/Output",
391 | "options": [
392 | "--input",
393 | "--output",
394 | "--include-pattern",
395 | "--exclude-pattern",
396 | "--jinja-suffix",
397 | "--keep-jinja-suffix",
398 | "--keep-empty",
399 | "--copy-metadata",
400 | ],
401 | },
402 | {
403 | "name": "Jinja Environment",
404 | "options": [
405 | "--data",
406 | "--data-var",
407 | "--file-data",
408 | "--plugin",
409 | "--import-path",
410 | "--extension",
411 | "--undefined",
412 | ],
413 | },
414 | {
415 | "name": "Jinja Whitespace",
416 | "options": [
417 | "--lstrip-blocks",
418 | "--trim-blocks",
419 | "--keep-trailing-newline",
420 | "--newline-sequence",
421 | ],
422 | },
423 | {
424 | "name": "Jinja Delimiters",
425 | "options": [
426 | "--delimiter-block-start",
427 | "--delimiter-block-end",
428 | "--delimiter-comment-start",
429 | "--delimiter-comment-end",
430 | "--delimiter-variable-start",
431 | "--delimiter-variable-end",
432 | ],
433 | },
434 | {
435 | "name": "Jinja Prefixes",
436 | "options": [
437 | "--prefix-line-statement",
438 | "--prefix-line-comment",
439 | ],
440 | },
441 | {
442 | "name": "Shell Hooks",
443 | "options": [
444 | "--exec-pre",
445 | "--exec-post",
446 | ],
447 | },
448 | ]
449 | }
450 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [2.8.2](https://github.com/mirkolenz/makejinja/compare/v2.8.1...v2.8.2) (2025-12-05)
4 |
5 | ### Bug Fixes
6 |
7 | * **app:** verify supplied data is a mapping ([133ebe6](https://github.com/mirkolenz/makejinja/commit/133ebe6158872effb4c826234b5e45662eadadce))
8 | * **build:** drop support for python 3.11 ([3b83516](https://github.com/mirkolenz/makejinja/commit/3b835167f733c75934411ec7c864438d63596765))
9 | * **build:** switch from immutables to frozendict ([bdb843f](https://github.com/mirkolenz/makejinja/commit/bdb843fdf68245d7b979f5ca3c09b2d21553e959))
10 | * **build:** switch from setuptools to uv-build ([6e4573d](https://github.com/mirkolenz/makejinja/commit/6e4573dd69266ee4a433e3c7561ce0fafd3edb41))
11 | * **logging:** use stderr instead of stdout ([667a0ae](https://github.com/mirkolenz/makejinja/commit/667a0ae4c4856394554f42632d42bbbd0eae22a7))
12 | * **typing:** enforce rich option group types ([1499d15](https://github.com/mirkolenz/makejinja/commit/1499d15dcfde5d581a987f7ebab32af7729226bf))
13 |
14 | ## [2.8.1](https://github.com/mirkolenz/makejinja/compare/v2.8.0...v2.8.1) (2025-07-28)
15 |
16 | ### Bug Fixes
17 |
18 | * **deps:** allow typed-settings v25 ([f8be326](https://github.com/mirkolenz/makejinja/commit/f8be32643d9b971873beb5a0c6283b7e84cef78f))
19 |
20 | ## [2.8.0](https://github.com/mirkolenz/makejinja/compare/v2.7.2...v2.8.0) (2025-06-14)
21 |
22 | ### Features
23 |
24 | * allow passing file-specific data ([ea2f3af](https://github.com/mirkolenz/makejinja/commit/ea2f3af332a17e4f6a909b3e2e1deda84394242d))
25 |
26 | ## [2.7.2](https://github.com/mirkolenz/makejinja/compare/v2.7.1...v2.7.2) (2024-11-17)
27 |
28 | ### Bug Fixes
29 |
30 | * update paths to get docs working again ([a27f7c7](https://github.com/mirkolenz/makejinja/commit/a27f7c74259b61f0bf2b3574e7656e640d33a955))
31 |
32 | ## [2.7.1](https://github.com/mirkolenz/makejinja/compare/v2.7.0...v2.7.1) (2024-11-17)
33 |
34 | ### Bug Fixes
35 |
36 | * update metadata and add build-system to pyproject.toml ([df11d96](https://github.com/mirkolenz/makejinja/commit/df11d96e31bf1136dc203180228b83e0dec4088e))
37 |
38 | ## [2.7.0](https://github.com/mirkolenz/makejinja/compare/v2.6.2...v2.7.0) (2024-11-14)
39 |
40 | ### Features
41 |
42 | * move from poetry to uv ([0ac9325](https://github.com/mirkolenz/makejinja/commit/0ac93253fafd8c823a9a8c6d7cb83bb137799de9))
43 |
44 | ## [2.6.2](https://github.com/mirkolenz/makejinja/compare/v2.6.1...v2.6.2) (2024-07-28)
45 |
46 |
47 | ### Bug Fixes
48 |
49 | * warn about recursive exclude patterns ([22a0918](https://github.com/mirkolenz/makejinja/commit/22a09189451f9004166ef87095ed2b82c1036b36))
50 |
51 | ## [2.6.1](https://github.com/mirkolenz/makejinja/compare/v2.6.0...v2.6.1) (2024-06-26)
52 |
53 |
54 | ### Bug Fixes
55 |
56 | * **deps:** bump pdoc due to security issue ([9c7df0c](https://github.com/mirkolenz/makejinja/commit/9c7df0c95fe0748bfbb8b53ee300f99f452dbb48))
57 |
58 | ## [2.6.0](https://github.com/mirkolenz/makejinja/compare/v2.5.0...v2.6.0) (2024-05-11)
59 |
60 |
61 | ### Features
62 |
63 | * allow rendering all files by providing empty jinja suffix ([697b0ce](https://github.com/mirkolenz/makejinja/commit/697b0cea6c43d8aa27453211e40efbc766529204))
64 |
65 |
66 | ### Bug Fixes
67 |
68 | * add metadata to nix derivation ([31ebb23](https://github.com/mirkolenz/makejinja/commit/31ebb23e45145ca841127e467e586d30619082e1))
69 |
70 | ## [2.5.0](https://github.com/mirkolenz/makejinja/compare/v2.4.0...v2.5.0) (2024-01-21)
71 |
72 |
73 | ### Features
74 |
75 | * add exec-pre and exec-post ([#96](https://github.com/mirkolenz/makejinja/issues/96)) ([c808400](https://github.com/mirkolenz/makejinja/commit/c808400b9dd668e81b1fefc68ac1ac9f401c3e27))
76 | * allow stdin/stdout on unix systems ([#58](https://github.com/mirkolenz/makejinja/issues/58)) ([7cd8e94](https://github.com/mirkolenz/makejinja/commit/7cd8e946f8c94431bcecad609ed03afbcfc1d59f))
77 |
78 | ## [2.4.0](https://github.com/mirkolenz/makejinja/compare/v2.3.5...v2.4.0) (2024-01-21)
79 |
80 |
81 | ### Features
82 |
83 | * add exclusion functions to loader ([#102](https://github.com/mirkolenz/makejinja/issues/102)) ([1ad61f3](https://github.com/mirkolenz/makejinja/commit/1ad61f3024cc4787e0bb91052c20207bec9d9c53))
84 | * deprecate loaders, add plugins ([2c291bb](https://github.com/mirkolenz/makejinja/commit/2c291bb17e986c915d3dd64115fbc01935f1d25f))
85 | * keep trailing newlines by default ([a8436a8](https://github.com/mirkolenz/makejinja/commit/a8436a8bb54ca0416e34a7119c9160dc837b884f))
86 | * pass config to custom loaders ([61ae423](https://github.com/mirkolenz/makejinja/commit/61ae423b883ed8721ff7d7e1e858bd06e1cef31c))
87 | * replace loader exclusions with path filters ([7cba6c8](https://github.com/mirkolenz/makejinja/commit/7cba6c838940d2178c082d6bc8192412d518e111))
88 |
89 |
90 | ### Bug Fixes
91 |
92 | * update plugin exports ([2dfa8a0](https://github.com/mirkolenz/makejinja/commit/2dfa8a023bf07a290026d7ccad5d4ef42e812b92))
93 | * use correct negation for path filters ([2992496](https://github.com/mirkolenz/makejinja/commit/299249612528c17bc19f299657534b44360316e6))
94 |
95 | ## [2.3.5](https://github.com/mirkolenz/makejinja/compare/v2.3.4...v2.3.5) (2024-01-16)
96 |
97 |
98 | ### Bug Fixes
99 |
100 | * apply config.force to directories as well ([#98](https://github.com/mirkolenz/makejinja/issues/98)) ([04b2521](https://github.com/mirkolenz/makejinja/commit/04b2521b1840e97bfccfc4f58b8eb3202f2038b3))
101 |
102 | ## [2.3.4](https://github.com/mirkolenz/makejinja/compare/v2.3.3...v2.3.4) (2024-01-15)
103 |
104 |
105 | ### Bug Fixes
106 |
107 | * remove empty directories if --no-keep-empty ([a586e8f](https://github.com/mirkolenz/makejinja/commit/a586e8f6026d814b5a393bfa004ccf77f19eea9a))
108 |
109 | ## [2.3.3](https://github.com/mirkolenz/makejinja/compare/v2.3.2...v2.3.3) (2024-01-14)
110 |
111 |
112 | ### Bug Fixes
113 |
114 | * properly parse data-vars ([670f8bd](https://github.com/mirkolenz/makejinja/commit/670f8bd459c7fa44be03ad44cd0874723ac676b9))
115 |
116 | ## [2.3.2](https://github.com/mirkolenz/makejinja/compare/v2.3.1...v2.3.2) (2023-11-08)
117 |
118 |
119 | ### Bug Fixes
120 |
121 | * trigger release ([a331675](https://github.com/mirkolenz/makejinja/commit/a33167527ff887e6d77c40459b1ccb92b6ece964))
122 |
123 | ## [2.3.1](https://github.com/mirkolenz/makejinja/compare/v2.3.0...v2.3.1) (2023-11-08)
124 |
125 |
126 | ### Bug Fixes
127 |
128 | * update links in readme ([8dfe58e](https://github.com/mirkolenz/makejinja/commit/8dfe58e2c58872d70a5b93d0b3a34d473a3382a8))
129 |
130 | ## [2.3.0](https://github.com/mirkolenz/makejinja/compare/v2.2.0...v2.3.0) (2023-11-06)
131 |
132 |
133 | ### Features
134 |
135 | * add flag to pass key-value options via cli ([c59e76f](https://github.com/mirkolenz/makejinja/commit/c59e76fa966cdf2cfca9b39ab5ea15cddc15e030))
136 | * add force option to enable overwriting ([a9adedc](https://github.com/mirkolenz/makejinja/commit/a9adedcee0ddbcad3fd5e6ab0db55e31c7622f08))
137 | * add quiet option to silence output ([78b6b44](https://github.com/mirkolenz/makejinja/commit/78b6b448d91973f0619807098cf6216fca4414fd))
138 | * add shorthand values for cli options ([dd033c2](https://github.com/mirkolenz/makejinja/commit/dd033c2e841d5ab5e780722ba338e4e8ffbe04ee))
139 | * allow files to be passed as inputs ([b78fa4b](https://github.com/mirkolenz/makejinja/commit/b78fa4bc63c1cecf2db69782df1476189f8d50f5))
140 | * allow output to be a file in certain cases ([c85763f](https://github.com/mirkolenz/makejinja/commit/c85763f95394f378b9b72a76b8179565dbc62858))
141 | * optimize data handling (update globals) ([1016fe9](https://github.com/mirkolenz/makejinja/commit/1016fe94d510b7cf4dae99760ab923800a85ce10))
142 | * pass os.environ to globals ([c9f646c](https://github.com/mirkolenz/makejinja/commit/c9f646c6571b95b5dbe6b3746c1b48a03db1744f))
143 | * require using a flag to remove the output directory ([f0b288d](https://github.com/mirkolenz/makejinja/commit/f0b288d3fc15fcf23455f3b8434a0a2c87aed5a9))
144 | * update loader to better support globals ([c38b564](https://github.com/mirkolenz/makejinja/commit/c38b564b4ccbd4543975e18fa4e0f1cc1eec3d10))
145 |
146 |
147 | ### Bug Fixes
148 |
149 | * add shorthand options for version/help ([e4f1cf0](https://github.com/mirkolenz/makejinja/commit/e4f1cf01c24ab121a9dfa6c01869db1f278c835d))
150 | * do not enforce jinja suffix for input files ([152d220](https://github.com/mirkolenz/makejinja/commit/152d220b4b8d274c6ea638731ed50adfa65ba92c))
151 | * pass immutable data to loader ([1351f2c](https://github.com/mirkolenz/makejinja/commit/1351f2c4a39af7b082fbd8c0c0e7cc804569aa6a))
152 | * rename clean-output to clean ([523556c](https://github.com/mirkolenz/makejinja/commit/523556cc3d63afa9008037db2990a37e222bff7f))
153 | * update cli option groups ([380d0db](https://github.com/mirkolenz/makejinja/commit/380d0db3218ba3d41460f68bb6e2aa1f672b168e))
154 |
155 | ## [2.2.0](https://github.com/mirkolenz/makejinja/compare/v2.1.4...v2.2.0) (2023-11-03)
156 |
157 |
158 | ### Features
159 |
160 | * allow customization of undefined behavior ([fd84618](https://github.com/mirkolenz/makejinja/commit/fd846189f4e9702c5f08dc344abb3dff062b1a5d))
161 |
162 | ## [2.1.4](https://github.com/mirkolenz/makejinja/compare/v2.1.3...v2.1.4) (2023-11-03)
163 |
164 |
165 | ### Bug Fixes
166 |
167 | * update description of project ([2f4e405](https://github.com/mirkolenz/makejinja/commit/2f4e40563350d4eb12912db9bdb0e19b4231a791))
168 |
169 | ## [2.1.3](https://github.com/mirkolenz/makejinja/compare/v2.1.2...v2.1.3) (2023-11-03)
170 |
171 |
172 | ### Bug Fixes
173 |
174 | * use correct ref for building docs ([b7e30f7](https://github.com/mirkolenz/makejinja/commit/b7e30f773c2bd18e29333ba654ee7a3cdc85e07d))
175 |
176 | ## [2.1.2](https://github.com/mirkolenz/makejinja/compare/v2.1.1...v2.1.2) (2023-11-03)
177 |
178 |
179 | ### Bug Fixes
180 |
181 | * trigger release ([fd6579d](https://github.com/mirkolenz/makejinja/commit/fd6579d7650889843f10699999bce2cef4c18f53))
182 |
183 | ## [2.1.1](https://github.com/mirkolenz/makejinja/compare/v2.1.0...v2.1.1) (2023-10-31)
184 |
185 |
186 | ### Bug Fixes
187 |
188 | * expose cli module in main init ([ed429a0](https://github.com/mirkolenz/makejinja/commit/ed429a0b61814bbd35a7ab36014e556c65dec597))
189 |
190 | ## [2.1.0](https://github.com/mirkolenz/makejinja/compare/v2.0.2...v2.1.0) (2023-10-30)
191 |
192 |
193 | ### Features
194 |
195 | * add data handler for json files ([d18f514](https://github.com/mirkolenz/makejinja/commit/d18f514a0722fbbea3886d538deec45470207d68))
196 |
197 |
198 | ### Bug Fixes
199 |
200 | * require at least python 3.11 and drop tomli ([ebdeb64](https://github.com/mirkolenz/makejinja/commit/ebdeb64c765eefcb250c541c46167059af0c154e))
201 |
202 | ## [2.0.2](https://github.com/mirkolenz/makejinja/compare/v2.0.1...v2.0.2) (2023-10-26)
203 |
204 |
205 | ### Bug Fixes
206 |
207 | * use poetry2nix again after upstream fixes ([3b7dac0](https://github.com/mirkolenz/makejinja/commit/3b7dac03faf7418d0eb78cd5ed3f3f970235b0cb))
208 |
209 | ## [2.0.1](https://github.com/mirkolenz/makejinja/compare/v2.0.0...v2.0.1) (2023-09-30)
210 |
211 |
212 | ### Bug Fixes
213 |
214 | * bump deps ([193f396](https://github.com/mirkolenz/makejinja/commit/193f396a233d77cf1390622819990563f3055162))
215 | * remove default command from docker image ([c562377](https://github.com/mirkolenz/makejinja/commit/c5623773947d602f7892bef5c8723a5f03da5c4e))
216 |
217 | ## [2.0.0](https://github.com/mirkolenz/makejinja/compare/v1.1.5...v2.0.0) (2023-06-18)
218 |
219 |
220 | ### ⚠ BREAKING CHANGES
221 |
222 | * The configuration file has been renamed from `.makejinja.toml` to `makejinja.toml`. Please rename your files accordingly.
223 | * The parameter `input_pattern` has been changed to `include_patterns` which now accepts a list of patterns.
224 | * In this version, `input` has been removed an replaced with `inputs` (allowing to use multiple input folders). We also included a new option `exclude_patterns` to ignore files that would be matched by `input_pattern`. The option `copy_tree` is superseded by the new `copy_metadata` which is compatible with multiple inputs and preserves attributes for rendered files as well. Please adjust your config accordingly, otherwise `makejinja` will break!
225 |
226 | ### Features
227 |
228 | * add version option ([0585114](https://github.com/mirkolenz/makejinja/commit/058511497517724d6e37bd8e4054a16641476366))
229 | * completely rewrite file handling ([#20](https://github.com/mirkolenz/makejinja/issues/20)) ([97d6a51](https://github.com/mirkolenz/makejinja/commit/97d6a51d268689bd50fa1b4a9a70c099db42bda4))
230 | * remove leading dot from config file ([4742165](https://github.com/mirkolenz/makejinja/commit/4742165ed4e18c67543f5c46411989d752e867f9))
231 | * rename input_pattern to include_patterns ([21e3e85](https://github.com/mirkolenz/makejinja/commit/21e3e85d91cb0c2c2426bd36137998e36c5140ef))
232 |
233 |
234 | ### Bug Fixes
235 |
236 | * add multi-arch docker images ([6024633](https://github.com/mirkolenz/makejinja/commit/6024633e84a895eeb9cb2db0860cfa0bd77b7954))
237 | * apply exclude patterns to files and folders ([5d80747](https://github.com/mirkolenz/makejinja/commit/5d807474276b107047422f1430bbb890f9cb9d7d))
238 | * provide aarch64 docker image ([e983268](https://github.com/mirkolenz/makejinja/commit/e983268df920741921384d4ea4f75f92f79f524e))
239 | * remove aarch64 docker image due to bugs ([0af11a2](https://github.com/mirkolenz/makejinja/commit/0af11a24c24dc7200253f9864126bda14a9ebf29))
240 | * update nix flake ([05b9575](https://github.com/mirkolenz/makejinja/commit/05b95756e2682f4acad69f7ebbbcfd75eb945a02))
241 |
242 | ## [2.0.0-beta.8](https://github.com/mirkolenz/makejinja/compare/v2.0.0-beta.7...v2.0.0-beta.8) (2023-06-13)
243 |
244 |
245 | ### ⚠ BREAKING CHANGES
246 |
247 | * The configuration file has been renamed from `.makejinja.toml` to `makejinja.toml`. Please rename your files accordingly.
248 | * The parameter `input_pattern` has been changed to `include_patterns` which now accepts a list of patterns.
249 |
250 | ### Features
251 |
252 | * remove leading dot from config file ([4742165](https://github.com/mirkolenz/makejinja/commit/4742165ed4e18c67543f5c46411989d752e867f9))
253 | * rename input_pattern to include_patterns ([21e3e85](https://github.com/mirkolenz/makejinja/commit/21e3e85d91cb0c2c2426bd36137998e36c5140ef))
254 |
255 | ## [2.0.0-beta.7](https://github.com/mirkolenz/makejinja/compare/v2.0.0-beta.6...v2.0.0-beta.7) (2023-06-13)
256 |
257 |
258 | ### Bug Fixes
259 |
260 | * add multi-arch docker images ([6024633](https://github.com/mirkolenz/makejinja/commit/6024633e84a895eeb9cb2db0860cfa0bd77b7954))
261 |
262 | ## [2.0.0-beta.6](https://github.com/mirkolenz/makejinja/compare/v2.0.0-beta.5...v2.0.0-beta.6) (2023-06-01)
263 |
264 |
265 | ### Bug Fixes
266 |
267 | * apply exclude patterns to files and folders ([5d80747](https://github.com/mirkolenz/makejinja/commit/5d807474276b107047422f1430bbb890f9cb9d7d))
268 |
269 | ## [2.0.0-beta.5](https://github.com/mirkolenz/makejinja/compare/v2.0.0-beta.4...v2.0.0-beta.5) (2023-05-25)
270 |
271 |
272 | ### Bug Fixes
273 |
274 | * remove aarch64 docker image due to bugs ([0af11a2](https://github.com/mirkolenz/makejinja/commit/0af11a24c24dc7200253f9864126bda14a9ebf29))
275 |
276 | ## [2.0.0-beta.4](https://github.com/mirkolenz/makejinja/compare/v2.0.0-beta.3...v2.0.0-beta.4) (2023-05-25)
277 |
278 |
279 | ### Bug Fixes
280 |
281 | * update nix flake ([05b9575](https://github.com/mirkolenz/makejinja/commit/05b95756e2682f4acad69f7ebbbcfd75eb945a02))
282 |
283 | ## [2.0.0-beta.3](https://github.com/mirkolenz/makejinja/compare/v2.0.0-beta.2...v2.0.0-beta.3) (2023-05-25)
284 |
285 |
286 | ### Bug Fixes
287 |
288 | * provide aarch64 docker image ([e983268](https://github.com/mirkolenz/makejinja/commit/e983268df920741921384d4ea4f75f92f79f524e))
289 |
290 | ## [2.0.0-beta.2](https://github.com/mirkolenz/makejinja/compare/v2.0.0-beta.1...v2.0.0-beta.2) (2023-05-24)
291 |
292 |
293 | ### Features
294 |
295 | * add version option ([0585114](https://github.com/mirkolenz/makejinja/commit/058511497517724d6e37bd8e4054a16641476366))
296 |
297 | ## [2.0.0-beta.1](https://github.com/mirkolenz/makejinja/compare/v1.1.5...v2.0.0-beta.1) (2023-05-18)
298 |
299 |
300 | ### ⚠ BREAKING CHANGES
301 |
302 | * In this version, `input` has been removed an replaced with `inputs` (allowing to use multiple input folders). We also included a new option `exclude_patterns` to ignore files that would be matched by `input_pattern`. The option `copy_tree` is superseded by the new `copy_metadata` which is compatible with multiple inputs and preserves attributes for rendered files as well. Please adjust your config accordingly, otherwise `makejinja` will break!
303 |
304 | ### Features
305 |
306 | * completely rewrite file handling ([#20](https://github.com/mirkolenz/makejinja/issues/20)) ([97d6a51](https://github.com/mirkolenz/makejinja/commit/97d6a51d268689bd50fa1b4a9a70c099db42bda4))
307 |
308 | ## [1.1.5](https://github.com/mirkolenz/makejinja/compare/v1.1.4...v1.1.5) (2023-05-17)
309 |
310 |
311 | ### Bug Fixes
312 |
313 | * trigger ci build ([acd7609](https://github.com/mirkolenz/makejinja/commit/acd7609c4cdfaf546e4d28eee14642a8e9f580e5))
314 | * try to fix docker image pushing ([6634507](https://github.com/mirkolenz/makejinja/commit/663450742507ba308082d4f7e17b4e71c0f4ee23))
315 | * update readme ([2445d25](https://github.com/mirkolenz/makejinja/commit/2445d254cc6cde9953ebe8055a1c97640f01527e))
316 | * use impure nix run for pushing docker ([634f699](https://github.com/mirkolenz/makejinja/commit/634f699d3b68060d1265c47e044f993977028257))
317 |
318 | ## [1.1.5-beta.4](https://github.com/mirkolenz/makejinja/compare/v1.1.5-beta.3...v1.1.5-beta.4) (2023-05-09)
319 |
320 |
321 | ### Bug Fixes
322 |
323 | * update readme ([2445d25](https://github.com/mirkolenz/makejinja/commit/2445d254cc6cde9953ebe8055a1c97640f01527e))
324 |
325 | ## [1.1.5-beta.3](https://github.com/mirkolenz/makejinja/compare/v1.1.5-beta.2...v1.1.5-beta.3) (2023-05-09)
326 |
327 |
328 | ### Bug Fixes
329 |
330 | * use impure nix run for pushing docker ([634f699](https://github.com/mirkolenz/makejinja/commit/634f699d3b68060d1265c47e044f993977028257))
331 |
332 | ## [1.1.5-beta.2](https://github.com/mirkolenz/makejinja/compare/v1.1.5-beta.1...v1.1.5-beta.2) (2023-05-09)
333 |
334 |
335 | ### Bug Fixes
336 |
337 | * try to fix docker image pushing ([6634507](https://github.com/mirkolenz/makejinja/commit/663450742507ba308082d4f7e17b4e71c0f4ee23))
338 |
339 | ## [1.1.5-beta.1](https://github.com/mirkolenz/makejinja/compare/v1.1.4...v1.1.5-beta.1) (2023-05-08)
340 |
341 |
342 | ### Bug Fixes
343 |
344 | * trigger ci build ([acd7609](https://github.com/mirkolenz/makejinja/commit/acd7609c4cdfaf546e4d28eee14642a8e9f580e5))
345 |
346 | ## [1.1.4](https://github.com/mirkolenz/makejinja/compare/v1.1.3...v1.1.4) (2023-04-30)
347 |
348 |
349 | ### Bug Fixes
350 |
351 | * trigger ci build ([f529ff0](https://github.com/mirkolenz/makejinja/commit/f529ff0f323941dc0bafb2366c768f1a316ae293))
352 |
353 | ## [1.1.3](https://github.com/mirkolenz/makejinja/compare/v1.1.2...v1.1.3) (2023-04-30)
354 |
355 |
356 | ### Bug Fixes
357 |
358 | * help message was missing from cli ([34b626e](https://github.com/mirkolenz/makejinja/commit/34b626e52ff32ee6ce2dbba8351877441d1c9903))
359 |
360 | ## [1.1.2](https://github.com/mirkolenz/makejinja/compare/v1.1.1...v1.1.2) (2023-02-14)
361 |
362 |
363 | ### Bug Fixes
364 |
365 | * **loader:** remove protocol to enable subclassing ([db55ae3](https://github.com/mirkolenz/makejinja/commit/db55ae36478ddd7899ad6fc0395f3f84e796e637))
366 |
367 | ## [1.1.1](https://github.com/mirkolenz/makejinja/compare/v1.1.0...v1.1.1) (2023-02-14)
368 |
369 |
370 | ### Bug Fixes
371 |
372 | * use protocol instead of abc for loader class ([d72bec1](https://github.com/mirkolenz/makejinja/commit/d72bec10bf555d9aca53e712195171253ee3f003))
373 |
374 | ## [1.1.0](https://github.com/mirkolenz/makejinja/compare/v1.0.1...v1.1.0) (2023-02-06)
375 |
376 |
377 | ### Features
378 |
379 | * enable programmatic usage of the library ([ddc744b](https://github.com/mirkolenz/makejinja/commit/ddc744bd4427c6d7480f6c45b10b6ab329e24b90))
380 |
381 |
382 | ### Bug Fixes
383 |
384 | * add all annotations to config/loader ([6070e5a](https://github.com/mirkolenz/makejinja/commit/6070e5aca09adc07998dfa7240544badfd116331))
385 | * add py.typed file ([3756882](https://github.com/mirkolenz/makejinja/commit/3756882401b6e2402715b5ddaf484a8b3a3c5ecc))
386 | * modularize app, improve loader construction ([a8da7fa](https://github.com/mirkolenz/makejinja/commit/a8da7fac03a08ba23ca9a7debc9c183fc7688ce6))
387 |
388 | ## [1.0.1](https://github.com/mirkolenz/makejinja/compare/v1.0.0...v1.0.1) (2023-02-03)
389 |
390 |
391 | ### Bug Fixes
392 |
393 | * **docker:** use entrypoint for proper cli usage ([fcebe4d](https://github.com/mirkolenz/makejinja/commit/fcebe4de622bbbc654ee2799a94affb515a4ab30))
394 |
395 | ## [1.0.0](https://github.com/mirkolenz/makejinja/compare/v0.7.5...v1.0.0) (2023-01-25)
396 |
397 |
398 | ### ⚠ BREAKING CHANGES
399 |
400 | * use jinja methods to import custom loaders
401 | * enhance support for custom loaders
402 | * rename input/output options
403 | * enhance custom code & remove cli options
404 | * switch from typer to click & typed-settings
405 | * Massive performance boost over python-simpleconf. The CLI options changed: env-vars are no longer supported and we only handle files ending in `yaml` or `yml`.
406 |
407 | ### Features
408 |
409 | * add checks to verify correct file handling ([5d5d5fd](https://github.com/mirkolenz/makejinja/commit/5d5d5fdd3473efebf41fbad83891786f9e902688))
410 | * add initial support to load custom code ([9404ecc](https://github.com/mirkolenz/makejinja/commit/9404eccca2db01858242d2f445b814311188ba07))
411 | * add options to change jinja delimiters ([edd1caa](https://github.com/mirkolenz/makejinja/commit/edd1caac1b1cd22d14d0bd59aa33061934b1a25b))
412 | * add python data loader ([2a0b817](https://github.com/mirkolenz/makejinja/commit/2a0b8170f68e8e6a3658ff3c1bd79e7eeab4841b))
413 | * collect modules in subfolders ([ebfa242](https://github.com/mirkolenz/makejinja/commit/ebfa24230ca8056ad2ed2194f69530c6ff93a80b))
414 | * enhance custom code & remove cli options ([a8b0b64](https://github.com/mirkolenz/makejinja/commit/a8b0b641304583377975d9960d0677596ad88709))
415 | * enhance support for custom loaders ([46c8eb1](https://github.com/mirkolenz/makejinja/commit/46c8eb1eda830f36f1d0d657adfe28046a0b82fe))
416 | * pass jinja options to env constructor ([f39fe32](https://github.com/mirkolenz/makejinja/commit/f39fe32c61ef100241b58b14e9d53ba11ab20356))
417 | * rename input/output options ([2592c19](https://github.com/mirkolenz/makejinja/commit/2592c196fce2fd872e76c86d902f3322d6c5d02c))
418 | * switch from typer to click & typed-settings ([3e9d09d](https://github.com/mirkolenz/makejinja/commit/3e9d09d53c1a68fb47a40c25b088809198f30e10))
419 | * switch to pure yaml config parsing ([ac22a0d](https://github.com/mirkolenz/makejinja/commit/ac22a0df5e1a6bd48bda457e797b271aa9b9aae5))
420 | * use jinja methods to import custom loaders ([901f37a](https://github.com/mirkolenz/makejinja/commit/901f37a35e9287fc1f0a98c9f3ccc23cafd3cbc5))
421 |
422 |
423 | ### Bug Fixes
424 |
425 | * add missing main package file ([b436dda](https://github.com/mirkolenz/makejinja/commit/b436dda408e04b510d3bd6185e29dd257029aa84))
426 | * improve cli output ([1280fa7](https://github.com/mirkolenz/makejinja/commit/1280fa71c83af483419c6e0c58f3e5c4757c5c3c))
427 | * improve options ([e81d727](https://github.com/mirkolenz/makejinja/commit/e81d727469d012579ec04fb1e61d28076ffe7a7e))
428 | * improve types ([475e2a5](https://github.com/mirkolenz/makejinja/commit/475e2a54220998c5b1022f1b89228d42b04ccc91))
429 | * make custom import paths more robust ([7424729](https://github.com/mirkolenz/makejinja/commit/7424729cdba1b168193fec72b9d0639c16962107))
430 | * properly set pythonpath for module resolution ([6beb0b0](https://github.com/mirkolenz/makejinja/commit/6beb0b0a8bd4a7649dffd5f734805ae951c58841))
431 | * remove wrong flag decls from click params ([5d98f08](https://github.com/mirkolenz/makejinja/commit/5d98f08752b264b94d9091755e3ad1ca515496c0))
432 | * update typed-settings and remove type casts ([e42309d](https://github.com/mirkolenz/makejinja/commit/e42309de1020c4cd0463ec4948933b83caad9438))
433 |
434 | ## [1.0.0-beta.12](https://github.com/mirkolenz/makejinja/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2023-01-15)
435 |
436 |
437 | ### Bug Fixes
438 |
439 | * make custom import paths more robust ([7424729](https://github.com/mirkolenz/makejinja/commit/7424729cdba1b168193fec72b9d0639c16962107))
440 |
441 | ## [1.0.0-beta.11](https://github.com/mirkolenz/makejinja/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2023-01-15)
442 |
443 |
444 | ### Bug Fixes
445 |
446 | * properly set pythonpath for module resolution ([6beb0b0](https://github.com/mirkolenz/makejinja/commit/6beb0b0a8bd4a7649dffd5f734805ae951c58841))
447 |
448 | ## [1.0.0-beta.10](https://github.com/mirkolenz/makejinja/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2023-01-15)
449 |
450 |
451 | ### ⚠ BREAKING CHANGES
452 |
453 | * use jinja methods to import custom loaders
454 |
455 | ### Features
456 |
457 | * use jinja methods to import custom loaders ([901f37a](https://github.com/mirkolenz/makejinja/commit/901f37a35e9287fc1f0a98c9f3ccc23cafd3cbc5))
458 |
459 |
460 | ### Bug Fixes
461 |
462 | * improve types ([475e2a5](https://github.com/mirkolenz/makejinja/commit/475e2a54220998c5b1022f1b89228d42b04ccc91))
463 |
464 | ## [1.0.0-beta.9](https://github.com/mirkolenz/makejinja/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2023-01-15)
465 |
466 |
467 | ### ⚠ BREAKING CHANGES
468 |
469 | * enhance support for custom loaders
470 |
471 | ### Features
472 |
473 | * enhance support for custom loaders ([46c8eb1](https://github.com/mirkolenz/makejinja/commit/46c8eb1eda830f36f1d0d657adfe28046a0b82fe))
474 |
475 | ## [1.0.0-beta.8](https://github.com/mirkolenz/makejinja/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2023-01-15)
476 |
477 |
478 | ### ⚠ BREAKING CHANGES
479 |
480 | * rename input/output options
481 |
482 | ### Features
483 |
484 | * collect modules in subfolders ([ebfa242](https://github.com/mirkolenz/makejinja/commit/ebfa24230ca8056ad2ed2194f69530c6ff93a80b))
485 | * pass jinja options to env constructor ([f39fe32](https://github.com/mirkolenz/makejinja/commit/f39fe32c61ef100241b58b14e9d53ba11ab20356))
486 | * rename input/output options ([2592c19](https://github.com/mirkolenz/makejinja/commit/2592c196fce2fd872e76c86d902f3322d6c5d02c))
487 |
488 |
489 | ### Bug Fixes
490 |
491 | * improve options ([e81d727](https://github.com/mirkolenz/makejinja/commit/e81d727469d012579ec04fb1e61d28076ffe7a7e))
492 |
493 | ## [1.0.0-beta.7](https://github.com/mirkolenz/makejinja/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-14)
494 |
495 |
496 | ### ⚠ BREAKING CHANGES
497 |
498 | * enhance custom code & remove cli options
499 |
500 | ### Features
501 |
502 | * add initial support to load custom code ([9404ecc](https://github.com/mirkolenz/makejinja/commit/9404eccca2db01858242d2f445b814311188ba07))
503 | * add python data loader ([2a0b817](https://github.com/mirkolenz/makejinja/commit/2a0b8170f68e8e6a3658ff3c1bd79e7eeab4841b))
504 | * enhance custom code & remove cli options ([a8b0b64](https://github.com/mirkolenz/makejinja/commit/a8b0b641304583377975d9960d0677596ad88709))
505 |
506 | ## [1.0.0-beta.6](https://github.com/mirkolenz/makejinja/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2023-01-14)
507 |
508 |
509 | ### Bug Fixes
510 |
511 | * add missing main package file ([b436dda](https://github.com/mirkolenz/makejinja/commit/b436dda408e04b510d3bd6185e29dd257029aa84))
512 | * update typed-settings and remove type casts ([e42309d](https://github.com/mirkolenz/makejinja/commit/e42309de1020c4cd0463ec4948933b83caad9438))
513 |
514 | ## [1.0.0-beta.5](https://github.com/mirkolenz/makejinja/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2023-01-05)
515 |
516 |
517 | ### Bug Fixes
518 |
519 | * remove wrong flag decls from click params ([5d98f08](https://github.com/mirkolenz/makejinja/commit/5d98f08752b264b94d9091755e3ad1ca515496c0))
520 |
521 | ## [1.0.0-beta.4](https://github.com/mirkolenz/makejinja/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2023-01-03)
522 |
523 |
524 | ### ⚠ BREAKING CHANGES
525 |
526 | * switch from typer to click & typed-settings
527 |
528 | ### Features
529 |
530 | * switch from typer to click & typed-settings ([3e9d09d](https://github.com/mirkolenz/makejinja/commit/3e9d09d53c1a68fb47a40c25b088809198f30e10))
531 |
532 | ## [1.0.0-beta.3](https://github.com/mirkolenz/makejinja/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2023-01-02)
533 |
534 |
535 | ### Bug Fixes
536 |
537 | * **deps:** update dependency rich to v13 ([#11](https://github.com/mirkolenz/makejinja/issues/11)) ([86b15d7](https://github.com/mirkolenz/makejinja/commit/86b15d7325c9cc4e50f69cad6c3fd5628a242817))
538 |
539 | ## [0.7.5](https://github.com/mirkolenz/makejinja/compare/v0.7.4...v0.7.5) (2022-12-30)
540 |
541 |
542 | ### Bug Fixes
543 |
544 | * **deps:** update dependency rich to v13 ([#11](https://github.com/mirkolenz/makejinja/issues/11)) ([86b15d7](https://github.com/mirkolenz/makejinja/commit/86b15d7325c9cc4e50f69cad6c3fd5628a242817))
545 |
546 | ## [1.0.0-beta.2](https://github.com/mirkolenz/makejinja/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-12-28)
547 |
548 |
549 | ### Features
550 |
551 | * add options to change jinja delimiters ([edd1caa](https://github.com/mirkolenz/makejinja/commit/edd1caac1b1cd22d14d0bd59aa33061934b1a25b))
552 |
553 | ## [1.0.0-beta.1](https://github.com/mirkolenz/makejinja/compare/v0.7.4...v1.0.0-beta.1) (2022-12-26)
554 |
555 |
556 | ### ⚠ BREAKING CHANGES
557 |
558 | * Massive performance boost over python-simpleconf. The CLI options changed: env-vars are no longer supported and we only handle files ending in `yaml` or `yml`.
559 |
560 | ### Features
561 |
562 | * add checks to verify correct file handling ([5d5d5fd](https://github.com/mirkolenz/makejinja/commit/5d5d5fdd3473efebf41fbad83891786f9e902688))
563 | * switch to pure yaml config parsing ([ac22a0d](https://github.com/mirkolenz/makejinja/commit/ac22a0df5e1a6bd48bda457e797b271aa9b9aae5))
564 |
565 |
566 | ### Bug Fixes
567 |
568 | * improve cli output ([1280fa7](https://github.com/mirkolenz/makejinja/commit/1280fa71c83af483419c6e0c58f3e5c4757c5c3c))
569 |
570 | ## [0.7.4](https://github.com/mirkolenz/makejinja/compare/v0.7.3...v0.7.4) (2022-12-18)
571 |
572 |
573 | ### Bug Fixes
574 |
575 | * bump version ([2a80893](https://github.com/mirkolenz/makejinja/commit/2a808933a75cfdb5af5e2e4b6c1b982304ce1a9d))
576 |
577 | ## [0.7.3](https://github.com/mirkolenz/makejinja/compare/v0.7.2...v0.7.3) (2022-12-18)
578 |
579 |
580 | ### Bug Fixes
581 |
582 | * bump version ([ab04f19](https://github.com/mirkolenz/makejinja/commit/ab04f19714dfaaa2a44f7f37cb726744b184dd7b))
583 |
584 | ## [0.7.2](https://github.com/mirkolenz/makejinja/compare/v0.7.1...v0.7.2) (2022-12-18)
585 |
586 |
587 | ### Bug Fixes
588 |
589 | * bump version ([0a6611a](https://github.com/mirkolenz/makejinja/commit/0a6611ab6699891acbacb2fbd8488aeec6cc3122))
590 |
591 | ## [0.7.1](https://github.com/mirkolenz/makejinja/compare/v0.7.0...v0.7.1) (2022-12-18)
592 |
593 |
594 | ### Bug Fixes
595 |
596 | * wrong loading of env vars data ([4bd764b](https://github.com/mirkolenz/makejinja/commit/4bd764b85b09985ce1990ac90147f084394d3a9f))
597 |
598 | ## [0.7.0](https://github.com/mirkolenz/makejinja/compare/v0.6.0...v0.7.0) (2022-12-17)
599 |
600 |
601 | ### Features
602 |
603 | * add documentation to cli ([b001d04](https://github.com/mirkolenz/makejinja/commit/b001d04c0a622b3012e7a6d587be171d22331d12))
604 |
605 |
606 | ### Bug Fixes
607 |
608 | * improve command output ([36df06f](https://github.com/mirkolenz/makejinja/commit/36df06fecc14b443a452e2f2e49107870fb517d9))
609 | * process env vars after files ([31cb946](https://github.com/mirkolenz/makejinja/commit/31cb946b5ad47beed2788e53d4b39c50fe7da256))
610 | * sort files in iterdir ([5be3db1](https://github.com/mirkolenz/makejinja/commit/5be3db18898fe868d45fea6cfdab6ba3fe6bbbf3))
611 |
612 | ## [0.6.0](https://github.com/mirkolenz/makejinja/compare/v0.5.1...v0.6.0) (2022-12-15)
613 |
614 |
615 | ### Features
616 |
617 | * update cli param names ([c819a51](https://github.com/mirkolenz/makejinja/commit/c819a51d309803fb8e6a56d9ba6d52334b79bda0))
618 |
619 |
620 | ### Documentation
621 |
622 | * update readme ([dd3eec7](https://github.com/mirkolenz/makejinja/commit/dd3eec77ffc96f1cc544013c4ada4e4663bbe7b7))
623 |
624 | ## [0.5.1](https://github.com/mirkolenz/makejinja/compare/v0.5.0...v0.5.1) (2022-12-14)
625 |
626 |
627 | ### Bug Fixes
628 |
629 | * enable file-based loading of globals & filters ([cf9f331](https://github.com/mirkolenz/makejinja/commit/cf9f331f81c13cc8d2834f5c748776d7d332fd4d))
630 |
631 | ## [0.5.0](https://github.com/mirkolenz/makejinja/compare/v0.4.1...v0.5.0) (2022-12-14)
632 |
633 |
634 | ### Features
635 |
636 | * allow customization of globals and filters ([d86bd5a](https://github.com/mirkolenz/makejinja/commit/d86bd5a195b0e8ace992f28e13bb0c13f4bcea42))
637 |
638 | ## [0.4.1](https://github.com/mirkolenz/makejinja/compare/v0.4.0...v0.4.1) (2022-12-14)
639 |
640 |
641 | ### Bug Fixes
642 |
643 | * handle empty templates with newlines ([97123b6](https://github.com/mirkolenz/makejinja/commit/97123b6f20ac608edd42962b4f031ef967c8e5df))
644 |
645 | ## [0.4.0](https://github.com/mirkolenz/makejinja/compare/v0.3.0...v0.4.0) (2022-12-14)
646 |
647 |
648 | ### Features
649 |
650 | * add skip-entry cli param ([7d79fa9](https://github.com/mirkolenz/makejinja/commit/7d79fa95c2411aced7d7085d5d385b8f594cbd55))
651 |
652 | ## [0.3.0](https://github.com/mirkolenz/makejinja/compare/v0.2.1...v0.3.0) (2022-12-14)
653 |
654 |
655 | ### Features
656 |
657 | * add global function to select a language ([b26836d](https://github.com/mirkolenz/makejinja/commit/b26836df42f87af42a5145cd2ddfd3e61f8e5dd9))
658 |
659 | ## [0.2.1](https://github.com/mirkolenz/makejinja/compare/v0.2.0...v0.2.1) (2022-12-11)
660 |
661 |
662 | ### Bug Fixes
663 |
664 | * improve compatibility with python 3.9 ([30919e8](https://github.com/mirkolenz/makejinja/commit/30919e83e11fbc368b8d97d498dab7ae2e766671))
665 |
666 | ## v0.2.0 (2022-12-11)
667 |
668 | ### Feature
669 |
670 | - Add option to remove jinja suffix after rendering ([`d1ec7d6`](https://github.com/mirkolenz/makejinja/commit/d1ec7d6079ec3cf2e124a708dfe5688284add192))
671 |
672 | ### Documentation
673 |
674 | - Fix changelog ([`cfab1b4`](https://github.com/mirkolenz/makejinja/commit/cfab1b436036caae98a11798b98adc857f8fa189))
675 |
676 | ## [v0.1.1](https://github.com/mirkolenz/makejinja/compare/0.1.0...0.1.1) (2022-12-10)
677 |
678 | ### Bug Fixes
679 |
680 | - change script name to makejinja ([df14627](https://github.com/mirkolenz/makejinja/commit/df14627056c40e62adc489ac4c766b796e59f34f))
681 |
682 | ## v0.1.0 (2022-12-10)
683 |
684 | - Initial release
685 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | revision = 3
3 | requires-python = ">=3.12, <4"
4 |
5 | [[package]]
6 | name = "attrs"
7 | version = "25.4.0"
8 | source = { registry = "https://pypi.org/simple" }
9 | sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
10 | wheels = [
11 | { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
12 | ]
13 |
14 | [[package]]
15 | name = "cattrs"
16 | version = "25.3.0"
17 | source = { registry = "https://pypi.org/simple" }
18 | dependencies = [
19 | { name = "attrs" },
20 | { name = "typing-extensions" },
21 | ]
22 | sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" }
23 | wheels = [
24 | { url = "https://files.pythonhosted.org/packages/d8/2b/a40e1488fdfa02d3f9a653a61a5935ea08b3c2225ee818db6a76c7ba9695/cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff", size = 70738, upload-time = "2025-10-07T12:26:06.603Z" },
25 | ]
26 |
27 | [[package]]
28 | name = "click"
29 | version = "8.3.1"
30 | source = { registry = "https://pypi.org/simple" }
31 | dependencies = [
32 | { name = "colorama", marker = "sys_platform == 'win32'" },
33 | ]
34 | sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
35 | wheels = [
36 | { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
37 | ]
38 |
39 | [[package]]
40 | name = "colorama"
41 | version = "0.4.6"
42 | source = { registry = "https://pypi.org/simple" }
43 | 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" }
44 | wheels = [
45 | { 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" },
46 | ]
47 |
48 | [[package]]
49 | name = "coverage"
50 | version = "7.13.0"
51 | source = { registry = "https://pypi.org/simple" }
52 | sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" }
53 | wheels = [
54 | { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" },
55 | { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" },
56 | { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" },
57 | { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" },
58 | { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" },
59 | { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" },
60 | { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" },
61 | { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" },
62 | { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" },
63 | { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" },
64 | { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" },
65 | { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" },
66 | { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" },
67 | { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" },
68 | { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" },
69 | { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" },
70 | { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" },
71 | { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" },
72 | { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" },
73 | { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" },
74 | { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" },
75 | { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" },
76 | { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" },
77 | { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" },
78 | { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" },
79 | { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" },
80 | { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" },
81 | { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" },
82 | { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" },
83 | { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" },
84 | { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" },
85 | { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" },
86 | { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" },
87 | { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" },
88 | { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" },
89 | { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" },
90 | { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" },
91 | { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" },
92 | { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" },
93 | { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" },
94 | { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" },
95 | { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" },
96 | { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" },
97 | { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" },
98 | { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" },
99 | { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" },
100 | { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" },
101 | { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" },
102 | { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" },
103 | { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" },
104 | { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" },
105 | { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" },
106 | { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" },
107 | { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" },
108 | { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" },
109 | { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" },
110 | { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" },
111 | { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" },
112 | { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" },
113 | { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" },
114 | { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" },
115 | { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" },
116 | { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" },
117 | { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" },
118 | { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" },
119 | { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" },
120 | ]
121 |
122 | [[package]]
123 | name = "frozendict"
124 | version = "2.4.7"
125 | source = { registry = "https://pypi.org/simple" }
126 | sdist = { url = "https://files.pythonhosted.org/packages/90/b2/2a3d1374b7780999d3184e171e25439a8358c47b481f68be883c14086b4c/frozendict-2.4.7.tar.gz", hash = "sha256:e478fb2a1391a56c8a6e10cc97c4a9002b410ecd1ac28c18d780661762e271bd", size = 317082, upload-time = "2025-11-11T22:40:14.251Z" }
127 | wheels = [
128 | { url = "https://files.pythonhosted.org/packages/38/74/f94141b38a51a553efef7f510fc213894161ae49b88bffd037f8d2a7cb2f/frozendict-2.4.7-py3-none-any.whl", hash = "sha256:972af65924ea25cf5b4d9326d549e69a9a4918d8a76a9d3a7cd174d98b237550", size = 16264, upload-time = "2025-11-11T22:40:12.836Z" },
129 | ]
130 |
131 | [[package]]
132 | name = "iniconfig"
133 | version = "2.3.0"
134 | source = { registry = "https://pypi.org/simple" }
135 | sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
136 | wheels = [
137 | { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
138 | ]
139 |
140 | [[package]]
141 | name = "jinja2"
142 | version = "3.1.6"
143 | source = { registry = "https://pypi.org/simple" }
144 | dependencies = [
145 | { name = "markupsafe" },
146 | ]
147 | 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" }
148 | wheels = [
149 | { 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" },
150 | ]
151 |
152 | [[package]]
153 | name = "makejinja"
154 | version = "2.8.2"
155 | source = { editable = "." }
156 | dependencies = [
157 | { name = "frozendict" },
158 | { name = "jinja2" },
159 | { name = "pyyaml" },
160 | { name = "rich-click" },
161 | { name = "typed-settings", extra = ["attrs", "cattrs", "click"] },
162 | ]
163 |
164 | [package.dev-dependencies]
165 | docs = [
166 | { name = "pdoc" },
167 | ]
168 | test = [
169 | { name = "pytest" },
170 | { name = "pytest-cov" },
171 | ]
172 |
173 | [package.metadata]
174 | requires-dist = [
175 | { name = "frozendict", specifier = ">=2,<3" },
176 | { name = "jinja2", specifier = ">=3,<4" },
177 | { name = "pyyaml", specifier = ">=6,<7" },
178 | { name = "rich-click", specifier = ">=1,<2" },
179 | { name = "typed-settings", extras = ["attrs", "cattrs", "click"], specifier = ">=23,<26" },
180 | ]
181 |
182 | [package.metadata.requires-dev]
183 | docs = [{ name = "pdoc", specifier = ">=16,<17" }]
184 | test = [
185 | { name = "pytest", specifier = ">=9,<10" },
186 | { name = "pytest-cov", specifier = ">=7,<8" },
187 | ]
188 |
189 | [[package]]
190 | name = "markdown-it-py"
191 | version = "4.0.0"
192 | source = { registry = "https://pypi.org/simple" }
193 | dependencies = [
194 | { name = "mdurl" },
195 | ]
196 | sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
197 | wheels = [
198 | { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
199 | ]
200 |
201 | [[package]]
202 | name = "markdown2"
203 | version = "2.5.4"
204 | source = { registry = "https://pypi.org/simple" }
205 | sdist = { url = "https://files.pythonhosted.org/packages/42/f8/b2ae8bf5f28f9b510ae097415e6e4cb63226bb28d7ee01aec03a755ba03b/markdown2-2.5.4.tar.gz", hash = "sha256:a09873f0b3c23dbfae589b0080587df52ad75bb09a5fa6559147554736676889", size = 145652, upload-time = "2025-07-27T16:16:24.307Z" }
206 | wheels = [
207 | { url = "https://files.pythonhosted.org/packages/b8/06/2697b5043c3ecb720ce0d243fc7cf5024c0b5b1e450506e9b21939019963/markdown2-2.5.4-py3-none-any.whl", hash = "sha256:3c4b2934e677be7fec0e6f2de4410e116681f4ad50ec8e5ba7557be506d3f439", size = 49954, upload-time = "2025-07-27T16:16:23.026Z" },
208 | ]
209 |
210 | [[package]]
211 | name = "markupsafe"
212 | version = "3.0.3"
213 | source = { registry = "https://pypi.org/simple" }
214 | sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
215 | wheels = [
216 | { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
217 | { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
218 | { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
219 | { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
220 | { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
221 | { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
222 | { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
223 | { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
224 | { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
225 | { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
226 | { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
227 | { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
228 | { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
229 | { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
230 | { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
231 | { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
232 | { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
233 | { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
234 | { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
235 | { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
236 | { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
237 | { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
238 | { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
239 | { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
240 | { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
241 | { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
242 | { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
243 | { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
244 | { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
245 | { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
246 | { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
247 | { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
248 | { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
249 | { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
250 | { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
251 | { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
252 | { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
253 | { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
254 | { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
255 | { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
256 | { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
257 | { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
258 | { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
259 | { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
260 | { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
261 | { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
262 | { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
263 | { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
264 | { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
265 | { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
266 | { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
267 | { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
268 | { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
269 | { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
270 | { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
271 | ]
272 |
273 | [[package]]
274 | name = "mdurl"
275 | version = "0.1.2"
276 | source = { registry = "https://pypi.org/simple" }
277 | 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" }
278 | wheels = [
279 | { 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" },
280 | ]
281 |
282 | [[package]]
283 | name = "packaging"
284 | version = "25.0"
285 | source = { registry = "https://pypi.org/simple" }
286 | 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" }
287 | wheels = [
288 | { 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" },
289 | ]
290 |
291 | [[package]]
292 | name = "pdoc"
293 | version = "16.0.0"
294 | source = { registry = "https://pypi.org/simple" }
295 | dependencies = [
296 | { name = "jinja2" },
297 | { name = "markdown2" },
298 | { name = "markupsafe" },
299 | { name = "pygments" },
300 | ]
301 | sdist = { url = "https://files.pythonhosted.org/packages/ac/fe/ab3f34a5fb08c6b698439a2c2643caf8fef0d61a86dd3fdcd5501c670ab8/pdoc-16.0.0.tar.gz", hash = "sha256:fdadc40cc717ec53919e3cd720390d4e3bcd40405cb51c4918c119447f913514", size = 111890, upload-time = "2025-10-27T16:02:16.345Z" }
302 | wheels = [
303 | { url = "https://files.pythonhosted.org/packages/16/a1/56a17b7f9e18c2bb8df73f3833345d97083b344708b97bab148fdd7e0b82/pdoc-16.0.0-py3-none-any.whl", hash = "sha256:070b51de2743b9b1a4e0ab193a06c9e6c12cf4151cf9137656eebb16e8556628", size = 100014, upload-time = "2025-10-27T16:02:15.007Z" },
304 | ]
305 |
306 | [[package]]
307 | name = "pluggy"
308 | version = "1.6.0"
309 | source = { registry = "https://pypi.org/simple" }
310 | 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" }
311 | wheels = [
312 | { 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" },
313 | ]
314 |
315 | [[package]]
316 | name = "pygments"
317 | version = "2.19.2"
318 | source = { registry = "https://pypi.org/simple" }
319 | 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" }
320 | wheels = [
321 | { 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" },
322 | ]
323 |
324 | [[package]]
325 | name = "pytest"
326 | version = "9.0.2"
327 | source = { registry = "https://pypi.org/simple" }
328 | dependencies = [
329 | { name = "colorama", marker = "sys_platform == 'win32'" },
330 | { name = "iniconfig" },
331 | { name = "packaging" },
332 | { name = "pluggy" },
333 | { name = "pygments" },
334 | ]
335 | sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
336 | wheels = [
337 | { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
338 | ]
339 |
340 | [[package]]
341 | name = "pytest-cov"
342 | version = "7.0.0"
343 | source = { registry = "https://pypi.org/simple" }
344 | dependencies = [
345 | { name = "coverage" },
346 | { name = "pluggy" },
347 | { name = "pytest" },
348 | ]
349 | sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
350 | wheels = [
351 | { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
352 | ]
353 |
354 | [[package]]
355 | name = "pyyaml"
356 | version = "6.0.3"
357 | source = { registry = "https://pypi.org/simple" }
358 | 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" }
359 | wheels = [
360 | { 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" },
361 | { 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" },
362 | { 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" },
363 | { 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" },
364 | { 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" },
365 | { 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" },
366 | { 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" },
367 | { 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" },
368 | { 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" },
369 | { 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" },
370 | { 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" },
371 | { 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" },
372 | { 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" },
373 | { 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" },
374 | { 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" },
375 | { 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" },
376 | { 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" },
377 | { 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" },
378 | { 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" },
379 | { 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" },
380 | { 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" },
381 | { 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" },
382 | { 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" },
383 | { 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" },
384 | { 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" },
385 | { 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" },
386 | { 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" },
387 | { 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" },
388 | { 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" },
389 | { 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" },
390 | { 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" },
391 | { 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" },
392 | { 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" },
393 | { 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" },
394 | { 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" },
395 | { 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" },
396 | { 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" },
397 | { 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" },
398 | ]
399 |
400 | [[package]]
401 | name = "rich"
402 | version = "14.2.0"
403 | source = { registry = "https://pypi.org/simple" }
404 | dependencies = [
405 | { name = "markdown-it-py" },
406 | { name = "pygments" },
407 | ]
408 | sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
409 | wheels = [
410 | { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
411 | ]
412 |
413 | [[package]]
414 | name = "rich-click"
415 | version = "1.9.5"
416 | source = { registry = "https://pypi.org/simple" }
417 | dependencies = [
418 | { name = "click" },
419 | { name = "colorama", marker = "sys_platform == 'win32'" },
420 | { name = "rich" },
421 | ]
422 | sdist = { url = "https://files.pythonhosted.org/packages/6b/d1/b60ca6a8745e76800b50c7ee246fd73f08a3be5d8e0b551fc93c19fa1203/rich_click-1.9.5.tar.gz", hash = "sha256:48120531493f1533828da80e13e839d471979ec8d7d0ca7b35f86a1379cc74b6", size = 73927, upload-time = "2025-12-21T14:49:44.167Z" }
423 | wheels = [
424 | { url = "https://files.pythonhosted.org/packages/25/0a/d865895e1e5d88a60baee0fc3703eb111c502ee10c8c107516bc7623abf8/rich_click-1.9.5-py3-none-any.whl", hash = "sha256:9b195721a773b1acf0e16ff9ec68cef1e7d237e53471e6e3f7ade462f86c403a", size = 70580, upload-time = "2025-12-21T14:49:42.905Z" },
425 | ]
426 |
427 | [[package]]
428 | name = "typed-settings"
429 | version = "25.3.0"
430 | source = { registry = "https://pypi.org/simple" }
431 | sdist = { url = "https://files.pythonhosted.org/packages/30/19/a8155d9f411ed7cb4a5f510a6c37ac09ea6065542c597c23d0a88ec2fbed/typed_settings-25.3.0.tar.gz", hash = "sha256:865eb52c3184f467705645a1e58db9d719e08b9d7957448a2ad8cbbc22ed21a6", size = 3492969, upload-time = "2025-11-29T22:10:25.768Z" }
432 | wheels = [
433 | { url = "https://files.pythonhosted.org/packages/8b/9e/d01ef2bed8e995bbdeeb5307a571729a10545de4a6f8f06d9175fc7a1978/typed_settings-25.3.0-py3-none-any.whl", hash = "sha256:8fe578c84ae2e44f6e8bdde256d2449fbff45f47dca3c4092aeee03900efc12c", size = 63962, upload-time = "2025-11-29T22:10:23.77Z" },
434 | ]
435 |
436 | [package.optional-dependencies]
437 | attrs = [
438 | { name = "attrs" },
439 | ]
440 | cattrs = [
441 | { name = "cattrs" },
442 | ]
443 | click = [
444 | { name = "click" },
445 | ]
446 |
447 | [[package]]
448 | name = "typing-extensions"
449 | version = "4.15.0"
450 | source = { registry = "https://pypi.org/simple" }
451 | 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" }
452 | wheels = [
453 | { 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" },
454 | ]
455 |
--------------------------------------------------------------------------------