├── 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 | ![makejinja demonstration](../../assets/demo.gif) 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 | makejinja logo 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 | makejinja demonstration 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 | --------------------------------------------------------------------------------