├── .github
└── workflows
│ └── testing.yml
├── .gitignore
├── .python-version
├── CHANGELOG.md
├── LICENSE
├── README.md
├── hypermedia
├── __init__.py
├── audio_and_video.py
├── basic.py
├── fastapi.py
├── formatting.py
├── forms_and_input.py
├── frames.py
├── images.py
├── links.py
├── lists.py
├── meta_info.py
├── models
│ ├── __init__.py
│ ├── base.py
│ └── elements.py
├── programming.py
├── py.typed
├── styles_and_semantics.py
├── tables.py
└── types
│ ├── __init__.py
│ ├── attributes.py
│ ├── styles.py
│ └── types.py
├── poetry.lock
├── pyproject.toml
├── tests
├── __init__.py
├── attributes
│ ├── test_aliased_attributes.py
│ └── test_styles.py
├── examples
│ └── test_basic_html_page.py
├── integration
│ ├── test_audio_and_video.py
│ ├── test_basic_html.py
│ ├── test_formatting.py
│ ├── test_forms_and_input.py
│ ├── test_frames.py
│ ├── test_images.py
│ ├── test_links.py
│ ├── test_lists.py
│ ├── test_meta_info.py
│ ├── test_programming.py
│ ├── test_styles_and_semantics.py
│ └── test_tables.py
├── models
│ ├── basic_element
│ │ └── test_basic_element.py
│ ├── element
│ │ ├── test_attributes.py
│ │ ├── test_element.py
│ │ ├── test_extend.py
│ │ ├── test_render_children.py
│ │ ├── test_slots.py
│ │ ├── test_svg_elements.py
│ │ └── xml_void_element
│ │ │ └── test_xml_void_element.py
│ ├── element_list
│ │ └── test_element_list.py
│ └── void_element
│ │ └── test_void_element.py
├── test_custom_dump_elements.py
├── test_safe_string.py
└── utils.py
└── uv.lock
/.github/workflows/testing.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | max-parallel: 2
16 | matrix:
17 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v4
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v5
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 | - name: Install uv
26 | uses: astral-sh/setup-uv@v5
27 | with:
28 | # Install a specific version of uv.
29 | version: "0.5.24"
30 | enable-cache: true
31 | - name: Install the dependencies
32 | run: uv sync --all-extras --group dev
33 | - name: Create gcloud key file
34 | run: openssl base64 -d -A <<< '${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}' -out key.json
35 | - name: Run code quality
36 | run: |
37 | uv run ruff check
38 | uv run mypy .
39 | - name: Run tests
40 | run: |
41 | uv run -m pytest .
42 | - name: Upload coverage reports to Codecov
43 | if: matrix.python-version == 3.12
44 | uses: codecov/codecov-action@v4.0.1
45 | with:
46 | token: ${{ secrets.CODECOV_TOKEN }}
47 | slug: thomasborgen/hypermedia
48 | file: ./coverage.xml
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 |
3 | .idea
4 | .ipynb_checkpoints
5 | .mypy_cache
6 | .vscode
7 | __pycache__
8 | .pytest_cache
9 | htmlcov
10 | dist
11 | site
12 | .coverage
13 | coverage.xml
14 | .netlify
15 | test.db
16 | log.txt
17 | Pipfile.lock
18 | env3.*
19 | env
20 | docs_build
21 | site_build
22 | venv
23 | docs.zip
24 | archive.zip
25 |
26 | # vim temporary files
27 | *~
28 | .*.sw?
29 | .cache
30 |
31 | # macOS
32 | .DS_Store
33 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Version history
2 |
3 | | Change | Bumps |
4 | | - | - |
5 | | Breaking | major |
6 | | New Feature | minor |
7 | | otherwise | patch |
8 |
9 |
10 | ## Latest Changes
11 |
12 |
13 | ## Version 5.3.4
14 |
15 | ### Features
16 |
17 | * Add `capture` attribute to input element with the allowed values `"user"` and `"environment"`
18 | * Add `dialog` as a posible value to a forms `method` attribute
19 |
20 | ### Internal
21 |
22 | * Migrate dependency manager to uv.
23 | * Remove safety check that now requires login
24 |
25 |
26 | ## Version 5.3.3
27 |
28 | * Fixes bug with svg element dumping. (#48)[https://github.com/thomasborgen/hypermedia/issues/48]
29 |
30 | ## Version 5.3.2
31 |
32 | * Fixes `style` attribute rendering.
33 |
34 | ## Version 5.3.1
35 |
36 | * Adds `lang` as an attribute to the `Html` element.
37 |
38 | ## Version 5.3.0
39 |
40 | ### Feature
41 |
42 | * Adds `SafeString` type as a wrapper around the normal `str` class. This can be used to tell hypermedia not to `html.escape` the string in any element.
43 |
44 |
45 | ```python
46 | Div("this is always escaped")
47 | Div(SafeString("This will not be escaped"))
48 | ```
49 |
50 | * Hypermedia is now cachable - All calls to `.dump()` now returns a `SafeString`. This means that caching a dumped element is now possible because we won't call `html.escape` on it again.
51 |
52 | ```python
53 | @lru_cache()
54 | def stylesheets() -> SafeString:
55 | return ElementList(
56 | Link(
57 | rel="stylesheet",
58 | href="/static/css/normalize.css",
59 | type="text/css",
60 | ),
61 | Link(
62 | rel="stylesheet",
63 | href="/static/css/simple.min.css",
64 | type="text/css",
65 | )
66 | ).dump()
67 |
68 | def base():
69 | """
70 | Once `stylesheets()` is called once, it will always use the cached String in subsequent
71 | calls to `base()`
72 | """
73 | return ElementList(
74 | Doctype(),
75 | Html(
76 | Head(
77 | Meta(charset="UTF-8"),
78 | stylesheets(),
79 | slot="head",
80 | ),
81 | Body(...)
82 | lan="en",
83 | ),
84 | )
85 | ```
86 |
87 | * `Script` and `Style` elements wraps input in `SafeString` by default.
88 |
89 | ```python
90 | Script("This is wrapped in `SafeString` by default")
91 | Style("This is wrapped in `SafeString` by default")
92 | ```
93 |
94 |
95 | ## Version 5.2.0
96 |
97 | ### Feature
98 |
99 | * Can now use hyperscript with the `_` attribute that renders as: `_='value'`
100 |
101 | ### Fix
102 |
103 | * Fix missing Alias for the `for_` attribute. It now renders correctly as `for='value'`
104 |
105 |
106 | ## Version 5.1.0
107 |
108 | ### Fix
109 |
110 | * Calling `dump()`, more specifically `render_attributes()` popped out `class_` and `classes` attributes from the element. So subsequent calls would be missing the attributes. [PR](https://github.com/thomasborgen/hypermedia/pull/32)
111 |
112 |
113 | ## Version 5.0.0
114 |
115 | ### Breaking
116 |
117 | We need a way to handle html attributes like `hx-on:click` or `any_weird.format`. At the same time I want it to be easy to add attributes. I've decided to let all kwargs added normally like `data_test` where no `Alias` is defined will have its underscores replaced with hyphens. Any kwarg that has a `$` prefix, will be outputted as is minus the `$`. This kwarg can only have been added by spreading a dict into the constructor, so we can assume that it is done on purpose.
118 |
119 | * Any attribute that does not have an Alias will have any underscores (`_`) changed to hyphens (`-`).
120 | * Any attribute that is prefixed with `$` will be outputted as is without the first `$`.
121 |
122 | ie
123 |
124 | ```python
125 | # Is a specified attribute(typed) with an Alias:
126 | Div(on_afterprint="test") #
127 | # Unspecified attribute without Alias:
128 | Div(data_test="test") #
129 | # Spread without $ prefix gets its underscores changed to hyphens.
130 | Div(**{"funky-format_test.value": True}) #
131 | # Spread with $ prefix
132 | Div(**{"$funky-format_test.value": True}) #
133 | Div(**{"$funky-format_test.value": "name"}) #
134 | ```
135 |
136 | ### Feature
137 |
138 | * `classes` and `class_` attributes are now merged.
139 |
140 | ```python
141 | def test_class_and_classes_are_combined() -> None:
142 | element = TestElement(class_="three", classes=["one", "two"])
143 |
144 | assert element._render_attributes() == " class='one two three'"
145 | ```
146 |
147 | ## Version 4.1.0
148 |
149 | ### Feature
150 |
151 | Added types for html elements and their attributes. The type system is taken from [Ludic.](https://github.com/getludic/ludic). It is just so incredibly good. This gives us autocomplete support for everything.
152 |
153 | * Types html elements and attributes.
154 |
155 | ### Internal
156 |
157 | * Document how to add attributes with symbols like dash and period.
158 |
159 | ## Version 4.0.0
160 |
161 | ### Breaking
162 |
163 | Instead of having to use `text` and `composed_text` keywords you can now write text like this:
164 |
165 | ```python
166 | Div("My text", Bold("My bold text"), "Tail")
167 | ```
168 |
169 | * Removes `text` argument.
170 | * Removes `composed_text` argument.
171 |
172 | ### Features
173 |
174 | * The direct `*args` are now is a list of `Element | str`. This is basically exactly how `composed_text` worked, If we have a string child, we escape it, otherwise we call the child Elements `.dump()`. This should be a lot smoother to work with and read.
175 |
176 | ## Version 3.0.0
177 |
178 | ### Breaking
179 |
180 | * Rename `htmx.py` to `fastapi.py`. The decorators were only for fastAPI. If we support django or flask, then they should get their own versions. This change facilitates for that so we don't have to have a breaking change further down the line.
181 | * Don't expose `@htmx` and `@full` decorators directly from `hypermedia`, but require them to be imported from `hypermedia.fastapi`.
182 |
183 | ### Housekeeping
184 |
185 | * Readme file: Add features section, improve examples
186 |
187 | ## Version 2.2.0
188 |
189 | ### Features
190 |
191 | * Escape all text by default.
192 | * Add `composed_text` property that takes a list of strings or Elements and dumps that. This can be used to render strings with inline elements like ` `, `` or ``. used like: `composed_text=["regular", Italic(text="italic"), Bold(text="bold"), "regular"]`
193 |
194 | ### Internal
195 |
196 | * Test elements with custom dump override like `Docstring` and `Comment` for string escaping.
197 | * Add tests to composed_text.
198 |
199 | ## Version 2.1.2
200 |
201 | * Expose models directly in `__init__` file for typing/extension purposes.
202 | * #11 Ignore `None` valued attributes. This makes for easier programming.
203 | * Properly Type attributes (an elements kwargs) `str | bool | None`.
204 | * Improve test coverage.
205 |
206 |
207 | ## Version 2.1.1
208 |
209 | ### Fix
210 |
211 | * Expose Path, Rect, Rectangle, Circle, Ellipse, Line, Polyline and Polygon Elements directly in `__init__` file.
212 |
213 | ## Version 2.1.0
214 |
215 | ### Features
216 |
217 | * Added Path, Rect, Rectangle, Circle, Ellipse, Line, Polyline and Polygon Elements for when constructing and manipulating an SVG.
218 |
219 | ## Version 2.0.1
220 |
221 | ### Fix
222 |
223 | * Now return partial render if full is not available. Consider if we should raise an exception instead.
224 | * Protocol Typing for the FastAPI Request did not work as expected. Fall back to Any for now.
225 |
226 | ## Version 2.0.0 - Single quotes!
227 |
228 | ### Breaking
229 |
230 | * rename base_models.py -> models.py
231 | * Use single quotes for attribute values.
232 |
233 | ### Feature
234 |
235 | * Using single quotes for attributes lets us support `hx-vals`, since it expects a doubled quoted json string.
236 |
237 |
238 | ### Fix
239 |
240 | * Rename `hr`->`Hr`, add alias `HorizontalRule` and export both in `__init__.py`
241 |
242 | ## Version 1.0.0 - Hypermedia released
243 |
244 | Hypermedia is an opinionated way to work with HTML in python and FastAPI. Hypermedia is designed to work with htmx.
245 |
246 | ### Features
247 |
248 | * Composable Html generation with slots.
249 | * Typed
250 | * Works with FastAPI, and any other web framework.
251 | * Works with HTMX
252 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Thomas Borgen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hypermedia
2 |
3 | Hypermedia is a pure python library for working with `HTML`. Hypermedia's killer feature is that it is composable through a `slot` concept. Because of that, it works great with `> htmx` where you need to respond with both __partials__ and __full page__ html.
4 |
5 | Hypermedia is made to work with `FastAPI` and `> htmx`, but can be used by anything to create HTML.
6 |
7 | ## Features
8 |
9 | * Build __HTML__ with python classes
10 | * __Composable__ templates through a __slot__ system
11 | * Seamless integration with __> htmx__
12 | * Fully typed and __Autocompletion__ for html/htmx attributes and styles
13 | * Opinionated simple decorator for __FastAPI__
14 | * Unlike other template engines like Jinja2 we have full typing since we never leave python land.
15 |
16 | ## The Basics
17 |
18 | All html tags can be imported directly like:
19 |
20 | ```python
21 | from hypermedia import Html, Body, Div, A
22 | ```
23 |
24 | Tags are nested by adding children in the constructor:
25 |
26 | ```python
27 | from hypermedia import Html, Body, Div
28 |
29 | Html(Body(Div(), Div()))
30 | ```
31 |
32 | Add text to your tag:
33 |
34 | ```python
35 | from hypermedia import Div
36 |
37 | Div("Hello world!")
38 | ```
39 |
40 | use `.dump()` to dump your code to html.
41 |
42 |
43 | ```python
44 | from hypermedia import Bold, Div
45 |
46 | Div("Hello ", Bold("world!")).dump()
47 |
48 | # outputs
49 | # ' Hello world!
'
50 | ```
51 |
52 | ## Composability with slots
53 |
54 | ```python
55 | from hypermedia import Html, Body, Div, Menu, Header, Div, Ul, Li
56 |
57 | base = Html(
58 | Body(
59 | Menu(slot="menu"),
60 | Header("my header", slot="header"),
61 | Div(slot="content"),
62 | ),
63 | )
64 |
65 | menu = Ul(Li(text="main"))
66 | content = Div(text="Some content")
67 |
68 | base.extend("menu", menu)
69 | base.extend("content", content)
70 |
71 | base.dump()
72 |
73 | # outputs
74 | # ' '
75 | ```
76 |
77 | ## Attribute names with special characters
78 |
79 | Most `html` and `>htmx` attributes are typed and has Aliases where needed. That means that most of the time you won't have to think about this and it should _just work_.
80 |
81 | The attribute name output rules are:
82 |
83 | 1. Any attribute that does not have an Alias will have any underscores (`_`) changed to hyphens (`-`).
84 | 2. Any attribute that is prefixed with `$` will be outputted as is without the first `$`.
85 |
86 | ie
87 |
88 | ```python
89 | # Is a specified attribute(typed) with an Alias:
90 | Div(on_afterprint="test") #
91 |
92 | # Unspecified attribute without Alias:
93 | Div(data_test="test") #
94 |
95 | # Spread without $ prefix gets its underscores changed to hyphens:
96 | Div(**{"funky-format_test.value": True}) #
97 |
98 | # Spread with $ prefix
99 | Div(**{"$funky-format_test.value": True}) #
100 | Div(**{"$funky-format_test.value": "name"}) #
101 | ```
102 |
103 | Note: About the > HTMX attributes. [The documentation](https://htmx.org/attributes/hx-on/) specifies that all hx attributes can be written with all dashes. Because of that Hypermedia lets users write hx attributes with underscores and Hypermedia changes them to dashes for you.
104 |
105 | ```python
106 |
107 | Div(hx_on_click='alert("Making a request!")')
108 | #
109 | # Which is equivalent to:
110 | #
111 |
112 | Div(hx_on_htmx_before_request='alert("Making a request!")')
113 | #
114 |
115 | # shorthand version of above statement
116 | Div(hx_on__before_request='alert("Making a request!")')
117 | #
118 | ```
119 |
120 | # HTMX
121 |
122 | ## The Concept
123 |
124 | The core concept of HTMX is that the server responds with HTML, and that we can choose with a CSS selector which part of the page will be updated with the HTML response from the server.
125 |
126 | This means that we want to return snippets of HTML, or `partials`, as they are also called.
127 |
128 | ## The Problem
129 |
130 | The problem is that we need to differentiate if it's HTMX that called an endpoint for a `partial`, or if the user just navigated to it and needs the `whole page` back in the response.
131 |
132 | ## The Solution
133 |
134 | HTMX provides an `HX-Request` header that is always true. We can check for this header to know if it's an HTMX request or not.
135 |
136 | We've chosen to implement that check in a `@htmx` decorator. The decorator expects `partial` and optionally `full` arguments in the endpoint definition. These must be resolved by FastAPI's dependency injection system.
137 |
138 | ```python
139 | from hypermedia.fastapi import htmx, full
140 | ```
141 |
142 | The `partial` argument is a function that returns the partial HTML.
143 | The `full` argument is a function that needs to return the whole HTML, for example on first navigation or a refresh.
144 |
145 | > Note: The `full` argument needs to be wrapped in `Depends` so that the full function's dependencies are resolved! Hypermedia ships a `full` wrapper, which is basically just making the function lazily loaded. The `full` wrapper _must_ be used, and the `@htmx` decorator will call the lazily wrapped function to get the full HTML page when needed.
146 |
147 | > Note: The following code is in FastAPI, but could have been anything. As long as you check for HX-Request and return partial/full depending on if it exists or not.
148 |
149 | ```python
150 | def render_base():
151 | """Return base HTML, used by all full renderers."""
152 | return ElementList(Doctype(), Body(slot="body"))
153 |
154 |
155 | def render_fruits_partial():
156 | """Return partial HTML."""
157 | return Div(Ul(Li("Apple"), Li("Banana"), Button("reload", hx_get="/fruits")))
158 |
159 |
160 | def render_fruits():
161 | """Return base HTML extended with `render_fruits_partial`."""
162 | return render_base().extend("body", render_fruits_partial())
163 |
164 |
165 | @router.get("/fruits", response_class=HTMLResponse)
166 | @htmx
167 | async def fruits(
168 | request: Request,
169 | partial: Element = Depends(render_fruits_partial),
170 | full: Element = Depends(full(render_fruits)),
171 | ) -> None:
172 | """Return the fruits page, partial or full."""
173 | pass
174 | ```
175 |
176 | That's it. Now we have separated the rendering from the endpoint definition and handled returning partials and full pages when needed. Doing a full refresh will render the whole page. Clicking the button will make a htmx request and only return the partial.
177 |
178 | What is so cool about this is that it works so well with FastAPI's dependency injection.
179 |
180 | ## Really making use of dependency injection
181 |
182 |
183 | ```python
184 | fruits = {1: "apple", 2: "orange"}
185 |
186 | def get_fruit(fruit_id: int = Path(...)) -> str:
187 | """Get fruit ID from path and return the fruit."""
188 | return fruits[fruit_id]
189 |
190 | def render_fruit_partial(
191 | fruit: str = Depends(get_fruit),
192 | ) -> Element:
193 | """Return partial HTML."""
194 | return Div(fruit)
195 |
196 | def render_fruit(
197 | partial: Element = Depends(render_fruit_partial),
198 | ):
199 | return render_base().extend("content", partial)
200 |
201 | @router.get("/fruits/{fruit_id}", response_class=HTMLResponse)
202 | @htmx
203 | async def fruit(
204 | request: Request,
205 | partial: Element = Depends(render_fruit_partial),
206 | full: Element = Depends(full(render_fruit)),
207 | ) -> None:
208 | """Return the fruit page, partial or full."""
209 | pass
210 | ```
211 |
212 | Here we do basically the same as the previous example, except that we make use of FastAPI's great dependency injection system. Notice the path of our endpoint has `fruit_id`. This is not used in the definition. However, if we look at our partial renderer, it depends on `get_fruit`, which is a function that uses FastAPI's `Path resolver`. The DI then resolves (basically calls) the fruit function, passes the result into our partial function, and we can use it as a value!
213 |
214 | __This pattern with DI, Partials, and full renderers is what makes using FastAPI with HTMX worth it.__
215 |
216 | In addition to this, one thing many are concerned about with HTMX is that since we serve HTML, there will be no way for another app/consumer to get a fruit in JSON. But the solution is simple:
217 |
218 | Because we already have a dependency that retrieves the fruit, we just need to add a new endpoint:
219 |
220 | ```python
221 | @router.get("/api/fruit/{fruit_id}")
222 | async def fruit(
223 | request: Request,
224 | fruit: str = Depends(get_fruit),
225 | ) -> str:
226 | """Return the fruit data."""
227 | return fruit
228 | ```
229 |
230 | Notice we added `/api/` and just used DI to resolve the fruit and just returned it. Nice!
231 |
--------------------------------------------------------------------------------
/hypermedia/__init__.py:
--------------------------------------------------------------------------------
1 | from hypermedia.audio_and_video import Audio, Source, Track, Video
2 | from hypermedia.basic import (
3 | H1,
4 | H2,
5 | H3,
6 | H4,
7 | H5,
8 | H6,
9 | Body,
10 | Br,
11 | Break,
12 | Comment,
13 | Doctype,
14 | Head,
15 | Header1,
16 | Header2,
17 | Header3,
18 | Header4,
19 | Header5,
20 | Header6,
21 | HorizontalRule,
22 | Hr,
23 | Html,
24 | P,
25 | Paragraph,
26 | Title,
27 | )
28 | from hypermedia.formatting import (
29 | Abbr,
30 | Abbreviation,
31 | Address,
32 | B,
33 | Bdi,
34 | Bdo,
35 | BiDirectionalIsolation,
36 | BiDirectionalOverride,
37 | Blockquote,
38 | Bold,
39 | Cite,
40 | Code,
41 | DefinitionElement,
42 | Del,
43 | Deleted,
44 | Dfn,
45 | Em,
46 | Emphasized,
47 | I,
48 | Ins,
49 | Inserted,
50 | Italic,
51 | Kbd,
52 | Keyboard,
53 | Mark,
54 | Meter,
55 | Pre,
56 | Preformatted,
57 | Progress,
58 | Q,
59 | Quotation,
60 | Rp,
61 | Rt,
62 | S,
63 | Samp,
64 | SampleOutput,
65 | Small,
66 | StrikeThrough,
67 | Strong,
68 | Sub,
69 | Subscripted,
70 | Sup,
71 | Superscripted,
72 | Template,
73 | Time,
74 | U,
75 | Unarticulated,
76 | Var,
77 | Variable,
78 | Wbr,
79 | WordBreakOpportunity,
80 | ruby,
81 | )
82 | from hypermedia.forms_and_input import (
83 | Button,
84 | DataList,
85 | Fieldset,
86 | Form,
87 | Input,
88 | Label,
89 | Legend,
90 | OptGroup,
91 | Option,
92 | OptionGroup,
93 | Output,
94 | Select,
95 | TextArea,
96 | )
97 | from hypermedia.frames import IFrame
98 | from hypermedia.images import (
99 | Area,
100 | Canvas,
101 | Circle,
102 | Ellipse,
103 | FigCaption,
104 | Figure,
105 | FigureCaption,
106 | Image,
107 | Img,
108 | Line,
109 | Map,
110 | Path,
111 | Picture,
112 | Polygon,
113 | Polyline,
114 | Rect,
115 | Rectangle,
116 | Svg,
117 | )
118 | from hypermedia.links import (
119 | A,
120 | Anchor,
121 | Link,
122 | Nav,
123 | )
124 | from hypermedia.lists import (
125 | Dd,
126 | DescriptionList,
127 | DescriptionListTerm,
128 | DescriptionListTermDescription,
129 | Dl,
130 | Dt,
131 | Li,
132 | ListItem,
133 | Menu,
134 | Ol,
135 | OrderedList,
136 | Ul,
137 | UnorderedList,
138 | )
139 | from hypermedia.meta_info import Base, Meta
140 | from hypermedia.models import Element, ElementList
141 | from hypermedia.programming import Embed, NoScript, Object, Script
142 | from hypermedia.styles_and_semantics import (
143 | Article,
144 | Aside,
145 | Data,
146 | Details,
147 | Dialog,
148 | Div,
149 | Footer,
150 | Header,
151 | HeaderGroup,
152 | HGroup,
153 | Main,
154 | Search,
155 | Section,
156 | Span,
157 | Style,
158 | Summary,
159 | )
160 | from hypermedia.tables import (
161 | Caption,
162 | Col,
163 | ColGroup,
164 | Column,
165 | ColumnGroup,
166 | Table,
167 | TableBody,
168 | TableData,
169 | TableFoot,
170 | TableHead,
171 | TableHeader,
172 | TableRow,
173 | TBody,
174 | Td,
175 | TFoot,
176 | Th,
177 | THead,
178 | Tr,
179 | )
180 | from hypermedia.types.types import SafeString
181 |
182 | __all__ = [ # noqa: RUF022
183 | # Models
184 | "Element",
185 | "ElementList",
186 | # Types
187 | "SafeString",
188 | # audio and video
189 | "Audio",
190 | "Source",
191 | "Track",
192 | "Video",
193 | # basic
194 | "Doctype",
195 | "Html",
196 | "Head",
197 | "Title",
198 | "Body",
199 | "H1",
200 | "Header1",
201 | "H2",
202 | "Header2",
203 | "H3",
204 | "Header3",
205 | "H4",
206 | "Header4",
207 | "H5",
208 | "Header5",
209 | "H6",
210 | "Header6",
211 | "P",
212 | "Paragraph",
213 | "Br",
214 | "Break",
215 | "Hr",
216 | "HorizontalRule",
217 | "Comment",
218 | # formatting
219 | "Abbr",
220 | "Abbreviation",
221 | "Address",
222 | "B",
223 | "Bold",
224 | "Bdi",
225 | "BiDirectionalIsolation",
226 | "Bdo",
227 | "BiDirectionalOverride",
228 | "Blockquote",
229 | "Cite",
230 | "Code",
231 | "Del",
232 | "Deleted",
233 | "Dfn",
234 | "DefinitionElement",
235 | "Em",
236 | "Emphasized",
237 | "I",
238 | "Italic",
239 | "Ins",
240 | "Inserted",
241 | "Kbd",
242 | "Keyboard",
243 | "Mark",
244 | "Meter",
245 | "Pre",
246 | "Preformatted",
247 | "Progress",
248 | "Q",
249 | "Quotation",
250 | "Rp",
251 | "Rt",
252 | "ruby",
253 | "S",
254 | "StrikeThrough",
255 | "Samp",
256 | "SampleOutput",
257 | "Small",
258 | "Strong",
259 | "Sub",
260 | "Subscripted",
261 | "Sup",
262 | "Superscripted",
263 | "Template",
264 | "Time",
265 | "U",
266 | "Unarticulated",
267 | "Var",
268 | "Variable",
269 | "Wbr",
270 | "WordBreakOpportunity",
271 | # forms and input
272 | "Form",
273 | "Input",
274 | "TextArea",
275 | "Button",
276 | "Select",
277 | "OptGroup",
278 | "OptionGroup",
279 | "Option",
280 | "Label",
281 | "Fieldset",
282 | "Legend",
283 | "DataList",
284 | "Output",
285 | # frames
286 | "IFrame",
287 | # images
288 | "Img",
289 | "Image",
290 | "Map",
291 | "Area",
292 | "Canvas",
293 | "FigCaption",
294 | "FigureCaption",
295 | "Figure",
296 | "Picture",
297 | "Svg",
298 | "Path",
299 | "Rect",
300 | "Rectangle",
301 | "Circle",
302 | "Ellipse",
303 | "Line",
304 | "Polyline",
305 | "Polygon",
306 | # links
307 | "A",
308 | "Anchor",
309 | "Link",
310 | "Nav",
311 | # lists
312 | "Menu",
313 | "Ul",
314 | "UnorderedList",
315 | "Ol",
316 | "OrderedList",
317 | "Li",
318 | "ListItem",
319 | "Dl",
320 | "DescriptionList",
321 | "Dt",
322 | "DescriptionListTerm",
323 | "Dd",
324 | "DescriptionListTermDescription",
325 | # meta info
326 | "Head",
327 | "Meta",
328 | "Base",
329 | # programming
330 | "Script",
331 | "NoScript",
332 | "Embed",
333 | "Object",
334 | # styles and semantics
335 | "Style",
336 | "Div",
337 | "Span",
338 | "Header",
339 | "HGroup",
340 | "HeaderGroup",
341 | "Footer",
342 | "Main",
343 | "Section",
344 | "Search",
345 | "Article",
346 | "Aside",
347 | "Details",
348 | "Dialog",
349 | "Summary",
350 | "Data",
351 | # table
352 | "Table",
353 | "Caption",
354 | "Th",
355 | "TableHeader",
356 | "Tr",
357 | "TableRow",
358 | "Td",
359 | "TableData",
360 | "THead",
361 | "TableHead",
362 | "TBody",
363 | "TableBody",
364 | "TFoot",
365 | "TableFoot",
366 | "Col",
367 | "Column",
368 | "ColGroup",
369 | "ColumnGroup",
370 | ]
371 |
--------------------------------------------------------------------------------
/hypermedia/audio_and_video.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Unpack
2 |
3 | from hypermedia.models import BasicElement, VoidElement
4 | from hypermedia.types.attributes import (
5 | AudioAttrs,
6 | SourceAttrs,
7 | TrackAttrs,
8 | VideoAttrs,
9 | )
10 | from hypermedia.types.types import AnyChildren
11 |
12 |
13 | class Audio(BasicElement[AnyChildren, AudioAttrs]):
14 | """Defines sound content."""
15 |
16 | tag: str = "audio"
17 |
18 | def __init__(self, **attributes: Unpack[AudioAttrs]) -> None:
19 | super().__init__(**attributes)
20 |
21 |
22 | class Source(VoidElement[SourceAttrs]):
23 | """Defines multiple media resources for media elements.
24 |
25 | `video`, `audio` and `picture`.
26 | """
27 |
28 | tag: str = "source"
29 |
30 | def __init__(self, **attributes: Unpack[SourceAttrs]) -> None:
31 | super().__init__(**attributes)
32 |
33 |
34 | class Track(VoidElement[TrackAttrs]):
35 | """Defines text tracks for media elements (`video` and `audio`)."""
36 |
37 | tag: str = "track"
38 |
39 | def __init__(self, **attributes: Unpack[TrackAttrs]) -> None:
40 | super().__init__(**attributes)
41 |
42 |
43 | class Video(BasicElement[AnyChildren, VideoAttrs]):
44 | """Defines a video or movie."""
45 |
46 | tag: str = "video"
47 |
48 | def __init__(self, **attributes: Unpack[VideoAttrs]) -> None:
49 | super().__init__(**attributes)
50 |
--------------------------------------------------------------------------------
/hypermedia/basic.py:
--------------------------------------------------------------------------------
1 | from html import escape
2 |
3 | from typing_extensions import Unpack
4 |
5 | from hypermedia.models import BasicElement, Element, VoidElement
6 | from hypermedia.models.elements import ElementStrict
7 | from hypermedia.types.attributes import (
8 | GlobalAttrs,
9 | HtmlTagAttrs,
10 | HypermediaAttrs,
11 | NoAttrs,
12 | )
13 | from hypermedia.types.types import AnyChildren, PrimitiveChildren, SafeString
14 |
15 | """
16 | All basic html tags as defined by W3Schools.
17 | """
18 |
19 |
20 | class Doctype(Element):
21 | """Defines the document type."""
22 |
23 | def dump(self) -> SafeString:
24 | """Dump doctype string."""
25 | return SafeString("")
26 |
27 |
28 | class Html(ElementStrict["Head", "Body", HtmlTagAttrs]):
29 | """Defines an HTML document."""
30 |
31 | tag: str = "html"
32 |
33 | def __init__(
34 | self,
35 | *children: Unpack[tuple["Head", "Body"]],
36 | **attributes: Unpack[HtmlTagAttrs],
37 | ) -> None:
38 | super().__init__(*children, **attributes)
39 |
40 |
41 | class Head(BasicElement[AnyChildren, HypermediaAttrs]):
42 | """Contains metadata/information for the document."""
43 |
44 | tag: str = "head"
45 |
46 | def __init__(
47 | self, *children: AnyChildren, **attributes: Unpack[HypermediaAttrs]
48 | ) -> None:
49 | super().__init__(*children, **attributes)
50 |
51 |
52 | class Title(BasicElement[PrimitiveChildren, NoAttrs]):
53 | """Defines a title for the document."""
54 |
55 | tag: str = "title"
56 |
57 | def __init__(
58 | self, *children: PrimitiveChildren, **attributes: Unpack[NoAttrs]
59 | ) -> None:
60 | super().__init__(*children, **attributes)
61 |
62 |
63 | class Body(BasicElement[AnyChildren, GlobalAttrs]):
64 | """Defines the document's body."""
65 |
66 | tag: str = "body"
67 |
68 | def __init__(
69 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
70 | ) -> None:
71 | super().__init__(*children, **attributes)
72 |
73 |
74 | class H1(BasicElement[AnyChildren, GlobalAttrs]):
75 | """Defines HTML heading 1."""
76 |
77 | tag: str = "h1"
78 |
79 | def __init__(
80 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
81 | ) -> None:
82 | super().__init__(*children, **attributes)
83 |
84 |
85 | class Header1(H1):
86 | """Alias for h1 tag."""
87 |
88 |
89 | class H2(BasicElement[AnyChildren, GlobalAttrs]):
90 | """Defines HTML heading 2."""
91 |
92 | tag: str = "h2"
93 |
94 | def __init__(
95 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
96 | ) -> None:
97 | super().__init__(*children, **attributes)
98 |
99 |
100 | class Header2(H2):
101 | """Alias for h2 tag."""
102 |
103 |
104 | class H3(BasicElement[AnyChildren, GlobalAttrs]):
105 | """Defines HTML heading 3."""
106 |
107 | tag: str = "h3"
108 |
109 | def __init__(
110 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
111 | ) -> None:
112 | super().__init__(*children, **attributes)
113 |
114 |
115 | class Header3(H3):
116 | """Alias for h3 tag."""
117 |
118 |
119 | class H4(BasicElement[AnyChildren, GlobalAttrs]):
120 | """Defines HTML heading 4."""
121 |
122 | tag: str = "h4"
123 |
124 | def __init__(
125 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
126 | ) -> None:
127 | super().__init__(*children, **attributes)
128 |
129 |
130 | class Header4(H4):
131 | """Alias for h4 tag."""
132 |
133 |
134 | class H5(BasicElement[AnyChildren, GlobalAttrs]):
135 | """Defines HTML heading 5."""
136 |
137 | tag: str = "h5"
138 |
139 | def __init__(
140 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
141 | ) -> None:
142 | super().__init__(*children, **attributes)
143 |
144 |
145 | class Header5(H5):
146 | """Alias for h5 tag."""
147 |
148 |
149 | class H6(BasicElement[AnyChildren, GlobalAttrs]):
150 | """Defines HTML heading 6."""
151 |
152 | tag: str = "h6"
153 |
154 | def __init__(
155 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
156 | ) -> None:
157 | super().__init__(*children, **attributes)
158 |
159 |
160 | class Header6(H6):
161 | """For h6 tag."""
162 |
163 |
164 | class P(BasicElement[AnyChildren, GlobalAttrs]):
165 | """Defines a paragraph."""
166 |
167 | tag: str = "p"
168 |
169 | def __init__(
170 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
171 | ) -> None:
172 | super().__init__(*children, **attributes)
173 |
174 |
175 | class Paragraph(P):
176 | """Alias for p tag."""
177 |
178 |
179 | class Br(VoidElement[GlobalAttrs]):
180 | """Inserts a single line break."""
181 |
182 | tag: str = "br"
183 |
184 | def __init__(self, **attributes: Unpack[GlobalAttrs]) -> None:
185 | super().__init__(**attributes)
186 |
187 |
188 | class Break(Br):
189 | """Alias for br tag."""
190 |
191 |
192 | class Hr(VoidElement[GlobalAttrs]):
193 | """Defines a thematic change in the content."""
194 |
195 | tag: str = "hr"
196 |
197 | def __init__(self, **attributes: Unpack[GlobalAttrs]) -> None:
198 | super().__init__(**attributes)
199 |
200 |
201 | class HorizontalRule(Hr):
202 | """Alias for hr tag."""
203 |
204 |
205 | class Comment(ElementStrict[PrimitiveChildren, NoAttrs]):
206 | """Defines a comment."""
207 |
208 | children = list[str]
209 |
210 | def __init__(self, *children: PrimitiveChildren) -> None:
211 | """Initialize class."""
212 | super().__init__(*children)
213 |
214 | def dump(self) -> SafeString:
215 | """Dump to html."""
216 | return SafeString(
217 | "".format(
218 | text="".join(escape(child) for child in self.children) # type: ignore
219 | )
220 | )
221 |
222 |
223 | # william <- my son writing his name.
224 |
--------------------------------------------------------------------------------
/hypermedia/fastapi.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from typing import (
3 | Any,
4 | Callable,
5 | Coroutine,
6 | ParamSpec,
7 | Protocol,
8 | TypeVar,
9 | )
10 |
11 | from hypermedia.models import Element
12 |
13 | Param = ParamSpec("Param")
14 | ReturnType = TypeVar("ReturnType")
15 |
16 |
17 | class RequestPartialAndFull(Protocol):
18 | """Requires, `request`, `partial` and `full` args on decorated function."""
19 |
20 | def __call__( # noqa: D102
21 | self, request: Any, partial: Element, full: Element
22 | ) -> Coroutine[Any, Any, None]: ...
23 |
24 |
25 | class RequestAndPartial(Protocol):
26 | """Requires, `request` and `partial` args on decorated function."""
27 |
28 | def __call__( # noqa: D102
29 | self, request: Any, partial: Element
30 | ) -> Coroutine[Any, Any, None]: ...
31 |
32 |
33 | def htmx(
34 | func: RequestPartialAndFull | RequestAndPartial,
35 | ) -> Callable[..., str]:
36 | """Wrap a FastAPI endpoint, to enable partial and full rendering.
37 |
38 | The endpoint function _must_ have a partial render dependency, and
39 | _can_ have a full render dependency.
40 |
41 | When htmx makes a request from the the users browser, the partial render
42 | is called and returned. Otherwise, the full renderer is called.
43 |
44 | Use FastAPI dependency injection to resolve data for your templates.
45 |
46 | Make sure to use the `@full` decorator on the full renderer to prevent
47 | it from being evaluated before it is needed.
48 | """
49 |
50 | @wraps(func)
51 | async def wrapper(
52 | *,
53 | request: Any,
54 | partial: Element,
55 | full: None | Callable[..., Element] = None,
56 | ) -> str:
57 | """Wrap function."""
58 | hx_request = "HX-Request" in request.headers
59 | if hx_request:
60 | return partial.dump()
61 |
62 | # Return partial if full render is not available.
63 | if full is None:
64 | return partial.dump()
65 |
66 | return full().dump()
67 |
68 | return wrapper # type: ignore
69 |
70 |
71 | def full(
72 | func: Callable[Param, ReturnType],
73 | ) -> Callable[Param, Coroutine[Any, Any, Callable[[], ReturnType]]]:
74 | """Wrap the full page render dependency and makes it lazy."""
75 |
76 | @wraps(func)
77 | async def wrapper(
78 | *args: Param.args,
79 | **kwargs: Param.kwargs,
80 | ) -> Callable[[], ReturnType]:
81 | """Wrap function."""
82 | return lambda: func(*args, **kwargs)
83 |
84 | return wrapper
85 |
--------------------------------------------------------------------------------
/hypermedia/formatting.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Unpack
2 |
3 | from hypermedia.models import BasicElement, VoidElement
4 | from hypermedia.types.attributes import (
5 | BlockquoteAttrs,
6 | DelAttrs,
7 | GlobalAttrs,
8 | HtmlAttrs,
9 | InsAttrs,
10 | MeterAttrs,
11 | ProgressAttrs,
12 | QAttrs,
13 | TimeAttrs,
14 | )
15 | from hypermedia.types.types import AnyChildren
16 |
17 |
18 | class Abbr(BasicElement[AnyChildren, GlobalAttrs]):
19 | """Defines an abbreviation or an acronym."""
20 |
21 | tag: str = "abbr"
22 |
23 | def __init__(
24 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
25 | ) -> None:
26 | super().__init__(*children, **attributes)
27 |
28 |
29 | class Abbreviation(Abbr):
30 | """Alias for `Abbr`."""
31 |
32 |
33 | class Address(BasicElement[AnyChildren, GlobalAttrs]):
34 | """Defines contact information for the author of a document/article."""
35 |
36 | tag: str = "address"
37 |
38 | def __init__(
39 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
40 | ) -> None:
41 | super().__init__(*children, **attributes)
42 |
43 |
44 | class B(BasicElement[AnyChildren, GlobalAttrs]):
45 | """Defines bold text."""
46 |
47 | tag: str = "b"
48 |
49 | def __init__(
50 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
51 | ) -> None:
52 | super().__init__(*children, **attributes)
53 |
54 |
55 | class Bold(B):
56 | """Alias for `B`."""
57 |
58 |
59 | class Bdi(BasicElement[AnyChildren, GlobalAttrs]):
60 | """BDI stands for Bi-Directional Isolation.
61 |
62 | Isolates a part of text that might be formatted in a different direction
63 | from other text outside it.
64 | """
65 |
66 | tag: str = "bdi"
67 |
68 | def __init__(
69 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
70 | ) -> None:
71 | super().__init__(*children, **attributes)
72 |
73 |
74 | class BiDirectionalIsolation(Bdi):
75 | """Alias for `Bdi`."""
76 |
77 |
78 | class Bdo(BasicElement[AnyChildren, GlobalAttrs]):
79 | """BDO stands for Bi-Directional Override.
80 |
81 | Overrides the current text direction.
82 | """
83 |
84 | tag: str = "bdo"
85 |
86 | def __init__(
87 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
88 | ) -> None:
89 | super().__init__(*children, **attributes)
90 |
91 |
92 | class BiDirectionalOverride(Bdo):
93 | """Alias for `Bdo`."""
94 |
95 |
96 | class Blockquote(BasicElement[AnyChildren, BlockquoteAttrs]):
97 | """Defines a section that is quoted from another source."""
98 |
99 | tag: str = "blockquote"
100 |
101 | def __init__(
102 | self, *children: AnyChildren, **attributes: Unpack[BlockquoteAttrs]
103 | ) -> None:
104 | super().__init__(*children, **attributes)
105 |
106 |
107 | class Cite(BasicElement[AnyChildren, GlobalAttrs]):
108 | """Defines the title of a work."""
109 |
110 | tag: str = "cite"
111 |
112 | def __init__(
113 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
114 | ) -> None:
115 | super().__init__(*children, **attributes)
116 |
117 |
118 | class Code(BasicElement[AnyChildren, GlobalAttrs]):
119 | """Defines a piece of computer code."""
120 |
121 | tag: str = "code"
122 |
123 | def __init__(
124 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
125 | ) -> None:
126 | super().__init__(*children, **attributes)
127 |
128 |
129 | class Del(BasicElement[AnyChildren, DelAttrs]):
130 | """Defines text that has been deleted from a document."""
131 |
132 | tag: str = "del"
133 |
134 | def __init__(
135 | self, *children: AnyChildren, **attributes: Unpack[DelAttrs]
136 | ) -> None:
137 | super().__init__(*children, **attributes)
138 |
139 |
140 | class Deleted(Del):
141 | """Alias for del tag."""
142 |
143 |
144 | class Dfn(BasicElement[AnyChildren, GlobalAttrs]):
145 | """DFN stands for definition element.
146 |
147 | Specifies a term that is going to be defined within the content.
148 | """
149 |
150 | tag: str = "dfn"
151 |
152 | def __init__(
153 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
154 | ) -> None:
155 | super().__init__(*children, **attributes)
156 |
157 |
158 | class DefinitionElement(Dfn):
159 | """Alias for `Dfn`."""
160 |
161 |
162 | class Em(BasicElement[AnyChildren, GlobalAttrs]):
163 | """Defines emphasized text ."""
164 |
165 | tag: str = "em"
166 |
167 | def __init__(
168 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
169 | ) -> None:
170 | super().__init__(*children, **attributes)
171 |
172 |
173 | class Emphasized(Em):
174 | """Alias for `Em`."""
175 |
176 |
177 | class I(BasicElement[AnyChildren, GlobalAttrs]): # noqa: E742
178 | """Defines a part of text in an alternate voice or mood."""
179 |
180 | tag: str = "i"
181 |
182 | def __init__(
183 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
184 | ) -> None:
185 | super().__init__(*children, **attributes)
186 |
187 |
188 | class Italic(I):
189 | """Alias for `I`."""
190 |
191 |
192 | class Ins(BasicElement[AnyChildren, InsAttrs]):
193 | """Defines a text that has been inserted into a document."""
194 |
195 | tag: str = "ins"
196 |
197 | def __init__(
198 | self, *children: AnyChildren, **attributes: Unpack[InsAttrs]
199 | ) -> None:
200 | super().__init__(*children, **attributes)
201 |
202 |
203 | class Inserted(Ins):
204 | """Alias for `Ins`."""
205 |
206 |
207 | class Kbd(BasicElement[AnyChildren, GlobalAttrs]):
208 | """Defines keyboard input."""
209 |
210 | tag: str = "kbd"
211 |
212 | def __init__(
213 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
214 | ) -> None:
215 | super().__init__(*children, **attributes)
216 |
217 |
218 | class Keyboard(Kbd):
219 | """Alias for `Kbd`."""
220 |
221 |
222 | class Mark(BasicElement[AnyChildren, GlobalAttrs]):
223 | """Defines marked/highlighted text."""
224 |
225 | tag: str = "mark"
226 |
227 | def __init__(
228 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
229 | ) -> None:
230 | super().__init__(*children, **attributes)
231 |
232 |
233 | class Meter(BasicElement[AnyChildren, MeterAttrs]):
234 | """Defines a scalar measurement within a known range (a gauge)."""
235 |
236 | tag: str = "meter"
237 |
238 | def __init__(
239 | self, *children: AnyChildren, **attributes: Unpack[MeterAttrs]
240 | ) -> None:
241 | super().__init__(*children, **attributes)
242 |
243 |
244 | class Pre(BasicElement[AnyChildren, GlobalAttrs]):
245 | """Defines preformatted text."""
246 |
247 | tag: str = "pre"
248 |
249 | def __init__(
250 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
251 | ) -> None:
252 | super().__init__(*children, **attributes)
253 |
254 |
255 | class Preformatted(Pre):
256 | """Alias for `Pre`."""
257 |
258 |
259 | class Progress(BasicElement[AnyChildren, ProgressAttrs]):
260 | """Represents the progress of a task."""
261 |
262 | tag: str = "progress"
263 |
264 | def __init__(
265 | self, *children: AnyChildren, **attributes: Unpack[ProgressAttrs]
266 | ) -> None:
267 | super().__init__(*children, **attributes)
268 |
269 |
270 | class Q(BasicElement[AnyChildren, QAttrs]):
271 | """Defines a short quotation."""
272 |
273 | tag: str = "q"
274 |
275 | def __init__(
276 | self, *children: AnyChildren, **attributes: Unpack[QAttrs]
277 | ) -> None:
278 | super().__init__(*children, **attributes)
279 |
280 |
281 | class Quotation(Q):
282 | """Alias for `Abbr`."""
283 |
284 |
285 | class Rp(BasicElement[AnyChildren, GlobalAttrs]):
286 | """Defines what to show in browsers that don't support ruby annotations."""
287 |
288 | tag: str = "rp"
289 |
290 | def __init__(
291 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
292 | ) -> None:
293 | super().__init__(*children, **attributes)
294 |
295 |
296 | class Rt(BasicElement[AnyChildren, GlobalAttrs]):
297 | """Defines an explanation/pronunciation of characters."""
298 |
299 | tag: str = "rt"
300 |
301 | def __init__(
302 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
303 | ) -> None:
304 | super().__init__(*children, **attributes)
305 |
306 |
307 | class ruby(BasicElement[AnyChildren, GlobalAttrs]):
308 | """Defines a ruby annotation (for East Asian typography)."""
309 |
310 | tag: str = "ruby"
311 |
312 | def __init__(
313 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
314 | ) -> None:
315 | super().__init__(*children, **attributes)
316 |
317 |
318 | class S(BasicElement[AnyChildren, GlobalAttrs]):
319 | """Defines text that is no longer correct."""
320 |
321 | tag: str = "s"
322 |
323 | def __init__(
324 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
325 | ) -> None:
326 | super().__init__(*children, **attributes)
327 |
328 |
329 | class StrikeThrough(S):
330 | """Alias for `S`."""
331 |
332 |
333 | class Samp(BasicElement[AnyChildren, GlobalAttrs]):
334 | """Defines sample output from a computer program."""
335 |
336 | tag: str = "samp"
337 |
338 | def __init__(
339 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
340 | ) -> None:
341 | super().__init__(*children, **attributes)
342 |
343 |
344 | class SampleOutput(Samp):
345 | """Alias for `Samp`."""
346 |
347 |
348 | class Small(BasicElement[AnyChildren, GlobalAttrs]):
349 | """Defines smaller text."""
350 |
351 | tag: str = "small"
352 |
353 | def __init__(
354 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
355 | ) -> None:
356 | super().__init__(*children, **attributes)
357 |
358 |
359 | class Strong(BasicElement[AnyChildren, GlobalAttrs]):
360 | """Defines important text."""
361 |
362 | tag: str = "strong"
363 |
364 | def __init__(
365 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
366 | ) -> None:
367 | super().__init__(*children, **attributes)
368 |
369 |
370 | class Sub(BasicElement[AnyChildren, GlobalAttrs]):
371 | """Defines subscripted text."""
372 |
373 | tag: str = "sub"
374 |
375 | def __init__(
376 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
377 | ) -> None:
378 | super().__init__(*children, **attributes)
379 |
380 |
381 | class Subscripted(Sub):
382 | """Alias for `Sub`."""
383 |
384 |
385 | class Sup(BasicElement[AnyChildren, GlobalAttrs]):
386 | """Defines superscripted text."""
387 |
388 | tag: str = "sup"
389 |
390 | def __init__(
391 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
392 | ) -> None:
393 | super().__init__(*children, **attributes)
394 |
395 |
396 | class Superscripted(Sup):
397 | """Alias for `Sup`."""
398 |
399 |
400 | class Template(BasicElement[AnyChildren, HtmlAttrs]):
401 | """Defines a container for content, that should be hidden on page load."""
402 |
403 | tag: str = "template"
404 |
405 | def __init__(
406 | self, *children: AnyChildren, **attributes: Unpack[HtmlAttrs]
407 | ) -> None:
408 | super().__init__(*children, **attributes)
409 |
410 |
411 | class Time(BasicElement[AnyChildren, TimeAttrs]):
412 | """Defines a specific time (or datetime)."""
413 |
414 | tag: str = "time"
415 |
416 | def __init__(
417 | self, *children: AnyChildren, **attributes: Unpack[TimeAttrs]
418 | ) -> None:
419 | super().__init__(*children, **attributes)
420 |
421 |
422 | class U(BasicElement[AnyChildren, GlobalAttrs]):
423 | """Defines unarticulated text.
424 |
425 | This is also styled differently from normal text.
426 | """
427 |
428 | tag: str = "u"
429 |
430 | def __init__(
431 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
432 | ) -> None:
433 | super().__init__(*children, **attributes)
434 |
435 |
436 | class Unarticulated(U):
437 | """Alias for `U`."""
438 |
439 |
440 | class Var(BasicElement[AnyChildren, GlobalAttrs]):
441 | """Defines a variable."""
442 |
443 | tag: str = "var"
444 |
445 | def __init__(
446 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
447 | ) -> None:
448 | super().__init__(*children, **attributes)
449 |
450 |
451 | class Variable(Var):
452 | """Alias for `Var`."""
453 |
454 |
455 | class Wbr(VoidElement[GlobalAttrs]):
456 | """Defines a possible line-break.
457 |
458 | WBR stands for Word Break Opportunity.
459 | """
460 |
461 | tag: str = "wbr"
462 |
463 | def __init__(self, **attributes: Unpack[GlobalAttrs]) -> None:
464 | super().__init__(**attributes)
465 |
466 |
467 | class WordBreakOpportunity(Wbr):
468 | """Alias for `Wbr`."""
469 |
--------------------------------------------------------------------------------
/hypermedia/forms_and_input.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Unpack
2 |
3 | from hypermedia.models import BasicElement, VoidElement
4 | from hypermedia.types.attributes import (
5 | ButtonAttrs,
6 | FieldsetAttrs,
7 | FormAttrs,
8 | GlobalAttrs,
9 | InputAttrs,
10 | LabelAttrs,
11 | OptgroupAttrs,
12 | OptionAttrs,
13 | OutputAttrs,
14 | SelectAttrs,
15 | TextAreaAttrs,
16 | )
17 | from hypermedia.types.types import AnyChildren, PrimitiveChildren
18 |
19 |
20 | class Form(BasicElement[AnyChildren, FormAttrs]):
21 | """Defines an HTML form for user input."""
22 |
23 | tag: str = "form"
24 |
25 | def __init__(
26 | self, *children: AnyChildren, **attributes: Unpack[FormAttrs]
27 | ) -> None:
28 | super().__init__(*children, **attributes)
29 |
30 |
31 | class Input(VoidElement[InputAttrs]):
32 | """Defines an input control."""
33 |
34 | tag: str = "input"
35 |
36 | def __init__(self, **attributes: Unpack[InputAttrs]) -> None:
37 | super().__init__(**attributes)
38 |
39 |
40 | class TextArea(BasicElement[AnyChildren, TextAreaAttrs]):
41 | """Defines a multiline input control (text area)."""
42 |
43 | tag: str = "textarea"
44 |
45 | def __init__(
46 | self, *children: AnyChildren, **attributes: Unpack[TextAreaAttrs]
47 | ) -> None:
48 | super().__init__(*children, **attributes)
49 |
50 |
51 | class Button(BasicElement[AnyChildren, ButtonAttrs]):
52 | """Defines a clickable button."""
53 |
54 | tag: str = "button"
55 |
56 | def __init__(
57 | self, *children: AnyChildren, **attributes: Unpack[ButtonAttrs]
58 | ) -> None:
59 | super().__init__(*children, **attributes)
60 |
61 |
62 | class Select(BasicElement[AnyChildren, SelectAttrs]):
63 | """Defines a drop-down list."""
64 |
65 | tag: str = "select"
66 |
67 | def __init__(
68 | self, *children: AnyChildren, **attributes: Unpack[SelectAttrs]
69 | ) -> None:
70 | super().__init__(*children, **attributes)
71 |
72 |
73 | class OptGroup(BasicElement[AnyChildren, OptgroupAttrs]):
74 | """Defines a group of related options in a drop-down list."""
75 |
76 | tag: str = "optgroup"
77 |
78 | def __init__(
79 | self, *children: AnyChildren, **attributes: Unpack[OptgroupAttrs]
80 | ) -> None:
81 | super().__init__(*children, **attributes)
82 |
83 |
84 | class OptionGroup(OptGroup):
85 | """Alias for `OptGroup`."""
86 |
87 |
88 | class Option(BasicElement[PrimitiveChildren, OptionAttrs]):
89 | """Defines an option in a drop-down list."""
90 |
91 | tag: str = "option"
92 |
93 | def __init__(
94 | self, *children: PrimitiveChildren, **attributes: Unpack[OptionAttrs]
95 | ) -> None:
96 | super().__init__(*children, **attributes)
97 |
98 |
99 | class Label(BasicElement[AnyChildren, LabelAttrs]):
100 | """Defines a label for an `input` element."""
101 |
102 | tag: str = "label"
103 |
104 | def __init__(
105 | self, *children: AnyChildren, **attributes: Unpack[LabelAttrs]
106 | ) -> None:
107 | super().__init__(*children, **attributes)
108 |
109 |
110 | class Fieldset(BasicElement[AnyChildren, FieldsetAttrs]):
111 | """Groups related elements in a form."""
112 |
113 | tag: str = "fieldset"
114 |
115 | def __init__(
116 | self, *children: AnyChildren, **attributes: Unpack[FieldsetAttrs]
117 | ) -> None:
118 | super().__init__(*children, **attributes)
119 |
120 |
121 | class Legend(BasicElement[PrimitiveChildren, GlobalAttrs]):
122 | """Defines a caption for a `fieldset` element."""
123 |
124 | tag: str = "legend"
125 |
126 | def __init__(
127 | self, *children: PrimitiveChildren, **attributes: Unpack[GlobalAttrs]
128 | ) -> None:
129 | super().__init__(*children, **attributes)
130 |
131 |
132 | class DataList(BasicElement[AnyChildren, GlobalAttrs]):
133 | """Specifies a list of pre-defined options for input controls."""
134 |
135 | tag: str = "datalist"
136 |
137 | def __init__(
138 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
139 | ) -> None:
140 | super().__init__(*children, **attributes)
141 |
142 |
143 | class Output(BasicElement[AnyChildren, OutputAttrs]):
144 | """Defines the result of a calculation."""
145 |
146 | tag: str = "output"
147 |
148 | def __init__(
149 | self, *children: AnyChildren, **attributes: Unpack[OutputAttrs]
150 | ) -> None:
151 | super().__init__(*children, **attributes)
152 |
--------------------------------------------------------------------------------
/hypermedia/frames.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Unpack
2 |
3 | from hypermedia.models.elements import BasicElement
4 | from hypermedia.types.attributes import IframeAttrs
5 | from hypermedia.types.types import NoChildren
6 |
7 |
8 | class IFrame(BasicElement[NoChildren, IframeAttrs]):
9 | """Defines an inline frame."""
10 |
11 | tag: str = "iframe"
12 |
13 | def __init__(self, **attributes: Unpack[IframeAttrs]) -> None:
14 | super().__init__(**attributes)
15 |
--------------------------------------------------------------------------------
/hypermedia/images.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Unpack
2 |
3 | from hypermedia.models import BasicElement, VoidElement
4 | from hypermedia.models.elements import XMLVoidElement
5 | from hypermedia.types.attributes import (
6 | AreaAttrs,
7 | CanvasAttrs,
8 | CircleAttrs,
9 | EllipseAttrs,
10 | GlobalAttrs,
11 | ImgAttrs,
12 | LineAttrs,
13 | MapAttrs,
14 | PathAttrs,
15 | PolygonAttrs,
16 | PolylineAttrs,
17 | RectAttrs,
18 | SvgAttrs,
19 | )
20 | from hypermedia.types.types import AnyChildren
21 |
22 |
23 | class Img(VoidElement[ImgAttrs]):
24 | """Defines an image."""
25 |
26 | tag: str = "img"
27 |
28 | def __init__(
29 | self, *children: AnyChildren, **attributes: Unpack[ImgAttrs]
30 | ) -> None:
31 | super().__init__(*children, **attributes)
32 |
33 |
34 | class Image(Img):
35 | """Alias for `Img`."""
36 |
37 |
38 | class Map(BasicElement[AnyChildren, MapAttrs]):
39 | """Defines a client-side image map."""
40 |
41 | tag: str = "map"
42 |
43 | def __init__(
44 | self, *children: AnyChildren, **attributes: Unpack[MapAttrs]
45 | ) -> None:
46 | super().__init__(*children, **attributes)
47 |
48 |
49 | class Area(VoidElement[AreaAttrs]):
50 | """Defines an area inside an image map."""
51 |
52 | tag: str = "area"
53 |
54 | def __init__(self, **attributes: Unpack[AreaAttrs]) -> None:
55 | super().__init__(**attributes)
56 |
57 |
58 | class Canvas(BasicElement[AnyChildren, CanvasAttrs]):
59 | """Used to draw graphics, on the fly, via scripting."""
60 |
61 | tag: str = "canvas"
62 |
63 | def __init__(
64 | self, *children: AnyChildren, **attributes: Unpack[CanvasAttrs]
65 | ) -> None:
66 | super().__init__(*children, **attributes)
67 |
68 |
69 | class FigCaption(BasicElement[AnyChildren, GlobalAttrs]):
70 | """Defines a caption for a `figure` element."""
71 |
72 | tag: str = "figcaption"
73 |
74 | def __init__(
75 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
76 | ) -> None:
77 | super().__init__(*children, **attributes)
78 |
79 |
80 | class FigureCaption(FigCaption):
81 | """Alias for `FigCaption`."""
82 |
83 |
84 | class Figure(BasicElement[AnyChildren, GlobalAttrs]):
85 | """Specifies self-contained content."""
86 |
87 | tag: str = "figure"
88 |
89 | def __init__(
90 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
91 | ) -> None:
92 | super().__init__(*children, **attributes)
93 |
94 |
95 | class Picture(BasicElement[AnyChildren, GlobalAttrs]):
96 | """Defines a container for multiple image resources."""
97 |
98 | tag: str = "picture"
99 |
100 | def __init__(
101 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
102 | ) -> None:
103 | super().__init__(*children, **attributes)
104 |
105 |
106 | class Svg(BasicElement[AnyChildren, SvgAttrs]):
107 | """Defines a container for SVG graphics."""
108 |
109 | tag: str = "svg"
110 |
111 | def __init__(
112 | self, *children: AnyChildren, **attributes: Unpack[SvgAttrs]
113 | ) -> None:
114 | super().__init__(*children, **attributes)
115 |
116 |
117 | # Basic SVG elements. These should hopefully cover most use cases.
118 | # If they don't, feel free to add them.
119 | # Alternatively, consider using Image with a `src="my.svg` or css.
120 | class Path(XMLVoidElement[PathAttrs]):
121 | """Element used to define paths for SVG graphics."""
122 |
123 | tag: str = "path"
124 |
125 | def __init__(self, **attributes: Unpack[PathAttrs]) -> None:
126 | super().__init__(**attributes)
127 |
128 |
129 | class Rect(XMLVoidElement[RectAttrs]):
130 | """Element used to define rectangles for SVG graphics."""
131 |
132 | tag: str = "rect"
133 |
134 | def __init__(self, **attributes: Unpack[RectAttrs]) -> None:
135 | super().__init__(**attributes)
136 |
137 |
138 | class Rectangle(Rect):
139 | """Alias for `Rect`."""
140 |
141 |
142 | class Circle(XMLVoidElement[CircleAttrs]):
143 | """Element used to define circles for SVG graphics."""
144 |
145 | tag: str = "circle"
146 |
147 | def __init__(self, **attributes: Unpack[CircleAttrs]) -> None:
148 | super().__init__(**attributes)
149 |
150 |
151 | class Ellipse(XMLVoidElement[EllipseAttrs]):
152 | """Element used to define ellipses for SVG graphics."""
153 |
154 | tag: str = "ellipse"
155 |
156 | def __init__(self, **attributes: Unpack[EllipseAttrs]) -> None:
157 | super().__init__(**attributes)
158 |
159 |
160 | class Line(XMLVoidElement[LineAttrs]):
161 | """Element used to define lines for SVG graphics."""
162 |
163 | tag: str = "line"
164 |
165 | def __init__(self, **attributes: Unpack[LineAttrs]) -> None:
166 | super().__init__(**attributes)
167 |
168 |
169 | class Polyline(XMLVoidElement[PolylineAttrs]):
170 | """Element used to define polylines for SVG graphics."""
171 |
172 | tag: str = "polyline"
173 |
174 | def __init__(self, **attributes: Unpack[PolylineAttrs]) -> None:
175 | super().__init__(**attributes)
176 |
177 |
178 | class Polygon(XMLVoidElement[PolygonAttrs]):
179 | """Element used to define polygons for SVG graphics."""
180 |
181 | tag: str = "polygon"
182 |
183 | def __init__(self, **attributes: Unpack[PolygonAttrs]) -> None:
184 | super().__init__(**attributes)
185 |
--------------------------------------------------------------------------------
/hypermedia/links.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Unpack
2 |
3 | from hypermedia.models import BasicElement, VoidElement
4 | from hypermedia.types.attributes import (
5 | GlobalAttrs,
6 | HeadLinkAttrs,
7 | HyperlinkAttrs,
8 | )
9 | from hypermedia.types.types import AnyChildren
10 |
11 |
12 | class A(BasicElement[AnyChildren, HyperlinkAttrs]):
13 | """Defines a hyperlink."""
14 |
15 | tag: str = "a"
16 |
17 | def __init__(
18 | self, *children: AnyChildren, **attributes: Unpack[HyperlinkAttrs]
19 | ) -> None:
20 | super().__init__(*children, **attributes)
21 |
22 |
23 | class Anchor(A):
24 | """Alias for `A`."""
25 |
26 |
27 | class Link(VoidElement[HeadLinkAttrs]):
28 | """Defines the relationship between a document and an external resource.
29 |
30 | (most used to link to style sheets).
31 | """
32 |
33 | tag: str = "link"
34 |
35 | def __init__(self, **attributes: Unpack[HeadLinkAttrs]) -> None:
36 | super().__init__(**attributes)
37 |
38 |
39 | class Nav(BasicElement[AnyChildren, GlobalAttrs]):
40 | """Defines navigation links."""
41 |
42 | tag: str = "nav"
43 |
44 | def __init__(
45 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
46 | ) -> None:
47 | super().__init__(*children, **attributes)
48 |
--------------------------------------------------------------------------------
/hypermedia/lists.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Unpack
2 |
3 | from hypermedia.models import BasicElement
4 | from hypermedia.types.attributes import GlobalAttrs, LiAttrs, OlAttrs
5 | from hypermedia.types.types import AnyChildren, ComplexChildren
6 |
7 |
8 | class Menu(BasicElement[AnyChildren, GlobalAttrs]):
9 | """Defines an alternative unordered list."""
10 |
11 | tag: str = "menu"
12 |
13 | def __init__(
14 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
15 | ) -> None:
16 | super().__init__(*children, **attributes)
17 |
18 |
19 | class Ul(BasicElement[ComplexChildren, GlobalAttrs]):
20 | """Defines an unordered list."""
21 |
22 | tag: str = "ul"
23 |
24 | def __init__(
25 | self, *children: ComplexChildren, **attributes: Unpack[GlobalAttrs]
26 | ) -> None:
27 | super().__init__(*children, **attributes)
28 |
29 |
30 | class UnorderedList(Ul):
31 | """Alias for `Ul`."""
32 |
33 |
34 | class Ol(BasicElement[ComplexChildren, OlAttrs]):
35 | """Defines an ordered list."""
36 |
37 | tag: str = "ol"
38 |
39 | def __init__(
40 | self, *children: ComplexChildren, **attributes: Unpack[OlAttrs]
41 | ) -> None:
42 | super().__init__(*children, **attributes)
43 |
44 |
45 | class OrderedList(Ol):
46 | """Alias for `Ol`."""
47 |
48 |
49 | class Li(BasicElement[AnyChildren, LiAttrs]):
50 | """Defines a list item."""
51 |
52 | tag: str = "li"
53 |
54 | def __init__(
55 | self, *children: AnyChildren, **attributes: Unpack[LiAttrs]
56 | ) -> None:
57 | super().__init__(*children, **attributes)
58 |
59 |
60 | class ListItem(Li):
61 | """Alias for `Li`."""
62 |
63 |
64 | class Dl(BasicElement[ComplexChildren, GlobalAttrs]):
65 | """Defines a description list."""
66 |
67 | tag: str = "dl"
68 |
69 | def __init__(
70 | self, *children: ComplexChildren, **attributes: Unpack[GlobalAttrs]
71 | ) -> None:
72 | super().__init__(*children, **attributes)
73 |
74 |
75 | class DescriptionList(Dl):
76 | """Alias for `Dl`."""
77 |
78 |
79 | class Dt(BasicElement[AnyChildren, GlobalAttrs]):
80 | """Defines a term/name in a description list."""
81 |
82 | tag: str = "dt"
83 |
84 | def __init__(
85 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
86 | ) -> None:
87 | super().__init__(*children, **attributes)
88 |
89 |
90 | class DescriptionListTerm(Dt):
91 | """Alias for `Dt`."""
92 |
93 |
94 | class Dd(BasicElement[AnyChildren, GlobalAttrs]):
95 | """Defines a description of a term/name in a description list."""
96 |
97 | tag: str = "dd"
98 |
99 | def __init__(
100 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
101 | ) -> None:
102 | super().__init__(*children, **attributes)
103 |
104 |
105 | class DescriptionListTermDescription(Dd):
106 | """Alias for `Dd`."""
107 |
--------------------------------------------------------------------------------
/hypermedia/meta_info.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Unpack
2 |
3 | from hypermedia.models import VoidElement
4 | from hypermedia.types.attributes import BaseAttrs, MetaAttrs
5 |
6 |
7 | class Meta(VoidElement[MetaAttrs]):
8 | """Defines metadata about an HTML document."""
9 |
10 | tag: str = "meta"
11 |
12 | def __init__(self, **attributes: Unpack[MetaAttrs]) -> None:
13 | super().__init__(**attributes)
14 |
15 |
16 | class Base(VoidElement[BaseAttrs]):
17 | """Specifies the base URL/target for all relative URLs in a document."""
18 |
19 | tag: str = "base"
20 |
21 | def __init__(self, **attributes: Unpack[BaseAttrs]) -> None:
22 | super().__init__(**attributes)
23 |
--------------------------------------------------------------------------------
/hypermedia/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import Element
2 | from .elements import BasicElement, ElementList, VoidElement
3 |
4 | __all__ = ["BasicElement", "Element", "ElementList", "VoidElement"]
5 |
--------------------------------------------------------------------------------
/hypermedia/models/base.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | from abc import ABCMeta, abstractmethod
3 | from functools import lru_cache
4 | from html import escape
5 | from typing import (
6 | Any,
7 | Mapping,
8 | Sequence,
9 | Union,
10 | get_type_hints,
11 | )
12 |
13 | from typing_extensions import Final, Self
14 |
15 | from hypermedia.types.types import SafeString
16 |
17 | FINAL_KEY_PREFIX: Final[str] = "$"
18 |
19 | _CUSTOM_RENDERED_ATTRIBUTES: Final[set[str]] = {
20 | "classes",
21 | "class_",
22 | "style",
23 | }
24 |
25 |
26 | @lru_cache
27 | def _load_attribute_aliases() -> Mapping[str, str]: # noqa: C901
28 | """Get a mapping of attribute names to their aliases.
29 |
30 | Taken from Ludic:
31 | https://github.com/getludic/ludic/blob/main/ludic/format.py
32 | """
33 | from hypermedia.types import attributes
34 |
35 | result = {}
36 | for name, cls in inspect.getmembers(attributes, inspect.isclass):
37 | if not name.endswith("Attrs"):
38 | continue
39 |
40 | hints = get_type_hints(cls, include_extras=True)
41 | for key, value in hints.items():
42 | if metadata := getattr(value, "__metadata__", None):
43 | for meta in metadata:
44 | if isinstance(meta, attributes.Alias):
45 | result[key] = str(meta)
46 |
47 | return result
48 |
49 |
50 | def get_child_slots(
51 | slots: dict[str, "Element"],
52 | children: Sequence[Union[str, "Element"]],
53 | ) -> dict[str, "Element"]:
54 | """Get slots from direct child."""
55 | slot_keys = slots.keys()
56 |
57 | for child in children:
58 | if isinstance(child, str):
59 | continue
60 |
61 | if duplicate_keys := [
62 | key for key in child.slots.keys() if key in slot_keys
63 | ]:
64 | raise ValueError(
65 | f"All slot names must be unique: {duplicate_keys}"
66 | )
67 | else:
68 | slots.update(child.slots)
69 | return slots
70 |
71 |
72 | def get_slots(
73 | element: "Element",
74 | ) -> dict[str, "Element"]:
75 | """Calculate slots."""
76 | slots: dict[str, "Element"] = {}
77 |
78 | if element.slot:
79 | slots[element.slot] = element
80 |
81 | if element.children:
82 | get_child_slots(slots, element.children)
83 |
84 | return slots
85 |
86 |
87 | class Element(metaclass=ABCMeta):
88 | """Base class for all elements.
89 |
90 | This handles handles slot extension, children, attributes and
91 | css classes.
92 | """
93 |
94 | children: tuple[Any, ...]
95 | slot: str | None = None
96 | slots: dict[str, "Element"]
97 | attributes: Mapping[str, Any]
98 |
99 | def __init__(
100 | self,
101 | *children: Any,
102 | slot: str | None = None,
103 | **attributes: Any,
104 | ) -> None:
105 | """Initialize Root with children."""
106 | self.children = children
107 | self.slot = slot
108 | self.slots = get_slots(self)
109 | self.attributes = attributes
110 |
111 | @abstractmethod
112 | def dump(self) -> SafeString:
113 | """Dump the objects to a html document string."""
114 | pass
115 |
116 | def extend(self, slot: str, *children: Union[str, "Element"]) -> Self:
117 | """Extend the child with the given slots children."""
118 | if slot not in self.slots:
119 | raise ValueError(f"Could not find a slot with name: {slot}")
120 | element = self.slots[slot]
121 | element.children = element.children + children
122 |
123 | get_child_slots(self.slots, list(children))
124 | return self
125 |
126 | def _render_attributes(self) -> str: # noqa: C901
127 | result = []
128 |
129 | attribute_aliases = _load_attribute_aliases()
130 |
131 | # make a copy of classes list so we don't modify the original
132 | classes = list(self.attributes.get("classes", []))
133 | if class_ := self.attributes.get("class_", None):
134 | classes.append(class_)
135 |
136 | for key, value in self.attributes.items():
137 | # Skip class and style attributes that we handle separately
138 | if key in _CUSTOM_RENDERED_ATTRIBUTES:
139 | continue
140 | # Skip None values, use `True` for key only values
141 | if value is None:
142 | continue
143 | # Skip false boolean attributes
144 | if value is False:
145 | continue
146 |
147 | if key in attribute_aliases:
148 | alias = attribute_aliases[key]
149 | elif key.startswith(FINAL_KEY_PREFIX):
150 | alias = key[1:]
151 | else:
152 | alias = key.replace("_", "-")
153 |
154 | # Add true boolean attributes as key only.
155 | if value is True:
156 | result.append(alias)
157 | continue
158 |
159 | result.append(f"{alias}='{value}'")
160 |
161 | # Add css classes
162 | if len(classes) > 0:
163 | result.append(f"class='{' '.join(classes)}'")
164 |
165 | if style := self.attributes.get("style", None):
166 | result.append(
167 | "style='{styles}'".format(
168 | styles="".join(
169 | f"{key}:{value};" for key, value in style.items()
170 | )
171 | )
172 | )
173 | if result:
174 | return " " + " ".join(result)
175 | return ""
176 |
177 | def _render_children(self) -> str:
178 | return "".join(
179 | [
180 | child
181 | if isinstance(child, SafeString)
182 | else escape(child)
183 | if isinstance(child, str)
184 | else child.dump()
185 | for child in self.children
186 | ]
187 | )
188 |
--------------------------------------------------------------------------------
/hypermedia/models/elements.py:
--------------------------------------------------------------------------------
1 | from typing import Generic
2 |
3 | from typing_extensions import Unpack
4 |
5 | from hypermedia.models.base import Element
6 | from hypermedia.types.types import SafeString, TAttrs, TChildren, TChildrenArgs
7 |
8 |
9 | class BasicElement(Generic[TChildren, TAttrs], Element):
10 | """Base class for Hypermedia elements."""
11 |
12 | children: tuple[TChildren, ...]
13 | attributes: TAttrs
14 |
15 | tag: str
16 |
17 | def __init__(
18 | self,
19 | *children: TChildren,
20 | # FIXME: https://github.com/python/typing/issues/1399
21 | **attributes: Unpack[TAttrs], # type: ignore
22 | ) -> None:
23 | super().__init__(*children, **attributes)
24 |
25 | def dump(self) -> SafeString:
26 | """Dump to html, while escaping text data."""
27 | return SafeString(
28 | "<{tag}{attributes}>{children}{tag}>".format(
29 | tag=self.tag,
30 | attributes=self._render_attributes(),
31 | children=self._render_children(),
32 | )
33 | )
34 |
35 |
36 | class ElementStrict(Generic[Unpack[TChildrenArgs], TAttrs], Element):
37 | """Base class for strict elements elements with concrete types of children.
38 |
39 | Args:
40 | ----
41 | *children: (*TChildTuple): The children of the element.
42 | **attributes: (Unpack[TAttrs]): The attributes of the element.
43 |
44 | """
45 |
46 | children: tuple[Unpack[TChildrenArgs]]
47 | attributes: TAttrs
48 |
49 | tag: str
50 |
51 | def __init__(
52 | self,
53 | *children: Unpack[TChildrenArgs],
54 | # FIXME: https://github.com/python/typing/issues/1399
55 | **attributes: Unpack[TAttrs], # type: ignore
56 | ) -> None:
57 | super().__init__(*children, **attributes)
58 |
59 | def dump(self) -> SafeString:
60 | """Dump to html, while escaping text data."""
61 | return SafeString(
62 | "<{tag}{attributes}>{children}{tag}>".format(
63 | tag=self.tag,
64 | attributes=self._render_attributes(),
65 | children=self._render_children(),
66 | )
67 | )
68 |
69 |
70 | class ElementList(Generic[TChildren], Element):
71 | """Use to render a list of child elements without a parent."""
72 |
73 | children: tuple[TChildren, ...]
74 |
75 | def __init__(
76 | self,
77 | *children: TChildren,
78 | slot: str | None = None,
79 | ) -> None:
80 | super().__init__(*children, slot=slot)
81 |
82 | def dump(self) -> SafeString:
83 | """Dump the objects to a html document string."""
84 | return SafeString(self._render_children())
85 |
86 |
87 | class VoidElement(Generic[TAttrs], Element):
88 | """A void element is an element in HTML that cannot have any child nodes.
89 |
90 | Void elements only have a start tag; end tags must not be specified for
91 | void elements.
92 | """
93 |
94 | attributes: TAttrs
95 |
96 | tag: str
97 |
98 | def __init__(
99 | self,
100 | *,
101 | slot: str | None = None,
102 | # FIXME: https://github.com/python/typing/issues/1399
103 | **attributes: Unpack[TAttrs], # type: ignore
104 | ) -> None:
105 | super().__init__(slot=slot, **attributes)
106 |
107 | def dump(self) -> SafeString:
108 | """Dump to html."""
109 | return SafeString(
110 | "<{tag}{attributes}>".format(
111 | tag=self.tag,
112 | attributes=self._render_attributes(),
113 | )
114 | )
115 |
116 | def __str__(self) -> str:
117 | """Return tag."""
118 | return self.tag
119 |
120 |
121 | class XMLVoidElement(Generic[TAttrs], Element):
122 | """Same as VoidElement, but for XML. Requires closing tags with `/>`."""
123 |
124 | attributes: TAttrs
125 |
126 | tag: str
127 |
128 | def __init__(
129 | self,
130 | *,
131 | slot: str | None = None,
132 | # FIXME: https://github.com/python/typing/issues/1399
133 | **attributes: Unpack[TAttrs], # type: ignore
134 | ) -> None:
135 | super().__init__(slot=slot, **attributes)
136 |
137 | def dump(self) -> SafeString:
138 | """Dump to html."""
139 | return SafeString(
140 | "<{tag}{attributes} />".format(
141 | tag=self.tag,
142 | attributes=self._render_attributes(),
143 | )
144 | )
145 |
146 | def __str__(self) -> str:
147 | """Return tag."""
148 | return self.tag
149 |
--------------------------------------------------------------------------------
/hypermedia/programming.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Never, Unpack
2 |
3 | from hypermedia.models import BasicElement, VoidElement
4 | from hypermedia.types.attributes import (
5 | EmbedAttrs,
6 | GlobalAttrs,
7 | ObjectAttrs,
8 | ScriptAttrs,
9 | )
10 | from hypermedia.types.types import AnyChildren, SafeString
11 |
12 |
13 | class Script(BasicElement[str, ScriptAttrs]):
14 | """Defines a client-side script."""
15 |
16 | tag: str = "script"
17 |
18 | def __init__(
19 | self,
20 | child: str | None = None,
21 | *args: Never,
22 | **attributes: Unpack[ScriptAttrs],
23 | ) -> None:
24 | if child is None:
25 | super().__init__(**attributes)
26 | else:
27 | super().__init__(SafeString(child), **attributes)
28 |
29 |
30 | class NoScript(BasicElement[AnyChildren, GlobalAttrs]):
31 | """Defines alternate content when client-side scripts aren't supported."""
32 |
33 | tag: str = "noscript"
34 |
35 | def __init__(
36 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
37 | ) -> None:
38 | super().__init__(*children, **attributes)
39 |
40 |
41 | class Embed(VoidElement[EmbedAttrs]):
42 | """Defines a container for an external (non-HTML) application."""
43 |
44 | tag: str = "embed"
45 |
46 | def __init__(self, **attributes: Unpack[EmbedAttrs]) -> None:
47 | super().__init__(**attributes)
48 |
49 |
50 | class Object(BasicElement[AnyChildren, ObjectAttrs]):
51 | """Defines an embedded object."""
52 |
53 | tag: str = "object"
54 |
55 | def __init__(
56 | self, *children: AnyChildren, **attributes: Unpack[ObjectAttrs]
57 | ) -> None:
58 | super().__init__(*children, **attributes)
59 |
--------------------------------------------------------------------------------
/hypermedia/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thomasborgen/hypermedia/f9e11df81c06750a7f24616da1c5811412f6bb56/hypermedia/py.typed
--------------------------------------------------------------------------------
/hypermedia/styles_and_semantics.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Never, Unpack
2 |
3 | from hypermedia.models import BasicElement
4 | from hypermedia.models.elements import ElementStrict
5 | from hypermedia.types.attributes import (
6 | DataAttrs,
7 | DetailsAttrs,
8 | DialogAttrs,
9 | GlobalAttrs,
10 | )
11 | from hypermedia.types.types import (
12 | AnyChildren,
13 | ComplexChildren,
14 | SafeString,
15 | )
16 |
17 |
18 | class Style(ElementStrict[str, GlobalAttrs]):
19 | """Defines style information for a document."""
20 |
21 | tag: str = "style"
22 |
23 | def __init__(
24 | self, child: str, *children: Never, **attributes: Unpack[GlobalAttrs]
25 | ) -> None:
26 | super().__init__(SafeString(child), **attributes)
27 |
28 |
29 | class Div(BasicElement[AnyChildren, GlobalAttrs]):
30 | """Defines a section in a document."""
31 |
32 | tag: str = "div"
33 |
34 | def __init__(
35 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
36 | ) -> None:
37 | super().__init__(*children, **attributes)
38 |
39 |
40 | class Span(BasicElement[AnyChildren, GlobalAttrs]):
41 | """Defines a section in a document."""
42 |
43 | tag: str = "span"
44 |
45 | def __init__(
46 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
47 | ) -> None:
48 | super().__init__(*children, **attributes)
49 |
50 |
51 | class Header(BasicElement[AnyChildren, GlobalAttrs]):
52 | """Defines a header for a document or section."""
53 |
54 | tag: str = "header"
55 |
56 | def __init__(
57 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
58 | ) -> None:
59 | super().__init__(*children, **attributes)
60 |
61 |
62 | class HGroup(BasicElement[ComplexChildren, GlobalAttrs]):
63 | """Defines a header and related content."""
64 |
65 | tag: str = "hgroup"
66 |
67 | def __init__(
68 | self, *children: ComplexChildren, **attributes: Unpack[GlobalAttrs]
69 | ) -> None:
70 | super().__init__(*children, **attributes)
71 |
72 |
73 | class HeaderGroup(HGroup):
74 | """Alias for `HGroup`."""
75 |
76 |
77 | class Footer(BasicElement[AnyChildren, GlobalAttrs]):
78 | """Defines a footer for a document or section."""
79 |
80 | tag: str = "footer"
81 |
82 | def __init__(
83 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
84 | ) -> None:
85 | super().__init__(*children, **attributes)
86 |
87 |
88 | class Main(BasicElement[AnyChildren, GlobalAttrs]):
89 | """Specifies the main content of a document."""
90 |
91 | tag: str = "main"
92 |
93 | def __init__(
94 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
95 | ) -> None:
96 | super().__init__(*children, **attributes)
97 |
98 |
99 | class Section(BasicElement[AnyChildren, GlobalAttrs]):
100 | """Defines a section in a document."""
101 |
102 | tag: str = "section"
103 |
104 | def __init__(
105 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
106 | ) -> None:
107 | super().__init__(*children, **attributes)
108 |
109 |
110 | class Search(BasicElement[AnyChildren, GlobalAttrs]):
111 | """Defines a search section."""
112 |
113 | tag: str = "search"
114 |
115 | def __init__(
116 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
117 | ) -> None:
118 | super().__init__(*children, **attributes)
119 |
120 |
121 | class Article(BasicElement[AnyChildren, GlobalAttrs]):
122 | """Defines an article."""
123 |
124 | tag: str = "article"
125 |
126 | def __init__(
127 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
128 | ) -> None:
129 | super().__init__(*children, **attributes)
130 |
131 |
132 | class Aside(BasicElement[AnyChildren, GlobalAttrs]):
133 | """Defines content aside from the page content."""
134 |
135 | tag: str = "aside"
136 |
137 | def __init__(
138 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
139 | ) -> None:
140 | super().__init__(*children, **attributes)
141 |
142 |
143 | class Details(BasicElement[AnyChildren, DetailsAttrs]):
144 | """Defines additional details that the user can view or hide."""
145 |
146 | tag: str = "details"
147 |
148 | def __init__(
149 | self, *children: AnyChildren, **attributes: Unpack[DetailsAttrs]
150 | ) -> None:
151 | super().__init__(*children, **attributes)
152 |
153 |
154 | class Dialog(BasicElement[AnyChildren, DialogAttrs]):
155 | """Defines a dialog box or window."""
156 |
157 | tag: str = "dialog"
158 |
159 | def __init__(
160 | self, *children: AnyChildren, **attributes: Unpack[DialogAttrs]
161 | ) -> None:
162 | super().__init__(*children, **attributes)
163 |
164 |
165 | class Summary(BasicElement[AnyChildren, GlobalAttrs]):
166 | """Defines a visible heading for a `details` element."""
167 |
168 | tag: str = "summary"
169 |
170 | def __init__(
171 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
172 | ) -> None:
173 | super().__init__(*children, **attributes)
174 |
175 |
176 | class Data(BasicElement[AnyChildren, DataAttrs]):
177 | """Adds a machine-readable translation of a given content."""
178 |
179 | tag: str = "data"
180 |
181 | def __init__(
182 | self, *children: AnyChildren, **attributes: Unpack[DataAttrs]
183 | ) -> None:
184 | super().__init__(*children, **attributes)
185 |
--------------------------------------------------------------------------------
/hypermedia/tables.py:
--------------------------------------------------------------------------------
1 | from typing_extensions import Unpack
2 |
3 | from hypermedia.models import BasicElement, VoidElement
4 | from hypermedia.types.attributes import ColAttrs, GlobalAttrs, TdAttrs, ThAttrs
5 | from hypermedia.types.types import AnyChildren, ComplexChildren
6 |
7 |
8 | class Table(BasicElement[ComplexChildren, GlobalAttrs]):
9 | """Defines a table."""
10 |
11 | tag: str = "table"
12 |
13 | def __init__(
14 | self, *children: ComplexChildren, **attributes: Unpack[GlobalAttrs]
15 | ) -> None:
16 | super().__init__(*children, **attributes)
17 |
18 |
19 | class Caption(BasicElement[AnyChildren, GlobalAttrs]):
20 | """Defines a table caption."""
21 |
22 | tag: str = "caption"
23 |
24 | def __init__(
25 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
26 | ) -> None:
27 | super().__init__(*children, **attributes)
28 |
29 |
30 | class Th(BasicElement[AnyChildren, ThAttrs]):
31 | """Defines a header cell in a table."""
32 |
33 | tag: str = "th"
34 |
35 | def __init__(
36 | self, *children: AnyChildren, **attributes: Unpack[ThAttrs]
37 | ) -> None:
38 | super().__init__(*children, **attributes)
39 |
40 |
41 | class TableHeader(Th):
42 | """Alias for `Th`."""
43 |
44 |
45 | class Tr(BasicElement[ComplexChildren, GlobalAttrs]):
46 | """Defines a row in a table."""
47 |
48 | tag: str = "tr"
49 |
50 | def __init__(
51 | self, *children: ComplexChildren, **attributes: Unpack[GlobalAttrs]
52 | ) -> None:
53 | super().__init__(*children, **attributes)
54 |
55 |
56 | class TableRow(Tr):
57 | """Alias for `Tr`."""
58 |
59 |
60 | class Td(BasicElement[AnyChildren, TdAttrs]):
61 | """Defines a cell in a table."""
62 |
63 | tag: str = "td"
64 |
65 | def __init__(
66 | self, *children: AnyChildren, **attributes: Unpack[TdAttrs]
67 | ) -> None:
68 | super().__init__(*children, **attributes)
69 |
70 |
71 | class TableData(Td):
72 | """Alias for `Td`."""
73 |
74 |
75 | class THead(BasicElement[ComplexChildren, GlobalAttrs]):
76 | """Groups the header content in a table."""
77 |
78 | tag: str = "thead"
79 |
80 | def __init__(
81 | self, *children: ComplexChildren, **attributes: Unpack[GlobalAttrs]
82 | ) -> None:
83 | super().__init__(*children, **attributes)
84 |
85 |
86 | class TableHead(THead):
87 | """Alias for `THead`."""
88 |
89 |
90 | class TBody(BasicElement[ComplexChildren, GlobalAttrs]):
91 | """Groups the body content in a table."""
92 |
93 | tag: str = "tbody"
94 |
95 | def __init__(
96 | self, *children: ComplexChildren, **attributes: Unpack[GlobalAttrs]
97 | ) -> None:
98 | super().__init__(*children, **attributes)
99 |
100 |
101 | class TableBody(TBody):
102 | """Alias for `TBody`."""
103 |
104 |
105 | class TFoot(BasicElement[ComplexChildren, GlobalAttrs]):
106 | """Groups the footer content in a table."""
107 |
108 | tag: str = "tfoot"
109 |
110 | def __init__(
111 | self, *children: ComplexChildren, **attributes: Unpack[GlobalAttrs]
112 | ) -> None:
113 | super().__init__(*children, **attributes)
114 |
115 |
116 | class TableFoot(TFoot):
117 | """Alias for `TFoot`."""
118 |
119 |
120 | class Col(VoidElement[ColAttrs]):
121 | """Specifies column properties.
122 |
123 | For each column within a `colgroup` element.
124 | """
125 |
126 | tag: str = "col"
127 |
128 | def __init__(self, **attributes: Unpack[ColAttrs]) -> None:
129 | super().__init__(**attributes)
130 |
131 |
132 | class Column(Col):
133 | """Alias for `Col`."""
134 |
135 |
136 | class ColGroup(BasicElement[AnyChildren, GlobalAttrs]):
137 | """Specifies a group of one or more columns in a table for formatting."""
138 |
139 | tag: str = "colgroup"
140 |
141 | def __init__(
142 | self, *children: AnyChildren, **attributes: Unpack[GlobalAttrs]
143 | ) -> None:
144 | super().__init__(*children, **attributes)
145 |
146 |
147 | class ColumnGroup(ColGroup):
148 | """Alias for `ColGroup`."""
149 |
--------------------------------------------------------------------------------
/hypermedia/types/__init__.py:
--------------------------------------------------------------------------------
1 | """Most of the typing used in hypermedia is taken from Ludic.
2 |
3 | https://github.com/getludic/ludic
4 | """
5 |
--------------------------------------------------------------------------------
/hypermedia/types/attributes.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated, Literal, Protocol, TypedDict
2 |
3 | from hypermedia.types.styles import CSSProperties
4 |
5 |
6 | class URLType(Protocol):
7 | """Protocol for URL-like types."""
8 |
9 | def __str__(self) -> str:
10 | """Url types must implement `__str__`."""
11 |
12 |
13 | class Alias(str):
14 | """Alias type for attributes."""
15 |
16 |
17 | class Attrs(TypedDict, total=False):
18 | """Attributes of an element."""
19 |
20 |
21 | class NoAttrs(TypedDict):
22 | """Placeholder for element with no attributes."""
23 |
24 |
25 | class HypermediaAttrs(Attrs, total=False):
26 | """Attributes for hypermedia elements."""
27 |
28 | slot: str | None
29 |
30 |
31 | class HtmlAttrs(HypermediaAttrs, total=False):
32 | """Common attributes for HTML elements."""
33 |
34 | id: str
35 | accesskey: str
36 | class_: Annotated[str, Alias("class")]
37 | classes: Annotated[list[str], Alias("class")] # merged with class_
38 | contenteditable: Literal["true", "false"]
39 | dir: Literal["ltr", "rtl", "auto"]
40 | draggable: Literal["true", "false"]
41 | enterkeyhint: Literal[
42 | "enter", "done", "go", "next", "previous", "search", "send"
43 | ]
44 | hidden: Literal["true", "false"]
45 | inert: bool
46 | inputmode: Literal[
47 | "none", "text", "search", "tel", "url", "email", "numeric", "decimal"
48 | ]
49 | lang: str
50 | popover: bool
51 | spellcheck: Literal["true", "false"]
52 | style: CSSProperties
53 | tabindex: int
54 | title: str
55 | translate: Literal["yes", "no"]
56 |
57 |
58 | class HyperscriptAttrs(Attrs, total=False):
59 | """Hyperscript attributes for HTML elements."""
60 |
61 | _: Annotated[str, Alias("_")]
62 |
63 |
64 | class HtmxAttrs(Attrs, total=False):
65 | """HTMX attributes for HTML elements.
66 |
67 | See: https://htmx.org/
68 | """
69 |
70 | hx_get: Annotated[URLType, Alias("hx-get")]
71 | hx_post: Annotated[URLType, Alias("hx-post")]
72 | hx_put: Annotated[URLType, Alias("hx-put")]
73 | hx_delete: Annotated[URLType, Alias("hx-delete")]
74 | hx_patch: Annotated[URLType, Alias("hx-patch")]
75 |
76 | hx_on: Annotated[str, Alias("hx-on")]
77 | hx_include: Annotated[str, Alias("hx-include")]
78 | hx_confirm: Annotated[str, Alias("hx-confirm")]
79 | hx_trigger: Annotated[
80 | Literal["load", "click", "dblclick", "hover", "focus", "blur"] | str,
81 | Alias("hx-trigger"),
82 | ]
83 | hx_target: Annotated[
84 | Literal["this", "next", "previous"] | str, Alias("hx-target")
85 | ]
86 | hx_select: Annotated[str, Alias("hx-select")]
87 | hx_select_oob: Annotated[str, Alias("hx-select-oob")]
88 | hx_swap: Annotated[
89 | Literal[
90 | "innerHTML",
91 | "outerHTML",
92 | "beforebegin",
93 | "afterbegin",
94 | "beforeend",
95 | "afterend",
96 | "delete",
97 | "none",
98 | ]
99 | | str,
100 | Alias("hx-swap"),
101 | ]
102 | hx_swap_oob: Annotated[Literal["true", "false"], Alias("hx-swap-oob")]
103 | hx_vals: Annotated[str, Alias("hx-vals")] # TODO: dict
104 | hx_sync: Annotated[str, Alias("hx-sync")]
105 | hx_boost: Annotated[Literal["true", "false"], Alias("hx-boost")]
106 | hx_indicator: Annotated[str, Alias("hx-indicator")]
107 | hx_push_url: Annotated[
108 | Literal["true", "false"] | str, Alias("hx-push-url")
109 | ]
110 | hx_history: Annotated[Literal["false"], Alias("hx-history")]
111 | hx_history_elt: Annotated[str, Alias("hx-history-elt")]
112 | hx_ext: Annotated[str, Alias("hx-ext")]
113 | hx_disable: Annotated[bool, Alias("hx-disable")]
114 | hx_disabled_elt: Annotated[str, Alias("hx-disabled-elt")]
115 | hx_disinherit: Annotated[str, Alias("hx-disinherit")]
116 | hx_encoding: Annotated[str, Alias("hx-encoding")]
117 | hx_headers: Annotated[str, Alias("hx-headers")] # TODO: dict
118 | hx_params: Annotated[Literal["*", "none"] | str, Alias("hx-params")]
119 | hx_preserve: Annotated[bool, Alias("hx-preserve")]
120 | hx_prompt: Annotated[str, Alias("hx-prompt")]
121 | hx_replace_url: Annotated[URLType, Alias("hx-replace-url")]
122 | hx_request: Annotated[str, Alias("hx-request")]
123 | hx_validate: Annotated[Literal["true", "false"], Alias("hx-validate")]
124 | hx_ws: Annotated[str, Alias("hx-ws")]
125 | hx_sse: Annotated[str, Alias("hx-sse")]
126 |
127 | # Extensions
128 | ws_connect: Annotated[str, Alias("ws-connect")]
129 | ws_send: Annotated[str, Alias("ws-send")]
130 | sse_connect: Annotated[str, Alias("sse-connect")]
131 | sse_send: Annotated[str, Alias("sse-send")]
132 | sse_swap: Annotated[str, Alias("sse-swap")]
133 |
134 |
135 | class WindowEventAttrs(Attrs, total=False):
136 | """Event Attributes for HTML elements."""
137 |
138 | on_afterprint: Annotated[str, Alias("onafterprint")]
139 | on_beforeprint: Annotated[str, Alias("onbeforeprint")]
140 | on_beforeunload: Annotated[str, Alias("onbeforeunload")]
141 | on_error: Annotated[str, Alias("onerror")]
142 | on_hashchange: Annotated[str, Alias("onhashchange")]
143 | on_load: Annotated[str, Alias("onload")]
144 | on_message: Annotated[str, Alias("onmessage")]
145 | on_offline: Annotated[str, Alias("onoffline")]
146 | on_online: Annotated[str, Alias("ononline")]
147 | on_pagehide: Annotated[str, Alias("onpagehide")]
148 | on_pageshow: Annotated[str, Alias("onpageshow")]
149 | on_popstate: Annotated[str, Alias("onpopstate")]
150 | on_resize: Annotated[str, Alias("onresize")]
151 | on_storage: Annotated[str, Alias("onstorage")]
152 | on_unhandledrejection: Annotated[str, Alias("onunhandledrejection")]
153 | on_unload: Annotated[str, Alias("onunload")]
154 |
155 |
156 | class FormEventAttrs(Attrs, total=False):
157 | """Event Attributes for HTML Form elements."""
158 |
159 | on_blur: Annotated[str, Alias("onblur")]
160 | on_change: Annotated[str, Alias("onchange")]
161 | on_contextmenu: Annotated[str, Alias("oncontextmenu")]
162 | on_focus: Annotated[str, Alias("onfocus")]
163 | on_input: Annotated[str, Alias("oninput")]
164 | on_invalid: Annotated[str, Alias("oninvalid")]
165 | on_reset: Annotated[str, Alias("onreset")]
166 | on_search: Annotated[str, Alias("onsearch")]
167 | on_select: Annotated[str, Alias("onselect")]
168 | on_submit: Annotated[str, Alias("onsubmit")]
169 |
170 |
171 | class KeyboardEventAttrs(Attrs, total=False):
172 | """Event Attributes for Keyboard events."""
173 |
174 | on_keydown: Annotated[str, Alias("onkeydown")]
175 | on_keypress: Annotated[str, Alias("onkeypress")]
176 | on_keyup: Annotated[str, Alias("onkeyup")]
177 |
178 |
179 | class MouseEventAttrs(Attrs, total=False):
180 | """Event Attributes for Mouse events."""
181 |
182 | on_click: Annotated[str, Alias("onclick")]
183 | on_dblclick: Annotated[str, Alias("ondblclick")]
184 | on_mousedown: Annotated[str, Alias("onmousedown")]
185 | on_mousemove: Annotated[str, Alias("onmousemove")]
186 | on_mouseout: Annotated[str, Alias("onmouseout")]
187 | on_mouseover: Annotated[str, Alias("onmouseover")]
188 | on_mouseup: Annotated[str, Alias("onmouseup")]
189 | on_wheel: Annotated[str, Alias("onwheel")]
190 |
191 |
192 | class DragEventAttrs(Attrs, total=False):
193 | """Event Attributes for Drag events."""
194 |
195 | on_drag: Annotated[str, Alias("ondrag")]
196 | on_dragend: Annotated[str, Alias("ondragend")]
197 | on_dragenter: Annotated[str, Alias("ondragenter")]
198 | on_dragleave: Annotated[str, Alias("ondragleave")]
199 | on_dragover: Annotated[str, Alias("ondragover")]
200 | on_dragstart: Annotated[str, Alias("ondragstart")]
201 | on_drop: Annotated[str, Alias("ondrop")]
202 |
203 |
204 | class ClipboardEventAttrs(Attrs, total=False):
205 | """Event Attributes for Clipboard events."""
206 |
207 | on_copy: Annotated[str, Alias("oncopy")]
208 | on_cut: Annotated[str, Alias("oncut")]
209 | on_paste: Annotated[str, Alias("onpaste")]
210 |
211 |
212 | class EventAttrs(
213 | WindowEventAttrs,
214 | FormEventAttrs,
215 | KeyboardEventAttrs,
216 | MouseEventAttrs,
217 | DragEventAttrs,
218 | ClipboardEventAttrs,
219 | total=False,
220 | ):
221 | """Event Attributes for HTML elements."""
222 |
223 |
224 | class HtmlAndEventAttrs(HtmlAttrs, EventAttrs, total=False):
225 | """Common HTML and event attributes."""
226 |
227 |
228 | class GlobalAttrs(HtmlAndEventAttrs, HtmxAttrs, HyperscriptAttrs, total=False):
229 | """Global attributes for HTML elements."""
230 |
231 |
232 | class HtmlTagAttrs(HypermediaAttrs, total=False):
233 | """Common attributes for HTML elements."""
234 |
235 | lang: str
236 | xmlns: str
237 |
238 |
239 | class MetaAttrs(HtmlAttrs, total=False):
240 | """Attributes for ` ` elements."""
241 |
242 | name: str
243 | content: str
244 | charset: str
245 | property: str
246 |
247 |
248 | class StyleAttrs(HtmlAndEventAttrs, total=False):
249 | """Attributes for `"),
52 | (Summary, "test "),
53 | ],
54 | )
55 | def test_normal_elements(element: type, result: str) -> None:
56 | assert element("test").dump() == result
57 |
58 |
59 | def test_style_is_not_escaped() -> None:
60 | assert Style('"<>').dump() == ''
61 |
--------------------------------------------------------------------------------
/tests/integration/test_tables.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from hypermedia.tables import (
4 | Caption,
5 | Col,
6 | ColGroup,
7 | Column,
8 | ColumnGroup,
9 | Table,
10 | TableBody,
11 | TableData,
12 | TableFoot,
13 | TableHead,
14 | TableHeader,
15 | TableRow,
16 | TBody,
17 | Td,
18 | TFoot,
19 | Th,
20 | THead,
21 | Tr,
22 | )
23 |
24 |
25 | @pytest.mark.parametrize(
26 | "element,alias,result",
27 | [
28 | (ColGroup, ColumnGroup, "test "),
29 | (THead, TableHead, "test "),
30 | (TBody, TableBody, "test "),
31 | (TFoot, TableFoot, "test "),
32 | (Th, TableHeader, "test "),
33 | (Tr, TableRow, "test "),
34 | (Td, TableData, "test "),
35 | ],
36 | )
37 | def test_normal_elements_and_aliases(
38 | element: type, alias: type, result: str
39 | ) -> None:
40 | assert element("test").dump() == result
41 | assert alias("test").dump() == result
42 |
43 |
44 | @pytest.mark.parametrize(
45 | "element,result",
46 | [
47 | (Caption, "test "),
48 | (Table, ""),
49 | ],
50 | )
51 | def test_normal_elements(element: type, result: str) -> None:
52 | assert element("test").dump() == result
53 |
54 |
55 | @pytest.mark.parametrize(
56 | "element,alias,result",
57 | [
58 | (Col, Column, " "),
59 | ],
60 | )
61 | def test_void_elements(element: type, alias: type, result: str) -> None:
62 | assert element().dump() == result
63 | assert alias().dump() == result
64 |
--------------------------------------------------------------------------------
/tests/models/basic_element/test_basic_element.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from hypermedia.models.base import Element
4 | from hypermedia.models.elements import (
5 | BasicElement,
6 | ElementList,
7 | ElementStrict,
8 | VoidElement,
9 | XMLVoidElement,
10 | )
11 | from hypermedia.types.types import SafeString
12 | from tests.utils import TestBasicElement
13 |
14 |
15 | def test_tag_rendering() -> None:
16 | assert TestBasicElement().dump() == " "
17 |
18 |
19 | def test_id_rendering() -> None:
20 | assert TestBasicElement(id="test").dump() == " "
21 |
22 |
23 | def test_class_rendering() -> None:
24 | assert TestBasicElement(classes=["one", "two"]).dump() == (
25 | " "
26 | )
27 |
28 |
29 | def test_attribute_rendering() -> None:
30 | assert (
31 | TestBasicElement(test="green").dump() == " "
32 | )
33 |
34 |
35 | def test_all_attributes() -> None:
36 | assert TestBasicElement(
37 | id="test", classes=["one", "two"], test="green"
38 | ).dump() == (" ")
39 |
40 |
41 | def test_text_rendering() -> None:
42 | assert TestBasicElement("test").dump() == "test "
43 |
44 |
45 | @pytest.mark.parametrize(
46 | "test_input,expected",
47 | [
48 | ("&", "&"),
49 | ("<", "<"),
50 | (">", ">"),
51 | ('"', """),
52 | ("'", "'"),
53 | ],
54 | )
55 | def test_text_escaping_rendering(test_input: str, expected: str) -> None:
56 | assert TestBasicElement(test_input).dump() == f"{expected} "
57 |
58 |
59 | def test_children_rendering() -> None:
60 | child = TestBasicElement()
61 | element = TestBasicElement(child)
62 |
63 | assert element.dump() == " "
64 |
65 |
66 | def test_children_rendering_with_text() -> None:
67 | child = TestBasicElement("test")
68 | element = TestBasicElement(child)
69 |
70 | assert element.dump() == "test "
71 |
72 |
73 | def test_child_renders_after_text() -> None:
74 | element = TestBasicElement("test", TestBasicElement())
75 |
76 | assert element.dump() == "test "
77 |
78 |
79 | def test_child_renders_before_text() -> None:
80 | element = TestBasicElement(TestBasicElement(), "test")
81 |
82 | assert element.dump() == " test "
83 |
84 |
85 | def test_all_subclasses_dumps_to_safestring() -> None:
86 | """Make sure everything dumps to SafeString."""
87 | assert all(
88 | isinstance(sub().dump(), SafeString)
89 | for sub in BasicElement.__subclasses__()
90 | )
91 |
92 | assert all(
93 | isinstance(sub("test").dump(), SafeString)
94 | for sub in ElementStrict.__subclasses__()
95 | )
96 |
97 | assert all(
98 | isinstance(sub().dump(), SafeString)
99 | for sub in VoidElement.__subclasses__()
100 | )
101 |
102 | assert all(
103 | isinstance(sub().dump(), SafeString)
104 | for sub in XMLVoidElement.__subclasses__()
105 | )
106 |
107 | assert isinstance(ElementList().dump(), SafeString)
108 |
109 | skip = {
110 | "BasicElement",
111 | "ElementList",
112 | "ElementStrict",
113 | "VoidElement",
114 | "XMLVoidElement",
115 | }
116 | assert all(
117 | isinstance(sub().dump(), SafeString) # type: ignore
118 | for sub in Element.__subclasses__()
119 | if sub.__name__ not in skip
120 | )
121 |
--------------------------------------------------------------------------------
/tests/models/element/test_attributes.py:
--------------------------------------------------------------------------------
1 | from tests.utils import TestElement
2 |
3 |
4 | def test_kwargs_stored_in_attributes() -> None:
5 | element = TestElement(test="green")
6 |
7 | assert element.attributes["test"] == "green"
8 |
9 |
10 | def test_render_attributes() -> None:
11 | element = TestElement(test="green")
12 |
13 | assert element._render_attributes() == " test='green'"
14 |
15 |
16 | def test_render_multiple_attributes_separated_by_space() -> None:
17 | element = TestElement(one="one", two="two")
18 |
19 | assert element._render_attributes() == " one='one' two='two'"
20 |
21 |
22 | def test_false_value_is_skipped() -> None:
23 | element = TestElement(test=False)
24 |
25 | assert element._render_attributes() == ""
26 |
27 |
28 | def test_none_value_is_skipped() -> None:
29 | element = TestElement(test=None)
30 |
31 | assert element._render_attributes() == ""
32 |
33 |
34 | def test_true_value_is_added_as_key_only() -> None:
35 | element = TestElement(test=True)
36 |
37 | assert element._render_attributes() == " test"
38 |
39 |
40 | def test_class_and_classes_are_combined() -> None:
41 | element = TestElement(class_="three", classes=["one", "two"])
42 |
43 | assert element._render_attributes() == " class='one two three'"
44 |
45 |
46 | def test_class_and_classes_disappear() -> None:
47 | element = TestElement(class_="three", classes=["one", "two"])
48 |
49 | element._render_attributes()
50 |
51 | assert element.attributes["class_"] == "three"
52 | assert element.attributes["classes"] == ["one", "two"]
53 |
54 |
55 | def test_aliased_keys() -> None:
56 | element = TestElement(on_afterprint="test")
57 |
58 | assert element._render_attributes() == " onafterprint='test'"
59 |
60 |
61 | def test_underscored_keys_added_with_hyphen() -> None:
62 | element = TestElement(hx_get="url")
63 |
64 | assert element._render_attributes() == " hx-get='url'"
65 |
66 |
67 | def test_custom_attributes_with_dollarsign() -> None:
68 | element = TestElement(**{"$data-show.duration_500ms": "$show"})
69 | assert element._render_attributes() == " data-show.duration_500ms='$show'"
70 |
71 |
72 | def test_custom_attributes_with_normal_ones() -> None:
73 | element = TestElement(test="green", **{"$$@-.%": "test"}, bob="bob")
74 | assert (
75 | element._render_attributes() == " test='green' $@-.%='test' bob='bob'"
76 | )
77 |
--------------------------------------------------------------------------------
/tests/models/element/test_element.py:
--------------------------------------------------------------------------------
1 | from tests.utils import TestElement
2 |
3 |
4 | def test_with_dump_allowed() -> None:
5 | assert TestElement()
6 |
7 |
8 | def test_can_have_children() -> None:
9 | child = TestElement()
10 |
11 | parent = TestElement(child)
12 |
13 | assert parent.children == (child,)
14 |
15 |
16 | def test_can_double_dump_safely() -> None:
17 | child = TestElement()
18 |
19 | parent = TestElement(
20 | child, test="test", slot="slot", classes=["a", "b"], class_="c"
21 | )
22 |
23 | assert parent.dump() == parent.dump()
24 |
--------------------------------------------------------------------------------
/tests/models/element/test_extend.py:
--------------------------------------------------------------------------------
1 | from tests.utils import TestElement
2 |
3 |
4 | def test_extend_adds_child_to_slot() -> None:
5 | element = TestElement(slot="my_slot")
6 | child = TestElement()
7 |
8 | element.extend("my_slot", child)
9 |
10 | assert element.children == (child,)
11 |
12 |
13 | def test_extend_adds_children_to_slot() -> None:
14 | element = TestElement(slot="my_slot")
15 | child_1 = TestElement()
16 | child_2 = TestElement()
17 |
18 | element.extend("my_slot", child_1, child_2)
19 |
20 | assert element.children == (child_1, child_2)
21 |
22 |
23 | def test_extend_adds_child_to_any_descendant_slot() -> None:
24 | child = TestElement(slot="descendant_slot")
25 | parent = TestElement(child)
26 | element = TestElement()
27 |
28 | parent.extend("descendant_slot", element)
29 |
30 | assert child.children == (element,)
31 |
--------------------------------------------------------------------------------
/tests/models/element/test_render_children.py:
--------------------------------------------------------------------------------
1 | from hypermedia.types.types import SafeString
2 | from tests.utils import TestElement
3 |
4 |
5 | def test_render_children_calls_dump() -> None:
6 | """Test that all children are rendered.
7 |
8 | Our custom WithDump().dump() just returns str(self) so we can
9 | expect str(child_1) + str(child_2) as output.
10 | """
11 | child_1 = TestElement()
12 | child_2 = TestElement()
13 | element = TestElement(child_1, child_2)
14 |
15 | assert element._render_children() == str(child_1) + str(child_2)
16 |
17 |
18 | def test_renders_string_children_escaped() -> None:
19 | """Test that string children are escaped correctly."""
20 | assert TestElement("<>")._render_children() == "<>"
21 |
22 |
23 | def test_renders_safe_string_children_as_is() -> None:
24 | """Test that safe string children are not escaped."""
25 | assert TestElement(SafeString("<>"))._render_children() == "<>"
26 |
--------------------------------------------------------------------------------
/tests/models/element/test_slots.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from hypermedia.models import ElementList
4 | from tests.utils import TestElement
5 |
6 |
7 | def test_can_have_a_slot() -> None:
8 | element = TestElement(slot="my_slot")
9 |
10 | assert element.slot == "my_slot"
11 |
12 |
13 | def test_raises_when_wrong_slot_name() -> None:
14 | element = TestElement(slot="my_slot")
15 |
16 | with pytest.raises(ValueError) as excinfo:
17 | element.extend("wrong_slot_name", TestElement())
18 |
19 | assert "wrong_slot_name" in str(excinfo.value)
20 |
21 |
22 | def test_raises_when_duplicate_slot_names_element_list() -> None:
23 | with pytest.raises(ValueError) as excinfo:
24 | ElementList(
25 | TestElement(slot="my_slot"),
26 | TestElement(slot="my_slot"),
27 | )
28 |
29 | assert str(excinfo.value) == "All slot names must be unique: ['my_slot']"
30 |
31 |
32 | def test_raises_when_duplicate_slot_names_in_extended_tree() -> None:
33 | with pytest.raises(ValueError) as excinfo:
34 | TestElement(slot="my_slot").extend(
35 | "my_slot", TestElement(slot="my_slot")
36 | )
37 |
38 | assert str(excinfo.value) == "All slot names must be unique: ['my_slot']"
39 |
40 |
41 | def test_slots_return_own_slot() -> None:
42 | element = TestElement(slot="my_slot")
43 |
44 | assert element.slots == {"my_slot": element}
45 |
46 |
47 | def test_slots_return_childs_slot() -> None:
48 | child = TestElement(slot="my_slot")
49 | element = TestElement(child)
50 |
51 | assert element.slots == {"my_slot": child}
52 |
53 |
54 | def test_slots_return_all_descendants_slots() -> None:
55 | grandchild = TestElement(slot="grandchild_slot")
56 | child = TestElement(grandchild, slot="child_slot")
57 | element = TestElement(child)
58 |
59 | assert element.slots == {
60 | "child_slot": child,
61 | "grandchild_slot": grandchild,
62 | }
63 |
64 |
65 | def test_non_element_children_are_safely_skipped() -> None:
66 | child = TestElement("test")
67 | element = TestElement("test", child)
68 |
69 | assert element.slots == {}
70 |
--------------------------------------------------------------------------------
/tests/models/element/test_svg_elements.py:
--------------------------------------------------------------------------------
1 | from hypermedia.images import Line, Polygon, Svg
2 |
3 |
4 | def test_svg_renders_as_expected() -> None:
5 | assert Svg().dump() == " "
6 |
7 |
8 | def test_svg_renders_children_as_expected() -> None:
9 | assert (
10 | Svg(Line(fill="red"), Polygon(fill="blue")).dump()
11 | == " "
12 | )
13 |
--------------------------------------------------------------------------------
/tests/models/element/xml_void_element/test_xml_void_element.py:
--------------------------------------------------------------------------------
1 | from tests.utils import TestXMLVoidElement
2 |
3 |
4 | def test_tag_rendering() -> None:
5 | assert TestXMLVoidElement().dump() == " "
6 |
7 |
8 | def test_id_rendering() -> None:
9 | assert TestXMLVoidElement(id="test").dump() == " "
10 |
11 |
12 | def test_class_rendering() -> None:
13 | assert TestXMLVoidElement(classes=["one", "two"]).dump() == (
14 | " "
15 | )
16 |
17 |
18 | def test_attribute_rendering() -> None:
19 | assert TestXMLVoidElement(test="green").dump() == " "
20 |
21 |
22 | def test_all_attributes() -> None:
23 | assert TestXMLVoidElement(
24 | id="test", classes=["one", "two"], test="green"
25 | ).dump() == (" ")
26 |
27 |
28 | def test_to_string() -> None:
29 | assert str(TestXMLVoidElement()) == "test"
30 |
--------------------------------------------------------------------------------
/tests/models/element_list/test_element_list.py:
--------------------------------------------------------------------------------
1 | from hypermedia.models import ElementList
2 | from tests.utils import TestBasicElement, TestVoidElement
3 |
4 |
5 | def test_children_are_rendered() -> None:
6 | """Test that all children are rendered."""
7 | child_1 = TestBasicElement()
8 | child_2 = TestVoidElement()
9 | element = ElementList(child_1, child_2)
10 |
11 | assert element.dump() == ""
12 |
--------------------------------------------------------------------------------
/tests/models/void_element/test_void_element.py:
--------------------------------------------------------------------------------
1 | from tests.utils import TestVoidElement
2 |
3 |
4 | def test_tag_rendering() -> None:
5 | assert TestVoidElement().dump() == ""
6 |
7 |
8 | def test_id_rendering() -> None:
9 | assert TestVoidElement(id="test").dump() == ""
10 |
11 |
12 | def test_class_rendering() -> None:
13 | assert TestVoidElement(classes=["one", "two"]).dump() == (
14 | ""
15 | )
16 |
17 |
18 | def test_attribute_rendering() -> None:
19 | assert TestVoidElement(test="green").dump() == ""
20 |
21 |
22 | def test_all_attributes() -> None:
23 | assert TestVoidElement(
24 | id="test", classes=["one", "two"], test="green"
25 | ).dump() == ("")
26 |
27 |
28 | def test_to_string() -> None:
29 | assert str(TestVoidElement()) == "test"
30 |
--------------------------------------------------------------------------------
/tests/test_custom_dump_elements.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from hypermedia import Comment
4 | from hypermedia.basic import Doctype
5 |
6 |
7 | def test_comment_dump() -> None:
8 | assert Comment("foo").dump() == ""
9 |
10 |
11 | @pytest.mark.parametrize(
12 | "test_input,expected",
13 | [
14 | ("&", "&"),
15 | ("<", "<"),
16 | (">", ">"),
17 | ('"', """),
18 | ("'", "'"),
19 | ],
20 | )
21 | def test_comment_dump_is_escaped(test_input: str, expected: str) -> None:
22 | assert Comment(test_input).dump() == f""
23 |
24 |
25 | def test_doctype_dump() -> None:
26 | assert Doctype().dump() == ""
27 |
--------------------------------------------------------------------------------
/tests/test_safe_string.py:
--------------------------------------------------------------------------------
1 | from hypermedia.styles_and_semantics import Div
2 | from hypermedia.types.types import SafeString
3 |
4 |
5 | def test_safe_string_does_not_escape() -> None:
6 | assert Div(SafeString("<>")).dump() == "<>
"
7 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | from hypermedia.models import BasicElement, Element, VoidElement
2 | from hypermedia.models.elements import XMLVoidElement
3 | from hypermedia.types.attributes import GlobalAttrs
4 | from hypermedia.types.types import AnyChildren, SafeString
5 |
6 |
7 | class TestElement(Element): # noqa: D101
8 | __test__ = False # Not an executable test class
9 |
10 | def dump(self) -> SafeString: # noqa: D102
11 | return SafeString(self)
12 |
13 |
14 | class TestBasicElement(BasicElement[AnyChildren, GlobalAttrs]): # noqa: D101
15 | __test__ = False # Not an executable test class
16 |
17 | tag: str = "test"
18 |
19 |
20 | class TestVoidElement(VoidElement[GlobalAttrs]): # noqa: D101
21 | __test__ = False # Not an executable test class
22 | tag: str = "test"
23 |
24 |
25 | class TestXMLVoidElement(XMLVoidElement[GlobalAttrs]): # noqa: D101
26 | __test__ = False # Not an executable test class
27 | tag: str = "test"
28 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | revision = 2
3 | requires-python = ">=3.9"
4 | resolution-markers = [
5 | "python_full_version < '3.13'",
6 | "python_full_version >= '3.13'",
7 | ]
8 |
9 | [[package]]
10 | name = "colorama"
11 | version = "0.4.6"
12 | source = { registry = "https://pypi.org/simple" }
13 | 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" }
14 | wheels = [
15 | { 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" },
16 | ]
17 |
18 | [[package]]
19 | name = "coverage"
20 | version = "7.6.10"
21 | source = { registry = "https://pypi.org/simple" }
22 | sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868, upload-time = "2024-12-26T16:59:18.734Z" }
23 | wheels = [
24 | { url = "https://files.pythonhosted.org/packages/c5/12/2a2a923edf4ddabdffed7ad6da50d96a5c126dae7b80a33df7310e329a1e/coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", size = 207982, upload-time = "2024-12-26T16:57:00.767Z" },
25 | { url = "https://files.pythonhosted.org/packages/ca/49/6985dbca9c7be3f3cb62a2e6e492a0c88b65bf40579e16c71ae9c33c6b23/coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", size = 208414, upload-time = "2024-12-26T16:57:03.826Z" },
26 | { url = "https://files.pythonhosted.org/packages/35/93/287e8f1d1ed2646f4e0b2605d14616c9a8a2697d0d1b453815eb5c6cebdb/coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", size = 236860, upload-time = "2024-12-26T16:57:06.509Z" },
27 | { url = "https://files.pythonhosted.org/packages/de/e1/cfdb5627a03567a10031acc629b75d45a4ca1616e54f7133ca1fa366050a/coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", size = 234758, upload-time = "2024-12-26T16:57:09.089Z" },
28 | { url = "https://files.pythonhosted.org/packages/6d/85/fc0de2bcda3f97c2ee9fe8568f7d48f7279e91068958e5b2cc19e0e5f600/coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", size = 235920, upload-time = "2024-12-26T16:57:10.445Z" },
29 | { url = "https://files.pythonhosted.org/packages/79/73/ef4ea0105531506a6f4cf4ba571a214b14a884630b567ed65b3d9c1975e1/coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", size = 234986, upload-time = "2024-12-26T16:57:13.298Z" },
30 | { url = "https://files.pythonhosted.org/packages/c6/4d/75afcfe4432e2ad0405c6f27adeb109ff8976c5e636af8604f94f29fa3fc/coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", size = 233446, upload-time = "2024-12-26T16:57:14.742Z" },
31 | { url = "https://files.pythonhosted.org/packages/86/5b/efee56a89c16171288cafff022e8af44f8f94075c2d8da563c3935212871/coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", size = 234566, upload-time = "2024-12-26T16:57:17.368Z" },
32 | { url = "https://files.pythonhosted.org/packages/f2/db/67770cceb4a64d3198bf2aa49946f411b85ec6b0a9b489e61c8467a4253b/coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", size = 210675, upload-time = "2024-12-26T16:57:18.775Z" },
33 | { url = "https://files.pythonhosted.org/packages/8d/27/e8bfc43f5345ec2c27bc8a1fa77cdc5ce9dcf954445e11f14bb70b889d14/coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", size = 211518, upload-time = "2024-12-26T16:57:21.415Z" },
34 | { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088, upload-time = "2024-12-26T16:57:22.833Z" },
35 | { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536, upload-time = "2024-12-26T16:57:25.578Z" },
36 | { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474, upload-time = "2024-12-26T16:57:28.659Z" },
37 | { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880, upload-time = "2024-12-26T16:57:30.095Z" },
38 | { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750, upload-time = "2024-12-26T16:57:31.48Z" },
39 | { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642, upload-time = "2024-12-26T16:57:34.09Z" },
40 | { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266, upload-time = "2024-12-26T16:57:35.48Z" },
41 | { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045, upload-time = "2024-12-26T16:57:36.952Z" },
42 | { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647, upload-time = "2024-12-26T16:57:39.84Z" },
43 | { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508, upload-time = "2024-12-26T16:57:41.234Z" },
44 | { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281, upload-time = "2024-12-26T16:57:42.968Z" },
45 | { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514, upload-time = "2024-12-26T16:57:45.747Z" },
46 | { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537, upload-time = "2024-12-26T16:57:48.647Z" },
47 | { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572, upload-time = "2024-12-26T16:57:51.668Z" },
48 | { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639, upload-time = "2024-12-26T16:57:53.175Z" },
49 | { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072, upload-time = "2024-12-26T16:57:56.087Z" },
50 | { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386, upload-time = "2024-12-26T16:57:57.572Z" },
51 | { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054, upload-time = "2024-12-26T16:57:58.967Z" },
52 | { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904, upload-time = "2024-12-26T16:58:00.688Z" },
53 | { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692, upload-time = "2024-12-26T16:58:02.35Z" },
54 | { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308, upload-time = "2024-12-26T16:58:04.487Z" },
55 | { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565, upload-time = "2024-12-26T16:58:06.774Z" },
56 | { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083, upload-time = "2024-12-26T16:58:10.27Z" },
57 | { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235, upload-time = "2024-12-26T16:58:12.497Z" },
58 | { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220, upload-time = "2024-12-26T16:58:15.619Z" },
59 | { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847, upload-time = "2024-12-26T16:58:17.126Z" },
60 | { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922, upload-time = "2024-12-26T16:58:20.198Z" },
61 | { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783, upload-time = "2024-12-26T16:58:23.614Z" },
62 | { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965, upload-time = "2024-12-26T16:58:26.765Z" },
63 | { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719, upload-time = "2024-12-26T16:58:28.781Z" },
64 | { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050, upload-time = "2024-12-26T16:58:31.616Z" },
65 | { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321, upload-time = "2024-12-26T16:58:34.509Z" },
66 | { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039, upload-time = "2024-12-26T16:58:36.072Z" },
67 | { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758, upload-time = "2024-12-26T16:58:39.458Z" },
68 | { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119, upload-time = "2024-12-26T16:58:41.018Z" },
69 | { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597, upload-time = "2024-12-26T16:58:42.827Z" },
70 | { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473, upload-time = "2024-12-26T16:58:44.486Z" },
71 | { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737, upload-time = "2024-12-26T16:58:45.919Z" },
72 | { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611, upload-time = "2024-12-26T16:58:47.883Z" },
73 | { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781, upload-time = "2024-12-26T16:58:50.822Z" },
74 | { url = "https://files.pythonhosted.org/packages/40/41/473617aadf9a1c15bc2d56be65d90d7c29bfa50a957a67ef96462f7ebf8e/coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a", size = 207978, upload-time = "2024-12-26T16:58:52.834Z" },
75 | { url = "https://files.pythonhosted.org/packages/10/f6/480586607768b39a30e6910a3c4522139094ac0f1677028e1f4823688957/coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27", size = 208415, upload-time = "2024-12-26T16:58:56.317Z" },
76 | { url = "https://files.pythonhosted.org/packages/f1/af/439bb760f817deff6f4d38fe7da08d9dd7874a560241f1945bc3b4446550/coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4", size = 236452, upload-time = "2024-12-26T16:58:59.158Z" },
77 | { url = "https://files.pythonhosted.org/packages/d0/13/481f4ceffcabe29ee2332e60efb52e4694f54a402f3ada2bcec10bb32e43/coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f", size = 234374, upload-time = "2024-12-26T16:59:00.809Z" },
78 | { url = "https://files.pythonhosted.org/packages/c5/59/4607ea9d6b1b73e905c7656da08d0b00cdf6e59f2293ec259e8914160025/coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25", size = 235505, upload-time = "2024-12-26T16:59:03.869Z" },
79 | { url = "https://files.pythonhosted.org/packages/85/60/d66365723b9b7f29464b11d024248ed3523ce5aab958e4ad8c43f3f4148b/coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315", size = 234616, upload-time = "2024-12-26T16:59:05.876Z" },
80 | { url = "https://files.pythonhosted.org/packages/74/f8/2cf7a38e7d81b266f47dfcf137fecd8fa66c7bdbd4228d611628d8ca3437/coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90", size = 233099, upload-time = "2024-12-26T16:59:08.927Z" },
81 | { url = "https://files.pythonhosted.org/packages/50/2b/bff6c1c6b63c4396ea7ecdbf8db1788b46046c681b8fcc6ec77db9f4ea49/coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d", size = 234089, upload-time = "2024-12-26T16:59:10.574Z" },
82 | { url = "https://files.pythonhosted.org/packages/bf/b5/baace1c754d546a67779358341aa8d2f7118baf58cac235db457e1001d1b/coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18", size = 210701, upload-time = "2024-12-26T16:59:12.212Z" },
83 | { url = "https://files.pythonhosted.org/packages/b1/bf/9e1e95b8b20817398ecc5a1e8d3e05ff404e1b9fb2185cd71561698fe2a2/coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59", size = 211482, upload-time = "2024-12-26T16:59:15.165Z" },
84 | { url = "https://files.pythonhosted.org/packages/a1/70/de81bfec9ed38a64fc44a77c7665e20ca507fc3265597c28b0d989e4082e/coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f", size = 200223, upload-time = "2024-12-26T16:59:16.968Z" },
85 | ]
86 |
87 | [package.optional-dependencies]
88 | toml = [
89 | { name = "tomli", marker = "python_full_version <= '3.11'" },
90 | ]
91 |
92 | [[package]]
93 | name = "exceptiongroup"
94 | version = "1.2.2"
95 | source = { registry = "https://pypi.org/simple" }
96 | 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" }
97 | wheels = [
98 | { 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" },
99 | ]
100 |
101 | [[package]]
102 | name = "hypermedia"
103 | version = "5.3.4"
104 | source = { editable = "." }
105 | dependencies = [
106 | { name = "typing-extensions" },
107 | ]
108 |
109 | [package.dev-dependencies]
110 | dev = [
111 | { name = "mypy" },
112 | { name = "pytest" },
113 | { name = "pytest-cov" },
114 | { name = "ruff" },
115 | ]
116 |
117 | [package.metadata]
118 | requires-dist = [{ name = "typing-extensions", specifier = ">=4.12.2" }]
119 |
120 | [package.metadata.requires-dev]
121 | dev = [
122 | { name = "mypy", specifier = ">=1.14.1" },
123 | { name = "pytest", specifier = ">=8.3.4" },
124 | { name = "pytest-cov", specifier = ">=6.0.0" },
125 | { name = "ruff", specifier = ">=0.9.4" },
126 | ]
127 |
128 | [[package]]
129 | name = "iniconfig"
130 | version = "2.0.0"
131 | source = { registry = "https://pypi.org/simple" }
132 | 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" }
133 | wheels = [
134 | { 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" },
135 | ]
136 |
137 | [[package]]
138 | name = "mypy"
139 | version = "1.14.1"
140 | source = { registry = "https://pypi.org/simple" }
141 | dependencies = [
142 | { name = "mypy-extensions" },
143 | { name = "tomli", marker = "python_full_version < '3.11'" },
144 | { name = "typing-extensions" },
145 | ]
146 | sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" }
147 | wheels = [
148 | { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" },
149 | { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" },
150 | { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" },
151 | { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" },
152 | { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" },
153 | { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" },
154 | { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" },
155 | { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" },
156 | { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" },
157 | { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" },
158 | { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" },
159 | { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" },
160 | { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" },
161 | { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" },
162 | { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" },
163 | { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" },
164 | { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" },
165 | { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" },
166 | { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" },
167 | { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" },
168 | { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" },
169 | { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" },
170 | { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" },
171 | { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" },
172 | { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" },
173 | { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" },
174 | { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" },
175 | { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" },
176 | { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" },
177 | { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" },
178 | { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" },
179 | ]
180 |
181 | [[package]]
182 | name = "mypy-extensions"
183 | version = "1.0.0"
184 | source = { registry = "https://pypi.org/simple" }
185 | 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" }
186 | wheels = [
187 | { 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" },
188 | ]
189 |
190 | [[package]]
191 | name = "packaging"
192 | version = "24.2"
193 | source = { registry = "https://pypi.org/simple" }
194 | 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" }
195 | wheels = [
196 | { 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" },
197 | ]
198 |
199 | [[package]]
200 | name = "pluggy"
201 | version = "1.5.0"
202 | source = { registry = "https://pypi.org/simple" }
203 | 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" }
204 | wheels = [
205 | { 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" },
206 | ]
207 |
208 | [[package]]
209 | name = "pytest"
210 | version = "8.3.4"
211 | source = { registry = "https://pypi.org/simple" }
212 | dependencies = [
213 | { name = "colorama", marker = "sys_platform == 'win32'" },
214 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
215 | { name = "iniconfig" },
216 | { name = "packaging" },
217 | { name = "pluggy" },
218 | { name = "tomli", marker = "python_full_version < '3.11'" },
219 | ]
220 | 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" }
221 | wheels = [
222 | { 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" },
223 | ]
224 |
225 | [[package]]
226 | name = "pytest-cov"
227 | version = "6.0.0"
228 | source = { registry = "https://pypi.org/simple" }
229 | dependencies = [
230 | { name = "coverage", extra = ["toml"] },
231 | { name = "pytest" },
232 | ]
233 | sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945, upload-time = "2024-10-29T20:13:35.363Z" }
234 | wheels = [
235 | { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949, upload-time = "2024-10-29T20:13:33.215Z" },
236 | ]
237 |
238 | [[package]]
239 | name = "ruff"
240 | version = "0.9.4"
241 | source = { registry = "https://pypi.org/simple" }
242 | sdist = { url = "https://files.pythonhosted.org/packages/c0/17/529e78f49fc6f8076f50d985edd9a2cf011d1dbadb1cdeacc1d12afc1d26/ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7", size = 3599458, upload-time = "2025-01-30T18:09:51.03Z" }
243 | wheels = [
244 | { url = "https://files.pythonhosted.org/packages/b6/f8/3fafb7804d82e0699a122101b5bee5f0d6e17c3a806dcbc527bb7d3f5b7a/ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706", size = 11668400, upload-time = "2025-01-30T18:08:46.508Z" },
245 | { url = "https://files.pythonhosted.org/packages/2e/a6/2efa772d335da48a70ab2c6bb41a096c8517ca43c086ea672d51079e3d1f/ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf", size = 11628395, upload-time = "2025-01-30T18:08:50.87Z" },
246 | { url = "https://files.pythonhosted.org/packages/dc/d7/cd822437561082f1c9d7225cc0d0fbb4bad117ad7ac3c41cd5d7f0fa948c/ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b", size = 11090052, upload-time = "2025-01-30T18:08:54.498Z" },
247 | { url = "https://files.pythonhosted.org/packages/9e/67/3660d58e893d470abb9a13f679223368ff1684a4ef40f254a0157f51b448/ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137", size = 11882221, upload-time = "2025-01-30T18:08:57.784Z" },
248 | { url = "https://files.pythonhosted.org/packages/79/d1/757559995c8ba5f14dfec4459ef2dd3fcea82ac43bc4e7c7bf47484180c0/ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e", size = 11424862, upload-time = "2025-01-30T18:09:01.167Z" },
249 | { url = "https://files.pythonhosted.org/packages/c0/96/7915a7c6877bb734caa6a2af424045baf6419f685632469643dbd8eb2958/ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec", size = 12626735, upload-time = "2025-01-30T18:09:05.312Z" },
250 | { url = "https://files.pythonhosted.org/packages/0e/cc/dadb9b35473d7cb17c7ffe4737b4377aeec519a446ee8514123ff4a26091/ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b", size = 13255976, upload-time = "2025-01-30T18:09:09.425Z" },
251 | { url = "https://files.pythonhosted.org/packages/5f/c3/ad2dd59d3cabbc12df308cced780f9c14367f0321e7800ca0fe52849da4c/ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a", size = 12752262, upload-time = "2025-01-30T18:09:13.112Z" },
252 | { url = "https://files.pythonhosted.org/packages/c7/17/5f1971e54bd71604da6788efd84d66d789362b1105e17e5ccc53bba0289b/ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214", size = 14401648, upload-time = "2025-01-30T18:09:17.086Z" },
253 | { url = "https://files.pythonhosted.org/packages/30/24/6200b13ea611b83260501b6955b764bb320e23b2b75884c60ee7d3f0b68e/ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231", size = 12414702, upload-time = "2025-01-30T18:09:21.672Z" },
254 | { url = "https://files.pythonhosted.org/packages/34/cb/f5d50d0c4ecdcc7670e348bd0b11878154bc4617f3fdd1e8ad5297c0d0ba/ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b", size = 11859608, upload-time = "2025-01-30T18:09:25.663Z" },
255 | { url = "https://files.pythonhosted.org/packages/d6/f4/9c8499ae8426da48363bbb78d081b817b0f64a9305f9b7f87eab2a8fb2c1/ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6", size = 11485702, upload-time = "2025-01-30T18:09:28.903Z" },
256 | { url = "https://files.pythonhosted.org/packages/18/59/30490e483e804ccaa8147dd78c52e44ff96e1c30b5a95d69a63163cdb15b/ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c", size = 12067782, upload-time = "2025-01-30T18:09:32.371Z" },
257 | { url = "https://files.pythonhosted.org/packages/3d/8c/893fa9551760b2f8eb2a351b603e96f15af167ceaf27e27ad873570bc04c/ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0", size = 12483087, upload-time = "2025-01-30T18:09:36.124Z" },
258 | { url = "https://files.pythonhosted.org/packages/23/15/f6751c07c21ca10e3f4a51ea495ca975ad936d780c347d9808bcedbd7182/ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402", size = 9852302, upload-time = "2025-01-30T18:09:40.013Z" },
259 | { url = "https://files.pythonhosted.org/packages/12/41/2d2d2c6a72e62566f730e49254f602dfed23019c33b5b21ea8f8917315a1/ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e", size = 10850051, upload-time = "2025-01-30T18:09:43.42Z" },
260 | { url = "https://files.pythonhosted.org/packages/c6/e6/3d6ec3bc3d254e7f005c543a661a41c3e788976d0e52a1ada195bd664344/ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41", size = 10078251, upload-time = "2025-01-30T18:09:48.01Z" },
261 | ]
262 |
263 | [[package]]
264 | name = "tomli"
265 | version = "2.2.1"
266 | source = { registry = "https://pypi.org/simple" }
267 | 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" }
268 | wheels = [
269 | { 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" },
270 | { 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" },
271 | { 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" },
272 | { 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" },
273 | { 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" },
274 | { 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" },
275 | { 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" },
276 | { 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" },
277 | { 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" },
278 | { 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" },
279 | { 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" },
280 | { 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" },
281 | { 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" },
282 | { 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" },
283 | { 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" },
284 | { 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" },
285 | { 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" },
286 | { 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" },
287 | { 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" },
288 | { 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" },
289 | { 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" },
290 | { 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" },
291 | { 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" },
292 | { 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" },
293 | { 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" },
294 | { 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" },
295 | { 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" },
296 | { 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" },
297 | { 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" },
298 | { 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" },
299 | { 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" },
300 | ]
301 |
302 | [[package]]
303 | name = "typing-extensions"
304 | version = "4.12.2"
305 | source = { registry = "https://pypi.org/simple" }
306 | 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" }
307 | wheels = [
308 | { 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" },
309 | ]
310 |
--------------------------------------------------------------------------------