├── .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"{element}>"]
85 |
86 | if _closing_tag(element):
87 | return [f"<{begin}>", f"{element}>"]
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 | [](https://dl.circleci.com/status-badge/redirect/gh/DavidVujic/python-hiccup/tree/main)
6 |
7 | [](https://codescene.io/projects/59968)
8 |
9 | [](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 |
37 | - one
38 | - two
39 | - three
40 |
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 |
--------------------------------------------------------------------------------