├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .python-version ├── LICENSE ├── README.md ├── pyproject.toml ├── src └── block_fragments │ ├── __init__.py │ ├── apps.py │ ├── exceptions.py │ ├── loader.py │ └── template.py ├── tests ├── __init__.py ├── settings.py ├── templates │ ├── include.html │ ├── test1.html │ ├── test2.html │ ├── test3.html │ ├── test4.html │ ├── test5.html │ ├── test6.html │ ├── test7.html │ ├── test8.html │ ├── test9.html │ ├── test_base.html │ ├── test_exception.html │ ├── test_request_context.html │ └── test_sub.html ├── templatetags │ └── test_tags.py └── tests.py └── uv.lock /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run Continuous Integration 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | ci: 9 | runs-on: "ubuntu-latest" 10 | strategy: 11 | matrix: 12 | python-version: ["3.10", "3.11", "3.12", "3.13"] 13 | django-version: ["4.2", "5.0", "5.1"] 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | - name: Install uv 18 | uses: astral-sh/setup-uv@v5 19 | - name: Setup Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | uv sync 26 | uv pip install django==${{ matrix.django-version }} 27 | - name: Run linting 28 | run: uv run ruff check 29 | - name: Run tests 30 | run: uv run pytest 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish to PyPI 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build-and-publish: 7 | runs-on: ubuntu-latest 8 | environment: 9 | name: pypi 10 | url: https://pypi.org/p/django-block-fragments 11 | permissions: 12 | id-token: write 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | - name: Install uv 17 | uses: astral-sh/setup-uv@v5 18 | - name: Setup Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version-file: "pyproject.toml" 22 | - name: Build wheel and sdist 23 | run: uv build 24 | - name: Publish package distributions to PyPI 25 | uses: pypa/gh-action-pypi-publish@release/v1 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Unit test / coverage reports 10 | .coverage 11 | /htmlcov 12 | .tox 13 | 14 | # Editor files 15 | .idea 16 | 17 | # Virtual environments 18 | .venv 19 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 Kai Schlamp 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | --- 24 | 25 | The MIT License (MIT) 26 | 27 | Copyright (c) 2023 Carlton Gibson 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy 30 | of this software and associated documentation files (the "Software"), to deal 31 | in the Software without restriction, including without limitation the rights 32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 | copies of the Software, and to permit persons to whom the Software is 34 | furnished to do so, subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in 37 | all copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 45 | THE SOFTWARE. 46 | 47 | --- 48 | 49 | ISC License (ISC) 50 | 51 | Copyright (c) 2016, Patrick Cloke 52 | 53 | Permission to use, copy, modify, and/or distribute this software for any 54 | purpose with or without fee is hereby granted, provided that the above 55 | copyright notice and this permission notice appear in all copies. 56 | 57 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 58 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 59 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 60 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 61 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 62 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 63 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-block-fragments 2 | 3 | [![pypi](https://img.shields.io/pypi/v/django-block-fragments.svg)](https://pypi.org/project/django-block-fragments/) 4 | 5 | Render only the content of a specific `block` of a Django template. This also works for arbitrary template inheritance or when the block is in an included template. 6 | 7 | Rendering only a part of a template is especially useful when using Django together with libraries like HTMX, see [Template Fragments](https://htmx.org/essays/template-fragments/). 8 | 9 | ## Installation 10 | 11 | Install with pip: 12 | 13 | ```bash 14 | pip install django-block-fragments 15 | ``` 16 | 17 | Or with uv: 18 | 19 | ```bash 20 | uv add django-block-fragments 21 | ``` 22 | 23 | Then add `block_fragments` to `INSTALLED_APPS`: 24 | 25 | ```python 26 | INSTALLED_APPS = [ 27 | ..., 28 | "block_fragments", 29 | ..., 30 | ] 31 | ``` 32 | 33 | > [!NOTE] 34 | > `django-block-fragments` currently only supports the Django template backend. 35 | 36 | See __Advanced configuration (below)__ for more options. 37 | 38 | ## Usage 39 | 40 | Once installed and having a template like this: 41 | 42 | ```html 43 | ... 44 | {% block content %} 45 | Some content 46 | {% endblock content %} 47 | ... 48 | ``` 49 | 50 | You can render just the "content" block in a view with: 51 | 52 | ```python 53 | from django.shortcuts import render 54 | 55 | def my_view(request): 56 | return render(request, "template.html#content", {}) 57 | ``` 58 | 59 | You can also include just the "content" block in another template: 60 | 61 | ```html 62 | {% include "template.html#content" %} 63 | ``` 64 | 65 | ## Advanced configuration 66 | 67 | By default, adding `"block_fragments"` to your `INSTALLED_APPS` will try to configure any Django template backend to use the block fragments template loader. 68 | 69 | If you need to control this behavior, you can use the alternative `SimpleAppConfig`, which __will not__ adjust your `TEMPLATES` setting: 70 | 71 | ```python 72 | INSTALLED_APPS = [ 73 | "block_fragments.apps.SimpleAppConfig", 74 | ..., 75 | ] 76 | ``` 77 | 78 | If you use `SimpleAppConfig`, you will need to configure the template loader yourself. 79 | 80 | A `wrap_loaders()` function is available, and can be used to configure any specific template engine instance with the block fragments loader. 81 | 82 | You can use the backend's [`NAME`](https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-TEMPLATES-NAME) to `wrap_loaders()` to add the block fragments loader just for that backend: 83 | 84 | ```python 85 | from block_fragments.apps import wrap_loaders 86 | 87 | TEMPLATES = [ 88 | ..., 89 | { 90 | "BACKEND": "django.template.backends.django.DjangoTemplates", 91 | "NAME": "myname", 92 | "OPTIONS": { 93 | ..., 94 | }, 95 | }, 96 | ..., 97 | ] 98 | 99 | wrap_loaders("myname") 100 | ``` 101 | 102 | If the `NAME` isn't provided, the penultimate element of the `BACKEND` value is used - for example, `"django.template.backends.django.DjangoTemplates"` would be equivalent to a `NAME` of `"django"`. 103 | 104 | Under the hood, `wrap_loaders()` is equivalent to explicitly defining the `loaders` by-hand. Assuming defaults… 105 | 106 | ```python 107 | from django.conf import settings 108 | 109 | default_loaders = [ 110 | "django.template.loaders.filesystem.Loader", 111 | "django.template.loaders.app_directories.Loader", 112 | ] 113 | cached_loaders = [("django.template.loaders.cached.Loader", default_loaders)] 114 | block_fragment_loaders = [("block_fragments.loader.Loader", cached_loaders)] 115 | 116 | settings.TEMPLATES[...]['OPTIONS']['loaders'] = block_fragment_loaders 117 | ``` 118 | 119 | … where `TEMPLATES[...]` is the entry in `TEMPLATES` with the `NAME` matching 120 | that passed to `wrap_loaders()`. 121 | 122 | ## Development 123 | 124 | Fork, then clone the repo: 125 | 126 | ```sh 127 | git clone git@github.com:your-username/django-block-fragments.git 128 | ``` 129 | 130 | Install dependencies (needs [uv](https://docs.astral.sh/uv/) to be installed): 131 | 132 | ```sh 133 | uv sync 134 | ``` 135 | 136 | Then you can run the tests by using `pytest`: 137 | 138 | ```sh 139 | uv run pytest 140 | ``` 141 | 142 | Or with coverage: 143 | 144 | ```sh 145 | uv run pytest --cov 146 | ``` 147 | 148 | ## Acknowledgements 149 | 150 | This project is heavily inspired and uses code from [django-template-partials](https://github.com/carltongibson/django-template-partials) by Carlton Gibson and [django-render-block](https://github.com/clokep/django-render-block) by Patrick Cloke. So a big thank you to them! 151 | 152 | ## FAQ 153 | 154 | __Why django-block-fragments when django-template-partials and django-render-block already exist?__ 155 | 156 | I was looking for a way to reuse the already existing `block` tags of the Django Template Language (like `django-render-block` does) but also wanted to have the convenience of using template loaders (like `django-template-partials` does). So `django-block-fragments` tries to combine features of both of these great projects. 157 | 158 | __How to use `django-block-fragments` with `django-cotton`?__ 159 | 160 | When using `django-block-fragments` together with `django-cotton` the automatic loader configuration won't work (as both would overwrite each other). So you must use the `SimpleAppConfig` and configure the template loaders manually like in the example below. 161 | 162 | ```python 163 | INSTALLED_APPS = [ 164 | "django_cotton.apps.SimpleAppConfig", 165 | "block_fragments.apps.SimpleAppConfig", 166 | ] 167 | 168 | TEMPLATES = [ 169 | { 170 | "BACKEND": "django.template.backends.django.DjangoTemplates", 171 | "DIRS": [BASE_DIR / "templates"], 172 | "OPTIONS": { 173 | "loaders": [ 174 | ( 175 | "block_fragments.loader.Loader", 176 | [ 177 | ( 178 | "django.template.loaders.cached.Loader", 179 | [ 180 | "django_cotton.cotton_loader.Loader", 181 | "django.template.loaders.filesystem.Loader", 182 | "django.template.loaders.app_directories.Loader", 183 | ], 184 | ) 185 | ], 186 | ) 187 | ], 188 | "context_processors": [ 189 | # no changes 190 | ], 191 | "builtins": [ 192 | "django_cotton.templatetags.cotton", 193 | ], 194 | }, 195 | }, 196 | ] 197 | ``` 198 | 199 | > [!NOTE] 200 | > Because we're specifying the loaders manually, Django's APP_DIRS setting no longer has any effect. If you still want to load templates from the apps automatically, make sure to add the `django.template.loaders.app_directories.Loader` as in the example above. 201 | 202 | ## License 203 | 204 | MIT License 205 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "django-block-fragments" 3 | authors = [{ name = "Kai Schlamp", email = "kai.schlamp@gmail.com" }] 4 | description = "Reusable block fragments for the Django Template Language." 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = ["django>=5.0.0"] 8 | dynamic = ["version"] 9 | classifiers = [ 10 | "Development Status :: 3 - Alpha", 11 | "Intended Audience :: Developers", 12 | "License :: OSI Approved :: MIT License", 13 | "Programming Language :: Python", 14 | "Programming Language :: Python :: 3", 15 | "Programming Language :: Python :: 3.10", 16 | "Programming Language :: Python :: 3.11", 17 | "Programming Language :: Python :: 3.12", 18 | "Programming Language :: Python :: 3.13", 19 | "Framework :: Django", 20 | "Framework :: Django :: 4.2", 21 | "Framework :: Django :: 5.0", 22 | "Framework :: Django :: 5.1", 23 | ] 24 | 25 | [project.urls] 26 | source = "https://github.com/medihack/django-block-fragments" 27 | issues = "https://github.com/medihack/django-block-fragments/issues" 28 | documentation = "https://github.com/medihack/django-block-fragments/blob/main/README.md" 29 | 30 | [dependency-groups] 31 | dev = [ 32 | "pytest>=8.3.5", 33 | "pytest-cov>=6.0.0", 34 | "pytest-django>=4.10.0", 35 | "ruff>=0.9.10", 36 | ] 37 | 38 | [tool.ruff] 39 | target-version = "py310" 40 | line-length = 100 41 | lint.select = ["E", "F", "I", "W"] 42 | 43 | [tool.pytest.ini_options] 44 | DJANGO_SETTINGS_MODULE = "tests.settings" 45 | pythonpath = ["."] 46 | testpaths = ["tests"] 47 | python_files = ["tests.py"] 48 | 49 | [tool.coverage.run] 50 | branch = true 51 | source = ["src/block_fragments"] 52 | 53 | [tool.coverage.report] 54 | skip_empty = true 55 | show_missing = true 56 | 57 | [build-system] 58 | requires = ["hatchling", "uv-dynamic-versioning"] 59 | build-backend = "hatchling.build" 60 | 61 | [tool.hatch.build.targets.wheel] 62 | packages = ["src/block_fragments"] 63 | 64 | [tool.hatch.version] 65 | source = "uv-dynamic-versioning" 66 | 67 | [tool.uv-dynamic-versioning] 68 | pattern = "default-unprefixed" 69 | -------------------------------------------------------------------------------- /src/block_fragments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medihack/django-block-fragments/b119ad66dbb6b42e21000d837e28375ce9955494/src/block_fragments/__init__.py -------------------------------------------------------------------------------- /src/block_fragments/apps.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-block-fragments 3 | 4 | App configuration to set up a block fragments loader automatically. 5 | """ 6 | 7 | from contextlib import suppress 8 | 9 | import django.contrib.admin 10 | import django.template 11 | from django.apps import AppConfig 12 | from django.conf import settings 13 | 14 | 15 | def wrap_loaders(name): 16 | for template_config in settings.TEMPLATES: 17 | engine_name = template_config.get("NAME") 18 | if not engine_name: 19 | engine_name = template_config["BACKEND"].split(".")[-2] 20 | if engine_name == name: 21 | loaders = template_config.setdefault("OPTIONS", {}).get("loaders", []) 22 | already_configured = ( 23 | loaders 24 | and isinstance(loaders, (list, tuple)) 25 | and isinstance(loaders[0], tuple) 26 | and loaders[0][0] == "block_fragments.loader.Loader" 27 | ) 28 | if not already_configured: 29 | template_config.pop("APP_DIRS", None) 30 | default_loaders = [ 31 | "django.template.loaders.filesystem.Loader", 32 | "django.template.loaders.app_directories.Loader", 33 | ] 34 | cached_loaders = [("django.template.loaders.cached.Loader", default_loaders)] 35 | fragment_loaders = [("block_fragments.loader.Loader", cached_loaders)] 36 | template_config["OPTIONS"]["loaders"] = fragment_loaders 37 | break 38 | 39 | # Force re-evaluation of settings.TEMPLATES because EngineHandler caches it. 40 | with suppress(AttributeError): 41 | del django.template.engines.templates 42 | django.template.engines._engines = {} 43 | 44 | 45 | class LoaderAppConfig(AppConfig): 46 | """ 47 | This, the default configuration, does the automatic setup of a partials loader. 48 | """ 49 | 50 | name = "block_fragments" 51 | default = True 52 | 53 | def ready(self): 54 | wrap_loaders("django") 55 | 56 | 57 | class SimpleAppConfig(AppConfig): 58 | """ 59 | This, the non-default configuration, allows the user to opt-out of the automatic configuration. 60 | They just need to add "block_fragments.apps.SimpleAppConfig" to INSTALLED_APPS instead of 61 | "block_fragments". 62 | """ 63 | 64 | name = "block_fragments" 65 | -------------------------------------------------------------------------------- /src/block_fragments/exceptions.py: -------------------------------------------------------------------------------- 1 | from django.template.base import TemplateSyntaxError 2 | 3 | 4 | class BlockNotFound(TemplateSyntaxError): 5 | """The expected block was not found.""" 6 | -------------------------------------------------------------------------------- /src/block_fragments/loader.py: -------------------------------------------------------------------------------- 1 | from django.template import Template 2 | from django.template.loaders import cached 3 | from django.template.loaders.base import Loader as BaseLoader 4 | 5 | from .template import TemplateProxy 6 | 7 | 8 | class Loader(BaseLoader): 9 | """ 10 | A template loader that tries to resolve block fragments. If no block fragment 11 | is requested, it falls back to the default template loader. 12 | """ 13 | 14 | def __init__(self, engine, loaders): 15 | self.loaders = engine.get_template_loaders(loaders) 16 | super().__init__(engine) 17 | 18 | def get_dirs(self): 19 | for loader in self.loaders: 20 | if hasattr(loader, "get_dirs"): 21 | yield from loader.get_dirs() 22 | 23 | def get_contents(self, origin): 24 | return origin.loader.get_contents(origin) 25 | 26 | def get_template(self, template_name, skip=None) -> Template: 27 | """ 28 | Steps: 29 | - Split the template_name into template_name, block_name. 30 | - Use self.loaders to find the template. Raise if not found. 31 | - If block_name is not None then check for defined block. Raise if not found. 32 | """ 33 | template_base_name, _, block_name = template_name.partition("#") 34 | 35 | if len(self.loaders) == 1 and isinstance(self.loaders[0], cached.Loader): 36 | template = self.loaders[0].get_template(template_base_name, skip) 37 | else: 38 | template = super().get_template(template_base_name, skip) 39 | 40 | if not block_name: 41 | return template 42 | 43 | return TemplateProxy(template, block_name) 44 | 45 | def get_template_sources(self, template_name): 46 | for loader in self.loaders: 47 | yield from loader.get_template_sources(template_name) 48 | 49 | def reset(self): 50 | for loader in self.loaders: 51 | try: 52 | loader.reset() 53 | except AttributeError: 54 | pass 55 | -------------------------------------------------------------------------------- /src/block_fragments/template.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | 3 | from django.template import Context, Template 4 | from django.template.context import RenderContext 5 | from django.template.loader_tags import BLOCK_CONTEXT_KEY, BlockContext, BlockNode, ExtendsNode 6 | 7 | from block_fragments.exceptions import BlockNotFound 8 | 9 | 10 | class TemplateProxy(Template): 11 | """ 12 | A template wrapper that renders a specific block from a Django template. 13 | """ 14 | 15 | def __init__(self, template, block_name): 16 | self.template = template 17 | self.block_name = block_name 18 | 19 | self.name = template.name 20 | self.origin = template.origin 21 | self.engine = template.engine 22 | self.source = template.source 23 | 24 | def render(self, context): 25 | "Display stage -- can be called many times" 26 | 27 | # Make a copy of the context and reset the rendering state. 28 | # Trying to re-use a RenderContext in multiple renders can 29 | # lead to TemplateNotFound errors, as Django will skip past 30 | # any template files it thinks it has already rendered in a 31 | # template's inheritance stack. 32 | context_instance = copy(context) 33 | context_instance.render_context = RenderContext() 34 | 35 | with context_instance.render_context.push_state(self): 36 | if context_instance.template is None: 37 | with context_instance.bind_template(self): 38 | context.template_name = self.name 39 | return self._render(context_instance) 40 | else: 41 | return self._render(context_instance) 42 | 43 | def _render(self, context): 44 | # Before trying to render the template, we need to traverse the tree of 45 | # parent templates and find all blocks in them. 46 | self._build_block_context(self.template, context) 47 | 48 | return self._render_template_block(context) 49 | 50 | def _build_block_context(self, template: Template, context: Context) -> None: 51 | """ 52 | Populate the block context with BlockNodes from this template and parent templates. 53 | """ 54 | 55 | # Ensure there's a BlockContext before rendering. This allows blocks in 56 | # ExtendsNodes to be found by sub-templates (allowing {{ block.super }} and 57 | # overriding sub-blocks to work). 58 | if BLOCK_CONTEXT_KEY not in context.render_context: 59 | context.render_context[BLOCK_CONTEXT_KEY] = BlockContext() 60 | block_context = context.render_context[BLOCK_CONTEXT_KEY] 61 | 62 | # Add the template's blocks to the context. 63 | block_context.add_blocks( 64 | {n.name: n for n in template.nodelist.get_nodes_by_type(BlockNode)} 65 | ) 66 | 67 | # Check parent nodes (there should only ever be 0 or 1). 68 | for node in template.nodelist.get_nodes_by_type(ExtendsNode): 69 | parent = node.get_parent(context) 70 | 71 | # Recurse and search for blocks from the parent. 72 | self._build_block_context(parent, context) 73 | 74 | def _render_template_block(self, context): 75 | """Renders a single block from a template.""" 76 | block_node = context.render_context[BLOCK_CONTEXT_KEY].get_block(self.block_name) 77 | 78 | if block_node is None: 79 | # The wanted block_name was not found. 80 | raise BlockNotFound("block with name '%s' does not exist" % self.block_name) 81 | 82 | return block_node.render(context) 83 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medihack/django-block-fragments/b119ad66dbb6b42e21000d837e28375ce9955494/tests/__init__.py -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | TEMPLATES = [ 2 | { 3 | "BACKEND": "django.template.backends.django.DjangoTemplates", 4 | "DIRS": [], 5 | "APP_DIRS": True, 6 | "OPTIONS": { 7 | "context_processors": [ 8 | "django.template.context_processors.debug", 9 | "django.template.context_processors.request", 10 | "django.contrib.auth.context_processors.auth", 11 | "django.contrib.messages.context_processors.messages", 12 | ], 13 | "debug": True, 14 | }, 15 | }, 16 | ] 17 | INSTALLED_APPS = [ 18 | "block_fragments", 19 | "tests", 20 | ] 21 | 22 | # Additional settings. 23 | DEBUG = True 24 | SECRET_KEY = "a-not-very-secret-test-secret-key" 25 | DATABASES = { 26 | "default": { 27 | "ENGINE": "django.db.backends.sqlite3", 28 | "NAME": ":memory:", 29 | }, 30 | } 31 | USE_TZ = True 32 | -------------------------------------------------------------------------------- /tests/templates/include.html: -------------------------------------------------------------------------------- 1 | included template -------------------------------------------------------------------------------- /tests/templates/test1.html: -------------------------------------------------------------------------------- 1 | {% block block1 %}block1 from test1{{ suffix1 }}{% endblock %} 2 | {% block block2 %}block2 from test1{{ suffix2 }}{% endblock %} 3 | -------------------------------------------------------------------------------- /tests/templates/test2.html: -------------------------------------------------------------------------------- 1 | {% extends 'test1.html' %} 2 | {% block block1 %}block1 from test2{% endblock %} 3 | -------------------------------------------------------------------------------- /tests/templates/test3.html: -------------------------------------------------------------------------------- 1 | {% extends 'test1.html' %} 2 | 3 | {% block block1 %}{% include "include.html" %}{% endblock %} 4 | 5 | {% block block2 %}block2 from test3 - {{ block.super }}{% endblock %} 6 | -------------------------------------------------------------------------------- /tests/templates/test4.html: -------------------------------------------------------------------------------- 1 | {% extends 'test2.html' %} 2 | -------------------------------------------------------------------------------- /tests/templates/test5.html: -------------------------------------------------------------------------------- 1 | {% include 'test1.html' %} 2 | 3 | {% block block1 %}{% block block3 %}block3 from test5{% endblock %}{% endblock %} 4 | 5 | {% block block2 %}{{ foo }}{% endblock %} 6 | -------------------------------------------------------------------------------- /tests/templates/test6.html: -------------------------------------------------------------------------------- 1 | {% extends 'test3.html' %} 2 | 3 | {% block block2 %}block2 from test6 - {{ block.super }}{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/templates/test7.html: -------------------------------------------------------------------------------- 1 | {% include "test1.html#block2" %} -------------------------------------------------------------------------------- /tests/templates/test8.html: -------------------------------------------------------------------------------- 1 | {% include "test1.html#block1" %} 2 | {% include "test4.html#block1" %} 3 | -------------------------------------------------------------------------------- /tests/templates/test9.html: -------------------------------------------------------------------------------- 1 | {% block block1 %} 2 | {% include "test1.html#block2" %} 3 | block1 from test9 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /tests/templates/test_base.html: -------------------------------------------------------------------------------- 1 | {% block base %} 2 | {% block first %} 3 | foo 4 | {% endblock %} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /tests/templates/test_exception.html: -------------------------------------------------------------------------------- 1 | {% load test_tags %} 2 | 3 | {% block exception_block %} 4 | {% raise_exception %} 5 | {% endblock exception_block %} 6 | -------------------------------------------------------------------------------- /tests/templates/test_request_context.html: -------------------------------------------------------------------------------- 1 | {% include 'test1.html' %} 2 | 3 | {% block block1 %}{{ request.path }}{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/templates/test_sub.html: -------------------------------------------------------------------------------- 1 | {% extends 'test_base.html' %} 2 | {% block first %} 3 | bar 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /tests/templatetags/test_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.simple_tag() 7 | def raise_exception(): 8 | raise Exception("Exception raised in template tag.") 9 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import django.template 2 | import pytest 3 | from django.template import EngineHandler, engines 4 | 5 | from block_fragments.apps import wrap_loaders 6 | from block_fragments.exceptions import BlockNotFound 7 | 8 | 9 | @pytest.fixture 10 | def engine(): 11 | return engines["django"] 12 | 13 | 14 | def test_wrap_loaders(settings): 15 | settings.TEMPLATES = [ 16 | { 17 | "BACKEND": "django.template.backends.django.DjangoTemplates", 18 | "APP_DIRS": True, 19 | }, 20 | ] 21 | 22 | django.template.engines = EngineHandler() 23 | 24 | outermost_loader = django.template.engines["django"].engine.loaders[0][0] 25 | assert outermost_loader != "block_fragments.loader.Loader" 26 | 27 | wrap_loaders("django") 28 | 29 | outermost_loader = django.template.engines["django"].engine.loaders[0][0] 30 | assert outermost_loader == "block_fragments.loader.Loader" 31 | 32 | 33 | def test_block(engine): 34 | """Test rendering an individual block.""" 35 | template = engine.get_template("test1.html#block1") 36 | assert template.render() == "block1 from test1" 37 | 38 | template = engine.get_template("test1.html#block2") 39 | assert template.render() == "block2 from test1" 40 | 41 | 42 | def test_override(engine): 43 | """This block is overridden in test2.""" 44 | template = engine.get_template("test2.html#block1") 45 | assert template.render() == "block1 from test2" 46 | 47 | 48 | def test_inherit(engine): 49 | """This block is inherited from test1.""" 50 | template = engine.get_template("test2.html#block2") 51 | assert template.render() == "block2 from test1" 52 | 53 | 54 | def test_inherit_context(engine): 55 | """This block is inherited from test1.""" 56 | template = engine.get_template("test2.html#block2") 57 | assert template.render({"suffix2": " blah"}) == "block2 from test1 blah" 58 | 59 | 60 | def test_multi_inherited(engine): 61 | """A block from an included template should be available.""" 62 | template = engine.get_template("test4.html#block2") 63 | assert template.render() == "block2 from test1" 64 | 65 | 66 | def test_multi_inherited_context(engine): 67 | """A block from an included template should be available.""" 68 | template = engine.get_template("test4.html#block2") 69 | assert template.render({"suffix2": " blah"}) == "block2 from test1 blah" 70 | 71 | 72 | def test_no_block(engine): 73 | """Check if there's no block available an exception is raised.""" 74 | with pytest.raises(BlockNotFound) as exc: 75 | template = engine.get_template("test1.html#noblock") 76 | template.render() 77 | 78 | assert str(exc.value) == "block with name 'noblock' does not exist" 79 | 80 | 81 | def test_include(engine): 82 | """Ensure that an include tag in a block still works.""" 83 | template = engine.get_template("test3.html#block1") 84 | assert template.render() == "included template" 85 | 86 | 87 | def test_super(engine): 88 | """Test that block.super works.""" 89 | template = engine.get_template("test3.html#block2") 90 | assert template.render() == "block2 from test3 - block2 from test1" 91 | 92 | 93 | def test_multi_super(engine): 94 | template = engine.get_template("test6.html#block2") 95 | assert template.render() == "block2 from test6 - block2 from test3 - block2 from test1" 96 | 97 | 98 | def test_super_with_same_context_on_multiple_executions(engine): 99 | """Test that block.super works when fed the same context twice.""" 100 | context = {} 101 | template = engine.get_template("test3.html#block2") 102 | result_one = template.render(context) 103 | result_two = template.render(context) 104 | 105 | assert result_one == result_two 106 | assert result_one == "block2 from test3 - block2 from test1" 107 | 108 | 109 | def test_subblock(engine): 110 | """Test that a block within a block works.""" 111 | template = engine.get_template("test5.html#block1") 112 | assert template.render() == "block3 from test5" 113 | 114 | template = engine.get_template("test5.html#block3") 115 | assert template.render() == "block3 from test5" 116 | 117 | 118 | def test_subblock_no_parent(engine): 119 | """ 120 | Test that a block within a block works if the parent block is only found 121 | in the base template. 122 | 123 | This is very similar to test_subblock, but the templates differ. In this 124 | test the sub-template does not replace the entire block from the parent 125 | template. 126 | """ 127 | template = engine.get_template("test_sub.html#base") 128 | assert template.render() == "\n\nbar\n\n" 129 | 130 | template = engine.get_template("test_sub.html#first") 131 | assert template.render() == "\nbar\n" 132 | 133 | 134 | def test_exceptions(engine): 135 | with pytest.raises(Exception) as e: 136 | template = engine.get_template("test_exception.html#exception_block") 137 | template.render() 138 | 139 | assert str(e.value) == "Exception raised in template tag." 140 | 141 | 142 | def test_exceptions_debug(engine, settings): 143 | settings.DEBUG = True 144 | with pytest.raises(Exception) as exc: 145 | template = engine.get_template("test_exception.html#exception_block") 146 | template.render() 147 | 148 | assert str(exc.value) == "Exception raised in template tag." 149 | 150 | 151 | def test_context(engine): 152 | """Test that a context is properly rendered in a template.""" 153 | data = "block2 from test5" 154 | template = engine.get_template("test5.html#block2") 155 | assert template.render({"foo": data}) == data 156 | 157 | 158 | def test_context_autoescape_off(engine): 159 | """Test that the user can disable autoescape by providing a Context instance.""" 160 | data = "&'" 161 | template = engine.get_template("test5.html#block2") 162 | autoescape = template.backend.engine.autoescape 163 | template.backend.engine.autoescape = False 164 | assert template.render({"foo": data}) == data 165 | template.backend.engine.autoescape = autoescape 166 | 167 | 168 | def test_request_context(engine, rf): 169 | """Test that a request context data are properly rendered in a template.""" 170 | request = rf.get("/dummy-url") 171 | template = engine.get_template("test_request_context.html#block1") 172 | assert template.render({"request": request}) == "/dummy-url" 173 | 174 | 175 | def test_include_fragment(engine): 176 | """Test that an include tag works with block fragments.""" 177 | template = engine.get_template("test7.html") 178 | assert template.render() == "block2 from test1" 179 | 180 | 181 | def test_multiple_include_fragment(engine): 182 | """Test multiple include tags that use block fragments.""" 183 | template = engine.get_template("test8.html") 184 | assert template.render() == "block1 from test1\nblock1 from test2\n" 185 | 186 | 187 | def test_include_fragment_in_block(engine): 188 | """Test a block that does include a fragment.""" 189 | template = engine.get_template("test9.html#block1") 190 | assert template.render() == "\nblock2 from test1\nblock1 from test9\n" 191 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.10" 4 | 5 | [[package]] 6 | name = "asgiref" 7 | version = "3.8.1" 8 | source = { registry = "https://pypi.org/simple" } 9 | dependencies = [ 10 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 11 | ] 12 | sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } 13 | wheels = [ 14 | { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, 15 | ] 16 | 17 | [[package]] 18 | name = "colorama" 19 | version = "0.4.6" 20 | source = { registry = "https://pypi.org/simple" } 21 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 22 | wheels = [ 23 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 24 | ] 25 | 26 | [[package]] 27 | name = "coverage" 28 | version = "7.6.12" 29 | source = { registry = "https://pypi.org/simple" } 30 | sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } 31 | wheels = [ 32 | { url = "https://files.pythonhosted.org/packages/ba/67/81dc41ec8f548c365d04a29f1afd492d3176b372c33e47fa2a45a01dc13a/coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8", size = 208345 }, 33 | { url = "https://files.pythonhosted.org/packages/33/43/17f71676016c8829bde69e24c852fef6bd9ed39f774a245d9ec98f689fa0/coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879", size = 208775 }, 34 | { url = "https://files.pythonhosted.org/packages/86/25/c6ff0775f8960e8c0840845b723eed978d22a3cd9babd2b996e4a7c502c6/coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe", size = 237925 }, 35 | { url = "https://files.pythonhosted.org/packages/b0/3d/5f5bd37046243cb9d15fff2c69e498c2f4fe4f9b42a96018d4579ed3506f/coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674", size = 235835 }, 36 | { url = "https://files.pythonhosted.org/packages/b5/f1/9e6b75531fe33490b910d251b0bf709142e73a40e4e38a3899e6986fe088/coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb", size = 236966 }, 37 | { url = "https://files.pythonhosted.org/packages/4f/bc/aef5a98f9133851bd1aacf130e754063719345d2fb776a117d5a8d516971/coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c", size = 236080 }, 38 | { url = "https://files.pythonhosted.org/packages/eb/d0/56b4ab77f9b12aea4d4c11dc11cdcaa7c29130b837eb610639cf3400c9c3/coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c", size = 234393 }, 39 | { url = "https://files.pythonhosted.org/packages/0d/77/28ef95c5d23fe3dd191a0b7d89c82fea2c2d904aef9315daf7c890e96557/coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e", size = 235536 }, 40 | { url = "https://files.pythonhosted.org/packages/29/62/18791d3632ee3ff3f95bc8599115707d05229c72db9539f208bb878a3d88/coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425", size = 211063 }, 41 | { url = "https://files.pythonhosted.org/packages/fc/57/b3878006cedfd573c963e5c751b8587154eb10a61cc0f47a84f85c88a355/coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa", size = 211955 }, 42 | { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 }, 43 | { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 }, 44 | { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 }, 45 | { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 }, 46 | { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 }, 47 | { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 }, 48 | { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 }, 49 | { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 }, 50 | { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 }, 51 | { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 }, 52 | { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, 53 | { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, 54 | { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, 55 | { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, 56 | { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, 57 | { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, 58 | { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, 59 | { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, 60 | { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, 61 | { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, 62 | { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, 63 | { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, 64 | { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, 65 | { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, 66 | { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, 67 | { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, 68 | { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, 69 | { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, 70 | { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, 71 | { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, 72 | { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, 73 | { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, 74 | { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, 75 | { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, 76 | { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, 77 | { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, 78 | { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, 79 | { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, 80 | { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, 81 | { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, 82 | { url = "https://files.pythonhosted.org/packages/7a/7f/05818c62c7afe75df11e0233bd670948d68b36cdbf2a339a095bc02624a8/coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf", size = 200558 }, 83 | { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, 84 | ] 85 | 86 | [package.optional-dependencies] 87 | toml = [ 88 | { name = "tomli", marker = "python_full_version <= '3.11'" }, 89 | ] 90 | 91 | [[package]] 92 | name = "django" 93 | version = "5.1.7" 94 | source = { registry = "https://pypi.org/simple" } 95 | dependencies = [ 96 | { name = "asgiref" }, 97 | { name = "sqlparse" }, 98 | { name = "tzdata", marker = "sys_platform == 'win32'" }, 99 | ] 100 | sdist = { url = "https://files.pythonhosted.org/packages/5f/57/11186e493ddc5a5e92cc7924a6363f7d4c2b645f7d7cb04a26a63f9bfb8b/Django-5.1.7.tar.gz", hash = "sha256:30de4ee43a98e5d3da36a9002f287ff400b43ca51791920bfb35f6917bfe041c", size = 10716510 } 101 | wheels = [ 102 | { url = "https://files.pythonhosted.org/packages/ba/0f/7e042df3d462d39ae01b27a09ee76653692442bc3701fbfa6cb38e12889d/Django-5.1.7-py3-none-any.whl", hash = "sha256:1323617cb624add820cb9611cdcc788312d250824f92ca6048fda8625514af2b", size = 8276912 }, 103 | ] 104 | 105 | [[package]] 106 | name = "django-block-fragments" 107 | source = { editable = "." } 108 | dependencies = [ 109 | { name = "django" }, 110 | ] 111 | 112 | [package.dev-dependencies] 113 | dev = [ 114 | { name = "pytest" }, 115 | { name = "pytest-cov" }, 116 | { name = "pytest-django" }, 117 | { name = "ruff" }, 118 | ] 119 | 120 | [package.metadata] 121 | requires-dist = [{ name = "django", specifier = ">=5.0.0" }] 122 | 123 | [package.metadata.requires-dev] 124 | dev = [ 125 | { name = "pytest", specifier = ">=8.3.5" }, 126 | { name = "pytest-cov", specifier = ">=6.0.0" }, 127 | { name = "pytest-django", specifier = ">=4.10.0" }, 128 | { name = "ruff", specifier = ">=0.9.10" }, 129 | ] 130 | 131 | [[package]] 132 | name = "exceptiongroup" 133 | version = "1.2.2" 134 | source = { registry = "https://pypi.org/simple" } 135 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 136 | wheels = [ 137 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 138 | ] 139 | 140 | [[package]] 141 | name = "iniconfig" 142 | version = "2.0.0" 143 | source = { registry = "https://pypi.org/simple" } 144 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 145 | wheels = [ 146 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 147 | ] 148 | 149 | [[package]] 150 | name = "packaging" 151 | version = "24.2" 152 | source = { registry = "https://pypi.org/simple" } 153 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 154 | wheels = [ 155 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 156 | ] 157 | 158 | [[package]] 159 | name = "pluggy" 160 | version = "1.5.0" 161 | source = { registry = "https://pypi.org/simple" } 162 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 163 | wheels = [ 164 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 165 | ] 166 | 167 | [[package]] 168 | name = "pytest" 169 | version = "8.3.5" 170 | source = { registry = "https://pypi.org/simple" } 171 | dependencies = [ 172 | { name = "colorama", marker = "sys_platform == 'win32'" }, 173 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 174 | { name = "iniconfig" }, 175 | { name = "packaging" }, 176 | { name = "pluggy" }, 177 | { name = "tomli", marker = "python_full_version < '3.11'" }, 178 | ] 179 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } 180 | wheels = [ 181 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, 182 | ] 183 | 184 | [[package]] 185 | name = "pytest-cov" 186 | version = "6.0.0" 187 | source = { registry = "https://pypi.org/simple" } 188 | dependencies = [ 189 | { name = "coverage", extra = ["toml"] }, 190 | { name = "pytest" }, 191 | ] 192 | sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } 193 | wheels = [ 194 | { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, 195 | ] 196 | 197 | [[package]] 198 | name = "pytest-django" 199 | version = "4.10.0" 200 | source = { registry = "https://pypi.org/simple" } 201 | dependencies = [ 202 | { name = "pytest" }, 203 | ] 204 | sdist = { url = "https://files.pythonhosted.org/packages/a5/10/a096573b4b896f18a8390d9dafaffc054c1f613c60bf838300732e538890/pytest_django-4.10.0.tar.gz", hash = "sha256:1091b20ea1491fd04a310fc9aaff4c01b4e8450e3b157687625e16a6b5f3a366", size = 84710 } 205 | wheels = [ 206 | { url = "https://files.pythonhosted.org/packages/58/4c/a4fe18205926216e1aebe1f125cba5bce444f91b6e4de4f49fa87e322775/pytest_django-4.10.0-py3-none-any.whl", hash = "sha256:57c74ef3aa9d89cae5a5d73fbb69a720a62673ade7ff13b9491872409a3f5918", size = 23975 }, 207 | ] 208 | 209 | [[package]] 210 | name = "ruff" 211 | version = "0.9.10" 212 | source = { registry = "https://pypi.org/simple" } 213 | sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776 } 214 | wheels = [ 215 | { url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494 }, 216 | { url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584 }, 217 | { url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692 }, 218 | { url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760 }, 219 | { url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196 }, 220 | { url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985 }, 221 | { url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842 }, 222 | { url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804 }, 223 | { url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776 }, 224 | { url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673 }, 225 | { url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358 }, 226 | { url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177 }, 227 | { url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747 }, 228 | { url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441 }, 229 | { url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401 }, 230 | { url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360 }, 231 | { url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892 }, 232 | ] 233 | 234 | [[package]] 235 | name = "sqlparse" 236 | version = "0.5.3" 237 | source = { registry = "https://pypi.org/simple" } 238 | sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999 } 239 | wheels = [ 240 | { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, 241 | ] 242 | 243 | [[package]] 244 | name = "tomli" 245 | version = "2.2.1" 246 | source = { registry = "https://pypi.org/simple" } 247 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 248 | wheels = [ 249 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, 250 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, 251 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, 252 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, 253 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, 254 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, 255 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, 256 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, 257 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 258 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 259 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, 260 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, 261 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, 262 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, 263 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, 264 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, 265 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, 266 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, 267 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 268 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 269 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, 270 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, 271 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, 272 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, 273 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, 274 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, 275 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, 276 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, 277 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 278 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 279 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 280 | ] 281 | 282 | [[package]] 283 | name = "typing-extensions" 284 | version = "4.12.2" 285 | source = { registry = "https://pypi.org/simple" } 286 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 287 | wheels = [ 288 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 289 | ] 290 | 291 | [[package]] 292 | name = "tzdata" 293 | version = "2025.1" 294 | source = { registry = "https://pypi.org/simple" } 295 | sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 } 296 | wheels = [ 297 | { url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 }, 298 | ] 299 | --------------------------------------------------------------------------------