├── .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 | # '
  • main
my header
Some content
' 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}".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}".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, "test
"), 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 | --------------------------------------------------------------------------------