├── .python-version ├── src └── python_hiccup │ ├── py.typed │ ├── __init__.py │ ├── html │ ├── __init__.py │ └── core.py │ └── transform │ ├── __init__.py │ └── core.py ├── test ├── __init__.py └── test_render_html.py ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .circleci └── config.yml ├── pyproject.toml ├── CONTRIBUTING.md ├── LICENSE ├── CODE-OF-CONDUCT.md ├── README.md └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /src/python_hiccup/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests in here.""" 2 | -------------------------------------------------------------------------------- /src/python_hiccup/__init__.py: -------------------------------------------------------------------------------- 1 | """A Python implementation of the Hiccup syntax.""" 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: davidvujic 4 | -------------------------------------------------------------------------------- /src/python_hiccup/html/__init__.py: -------------------------------------------------------------------------------- 1 | """Render data into HTML.""" 2 | 3 | from python_hiccup.html.core import raw, render 4 | 5 | __all__ = ["raw", "render"] 6 | -------------------------------------------------------------------------------- /src/python_hiccup/transform/__init__.py: -------------------------------------------------------------------------------- 1 | """Transform Hiccup syntax.""" 2 | 3 | from python_hiccup.transform.core import CONTENT_TAG, transform 4 | 5 | __all__ = ["CONTENT_TAG", "transform"] 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Tools 10 | .mypy_cache/ 11 | .ruff_cache/ 12 | 13 | # Virtual environments 14 | .venv 15 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | python: circleci/python@2.1.1 4 | 5 | jobs: 6 | test: 7 | docker: 8 | - image: ghcr.io/astral-sh/uv:python3.10-bookworm 9 | 10 | steps: 11 | - checkout 12 | - run: 13 | name: install 14 | command: uv sync 15 | - run: 16 | name: lint 17 | command: uv run ruff check 18 | - run: 19 | name: test 20 | command: uv run pytest 21 | 22 | workflows: 23 | main: 24 | jobs: 25 | - test 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: 24 | - python-hiccup version 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "python-hiccup" 3 | version = "0.4.0" 4 | description = "Python Hiccup is a library for representing HTML using plain Python data structures" 5 | authors = [{name = "David Vujic"}] 6 | readme = "README.md" 7 | requires-python = ">=3.10" 8 | dependencies = [] 9 | 10 | [project.urls] 11 | Repository = "https://github.com/DavidVujic/python-hiccup" 12 | 13 | [tool.ruff] 14 | lint.select = ["ALL"] 15 | lint.ignore = ["COM812", "ISC001", "D203", "D213"] 16 | 17 | line-length = 100 18 | 19 | [tool.ruff.lint.per-file-ignores] 20 | "test/*.py" = ["S101"] 21 | 22 | [build-system] 23 | requires = ["hatchling"] 24 | build-backend = "hatchling.build" 25 | 26 | [dependency-groups] 27 | dev = [ 28 | "mypy>=1.13.0", 29 | "pytest>=8.3.4", 30 | "ruff>=0.8.3", 31 | ] 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the development of python-hiccup 2 | 3 | Fork this repo, write code and send a pull request. Easy! 4 | 5 | ### Guidelines 6 | * Create a branch for the feature you are about to write. 7 | * If possible, isolate the changes in the branch to the feature only. Merges will be easier if general refactorings, that are not specific to the feature, are made in separate branches. 8 | * If possible, avoid general dependency updates in the feature branch. 9 | * If possible, send pull requests early. Don't wait too long, even if the feature is not completely done. The new code can very likely be merged without causing any problems (if the code changes are not of type breaking features). Think of it as "silent releases". 10 | * If you think it is relevant and adds value: write unit test(s). 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 David Vujic 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 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | 14 | 15 | ## Types of changes 16 | 17 | - [ ] Bug fix (non-breaking change which fixes an issue) 18 | - [ ] New feature (non-breaking change which adds functionality) 19 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 20 | 21 | ## Checklist: 22 | 23 | 24 | - [ ] I have read the [code of conduct](https://github.com/davidvujic/python-hiccup/blob/master/CODE-OF-CONDUCT.md). 25 | - [ ] I have read the [contributing guide](https://github.com/davidvujic/python-hiccup/blob/master/CONTRIBUTING.md). 26 | - [ ] I have updated the documentation accordingly (if applicable). 27 | -------------------------------------------------------------------------------- /src/python_hiccup/transform/core.py: -------------------------------------------------------------------------------- 1 | """Transform a sequence of tag data into groups.""" 2 | 3 | from collections import defaultdict 4 | from collections.abc import Callable, Mapping, Sequence 5 | from collections.abc import Set as AbstractSet 6 | from functools import reduce 7 | 8 | Item = str | AbstractSet | Mapping | Sequence | Callable 9 | 10 | ATTRIBUTES = "attributes" 11 | BOOLEAN_ATTRIBUTES = "boolean_attributes" 12 | CHILDREN = "children" 13 | 14 | CONTENT_TAG = "<::HICCUP_CONTENT::>" 15 | 16 | 17 | def _is_attribute(item: Item) -> bool: 18 | return isinstance(item, dict) 19 | 20 | 21 | def _is_boolean_attribute(item: Item) -> bool: 22 | return isinstance(item, set) 23 | 24 | 25 | def _is_child(item: Item) -> bool: 26 | return isinstance(item, list | tuple) 27 | 28 | 29 | def _is_sibling(item: Item) -> bool: 30 | return _is_child(item) 31 | 32 | 33 | def _key_for_group(item: Item) -> str: 34 | if _is_attribute(item): 35 | return ATTRIBUTES 36 | if _is_boolean_attribute(item): 37 | return BOOLEAN_ATTRIBUTES 38 | 39 | return CHILDREN 40 | 41 | 42 | def _to_groups(acc: dict, item: Item) -> dict: 43 | key = _key_for_group(item) 44 | 45 | flattened = [*item] if isinstance(item, Sequence) and _is_child(item[0]) else [item] 46 | 47 | value = acc[key] + flattened 48 | 49 | return acc | {key: value} 50 | 51 | 52 | def _extract_from_tag(tag: str) -> tuple[str, dict]: 53 | first, *rest = tag.split(".") 54 | element_name, _id = first.split("#") if "#" in first else (first, "") 55 | 56 | element_id = {"id": _id} if _id else {} 57 | element_class = {"class": " ".join(rest)} if rest else {} 58 | 59 | return element_name, element_id | element_class 60 | 61 | 62 | def _transform_tags(tags: Sequence) -> dict: 63 | if not isinstance(tags, list | tuple): 64 | return {CONTENT_TAG: tags} 65 | 66 | first, *rest = tags 67 | 68 | element, extracted = _extract_from_tag(first) 69 | extra = [extracted, *rest] 70 | 71 | grouped: dict = reduce(_to_groups, extra, defaultdict(list)) 72 | children = grouped[CHILDREN] 73 | 74 | branch = {element: [_transform_tags(r) for r in children]} 75 | options = {k: v for k, v in grouped.items() if k != CHILDREN and v} 76 | 77 | return branch | options 78 | 79 | 80 | def transform(tags: Sequence) -> list: 81 | """Transform a sequence of tag data into goups: elements, attributes and content.""" 82 | first, *_ = tags 83 | 84 | if _is_sibling(first): 85 | return [_transform_tags(t) for t in tags] 86 | 87 | return [_transform_tags(tags)] 88 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # python-hiccup Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team. All complaints will be reviewed and 59 | investigated and will result in a response that is deemed necessary and appropriate to the circumstances. 60 | The project team is obligated to maintain confidentiality with regard to the reporter of an incident. 61 | Further details of specific enforcement policies may be posted separately. 62 | 63 | Project maintainers who do not follow or enforce the Code of Conduct in good 64 | faith may face temporary or permanent repercussions as determined by other 65 | members of the project's leadership. 66 | -------------------------------------------------------------------------------- /src/python_hiccup/html/core.py: -------------------------------------------------------------------------------- 1 | """Render HTML from a sequence of grouped data.""" 2 | 3 | import html 4 | import operator 5 | from collections.abc import Callable, Mapping, Sequence 6 | from functools import reduce 7 | 8 | from python_hiccup.transform import CONTENT_TAG, transform 9 | 10 | 11 | def _element_allows_raw_content(element: str) -> bool: 12 | return str.lower(element) in {"script", "style"} 13 | 14 | 15 | def _is_allowed_raw(content: str) -> bool: 16 | return str.startswith(content, "") 17 | 18 | 19 | def _allow_raw_content(content: str, element: str) -> bool: 20 | if _element_allows_raw_content(element): 21 | return True 22 | 23 | return _is_allowed_raw(content) 24 | 25 | 26 | def _escape(content: str, element: str) -> str: 27 | return content if _allow_raw_content(content, element) else html.escape(content) 28 | 29 | 30 | def _join(acc: str, attrs: Sequence) -> str: 31 | return " ".join([acc, *attrs]) 32 | 33 | 34 | def _to_attributes(acc: str, attributes: Mapping) -> str: 35 | attrs = [f'{k}="{v}"' for k, v in attributes.items()] 36 | 37 | return _join(acc, attrs) 38 | 39 | 40 | def _to_bool_attributes(acc: str, attributes: set) -> str: 41 | attrs = list(attributes) 42 | 43 | return _join(acc, attrs) 44 | 45 | 46 | def _closing_tag(element: str) -> bool: 47 | specials = {"script"} 48 | 49 | return str.lower(element) in specials 50 | 51 | 52 | def _suffix(element_data: str) -> str: 53 | specials = {"doctype"} 54 | normalized = str.lower(element_data) 55 | 56 | return "" if any(s in normalized for s in specials) else " /" 57 | 58 | 59 | def _is_content(element: str) -> bool: 60 | return element == CONTENT_TAG 61 | 62 | 63 | def _is_raw(content: str | Callable) -> bool: 64 | return callable(content) and content.__name__ == "raw_content" 65 | 66 | 67 | def _to_html(tag: Mapping, parent: str = "") -> list: 68 | element = next(iter(tag.keys())) 69 | child = next(iter(tag.values())) 70 | 71 | if _is_content(element): 72 | return child() if _is_raw(child) else [_escape(str(child), parent)] 73 | 74 | attributes = reduce(_to_attributes, tag.get("attributes", []), "") 75 | bool_attributes = reduce(_to_bool_attributes, tag.get("boolean_attributes", []), "") 76 | element_attributes = attributes + bool_attributes 77 | 78 | matrix = [_to_html(c, element) for c in child] 79 | flattened: list = reduce(operator.iadd, matrix, []) 80 | 81 | begin = f"{element}{element_attributes}" if element_attributes else element 82 | 83 | if flattened: 84 | return [f"<{begin}>", *flattened, f""] 85 | 86 | if _closing_tag(element): 87 | return [f"<{begin}>", f""] 88 | 89 | extra = _suffix(begin) 90 | 91 | return [f"<{begin}{extra}>"] 92 | 93 | 94 | def render(data: Sequence) -> str: 95 | """Transform a sequence of grouped data to HTML.""" 96 | transformed = transform(data) 97 | 98 | matrix = [_to_html(t) for t in transformed] 99 | 100 | transformed_html: list = reduce(operator.iadd, matrix, []) 101 | 102 | return "".join(transformed_html) 103 | 104 | 105 | def raw(content: str | float) -> Callable: 106 | """Content that should not be escaped when rendering HTML elements.""" 107 | 108 | def raw_content() -> str | float: 109 | return content 110 | 111 | return raw_content 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Hiccup 2 | 3 | Python Hiccup is a library for representing HTML using plain Python data structures. 4 | 5 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/DavidVujic/python-hiccup/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/DavidVujic/python-hiccup/tree/main) 6 | 7 | [![CodeScene Code Health](https://codescene.io/projects/59968/status-badges/code-health)](https://codescene.io/projects/59968) 8 | 9 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=DavidVujic_python-hiccup&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=DavidVujic_python-hiccup) 10 | 11 | ## What is Python Hiccup? 12 | This is a Python implementation of the Hiccup syntax. Python Hiccup is a library for representing HTML in Python. 13 | Using `list` or `tuple` to represent HTML elements, and `dict` to represent the element attributes. 14 | 15 | _This project started out as a fun coding challenge, and now evolving into something useful for Python Dev teams._ 16 | 17 | ## Usage 18 | Create server side HTML using plain Python data structures. 19 | You can also use it with __PyScript__. 20 | 21 | ## Example 22 | 23 | Python: 24 | ``` python 25 | from python_hiccup.html import render 26 | 27 | render(["div", "Hello world!"]) 28 | ``` 29 | 30 | The output will be a string: `
Hello world!
` 31 | 32 | 33 | With Hiccup, you can create HTML in a programmatic style. 34 | To render HTML like: 35 | ``` html 36 | 41 | ``` 42 | 43 | with Python: 44 | ``` python 45 | def todo(data: list) -> list: 46 | return [["li", i] for i in data] 47 | 48 | data = todo(["one", "two", "three"]) 49 | 50 | render(["ul", data]) 51 | ``` 52 | 53 | ## Basic syntax 54 | 55 | Python: 56 | ``` python 57 | ["div", "Hello world!"] 58 | ``` 59 | 60 | The HTML equivalent is: 61 | ``` html 62 |
Hello world!
63 | ``` 64 | 65 | Writing a nested HTML structure, using Python Hiccup: 66 | 67 | ``` python 68 | ["div", ["span", ["strong", "Hello world!"]]] 69 | ``` 70 | 71 | The HTML equivalent is: 72 | ``` html 73 |
74 | 75 | Hello world! 76 | 77 |
78 | ``` 79 | 80 | 81 | Adding attributes to an element, such as CSS id and classes, using Python Hiccup: 82 | 83 | ``` python 84 | ["div", {"id": "foo", "class": "bar"}, "Hello world!"] 85 | ``` 86 | 87 | or, using a more concise syntax: 88 | ``` python 89 | ["div#foo.bar", "Hello world!"] 90 | ``` 91 | 92 | The HTML equivalent is: 93 | ``` html 94 |
Hello world!
95 | ``` 96 | 97 | Adding valueless attributes to elements, such as the `async` or `defer`, by using Python `set`: 98 | ``` python 99 | ["!DOCTYPE", {"html"}] 100 | ["script", {"async"}, {"src": "js/script.js"}] 101 | ``` 102 | 103 | The HTML equivalent is: 104 | ``` html 105 | 106 | 107 | ``` 108 | 109 | ### Adding unescaped content 110 | This is useful when rendering HTML entities like `©`. 111 | 112 | ``` python 113 | from python_hiccup.html import raw 114 | 115 | data = ["div", raw("© this should not be escaped!")] 116 | ``` 117 | 118 | The HTML output: 119 | 120 | ``` html 121 |
© this should not be escaped!
122 | ``` 123 | 124 | ## Resources 125 | - [PyScript and python-hiccup example](https://pyscript.com/@davidvujic/pyscript-jokes-with-a-hiccup/latest?files=main.py) - PyScript Jokes with a Hiccup 126 | - [Hiccup](https://github.com/weavejester/hiccup) - the original implementation, for Clojure. 127 | 128 | ## Existing python alternatives 129 | - [pyhiccup](https://github.com/nbessi/pyhiccup) 130 | - [piccup](https://github.com/alexjuda/piccup) 131 | 132 | ## Development 133 | Running lint: 134 | 135 | ``` shell 136 | uv run ruff check 137 | ``` 138 | 139 | Running tests: 140 | 141 | ``` shell 142 | uv run pytest 143 | ``` 144 | -------------------------------------------------------------------------------- /test/test_render_html.py: -------------------------------------------------------------------------------- 1 | """Unit tests for the python_hiccup.html.render function.""" 2 | 3 | from python_hiccup.html import raw, render 4 | 5 | 6 | def test_returns_a_string() -> None: 7 | """Assert that the render function returns a string containing HTML.""" 8 | data = ["div", "HELLO"] 9 | 10 | assert render(data) == "
HELLO
" 11 | 12 | 13 | def test_accepts_a_sequence_of_tuples() -> None: 14 | """Assert that the render function accepts tuples as input.""" 15 | data = ("div", ("span", "HELLO")) 16 | 17 | assert render(data) == "
HELLO
" 18 | 19 | 20 | def test_handles_special_tags() -> None: 21 | """Assert that the HTML render function takes any special elements into account when.""" 22 | assert render(["!DOCTYPE"]) == "" 23 | assert render(["div"]) == "
" 24 | 25 | 26 | def test_parses_attributes() -> None: 27 | """Assert that element attributes are parsed as expected.""" 28 | data = ["div", {"id": "hello", "class": "first second"}, "HELLO WORLD"] 29 | 30 | expected = '
HELLO WORLD
' 31 | 32 | assert render(data) == expected 33 | 34 | 35 | def test_parses_attribute_shorthand() -> None: 36 | """Assert that the shorthand feature for element id and class is parsed as expected.""" 37 | data = ["div#hello.first.second", "HELLO WORLD"] 38 | 39 | expected = '
HELLO WORLD
' 40 | 41 | assert render(data) == expected 42 | 43 | 44 | def test_explicit_closing_tag() -> None: 45 | """Assert that some html elements are parsed with a closing tag.""" 46 | data = ["script"] 47 | 48 | expected = "" 49 | 50 | assert render(data) == expected 51 | 52 | 53 | def test_parses_boolean_attributes() -> None: 54 | """Assert that attributes without values, such as async or defer, is parsed as expected.""" 55 | data = ["script", {"async"}, {"src": "path/to/script"}] 56 | 57 | expected = '' 58 | 59 | assert render(data) == expected 60 | 61 | 62 | def test_accepts_sibling_elements() -> None: 63 | """Assert that the render function accepts a structure of top-level siblings.""" 64 | siblings = [ 65 | ["!DOCTYPE", {"html"}], 66 | ["html", ["head", ["title", "hey"]], ["body", "HELLO WORLD"]], 67 | ] 68 | 69 | expected = "heyHELLO WORLD" 70 | 71 | assert render(siblings) == expected 72 | 73 | 74 | def test_escapes_content() -> None: 75 | """Assert that the render function will HTML escape the inner content of elements.""" 76 | data = ["div", "Hello & "] 77 | 78 | expected = "
Hello & <Goodbye>
" 79 | 80 | assert render(data) == expected 81 | 82 | 83 | def test_does_not_escape_script_content() -> None: 84 | """Assert that content within a script tag is not escaped. 85 | 86 | Making it possible to add inline JS. 87 | """ 88 | script_content = "if(x.one > 2) {console.log('hello world');}" 89 | data = ["script", script_content] 90 | expected = f"" 91 | 92 | assert render(data) == expected 93 | 94 | 95 | def test_does_not_escape_comment_content() -> None: 96 | """Assert that content within an HTML comment tag is not escaped.""" 97 | data = ["div", ""] 98 | expected = "
" 99 | 100 | assert render(data) == expected 101 | 102 | 103 | def test_does_not_escape_inline_style_tag_content() -> None: 104 | """Assert that content within a style tag is not escaped. 105 | 106 | Making it possible to construct inline CSS. 107 | """ 108 | content = """ 109 | p { 110 | color: red; 111 | } 112 | 113 | div span.something { 114 | color: blue; 115 | } 116 | 117 | """ 118 | data = ["style", content] 119 | expected = f"" 120 | 121 | assert render(data) == expected 122 | 123 | 124 | def test_generates_an_element_with_children() -> None: 125 | """Assert that an element with children is rendered.""" 126 | items = ["a", "b", "c"] 127 | 128 | data = ["ul", [["li", i] for i in items]] 129 | 130 | assert render(data) == "" 131 | 132 | 133 | def test_allows_numeric_values_in_content() -> None: 134 | """Assert that numeric values are allowed as the content of an element.""" 135 | data = ["ul", ["li", 1], ["li", 2.2]] 136 | 137 | assert render(data) == "" 138 | 139 | 140 | def test_order_of_items() -> None: 141 | """Assert that items of different types are ordered as expected.""" 142 | data = ["h1", "some ", ["span.pys", ""]] 143 | 144 | assert render(data) == '

some <py>

' 145 | 146 | 147 | def test_content_as_function() -> None: 148 | """Allow defining content as a callable function, as a custom parser.""" 149 | content = "© this should not be escaped!" 150 | 151 | assert render(["div", raw("©")]) == "
©
" 152 | assert render(["div", raw(content)]) == f"
{content}
" 153 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.10" 4 | 5 | [[package]] 6 | name = "colorama" 7 | version = "0.4.6" 8 | source = { registry = "https://pypi.org/simple" } 9 | 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" } 10 | wheels = [ 11 | { 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" }, 12 | ] 13 | 14 | [[package]] 15 | name = "exceptiongroup" 16 | version = "1.2.2" 17 | source = { registry = "https://pypi.org/simple" } 18 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } 19 | wheels = [ 20 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, 21 | ] 22 | 23 | [[package]] 24 | name = "iniconfig" 25 | version = "2.0.0" 26 | source = { registry = "https://pypi.org/simple" } 27 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } 28 | wheels = [ 29 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, 30 | ] 31 | 32 | [[package]] 33 | name = "mypy" 34 | version = "1.13.0" 35 | source = { registry = "https://pypi.org/simple" } 36 | dependencies = [ 37 | { name = "mypy-extensions" }, 38 | { name = "tomli", marker = "python_full_version < '3.11'" }, 39 | { name = "typing-extensions" }, 40 | ] 41 | sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532, upload-time = "2024-10-22T21:55:47.458Z" } 42 | wheels = [ 43 | { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731, upload-time = "2024-10-22T21:54:54.221Z" }, 44 | { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276, upload-time = "2024-10-22T21:54:34.679Z" }, 45 | { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706, upload-time = "2024-10-22T21:55:45.309Z" }, 46 | { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586, upload-time = "2024-10-22T21:55:18.957Z" }, 47 | { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318, upload-time = "2024-10-22T21:55:13.791Z" }, 48 | { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027, upload-time = "2024-10-22T21:55:31.266Z" }, 49 | { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699, upload-time = "2024-10-22T21:55:34.646Z" }, 50 | { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263, upload-time = "2024-10-22T21:54:51.807Z" }, 51 | { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688, upload-time = "2024-10-22T21:55:08.476Z" }, 52 | { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811, upload-time = "2024-10-22T21:54:59.152Z" }, 53 | { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900, upload-time = "2024-10-22T21:55:37.103Z" }, 54 | { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818, upload-time = "2024-10-22T21:55:11.513Z" }, 55 | { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275, upload-time = "2024-10-22T21:54:37.694Z" }, 56 | { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783, upload-time = "2024-10-22T21:55:42.852Z" }, 57 | { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197, upload-time = "2024-10-22T21:54:43.68Z" }, 58 | { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721, upload-time = "2024-10-22T21:54:22.321Z" }, 59 | { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996, upload-time = "2024-10-22T21:54:46.023Z" }, 60 | { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043, upload-time = "2024-10-22T21:55:06.231Z" }, 61 | { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996, upload-time = "2024-10-22T21:55:25.811Z" }, 62 | { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709, upload-time = "2024-10-22T21:55:21.246Z" }, 63 | { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043, upload-time = "2024-10-22T21:55:16.617Z" }, 64 | ] 65 | 66 | [[package]] 67 | name = "mypy-extensions" 68 | version = "1.0.0" 69 | source = { registry = "https://pypi.org/simple" } 70 | sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } 71 | wheels = [ 72 | { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, 73 | ] 74 | 75 | [[package]] 76 | name = "packaging" 77 | version = "24.2" 78 | source = { registry = "https://pypi.org/simple" } 79 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } 80 | wheels = [ 81 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, 82 | ] 83 | 84 | [[package]] 85 | name = "pluggy" 86 | version = "1.5.0" 87 | source = { registry = "https://pypi.org/simple" } 88 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } 89 | wheels = [ 90 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, 91 | ] 92 | 93 | [[package]] 94 | name = "pytest" 95 | version = "8.3.4" 96 | source = { registry = "https://pypi.org/simple" } 97 | dependencies = [ 98 | { name = "colorama", marker = "sys_platform == 'win32'" }, 99 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 100 | { name = "iniconfig" }, 101 | { name = "packaging" }, 102 | { name = "pluggy" }, 103 | { name = "tomli", marker = "python_full_version < '3.11'" }, 104 | ] 105 | sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919, upload-time = "2024-12-01T12:54:25.98Z" } 106 | wheels = [ 107 | { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083, upload-time = "2024-12-01T12:54:19.735Z" }, 108 | ] 109 | 110 | [[package]] 111 | name = "python-hiccup" 112 | version = "0.4.0" 113 | source = { editable = "." } 114 | 115 | [package.dev-dependencies] 116 | dev = [ 117 | { name = "mypy" }, 118 | { name = "pytest" }, 119 | { name = "ruff" }, 120 | ] 121 | 122 | [package.metadata] 123 | 124 | [package.metadata.requires-dev] 125 | dev = [ 126 | { name = "mypy", specifier = ">=1.13.0" }, 127 | { name = "pytest", specifier = ">=8.3.4" }, 128 | { name = "ruff", specifier = ">=0.8.3" }, 129 | ] 130 | 131 | [[package]] 132 | name = "ruff" 133 | version = "0.8.3" 134 | source = { registry = "https://pypi.org/simple" } 135 | sdist = { url = "https://files.pythonhosted.org/packages/bf/5e/683c7ef7a696923223e7d95ca06755d6e2acbc5fd8382b2912a28008137c/ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3", size = 3378522, upload-time = "2024-12-12T15:17:56.196Z" } 136 | wheels = [ 137 | { url = "https://files.pythonhosted.org/packages/f8/c4/bfdbb8b9c419ff3b52479af8581026eeaac3764946fdb463dec043441b7d/ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6", size = 10535860, upload-time = "2024-12-12T15:16:58.655Z" }, 138 | { url = "https://files.pythonhosted.org/packages/ef/c5/0aabdc9314b4b6f051168ac45227e2aa8e1c6d82718a547455e40c9c9faa/ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939", size = 10346327, upload-time = "2024-12-12T15:17:02.88Z" }, 139 | { url = "https://files.pythonhosted.org/packages/1a/78/4843a59e7e7b398d6019cf91ab06502fd95397b99b2b858798fbab9151f5/ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d", size = 9942585, upload-time = "2024-12-12T15:17:05.629Z" }, 140 | { url = "https://files.pythonhosted.org/packages/91/5a/642ed8f1ba23ffc2dd347697e01eef3c42fad6ac76603be4a8c3a9d6311e/ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13", size = 10797597, upload-time = "2024-12-12T15:17:08.657Z" }, 141 | { url = "https://files.pythonhosted.org/packages/30/25/2e654bc7226da09a49730a1a2ea6e89f843b362db80b4b2a7a4f948ac986/ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18", size = 10307244, upload-time = "2024-12-12T15:17:11.603Z" }, 142 | { url = "https://files.pythonhosted.org/packages/c0/2d/a224d56bcd4383583db53c2b8f410ebf1200866984aa6eb9b5a70f04e71f/ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502", size = 11362439, upload-time = "2024-12-12T15:17:14.605Z" }, 143 | { url = "https://files.pythonhosted.org/packages/82/01/03e2857f9c371b8767d3e909f06a33bbdac880df17f17f93d6f6951c3381/ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d", size = 12078538, upload-time = "2024-12-12T15:17:18.155Z" }, 144 | { url = "https://files.pythonhosted.org/packages/af/ae/ff7f97b355da16d748ceec50e1604a8215d3659b36b38025a922e0612e9b/ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82", size = 11616172, upload-time = "2024-12-12T15:17:22.919Z" }, 145 | { url = "https://files.pythonhosted.org/packages/6a/d0/6156d4d1e53ebd17747049afe801c5d7e3014d9b2f398b9236fe36ba4320/ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452", size = 12919886, upload-time = "2024-12-12T15:17:26.693Z" }, 146 | { url = "https://files.pythonhosted.org/packages/4e/84/affcb30bacb94f6036a128ad5de0e29f543d3f67ee42b490b17d68e44b8a/ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd", size = 11212599, upload-time = "2024-12-12T15:17:31.053Z" }, 147 | { url = "https://files.pythonhosted.org/packages/60/b9/5694716bdefd8f73df7c0104334156c38fb0f77673d2966a5a1345bab94d/ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20", size = 10784637, upload-time = "2024-12-12T15:17:34.31Z" }, 148 | { url = "https://files.pythonhosted.org/packages/24/7e/0e8f835103ac7da81c3663eedf79dec8359e9ae9a3b0d704bae50be59176/ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc", size = 10390591, upload-time = "2024-12-12T15:17:37.518Z" }, 149 | { url = "https://files.pythonhosted.org/packages/27/da/180ec771fc01c004045962ce017ca419a0281f4bfaf867ed0020f555b56e/ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060", size = 10894298, upload-time = "2024-12-12T15:17:41.53Z" }, 150 | { url = "https://files.pythonhosted.org/packages/6d/f8/29f241742ed3954eb2222314b02db29f531a15cab3238d1295e8657c5f18/ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea", size = 11275965, upload-time = "2024-12-12T15:17:45.971Z" }, 151 | { url = "https://files.pythonhosted.org/packages/79/e9/5b81dc9afc8a80884405b230b9429efeef76d04caead904bd213f453b973/ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964", size = 8807651, upload-time = "2024-12-12T15:17:48.588Z" }, 152 | { url = "https://files.pythonhosted.org/packages/ea/67/7291461066007617b59a707887b90e319b6a043c79b4d19979f86b7a20e7/ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9", size = 9625289, upload-time = "2024-12-12T15:17:51.265Z" }, 153 | { url = "https://files.pythonhosted.org/packages/03/8f/e4fa95288b81233356d9a9dcaed057e5b0adc6399aa8fd0f6d784041c9c3/ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936", size = 9078754, upload-time = "2024-12-12T15:17:53.954Z" }, 154 | ] 155 | 156 | [[package]] 157 | name = "tomli" 158 | version = "2.2.1" 159 | source = { registry = "https://pypi.org/simple" } 160 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 161 | wheels = [ 162 | { 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, upload-time = "2024-11-27T22:37:54.956Z" }, 163 | { 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, upload-time = "2024-11-27T22:37:56.698Z" }, 164 | { 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, upload-time = "2024-11-27T22:37:57.63Z" }, 165 | { 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, upload-time = "2024-11-27T22:37:59.344Z" }, 166 | { 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, upload-time = "2024-11-27T22:38:00.429Z" }, 167 | { 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, upload-time = "2024-11-27T22:38:02.094Z" }, 168 | { 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, upload-time = "2024-11-27T22:38:03.206Z" }, 169 | { 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, upload-time = "2024-11-27T22:38:04.217Z" }, 170 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, 171 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, 172 | { 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, upload-time = "2024-11-27T22:38:07.731Z" }, 173 | { 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, upload-time = "2024-11-27T22:38:09.384Z" }, 174 | { 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, upload-time = "2024-11-27T22:38:10.329Z" }, 175 | { 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, upload-time = "2024-11-27T22:38:11.443Z" }, 176 | { 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, upload-time = "2024-11-27T22:38:13.099Z" }, 177 | { 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, upload-time = "2024-11-27T22:38:14.766Z" }, 178 | { 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, upload-time = "2024-11-27T22:38:15.843Z" }, 179 | { 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, upload-time = "2024-11-27T22:38:17.645Z" }, 180 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, 181 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, 182 | { 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, upload-time = "2024-11-27T22:38:21.659Z" }, 183 | { 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, upload-time = "2024-11-27T22:38:22.693Z" }, 184 | { 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, upload-time = "2024-11-27T22:38:24.367Z" }, 185 | { 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, upload-time = "2024-11-27T22:38:26.081Z" }, 186 | { 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, upload-time = "2024-11-27T22:38:27.921Z" }, 187 | { 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, upload-time = "2024-11-27T22:38:29.591Z" }, 188 | { 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, upload-time = "2024-11-27T22:38:30.639Z" }, 189 | { 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, upload-time = "2024-11-27T22:38:31.702Z" }, 190 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, 191 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, 192 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, 193 | ] 194 | 195 | [[package]] 196 | name = "typing-extensions" 197 | version = "4.12.2" 198 | source = { registry = "https://pypi.org/simple" } 199 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload-time = "2024-06-07T18:52:15.995Z" } 200 | wheels = [ 201 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload-time = "2024-06-07T18:52:13.582Z" }, 202 | ] 203 | --------------------------------------------------------------------------------