[
  {
    "path": ".gitattributes",
    "content": "docs/Makefile generated\ndocs/make.bat generated\n*.svg generated\n*.png binary\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: ZeroIntensity\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "name: Bug report\ndescription: Submit a bug report\nlabels: [\"bug\"]\nbody:\n    - type: textarea\n      attributes:\n          label: \"Problem:\"\n          description: >\n              Give a clear description on what's going wrong and how to reproduce it, if possible.\n\n          value: |\n              ```py\n              # Add your code here, if needed\n              ```\n      validations:\n          required: true\n    - type: input\n      attributes:\n          label: \"Version:\"\n          value: |\n              What version(s) of view.py are you using?\n      validations:\n          required: true\n    - type: dropdown\n      attributes:\n          label: \"Operating system(s) tested on:\"\n          multiple: true\n          options:\n              - Linux\n              - macOS\n              - Windows\n              - Other\n      validations:\n          required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "content": "name: Feature\ndescription: Suggest a new feature.\nlabels: [\"feature\"]\nbody:\n    - type: markdown\n      attributes:\n          value: |\n              # Feature Proposal\n\n              This is where you should propose a new feature to view.py.\n    - type: textarea\n      attributes:\n          label: \"Proposal:\"\n          description: >\n              Outline your idea and why it would be a good idea for view.py. Make sure to include an example API for what this could look like if implemented.\n          value: |\n              ```py\n              # Example API\n              ```\n      validations:\n          required: true\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n    push:\n        tags:\n            - v*\n        branches:\n            - main\n        paths:\n            - \"src/**\"\n\nconcurrency:\n    group: build-${{ github.head_ref }}\n    cancel-in-progress: true\n\njobs:\n    pure-python-wheel-and-sdist:\n        name: Build a pure Python wheel and source distribution\n        runs-on: ubuntu-latest\n\n        steps:\n            - uses: actions/checkout@v3\n              with:\n                  fetch-depth: 0\n\n            - name: Install build dependencies\n              run: python -m pip install --upgrade build\n\n            - name: Build\n              run: python -m build\n\n            - uses: actions/upload-artifact@v4\n              with:\n                  name: artifacts\n                  path: dist/*\n                  if-no-files-found: error\n\n    publish:\n        name: Publish release\n        needs:\n            - pure-python-wheel-and-sdist\n        runs-on: ubuntu-latest\n        if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')\n\n        steps:\n            - uses: actions/download-artifact@v4\n              with:\n                  name: artifacts\n                  path: dist\n\n            - name: Push build artifacts to PyPI\n              uses: pypa/gh-action-pypi-publish@v1.13.0\n              with:\n                  skip_existing: true\n                  user: __token__\n                  password: ${{ secrets.PYPI_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non:\n    pull_request:\n        branches:\n            - main\n\nconcurrency:\n    group: build-${{ github.head_ref }}\n    cancel-in-progress: true\n\njobs:\n    lint:\n        name: Lint source code\n        runs-on: ubuntu-latest\n\n        steps:\n            - uses: actions/checkout@v3\n              with:\n                  fetch-depth: 0\n\n            - name: Install Hatch\n              run: pip install hatch\n\n            - name: Run linter\n              run: hatch fmt -l\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non:\n    push:\n        branches:\n            - main\n    pull_request:\n        branches:\n            - main\n\nconcurrency:\n    group: test-${{ github.head_ref }}\n    cancel-in-progress: true\n\nenv:\n    PYTHONUNBUFFERED: \"1\"\n    FORCE_COLOR: \"1\"\n    PYTHONIOENCODING: \"utf8\"\n\njobs:\n    changes:\n        name: Check for changed files\n        runs-on: ubuntu-latest\n        outputs:\n            source: ${{ steps.filter.outputs.source }}\n            tests: ${{ steps.filter.outputs.tests }}\n        steps:\n            - uses: actions/checkout@v2\n            - uses: dorny/paths-filter@v3\n              id: filter\n              with:\n                  filters: |\n                      source:\n                        - 'src/**'\n                      tests:\n                        - 'tests/**'\n\n    run-tests:\n        needs: changes\n        if: ${{ needs.changes.outputs.source == 'true' || needs.changes.outputs.tests == 'true' }}\n        name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }}\n        runs-on: ${{ matrix.os }}\n        strategy:\n            fail-fast: true\n            matrix:\n                os: [ubuntu-latest, windows-latest, macos-latest]\n                python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n        steps:\n            - uses: actions/checkout@v3\n\n            - name: Set up Python ${{ matrix.python-version }}\n              uses: actions/setup-python@v4\n              with:\n                  python-version: ${{ matrix.python-version }}\n\n            - name: Install Hatch\n              uses: pypa/hatch@install\n\n            - name: Run tests\n              run: hatch test\n\n    tests-pass:\n        runs-on: ubuntu-latest\n        name: All tests passed\n        if: always()\n\n        needs:\n            - run-tests\n\n        steps:\n            - name: Check whether all tests passed\n              uses: re-actors/alls-green@release/v1\n              with:\n                  jobs: ${{ toJSON(needs) }}\n                  allowed-skips: ${{ toJSON(needs) }}\n"
  },
  {
    "path": ".github/workflows/triage.yml",
    "content": "name: Triage\non:\n    pull_request:\n        types:\n            - \"opened\"\n            - \"reopened\"\n            - \"synchronize\"\n            - \"labeled\"\n            - \"unlabeled\"\n\njobs:\n    changelog_check:\n        runs-on: ubuntu-latest\n        name: Check for changelog updates\n        steps:\n            - name: \"Check if the source directory was changed\"\n              uses: dorny/paths-filter@v3\n              id: changes\n              with:\n                  filters: |\n                      src:\n                        - 'src/**'\n\n            - name: \"Check for changelog updates\"\n              if: steps.changes.outputs.src == 'true'\n              uses: brettcannon/check-for-changed-files@v1\n              with:\n                  file-pattern: |\n                      CHANGELOG.md\n                  skip-label: \"skip changelog\"\n                  failure-message: \"Missing a CHANGELOG.md update; please add one or apply the ${skip-label} label to the pull request\"\n\n    tests_check:\n        runs-on: ubuntu-latest\n        name: Check for updated tests\n        steps:\n            - name: \"Check if the source directory was changed\"\n              uses: dorny/paths-filter@v3\n              id: changes\n              with:\n                  filters: |\n                      src:\n                        - 'src/**'\n\n            - name: \"Check for test updates\"\n              if: steps.changes.outputs.src == 'true'\n              uses: brettcannon/check-for-changed-files@v1\n              with:\n                  file-pattern: |\n                      tests/*\n                  skip-label: \"skip tests\"\n                  failure-message: \"Missing unit tests; please add some or apply the ${skip-label} label to the pull request\"\n\n    all_green:\n        runs-on: ubuntu-latest\n        name: PR has no missing information\n        if: always()\n\n        needs:\n            - changelog_check\n            - tests_check\n\n        steps:\n            - name: Check whether jobs passed\n              uses: re-actors/alls-green@release/v1\n              with:\n                  jobs: ${{ toJSON(needs) }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Python\n__pycache__/\n.venv/\n.hypothesis/\n\n# LSP\n.vscode/\ncompile_flags.txt\n\n# Sphinx\ndocs/_build/\ndocs/generated/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: check-toml\n      - id: check-yaml\n      - id: end-of-file-fixer\n      - id: trailing-whitespace\n        args: [--markdown-linebreak-ext=md]\n\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    # Ruff version.\n    rev: v0.14.10\n    hooks:\n      # Run the linter.\n      - id: ruff-check\n        args: [ --fix ]\n      # Run the formatter.\n      - id: ruff-format\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## Unreleased\n\n-   Removed everything from prior releases!\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025-present Peter Bierma <peter@python.org>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/ZeroIntensity/view.py/main/logos/logo_theme_dark.png\" alt=\"view.py logo (dark)\"  width=450 height=auto>\n      <source media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/ZeroIntensity/view.py/main/logos/logo_theme_light.png\" alt=\"view.py logo (light)\"  width=450 height=auto>\n      <img alt=\"view.py logo\">\n    </picture>\n</div>\n\n<div align=\"center\"><h2>The Batteries-Detachable Web Framework</h2></div>\n\nThis is a work-in-progress!\n\n## Installation\n\nIt's highly recommended to install from source at the moment:\n\n```\n$ pip install git+https://github.com/zerointensity/view.py\n```\n\n## Examples\n\n### Simple Hello World\n\n```py\nfrom view.core.app import App\n\nfrom view.dom.core import html_response\nfrom view.dom.components import page\nfrom view.dom.primitives import h1\n\napp = App()\n\n\n@app.get(\"/\")\n@html_response\nasync def home():\n    with page(\"Hello, view.py!\"):\n        yield h1(\"Nobody expects the Spanish Inquisition\")\n\n\napp.run()\n```\n\n### Button Counter\n\n```py\nfrom view.core.app import App\nfrom view.dom.core import HTMLNode, html_response\nfrom view.dom.components import page\nfrom view.dom.primitives import button, p\n\nfrom view.javascript import javascript_compiler, as_javascript_expression\n\napp = App()\n\n\n@javascript_compiler\ndef click_button(counter: HTMLNode):\n    yield f\"let node = {as_javascript_expression(counter)};\"\n    yield f\"let currentNumber = parseInt(node.innerHTML);\"\n    yield f\"node.innerHTML = ++currentNumber;\"\n\n\n@app.get(\"/\")\n@html_response\nasync def home():\n    with page(\"Counter\"):\n        count = p(\"0\")\n        yield count\n        yield button(\"Click me!\", onclick=click_button(count))\n\n\napp.run()\n```\n\n## Copyright\n\n`view.py` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/api.rst",
    "content": "API Reference\n=============\n\n.. autosummary::\n   :toctree: generated\n   :recursive:\n\n   view\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Project information -----------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information\n\nproject = \"view.py\"\ncopyright = \"2026, Peter Bierma\"\nauthor = \"Peter Bierma\"\n\n# -- General configuration ---------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration\n\nextensions = [\n    \"sphinx.ext.intersphinx\",\n    'sphinx.ext.autodoc',\n    'sphinx.ext.autosummary',\n]\nautosummary_generate = True\nadd_module_names = False  # Cleaner output\n\n# This is the key part for making detailed pages:\nautodoc_default_options = {\n    'members': True,\n    'undoc-members': True,\n    'show-inheritance': True,\n    \"inherited-members\": True,\n    \"ignore-module-all\": True,\n}\n\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\"]\nintersphinx_mapping = {\"python\": (\"https://docs.python.org/3\", None)}\n\n\n# -- Options for HTML output -------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output\n\nhtml_theme = \"shibuya\"\nhtml_theme_options = {\n    \"accent_color\": \"blue\",\n    \"light_logo\": \"_static/logo_black.svg\",\n    \"dark_logo\": \"_static/logo_white.svg\",\n    \"logo_target\": \"https://view.zintensity.dev\",\n    \"github_url\": \"https://github.com/ZeroIntensity/view.py\",\n    \"announcement\": \"view.py is currently in alpha and not considered ready for production\",\n}\nhtml_static_path = [\"_static\"]\nhtml_favicon = \"_static/favicon.ico\"\nhtml_context = {\n    \"source_type\": \"github\",\n    \"source_user\": \"ZeroIntensity\",\n    \"source_repo\": \"view.py\",\n    \"source_version\": \"main\",\n    \"source_docs_path\": \"/docs/\",\n}\n"
  },
  {
    "path": "docs/index.rst",
    "content": "view.py documentation\n=====================\n\nNothing here yet...\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n   api\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "hatch.toml",
    "content": "[version]\npath = \"src/view/__about__.py\"\n\n[build.targets.sdist]\nonly-include = [\"src/\"]\n\n[build.targets.wheel]\npackages = [\"src/view\"]\n\n[envs.hatch-test]\nextra-args = [\"-vv\"]\nextra-dependencies = [\n    \"pytest-asyncio\",\n    \"requests\",\n    \"uvicorn\",\n    \"hypercorn\",\n    \"daphne\",\n    \"gunicorn\",\n    \"werkzeug\",\n    \"hypothesis\",\n]\nrandomize = true\nretries = 3\nretries-delay = 1\nparallel = true\n\n[[envs.hatch-test.matrix]]\npython = [\"3.14\", \"3.13\", \"3.12\", \"3.11\", \"3.10\"]\n"
  },
  {
    "path": "netlify.toml",
    "content": "[build]\ncommand = \"pip install . && sphinx-build -M html ./docs ./site\"\npublish = \"site/html\"\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"hatchling>=1\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"view.py\"\ndescription = 'The Batteries-Detachable Web Framework.'\nreadme = \"README.md\"\nrequires-python = \">=3.9\"\nkeywords = []\nauthors = [\n  { name = \"Peter Bierma\", email = \"peter@python.org\" },\n]\nclassifiers = [\n  \"Programming Language :: Python\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: 3.13\",\n  \"Programming Language :: Python :: 3.14\",\n  \"Programming Language :: Python :: Implementation :: CPython\",\n]\ndependencies = [\"typing_extensions>=4\"]\ndynamic = [\"version\", \"license\"]\n\n#[project.optional-dependencies]\n\n[project.urls]\nDocumentation = \"https://view.zintensity.dev\"\nIssues = \"https://github.com/ZeroIntensity/view.py/issues\"\nSource = \"https://github.com/ZeroIntensity/view.py\"\nFunding = \"https://github.com/sponsors/ZeroIntensity\"\n\n#[project.scripts]\n#view = \"view.__main__:main\"\n#view-py = \"view.__main__:main\"\n\n[tool.ruff]\nexclude = [\"tests/\", \"docs/\"]\nline-length = 79\nindent-width = 4\n\n[tool.ruff.lint]\nignore = [\n  \"S101\", # We intentionally want assertions to be debug-only\n  \"EM101\", # Improves traceback readability(?), but damages code readability\n  \"EM102\", # Same as above\n  \"TRY003\", # Moves relevant messages away from where they are raised.\n  \"PLC0415\", # This is generally done to avoid circular imports.\n]\n\n[tool.ruff.lint.per-file-ignores]\n\"__init__.py\" = [\"PLC0414\"]\n\"status_codes.py\" = [\"N818\"]\n\"primitives.py\" = [\"A001\", \"A002\", \"B008\"]\n\"servers.py\" = [\"PLC0415\", \"RET503\"]\n"
  },
  {
    "path": "requirements.txt",
    "content": "# Requirements for Netlify\nsphinx>=7.0\nshibuya>=2025\n"
  },
  {
    "path": "runtime.txt",
    "content": "3.10\n"
  },
  {
    "path": "src/view/__about__.py",
    "content": "__version__ = \"0.1.0-dev\"\n__author__ = \"Peter Bierma <peter@python.org>\"\n__license__ = \"MIT\"\n"
  },
  {
    "path": "src/view/__init__.py",
    "content": "\"\"\"\nview.py - The Batteries-Detachable Web Framework.\n\"\"\"\n\nfrom view import cache as cache\nfrom view import core as core\nfrom view import dom as dom\nfrom view import javascript as javascript\nfrom view import run as run\nfrom view import testing as testing\nfrom view import utils as utils\nfrom view.__about__ import *  # noqa: F403\n"
  },
  {
    "path": "src/view/cache.py",
    "content": "\"\"\"\nUtilities for caching responses from views.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport math\nimport time\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass, field\nfrom typing import TYPE_CHECKING, Generic, ParamSpec, TypeVar\n\nif TYPE_CHECKING:\n    from collections.abc import Callable\n\n    from view.core.headers import HTTPHeaders\n\nfrom view.core.response import (\n    Response,\n    TextResponse,\n    ViewResult,\n    wrap_view_result,\n)\n\n__all__ = (\"in_memory_cache\",)\n\nT = TypeVar(\"T\", bound=ViewResult)\nP = ParamSpec(\"P\")\n\n\n@dataclass(slots=True)\nclass BaseCache(ABC, Generic[P, T]):\n    \"\"\"\n    Base class for caches.\n    \"\"\"\n\n    callable: Callable[P, T]\n\n    @abstractmethod\n    def invalidate(self) -> None:\n        \"\"\"\n        Invalidate the cache.\n        \"\"\"\n\n    @abstractmethod\n    async def __call__(\n        self, *args: P.args, **kwargs: P.kwargs\n    ) -> Response: ...\n\n\n@dataclass(slots=True, frozen=True)\nclass _CachedResponse:\n    body: bytes\n    headers: HTTPHeaders\n    status: int\n    last_reset: float\n\n    @classmethod\n    async def from_response(cls, response: Response) -> _CachedResponse:\n        body = await response.body()\n        return cls(body, response.headers, response.status_code, time.time())\n\n    def as_response(self) -> Response:\n        return TextResponse.from_content(\n            self.body, status_code=self.status, headers=self.headers\n        )\n\n\n@dataclass(slots=True)\nclass InMemoryCache(BaseCache[P, T]):\n    \"\"\"\n    Wrapper class for a cache stored in memory.\n    \"\"\"\n\n    callable: Callable[P, T]\n    reset_frequency: float\n    _cached_response: _CachedResponse | None = field(\n        init=False, repr=False, default=None\n    )\n\n    def invalidate(self) -> None:\n        self._cached_response = None\n\n    async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Response:\n        if self._cached_response is None:\n            result = await wrap_view_result(self.callable(*args, **kwargs))\n            cached = await _CachedResponse.from_response(result)\n            self._cached_response = cached\n            return cached.as_response()\n\n        if (\n            time.time() - self._cached_response.last_reset\n        ) > self.reset_frequency:\n            self.invalidate()\n            return await self(*args, **kwargs)\n\n        return self._cached_response.as_response()\n\n\ndef minutes(number: int, /) -> int:\n    \"\"\"\n    Convert minutes to seconds.\n\n    This is for use in cache decorators.\n    \"\"\"\n    return number * 60\n\n\ndef seconds(number: int, /) -> int:\n    \"\"\"\n    Do nothing and return ``number``. This only exists for making it\n    semantically clear that the intended time is seconds.\n\n    This is for use in cache decorators.\n    \"\"\"\n    return number\n\n\ndef hours(number: int, /) -> int:\n    \"\"\"\n    Convert hours to seconds.\n\n    This is for use in cache decorators.\n    \"\"\"\n    return minutes(60) * number\n\n\ndef days(number: int, /) -> int:\n    \"\"\"\n    Convert days to seconds.\n\n    This is for use in cache decorators.\n    \"\"\"\n    return hours(24) * number\n\n\ndef in_memory_cache(\n    reset_frequency: int | None = None,\n) -> Callable[[Callable[P, T]], InMemoryCache[P, T]]:\n    \"\"\"\n    Decorator to cache the result from a given view in-memory.\n    \"\"\"\n\n    def decorator_factory(function: Callable[P, T], /) -> InMemoryCache[P, T]:\n        return InMemoryCache(\n            function, reset_frequency=reset_frequency or math.inf\n        )\n\n    return decorator_factory\n"
  },
  {
    "path": "src/view/core/__init__.py",
    "content": "\"\"\"\nThe parts absolutely necessary for web applications using view.py.\n\"\"\"\n\nfrom view.core import app as app\nfrom view.core import headers as headers\nfrom view.core import request as request\nfrom view.core import response as response\nfrom view.core import router as router\nfrom view.core import status_codes as status_codes\n"
  },
  {
    "path": "src/view/core/_colors.py",
    "content": "\"\"\"\nThis is mostly stolen from CPython's _colorize module. If that becomes part of\nthe standard library someday, we can hopefully remove this.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport os\nimport sys\nfrom typing import IO\n\n\nclass ANSIColors:\n    \"\"\"\n    Namespace of ANSI color codes.\n    \"\"\"\n\n    RESET = \"\\x1b[0m\"\n\n    BLACK = \"\\x1b[30m\"\n    BLUE = \"\\x1b[34m\"\n    CYAN = \"\\x1b[36m\"\n    GREEN = \"\\x1b[32m\"\n    GREY = \"\\x1b[90m\"\n    MAGENTA = \"\\x1b[35m\"\n    RED = \"\\x1b[31m\"\n    WHITE = \"\\x1b[37m\"  # more like LIGHT GRAY\n    YELLOW = \"\\x1b[33m\"\n\n    BOLD = \"\\x1b[1m\"\n    BOLD_BLACK = \"\\x1b[1;30m\"  # DARK GRAY\n    BOLD_BLUE = \"\\x1b[1;34m\"\n    BOLD_CYAN = \"\\x1b[1;36m\"\n    BOLD_GREEN = \"\\x1b[1;32m\"\n    BOLD_MAGENTA = \"\\x1b[1;35m\"\n    BOLD_RED = \"\\x1b[1;31m\"\n    BOLD_WHITE = \"\\x1b[1;37m\"  # actual WHITE\n    BOLD_YELLOW = \"\\x1b[1;33m\"\n\n    # intense = like bold but without being bold\n    INTENSE_BLACK = \"\\x1b[90m\"\n    INTENSE_BLUE = \"\\x1b[94m\"\n    INTENSE_CYAN = \"\\x1b[96m\"\n    INTENSE_GREEN = \"\\x1b[92m\"\n    INTENSE_MAGENTA = \"\\x1b[95m\"\n    INTENSE_RED = \"\\x1b[91m\"\n    INTENSE_WHITE = \"\\x1b[97m\"\n    INTENSE_YELLOW = \"\\x1b[93m\"\n\n    BACKGROUND_BLACK = \"\\x1b[40m\"\n    BACKGROUND_BLUE = \"\\x1b[44m\"\n    BACKGROUND_CYAN = \"\\x1b[46m\"\n    BACKGROUND_GREEN = \"\\x1b[42m\"\n    BACKGROUND_MAGENTA = \"\\x1b[45m\"\n    BACKGROUND_RED = \"\\x1b[41m\"\n    BACKGROUND_WHITE = \"\\x1b[47m\"\n    BACKGROUND_YELLOW = \"\\x1b[43m\"\n\n    INTENSE_BACKGROUND_BLACK = \"\\x1b[100m\"\n    INTENSE_BACKGROUND_BLUE = \"\\x1b[104m\"\n    INTENSE_BACKGROUND_CYAN = \"\\x1b[106m\"\n    INTENSE_BACKGROUND_GREEN = \"\\x1b[102m\"\n    INTENSE_BACKGROUND_MAGENTA = \"\\x1b[105m\"\n    INTENSE_BACKGROUND_RED = \"\\x1b[101m\"\n    INTENSE_BACKGROUND_WHITE = \"\\x1b[107m\"\n    INTENSE_BACKGROUND_YELLOW = \"\\x1b[103m\"\n\n\nNoColors = ANSIColors()\n\nfor attribute in ANSIColors.__dict__:\n    if not attribute.startswith(\"__\"):\n        setattr(NoColors, attribute, \"\")\n\n\ndef _supports_colors(*, file: IO[str] | IO[bytes] | None = None) -> bool:\n    \"\"\"\n    Does the current environment support ANSI color codes?\n    \"\"\"\n\n    if file is None:\n        file = sys.stdout\n\n    assert file is not None\n    if os.environ.get(\"NO_COLOR\"):\n        return False\n    if os.environ.get(\"FORCE_COLOR\"):\n        return True\n    if os.environ.get(\"TERM\") == \"dumb\":\n        return False\n\n    if not hasattr(file, \"fileno\"):\n        return False\n\n    if sys.platform == \"win32\":\n        try:\n            import nt\n\n            if not nt._supports_virtual_terminal():  # noqa: SLF001\n                return False\n        except (ImportError, AttributeError):\n            return False\n\n    try:\n        return os.isatty(file.fileno())\n    except OSError:\n        return hasattr(file, \"isatty\") and file.isatty()\n\n\ndef get_colors(*, file: IO[str] | IO[bytes] | None = None) -> ANSIColors:\n    \"\"\"\n    Get a namespace containing color names as attributes. If colors are\n    enabled, these attributes will contain ANSI color codes. Otherwise, they'll\n    be empty string.\n\n    \"\"\"\n    if _supports_colors(file=file):\n        return ANSIColors()\n    return NoColors\n\n\nclass ColorfulFormatter(logging.Formatter):\n    def format(self, record: logging.LogRecord) -> str:\n        colors = get_colors()\n        mapping = {\n            logging.DEBUG: colors.BOLD_BLUE,\n            logging.INFO: colors.BOLD_GREEN,\n            logging.WARNING: colors.BOLD_YELLOW,\n            logging.ERROR: colors.BOLD_RED,\n            logging.CRITICAL: colors.INTENSE_BACKGROUND_RED,\n        }\n        color_code = mapping.get(record.levelno)\n        if color_code is not None:\n            record.levelname = f\"{color_code}{record.levelname}{colors.RESET}\"\n\n        return super().format(record)\n"
  },
  {
    "path": "src/view/core/app.py",
    "content": "\"\"\"\nPrimary app implementation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport contextvars\nimport json\nimport logging\nimport os\nimport sys\nimport warnings\nfrom abc import ABC, abstractmethod\nfrom collections.abc import Awaitable, Callable, Iterator\nfrom importlib.metadata import Distribution, PackageNotFoundError\nfrom multiprocessing import Process\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, ParamSpec, TypeAlias, TypeVar, Unpack\n\nfrom view.core._colors import ColorfulFormatter\nfrom view.core.request import Method, Request\nfrom view.core.response import (\n    Response,\n    ResponseLike,\n    ViewResult,\n    wrap_view_result,\n)\nfrom view.core.router import FoundRoute, Route, Router, RouteView\nfrom view.core.status_codes import (\n    Forbidden,\n    HTTPError,\n    InternalServerError,\n    NotFound,\n)\nfrom view.exceptions import InvalidTypeError\nfrom view.responses import FileResponse\nfrom view.run.servers import ServerConfigArgs, run_app_on_any_server\nfrom view.utils import reraise\n\nif TYPE_CHECKING:\n    from view.run.asgi import ASGIProtocol\n    from view.run.wsgi import WSGIProtocol\n\n__all__ = \"App\", \"BaseApp\", \"as_app\"\n\nT = TypeVar(\"T\")\nP = ParamSpec(\"P\")\n\n\ndef _is_development_mode() -> bool:\n    devmode_variable = os.environ.get(\"VIEW_DEVMODE\")\n    if devmode_variable is not None:\n        if not devmode_variable.isdigit():\n            raise RuntimeError(\n                f\"Invalid value for VIEW_DEVMODE: {devmode_variable!r}\"\n            )\n\n        return bool(int(devmode_variable))\n\n    try:\n        view_distribution = Distribution.from_name(\"view.py\")\n    except PackageNotFoundError:\n        # view.py isn't even installed -- we're definitely in some sort of\n        # local copy.\n        return True\n    json_data = view_distribution.read_text(\"direct_url.json\")\n    if json_data is None:\n        return False\n\n    return json.loads(json_data).get(\"dir_info\", {}).get(\"editable\", False)\n\n\nclass BaseApp(ABC):\n    \"\"\"Base view.py application.\"\"\"\n\n    _CURRENT_APP = contextvars.ContextVar[\"BaseApp\"](\"Current app being used.\")\n\n    def __init__(self) -> None:\n        self._request = contextvars.ContextVar[Request](\"request\")\n        self._production: bool | None = None\n\n        # We use a private variable for this to artificially disallow people\n        # from writing to development_mode.\n        self._development_mode: bool = _is_development_mode()\n\n        self.logger = self._new_logger()\n        \"\"\"\n        The logger used by the app.\n        \"\"\"\n\n    @property\n    def development_mode(self) -> bool:\n        \"\"\"\n        Whether view.py is in \"development mode\". If this is ``True``, then\n        that means you're working on contributing to the library itself.\n\n        This cannot be set from Python. If you'd like to control this behavior,\n        set the ``VIEW_DEVMODE`` environment variable to ``1`` or ``0``.\n        \"\"\"\n        return self._development_mode\n\n    def _new_logger(self) -> logging.Logger:\n        \"\"\"\n        Create a new logger for this app.\n        \"\"\"\n        # TODO: This should be configurable\n\n        log_level = logging.INFO\n        if self.development_mode:\n            log_level = logging.DEBUG\n\n        # In the future, we might want to add a use-case for multiple apps in\n        # the same process. To support this, we use the ID of this instance in\n        # the logger name to keep it unique.\n\n        # XXX: Should this create a new logger for each class, or for each instance?\n        logger = logging.getLogger(\n            f\"{__name__}.{self.__class__.__name__}-{id(self)}\"\n        )\n        logger.setLevel(log_level)\n        handler = logging.StreamHandler(sys.stdout)\n        handler.setLevel(log_level)\n\n        formatter = ColorfulFormatter(\n            \"view: %(asctime)s -- [%(levelname)s]: %(message)s\"\n        )\n        handler.setFormatter(formatter)\n\n        logger.addHandler(handler)\n        return logger\n\n    def shut_up(self) -> None:\n        \"\"\"\n        Stop the logger.\n        \"\"\"\n\n        self.logger.disabled = True\n\n    @property\n    def debug(self) -> bool:\n        \"\"\"\n        Is the app in debug mode?\n\n        If debug mode is enabled, some extra checks and settings are enabled\n        to improve the development experience, at the cost of being slower and\n        less secure.\n        \"\"\"\n        if self._production is None:\n            return __debug__\n\n        return self._production\n\n    @contextlib.contextmanager\n    def request_context(self, request: Request) -> Iterator[None]:\n        \"\"\"\n        Enter a context for the given request.\n        \"\"\"\n        app_token = self._CURRENT_APP.set(self)\n        request_token = self._request.set(request)\n        try:\n            yield\n        finally:\n            self._request.reset(request_token)\n            self._CURRENT_APP.reset(app_token)\n\n    @classmethod\n    def current_app(cls) -> BaseApp:\n        return cls._CURRENT_APP.get()\n\n    def current_request(self) -> Request:\n        \"\"\"\n        Get the current request being handled.\n        \"\"\"\n        return self._request.get()\n\n    @abstractmethod\n    async def process_request(self, request: Request) -> Response:\n        \"\"\"\n        Get the response from the server for a given request.\n        \"\"\"\n\n    def wsgi(self) -> WSGIProtocol:\n        \"\"\"\n        Get the WSGI callable for the app.\n        \"\"\"\n        from view.run.wsgi import wsgi_for_app\n\n        return wsgi_for_app(self)\n\n    def asgi(self) -> ASGIProtocol:\n        \"\"\"\n        Get the ASGI callable for the app.\n        \"\"\"\n        from view.run.asgi import asgi_for_app\n\n        return asgi_for_app(self)\n\n    def run(self, **kwargs: Unpack[ServerConfigArgs]) -> None:\n        \"\"\"\n        Run the app.\n\n        This is a sort of magic function that's supposed to \"just work\". If\n        finer control over the server settings is desired, explicitly use the\n        server's API with the app's :meth:`asgi` or :meth:`wsgi` method.\n        \"\"\"\n\n        production = kwargs.get(\"production\", False)\n        # If production is True, then __debug__ should be False.\n        # If production is False, then __debug__ should be True.\n        if production is __debug__:\n            warnings.warn(\n                f\"The app was run with {production=}, but Python's {__debug__=}\",\n                RuntimeWarning,\n                stacklevel=2,\n            )\n\n        if self.development_mode:\n            self.logger.info(\"You're in development mode!\")\n            self.logger.info(\n                \"Development mode implies that you're working on view.py itself and plan on contributing to the library.\"\n            )\n            self.logger.info(\n                \"If that doesn't sound correct, set VIEW_DEVMODE to 0.\"\n            )\n\n        self.logger.info(\n            \"Serving app on http://localhost:%d\", kwargs.get(\"port\") or 5000\n        )\n        try:\n            run_app_on_any_server(self, **kwargs)\n        except KeyboardInterrupt:\n            self.logger.info(\"CTRL^C received, shutting down\")\n        except Exception:\n            self.logger.exception(\"Error in server lifecycle\")\n        finally:\n            self.logger.info(\"Server finished\")\n\n    def run_detached(\n        self,\n        **kwargs: Unpack[ServerConfigArgs],\n    ) -> Process:\n        \"\"\"\n        Run the app in a separate process. This means that the server is\n        killable.\n        \"\"\"\n\n        process = Process(\n            target=self.run,\n            kwargs=kwargs,\n        )\n        process.start()\n        return process\n\n    async def _execute_view_internal(\n        self,\n        view: Callable[P, ViewResult],\n        *args: P.args,\n        **kwargs: P.kwargs,\n    ) -> Response:\n        self.logger.debug(\"Executing view: %s\", view)\n        try:\n            result = view(*args, **kwargs)\n            return await wrap_view_result(result)\n        except HTTPError as error:\n            self.logger.warning(\"HTTP Error %d\", error.status_code)\n            raise\n\n    async def execute_view(\n        self, view: Callable[P, ViewResult], *args: P.args, **kwargs: P.kwargs\n    ) -> Response:\n        try:\n            return await self._execute_view_internal(view, *args, **kwargs)\n        except BaseException as exception:\n            # Let HTTP errors pass through, so the caller can deal with it\n            if isinstance(exception, HTTPError):\n                raise\n            self.logger.exception(\"Error while processing response\")\n\n            if __debug__:\n                raise InternalServerError.from_current_exception() from exception\n\n            raise InternalServerError from exception\n\n\nSingleView = Callable[[\"Request\"], ViewResult]\n\n\nclass SingleViewApp(BaseApp):\n    \"\"\"\n    Application with a single view function that\n    processes all requests.\n    \"\"\"\n\n    def __init__(self, view: SingleView) -> None:\n        super().__init__()\n        self.view = view\n\n    async def process_request(self, request: Request) -> Response:\n        with self.request_context(request):\n            try:\n                return await self.execute_view(self.view, request)\n            except HTTPError as error:\n                return error.as_response()\n\n\ndef as_app(view: SingleView, /) -> SingleViewApp:\n    \"\"\"\n    Decorator for using a single function as an app.\n    \"\"\"\n    if __debug__ and not callable(view):\n        raise InvalidTypeError(view, Callable)\n\n    return SingleViewApp(view)\n\n\nRouteDecorator: TypeAlias = Callable[[RouteView], Route]\nSubRouterView: TypeAlias = Callable[\n    [str], ResponseLike | Awaitable[ResponseLike]\n]\nSubRouterViewT = TypeVar(\"SubRouterViewT\", bound=SubRouterView)\n\n\nclass App(BaseApp):\n    \"\"\"\n    An application containing an automatic routing mechanism\n    and error handling.\n    \"\"\"\n\n    def __init__(self, *, router: Router | None = None) -> None:\n        super().__init__()\n        self.router = router or Router()\n\n    async def _process_request_internal(self, request: Request) -> Response:\n        self.logger.info(\"%s on route %s\", request.method, request.path)\n        found_route: FoundRoute | None = self.router.lookup_route(\n            request.path, request.method\n        )\n        if found_route is None:\n            raise NotFound\n\n        # Extend instead of replacing?\n        request.path_parameters = found_route.path_parameters\n        return await self.execute_view(found_route.route.view)\n\n    async def process_request(self, request: Request) -> Response:\n        with self.request_context(request):\n            try:\n                return await self._process_request_internal(request)\n            except HTTPError as error:\n                error_view = self.router.lookup_error(type(error))\n                if error_view is not None:\n                    return await self.execute_view(error_view)\n\n                return error.as_response()\n\n    def route(self, path: str, /, *, method: Method) -> RouteDecorator:\n        \"\"\"\n        Decorator interface for adding a route to the app.\n        \"\"\"\n\n        if __debug__ and not isinstance(path, str):\n            raise InvalidTypeError(path, str)\n\n        if __debug__ and not isinstance(method, Method):\n            raise InvalidTypeError(method, Method)\n\n        def decorator(view: RouteView, /) -> Route:\n            return self.router.push_route(view, path, method)\n\n        return decorator\n\n    def get(self, path: str, /) -> RouteDecorator:\n        \"\"\"\n        Decorator interface for adding a GET route.\n        \"\"\"\n        return self.route(path, method=Method.GET)\n\n    def post(self, path: str, /) -> RouteDecorator:\n        \"\"\"\n        Decorator interface for adding a POST route.\n        \"\"\"\n        return self.route(path, method=Method.POST)\n\n    def put(self, path: str, /) -> RouteDecorator:\n        \"\"\"\n        Decorator interface for adding a PUT route.\n        \"\"\"\n        return self.route(path, method=Method.PUT)\n\n    def patch(self, path: str, /) -> RouteDecorator:\n        \"\"\"\n        Decorator interface for adding a PATCH route.\n        \"\"\"\n        return self.route(path, method=Method.PATCH)\n\n    def delete(self, path: str, /) -> RouteDecorator:\n        \"\"\"\n        Decorator interface for adding a DELETE route.\n        \"\"\"\n        return self.route(path, method=Method.DELETE)\n\n    def connect(self, path: str, /) -> RouteDecorator:\n        \"\"\"\n        Decorator interface for adding a CONNECT route.\n        \"\"\"\n        return self.route(path, method=Method.CONNECT)\n\n    def options(self, path: str, /) -> RouteDecorator:\n        \"\"\"\n        Decorator interface for adding an OPTIONS route.\n        \"\"\"\n        return self.route(path, method=Method.OPTIONS)\n\n    def trace(self, path: str, /) -> RouteDecorator:\n        \"\"\"\n        Decorator interface for adding a TRACE route.\n        \"\"\"\n        return self.route(path, method=Method.TRACE)\n\n    def head(self, path: str, /) -> RouteDecorator:\n        \"\"\"\n        Decorator interface for adding a HEAD route.\n        \"\"\"\n        return self.route(path, method=Method.HEAD)\n\n    def error(\n        self, status: int | type[HTTPError], /\n    ) -> Callable[[RouteView], RouteView]:\n        \"\"\"\n        Decorator interface for adding an error handler to the app.\n        \"\"\"\n\n        def decorator(view: RouteView, /) -> RouteView:\n            self.router.push_error(status, view)\n            return view\n\n        return decorator\n\n    def subrouter(\n        self, path: str\n    ) -> Callable[[SubRouterViewT], SubRouterViewT]:\n        if __debug__ and not isinstance(path, str):\n            raise InvalidTypeError(path, str)\n\n        def decorator(function: SubRouterViewT, /) -> SubRouterViewT:\n            if __debug__ and not callable(function):\n                raise InvalidTypeError(Callable, function)\n\n            def router_function(path_from_url: str) -> Route:\n                def route() -> ResponseLike | Awaitable[ResponseLike]:\n                    return function(path_from_url)\n\n                return Route(route, path_from_url, Method.GET)\n\n            self.router.push_subrouter(router_function, path)\n            return function\n\n        return decorator\n\n    def static_files(self, path: str, directory: str | Path) -> None:\n        if __debug__ and not isinstance(directory, (str, Path)):\n            raise InvalidTypeError(directory, str, Path)\n\n        directory = Path(directory)\n\n        @self.subrouter(path)\n        def serve_static_file(path_from_url: str) -> ResponseLike:\n            file = directory / path_from_url\n            if not file.is_file():\n                raise NotFound\n\n            if not file.is_relative_to(directory):\n                raise Forbidden\n\n            with reraise(Forbidden, OSError):\n                return FileResponse.from_file(file)\n"
  },
  {
    "path": "src/view/core/body.py",
    "content": "\"\"\"\nThe implementation of request and response bodies.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom collections.abc import AsyncIterator, Callable\nfrom dataclasses import dataclass, field\nfrom io import BytesIO\nfrom typing import Any, TypeAlias\n\nfrom view.exceptions import InvalidTypeError, ViewError\n\n__all__ = (\"BodyMixin\",)\n\nBodyStream: TypeAlias = AsyncIterator[bytes]\n\n\nclass BodyAlreadyUsedError(ViewError):\n    \"\"\"\n    The body was already used on this response.\n\n    Generally, this means that the same response object was executed multiple\n    times.\n    \"\"\"\n\n    def __init__(self, receive_data: BodyStream) -> None:\n        super().__init__(f\"Body {receive_data!r} has already been consumed\")\n\n\nclass InvalidJSONError(ViewError):\n    \"\"\"\n    The body is not valid JSON data or something went wrong when parsing it.\n\n    If this occurred when parsing the body for a request, the fix is\n    usually to reraise this with an error 400 (Bad Request).\n    \"\"\"\n\n\n@dataclass(slots=True)\nclass BodyMixin:\n    \"\"\"\n    Mixin dataclass for common HTTP body operations.\n    \"\"\"\n\n    receive_data: BodyStream\n    consumed: bool = field(init=False, default=False)\n\n    async def stream_body(self) -> AsyncIterator[bytes]:\n        \"\"\"\n        Incrementally stream the body without keeping the whole thing\n        in-memory at a given time.\n        \"\"\"\n        if __debug__ and not isinstance(self.receive_data, AsyncIterator):\n            raise InvalidTypeError(self.receive_data, AsyncIterator)\n\n        if self.consumed:\n            raise BodyAlreadyUsedError(self.receive_data)\n\n        self.consumed = True\n\n        async for data in self.receive_data:\n            if __debug__ and not isinstance(data, bytes):\n                raise InvalidTypeError(data, bytes)\n            yield data\n\n    async def body(self) -> bytes:\n        \"\"\"\n        Read the full body from the stream.\n        \"\"\"\n\n        buffer = BytesIO()\n        async for data in self.stream_body():\n            buffer.write(data)\n\n        return buffer.getvalue()\n\n    async def json(\n        self, *, parse_function: Callable[[str], dict[str, Any]] = json.loads\n    ) -> dict[str, Any]:\n        \"\"\"\n        Read the body as JSON data.\n        \"\"\"\n\n        data = await self.body()\n        try:\n            text = data.decode(\"utf-8\")\n        except UnicodeDecodeError as error:\n            raise InvalidJSONError(\n                \"Body does not contain valid UTF-8 data\"\n            ) from error\n\n        try:\n            return parse_function(text)\n        except Exception as error:\n            raise InvalidJSONError(\"Failed to parse JSON\") from error\n"
  },
  {
    "path": "src/view/core/headers.py",
    "content": "\"\"\"\nUtilities and implementation for HTTP request/response headers.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable, Mapping\nfrom typing import TYPE_CHECKING, Any, TypeAlias\n\nfrom typing_extensions import Self\n\nfrom view.core.multi_map import MultiMap\nfrom view.exceptions import InvalidTypeError\n\nif TYPE_CHECKING:\n    from view.run.asgi import ASGIHeaders\n    from view.run.wsgi import WSGIHeaders\n\n__all__ = (\n    \"HTTPHeaders\",\n    \"HeadersLike\",\n    \"as_real_headers\",\n    \"asgi_to_headers\",\n    \"headers_to_asgi\",\n    \"wsgi_to_headers\",\n)\n\n\nclass LowerStr(str):\n    \"\"\"\n    A string that always acts in lowercase. This is useful for case-insensitive\n    comparisons.\n    \"\"\"\n\n    __slots__ = ()\n\n    def __new__(cls, data: object) -> Self:\n        return super().__new__(cls, cls._to_lower(data))\n\n    @staticmethod\n    def _to_lower(data: object) -> object:\n        if isinstance(data, str):\n            data = data.lower()\n\n        return data\n\n    def __contains__(self, key: str, /) -> bool:\n        return super().__contains__(key.lower())\n\n    def __eq__(self, string: object) -> bool:\n        return super().__eq__(self._to_lower(string))\n\n    def __ne__(self, value: object, /) -> bool:\n        return super().__ne__(self._to_lower(value))\n\n    def __hash__(self) -> int:\n        return hash(str(self))\n\n\nclass HTTPHeaders(MultiMap[str, str]):\n    \"\"\"\n    Case-insensitive multi-map of HTTP headers.\n    \"\"\"\n\n    def __init__(self, items: Iterable[tuple[str, str]] = ()) -> None:\n        super().__init__((LowerStr(key), value) for key, value in items)\n\n    def __getitem__(self, key: str, /) -> str:\n        return super().__getitem__(LowerStr(key))\n\n    def __contains__(self, key: object, /) -> bool:\n        return super().__contains__(LowerStr(key))\n\n    def __repr__(self) -> str:\n        return f\"HTTPHeaders({self.as_sequence()})\"\n\n    def __eq__(self, other: object, /) -> bool:\n        if isinstance(other, HTTPHeaders):\n            return other._values == self._values\n\n        if isinstance(other, dict):\n            return self._as_flat() == {\n                LowerStr(key): value for key, value in other.items()\n            }\n\n        return NotImplemented\n\n    __hash__ = MultiMap.__hash__\n\n    def get_exactly_one(self, key: str) -> str:\n        return super().get_exactly_one(LowerStr(key))\n\n    def with_new_value(self, key: str, value: str) -> HTTPHeaders:\n        new_sequence = [*list(self.as_sequence()), (LowerStr(key), value)]\n        return type(self)(new_sequence)\n\n\nHeadersLike: TypeAlias = (\n    HTTPHeaders | Mapping[str, str] | Mapping[bytes, bytes]\n)\n\n\ndef as_real_headers(headers: HeadersLike | None, /) -> HTTPHeaders:\n    \"\"\"\n    Convenience function for casting a \"header-like object\" (or ``None``)\n    to a :class:`MultiMap`.\n    \"\"\"\n    if headers is None:\n        return HTTPHeaders()\n\n    if isinstance(headers, HTTPHeaders):\n        return headers\n\n    if __debug__ and not isinstance(headers, Mapping):\n        raise InvalidTypeError(Mapping, headers)\n\n    assert isinstance(headers, dict)\n    all_values: list[tuple[LowerStr, str]] = []\n\n    for key, value in headers.items():\n        if isinstance(key, bytes):\n            key = key.decode(\"utf-8\")  # noqa: PLW2901\n\n        if isinstance(value, bytes):\n            value = value.decode(\"utf-8\")  # noqa: PLW2901\n\n        all_values.append((LowerStr(key), value))\n\n    return HTTPHeaders(all_values)\n\n\ndef wsgi_to_headers(environ: Mapping[str, Any]) -> HTTPHeaders:\n    \"\"\"\n    Convert WSGI headers (from the ``environ``) to a case-insensitive multi-map.\n    \"\"\"\n    values: list[tuple[LowerStr, str]] = []\n\n    for key, value in environ.items():\n        if not key.startswith(\"HTTP_\"):\n            continue\n\n        assert isinstance(value, str)\n        key = key.removeprefix(\"HTTP_\").replace(\"_\", \"-\").lower()  # noqa: PLW2901\n        values.append((LowerStr(key), value))\n\n    return HTTPHeaders(values)\n\n\ndef headers_to_wsgi(headers: HTTPHeaders, /) -> WSGIHeaders:\n    \"\"\"\n    Convert a case-insensitive multi-map to a WSGI header iterable.\n    \"\"\"\n\n    wsgi_headers: WSGIHeaders = []\n    for key, value in headers.items():\n        wsgi_headers.append((str(key), value))\n\n    return wsgi_headers\n\n\ndef asgi_to_headers(headers: ASGIHeaders, /) -> HTTPHeaders:\n    \"\"\"\n    Convert ASGI headers to a case-insensitive multi-map.\n    \"\"\"\n    values: list[tuple[LowerStr, str]] = []\n\n    for key, value in headers:\n        lower_str = LowerStr(key.decode(\"utf-8\"))\n        values.append((lower_str, value.decode(\"utf-8\")))\n\n    return HTTPHeaders(values)\n\n\ndef headers_to_asgi(headers: HTTPHeaders, /) -> ASGIHeaders:\n    \"\"\"\n    Convert a case-insensitive multi-map to an ASGI header iterable.\n    \"\"\"\n    asgi_headers: ASGIHeaders = []\n\n    for key, value in headers:\n        asgi_headers.append((key.encode(\"utf-8\"), value.encode(\"utf-8\")))\n\n    return asgi_headers\n"
  },
  {
    "path": "src/view/core/multi_map.py",
    "content": "\"\"\"\nA \"multi-map\" implementation intended for use in HTTP headers and query strings.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import (\n    ItemsView,\n    Iterable,\n    Iterator,\n    KeysView,\n    Mapping,\n    Sequence,\n    ValuesView,\n)\nfrom typing import Any, TypeVar\n\nfrom view.exceptions import ViewError\n\n__all__ = \"HasMultipleValuesError\", \"MultiMap\"\n\nKeyT = TypeVar(\"KeyT\")\nValueT = TypeVar(\"ValueT\")\nT = TypeVar(\"T\")\n\n\nclass HasMultipleValuesError(ViewError):\n    \"\"\"\n    Multiple values were found when they were explicitly disallowed.\n    \"\"\"\n\n    def __init__(self, key: Any) -> None:\n        super().__init__(f\"{key!r} has multiple values\")\n\n\nclass MultiMap(Mapping[KeyT, ValueT]):\n    \"\"\"\n    Mapping of individual keys to one or many values.\n    \"\"\"\n\n    __slots__ = (\"_values\",)\n\n    def __init__(self, items: Iterable[tuple[KeyT, ValueT]] = ()) -> None:\n        self._values: dict[KeyT, list[ValueT]] = {}\n\n        for key, value in items:\n            values = self._values.setdefault(key, [])\n            values.append(value)\n\n    def __getitem__(self, key: KeyT, /) -> ValueT:\n        \"\"\"\n        Get the first value if it exists, or else raise a :exc:`KeyError`.\n        \"\"\"\n\n        return self._values[key][0]\n\n    def __len__(self) -> int:\n        return len(self._values)\n\n    def __iter__(self) -> Iterator[KeyT]:\n        return iter(self._values)\n\n    def __contains__(self, key: object, /) -> bool:\n        return key in self._values\n\n    def __eq__(self, other: object, /) -> bool:\n        if isinstance(other, MultiMap):\n            return other._values == self._values\n\n        if isinstance(other, dict):\n            return self._as_flat() == other\n\n        return NotImplemented\n\n    def __ne__(self, other: object, /) -> bool:\n        if isinstance(other, MultiMap):\n            return other._values != self._values\n\n        return NotImplemented\n\n    def __repr__(self) -> str:\n        return f\"MultiMap({self.as_sequence()})\"\n\n    def __hash__(self) -> int:\n        return hash(self._values)\n\n    def _as_flat(self) -> dict[KeyT, ValueT]:\n        \"\"\"\n        Turn this into a \"flat\" representation of the mapping in which all\n        keys have exactly one value.\n        \"\"\"\n        return {key: value[0] for key, value in self._values.items()}\n\n    def keys(self) -> KeysView[KeyT]:\n        \"\"\"\n        Return a view of all the keys in this map.\n        \"\"\"\n        return self._values.keys()\n\n    def values(self) -> ValuesView[ValueT]:\n        \"\"\"\n        Return a view of the first value for each key in the mapping.\n        \"\"\"\n        return self._as_flat().values()\n\n    def many_values(self) -> ValuesView[Sequence[ValueT]]:\n        \"\"\"\n        Return a view of all values in the mapping.\n        \"\"\"\n        return self._values.values()\n\n    def items(self) -> ItemsView[KeyT, ValueT]:\n        \"\"\"\n        Return a view of all items in the mapping, using the first value\n        for each key.\n        \"\"\"\n        return self._as_flat().items()\n\n    def many_items(self) -> ItemsView[KeyT, Sequence[ValueT]]:\n        \"\"\"\n        Return a view of all items in the mapping.\n        \"\"\"\n        return self._values.items()\n\n    def get_many(self, key: KeyT) -> Sequence[ValueT]:\n        \"\"\"\n        Get one or many values for a given key.\n        \"\"\"\n        return self._values[key]\n\n    def get_exactly_one(self, key: KeyT) -> ValueT:\n        \"\"\"\n        Get precisely one value for a key. If more than one value is present,\n        then this raises a :exc:`HasMultipleValuesError`.\n        \"\"\"\n        value = self._values[key]\n        if len(value) != 1:\n            raise HasMultipleValuesError(key)\n\n        return value[0]\n\n    def as_sequence(self) -> Sequence[tuple[KeyT, ValueT]]:\n        \"\"\"\n        Return all the keys and values in a sequence of (key, value) tuples.\n        \"\"\"\n        result: list[tuple[KeyT, ValueT]] = []\n        for key, values in self._values.items():\n            for value in values:\n                result.append((key, value))  # noqa: PERF401\n\n        return result\n\n    def with_new_value(\n        self, key: KeyT, value: ValueT\n    ) -> MultiMap[KeyT, ValueT]:\n        \"\"\"\n        Create a copy of this map with a new key and value included.\n        \"\"\"\n        new_sequence = [*list(self.as_sequence()), (key, value)]\n        return type(self)(new_sequence)\n"
  },
  {
    "path": "src/view/core/request.py",
    "content": "\"\"\"\nImplementation and utilities for HTTP requests.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nimport urllib.parse\nfrom dataclasses import dataclass, field\nfrom enum import auto\nfrom typing import TYPE_CHECKING, Any\n\nfrom view.core.body import BodyMixin\nfrom view.core.multi_map import MultiMap\nfrom view.core.router import normalize_route\n\nif TYPE_CHECKING:\n    from collections.abc import Mapping\n\n    from view.core.app import BaseApp\n    from view.core.headers import HTTPHeaders\n\n__all__ = \"Method\", \"Request\"\n\nif sys.version_info >= (3, 11):\n    from enum import StrEnum as _StrEnum\nelse:\n    from enum import Enum\n\n    class _StrEnum(str, Enum):\n        pass\n\n\nclass _UpperStrEnum(_StrEnum):\n    @staticmethod\n    def _generate_next_value_(\n        name: str,\n        *_: Any,\n    ) -> str:\n        return name.upper()\n\n\nclass Method(_UpperStrEnum):\n    \"\"\"\n    The HTTP request method.\n    \"\"\"\n\n    GET = auto()\n    \"\"\"\n    The GET method requests a representation of the specified resource.\n\n    Requests using GET should only retrieve data and should not contain\n    a request content.\n    \"\"\"\n\n    POST = auto()\n    \"\"\"\n    The POST method submits an entity to the specified resource, often causing\n    a change in state or side effects on the server.\n    \"\"\"\n\n    PUT = auto()\n    \"\"\"\n    The PUT method replaces all current representations of the target resource\n    with the request content.\n    \"\"\"\n\n    PATCH = auto()\n    \"\"\"\n    The PATCH method applies partial modifications to a resource.\n    \"\"\"\n\n    DELETE = auto()\n    \"\"\"\n    The DELETE method deletes the specified resource.\n    \"\"\"\n\n    CONNECT = auto()\n    \"\"\"\n    The CONNECT method establishes a tunnel to the server identified by the\n    target resource.\n    \"\"\"\n\n    OPTIONS = auto()\n    \"\"\"\n    The OPTIONS method describes the communication options for the target\n    resource.\n    \"\"\"\n\n    TRACE = auto()\n    \"\"\"\n    The TRACE method performs a message loop-back test along the path to the\n    target resource.\n    \"\"\"\n\n    HEAD = auto()\n    \"\"\"\n    The HEAD method asks for a response identical to a GET request, but\n    without a response body.\n    \"\"\"\n\n\n@dataclass(slots=True)\nclass Request(BodyMixin):\n    \"\"\"\n    Dataclass representing an HTTP request.\n    \"\"\"\n\n    app: BaseApp\n    \"\"\"\n    The app associated with the HTTP request.\n    \"\"\"\n\n    path: str\n    \"\"\"\n    The path of the request, with the leading '/' and without a trailing '/'\n    or query string.\n    \"\"\"\n\n    method: Method\n    \"\"\"\n    The HTTP method of the request. See :class:`Method`.\n    \"\"\"\n\n    headers: HTTPHeaders\n    \"\"\"\n    A \"multi-dictionary\" containing the request headers. This is :class:`dict`-like,\n    but if a header has multiple values, it is represented by a list.\n    \"\"\"\n\n    query_parameters: MultiMap[str, str]\n    \"\"\"\n    The query string parameters of the HTTP request.\n    \"\"\"\n\n    path_parameters: Mapping[str, str] = field(\n        default_factory=dict, init=False\n    )\n    \"\"\"\n    The path parameters of this request.\n    \"\"\"\n\n    def __post_init__(self) -> None:\n        self.path = normalize_route(self.path)\n\n\ndef extract_query_parameters(query_string: str | bytes) -> MultiMap[str, str]:\n    \"\"\"\n    Extract a query string from a URL and return it as a multi-map.\n    \"\"\"\n    if isinstance(query_string, bytes):\n        query_string = query_string.decode(\"utf-8\")\n\n    assert isinstance(query_string, str), query_string\n    return MultiMap(urllib.parse.parse_qsl(query_string))\n"
  },
  {
    "path": "src/view/core/response.py",
    "content": "\"\"\"\nImplementation and utilities for HTTP responses.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport warnings\nfrom collections.abc import (\n    AsyncGenerator,\n    Awaitable,\n    Generator,\n)\nfrom dataclasses import dataclass\nfrom typing import AnyStr, Generic, TypeAlias\n\nfrom view.core.body import BodyMixin\nfrom view.core.headers import (\n    HeadersLike,\n    HTTPHeaders,\n    as_real_headers,\n)\nfrom view.exceptions import InvalidTypeError, ViewError\n\n__all__ = \"Response\", \"ResponseLike\", \"ViewResult\"\n\n\n@dataclass(slots=True)\nclass Response(BodyMixin):\n    \"\"\"\n    Low-level dataclass representing a response from a view.\n    \"\"\"\n\n    status_code: int\n    headers: HTTPHeaders\n\n    def __post_init__(self) -> None:\n        if __debug__:\n            # Avoid circular import issues\n            from view.core.status_codes import STATUS_STRINGS\n\n            if self.status_code not in STATUS_STRINGS:\n                raise ValueError(\n                    f\"{self.status_code!r} is not a valid HTTP status code\"\n                )\n\n    async def as_tuple(self) -> tuple[bytes, int, HTTPHeaders]:\n        \"\"\"\n        Process the response as a tuple. This is mainly useful\n        for assertions in testing.\n        \"\"\"\n        return (await self.body(), self.status_code, self.headers)\n\n\n# AnyStr isn't working with the type checker, probably because it's a TypeVar\nStrOrBytes: TypeAlias = str | bytes\n_ResponseTuple: TypeAlias = (\n    tuple[StrOrBytes, int] | tuple[StrOrBytes, int, HeadersLike]\n)\nResponseLike: TypeAlias = (\n    Response\n    | StrOrBytes\n    | AsyncGenerator[StrOrBytes]\n    | Generator[StrOrBytes]\n    | _ResponseTuple\n)\nViewResult = ResponseLike | Awaitable[ResponseLike]\n\n\ndef _as_bytes(data: str | bytes) -> bytes:\n    \"\"\"\n    Utility to convert a string to a byte string, or let a byte string pass.\n    \"\"\"\n    if isinstance(data, str):\n        return data.encode(\"utf-8\")\n\n    return data\n\n\n@dataclass(slots=True)\nclass TextResponse(Response, Generic[AnyStr]):\n    \"\"\"\n    Simple in-memory response for a UTF-8 encoded string, or a raw ASCII byte string.\n    \"\"\"\n\n    content: AnyStr\n\n    @classmethod\n    def from_content(\n        cls,\n        content: AnyStr,\n        /,\n        *,\n        status_code: int = 200,\n        headers: HeadersLike | None = None,\n    ) -> TextResponse[AnyStr]:\n        \"\"\"\n        Generate a :class:`TextResponse` from either a :class:`str` or\n        :class:`bytes` object.\n        \"\"\"\n\n        if __debug__ and not isinstance(content, (str, bytes)):\n            raise InvalidTypeError(content, str, bytes)\n\n        async def stream() -> AsyncGenerator[bytes]:\n            yield _as_bytes(content)\n\n        return cls(stream(), status_code, as_real_headers(headers), content)\n\n\nclass InvalidResponseError(ViewError):\n    \"\"\"\n    A view returned an object that view.py doesn't know how to convert into a\n    response object.\n    \"\"\"\n\n\ndef _wrap_response_tuple(response: _ResponseTuple) -> Response:\n    if __debug__ and response == ():\n        raise InvalidResponseError(\"Response cannot be an empty tuple\")\n\n    if __debug__ and len(response) == 1:\n        warnings.warn(\n            f\"Returned tuple {response!r} with a single item,\"\n            \" which is useless. Return the item directly.\",\n            RuntimeWarning,\n            stacklevel=2,\n        )\n        return TextResponse.from_content(response[0])\n\n    content = response[0]\n    if __debug__ and isinstance(content, Response):\n        raise InvalidResponseError(\n            \"Response() objects cannot be used with response\"\n            \" tuples. Instead, use the status_code and/or headers parameter(s).\"\n        )\n\n    status = response[1]\n    headers: HeadersLike | None = None\n\n    # Ruff wants me to use a constant here, but I think this is clear enough\n    # for lengths.\n    if len(response) > 2:  # noqa: PLR2004\n        headers = response[2]\n\n    if __debug__ and len(response) > 3:  # noqa: PLR2004\n        raise InvalidResponseError(\n            f\"Got excess data in response tuple {response[3:]!r}\"\n        )\n\n    return TextResponse.from_content(\n        content, status_code=status, headers=headers\n    )\n\n\ndef _wrap_response(response: ResponseLike, /) -> Response:\n    \"\"\"\n    Wrap a response from a view into a :class:`Response` object.\n    \"\"\"\n    if isinstance(response, Response):\n        return response\n\n    if isinstance(response, (str, bytes)):\n        return TextResponse.from_content(response)\n\n    if isinstance(response, tuple):\n        return _wrap_response_tuple(response)\n\n    if isinstance(response, AsyncGenerator):\n\n        async def stream() -> AsyncGenerator[bytes]:\n            async for data in response:\n                yield _as_bytes(data)\n\n        return Response(stream(), status_code=200, headers=HTTPHeaders())\n\n    if isinstance(response, Generator):\n\n        async def stream() -> AsyncGenerator[bytes]:\n            for data in response:\n                yield _as_bytes(data)\n\n        return Response(stream(), status_code=200, headers=HTTPHeaders())\n\n    raise TypeError(f\"Invalid response: {response!r}\")\n\n\nasync def wrap_view_result(result: ViewResult, /) -> Response:\n    \"\"\"\n    Turn the raw result of a view, which might be a coroutine, into a usable\n    :class:`Response` object.\n    \"\"\"\n    if isinstance(result, Awaitable):\n        result = await result\n\n    return _wrap_response(result)\n"
  },
  {
    "path": "src/view/core/router.py",
    "content": "\"\"\"\nThe router implementation.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Awaitable, Callable, MutableMapping\nfrom dataclasses import dataclass, field\nfrom typing import TYPE_CHECKING, TypeAlias\n\nfrom view.core.status_codes import HTTPError, status_exception\nfrom view.exceptions import InvalidTypeError, ViewError\n\nif TYPE_CHECKING:\n    from view.core.request import Method\n    from view.core.response import ResponseLike\n\n__all__ = \"Route\", \"Router\"\n\n\nRouteView: TypeAlias = Callable[[], \"ResponseLike | Awaitable[ResponseLike]\"]\n\n\n@dataclass(slots=True, frozen=True)\nclass Route:\n    \"\"\"\n    Dataclass representing a route in a router.\n    \"\"\"\n\n    view: RouteView\n    path: str\n    method: Method\n\n    def __truediv__(self, other: object) -> str:\n        if not isinstance(other, str):\n            return NotImplemented\n\n        path = f\"{self.path}/{other}\"\n        return normalize_route(path)\n\n\ndef normalize_route(route: str, /) -> str:\n    \"\"\"\n    Format a route (without any leading URL) into a common style.\n    \"\"\"\n    if route in {\"\", \"/\"}:\n        return \"/\"\n\n    route = route.rstrip(\"/\")\n    if not route.startswith(\"/\"):\n        route = \"/\" + route\n\n    return route\n\n\nclass DuplicateRouteError(ViewError):\n    \"\"\"\n    The router found multiple views for the same route.\n\n    Generally, this means that a typo is present, or perhaps the user\n    misunderstood something about route normalization. For example, \"/\" and \"\"\n    are equivalent to the router.\n    \"\"\"\n\n\nSubRouter: TypeAlias = Callable[[str], \"Route\"]\n\n\n@dataclass(slots=True)\nclass _PathNode:\n    \"\"\"\n    A node in the \"path tree\".\n    \"\"\"\n\n    name: str\n    routes: MutableMapping[Method, Route] = field(default_factory=dict)\n    children: MutableMapping[str, _PathNode] = field(default_factory=dict)\n    path_parameter: _PathNode | None = None\n    subrouter: SubRouter | None = None\n\n    def parameter(self, name: str) -> _PathNode:\n        \"\"\"\n        Mark this node as having a path parameter (if not already), and\n        return the path parameter node.\n        \"\"\"\n        if self.path_parameter is None:\n            next_node = _PathNode(name=name)\n            self.path_parameter = next_node\n            return next_node\n        if __debug__ and name != self.path_parameter.name:\n            raise DuplicateRouteError(\n                f\"Path parameter {name} is in the same place as\"\n                f\" {self.path_parameter.name}, but with a different name\",\n            )\n        return self.path_parameter\n\n    def next_node(self, part: str) -> _PathNode:\n        \"\"\"\n        Get the next node for the given path part, creating it if it doesn't\n        exist.\n        \"\"\"\n        node = self.children.get(part)\n        if node is not None:\n            return node\n\n        new_node = _PathNode(name=part)\n        self.children[part] = new_node\n        return new_node\n\n\ndef _is_path_parameter(part: str) -> bool:\n    \"\"\"\n    Is this part a path parameter?\n    \"\"\"\n    return part.startswith(\"{\") and part.endswith(\"}\")\n\n\ndef _extract_path_parameter(part: str) -> str:\n    \"\"\"\n    Extract the name of a path parameter from a string given by the user\n    in a route string.\n    \"\"\"\n    return part[1 : len(part) - 1]\n\n\n@dataclass(slots=True, frozen=True)\nclass FoundRoute:\n    \"\"\"\n    Dataclass representing a route that was looked up by the router\n    for a given path.\n    \"\"\"\n\n    route: Route\n    path_parameters: MutableMapping[str, str] = field(default_factory=dict)\n\n\n@dataclass(slots=True, frozen=True)\nclass Router:\n    \"\"\"\n    Standard router that supports error and route lookups.\n    \"\"\"\n\n    error_views: MutableMapping[type[HTTPError], RouteView] = field(\n        default_factory=dict\n    )\n    parent_node: _PathNode = field(default_factory=lambda: _PathNode(name=\"\"))\n\n    def _get_node_for_path(\n        self, path: str, *, allow_path_parameters: bool\n    ) -> _PathNode:\n        if __debug__ and not isinstance(path, str):\n            raise InvalidTypeError(path, str)\n\n        path = normalize_route(path)\n        parent_node = self.parent_node\n        parts = path.split(\"/\")\n\n        for part in parts:\n            if _is_path_parameter(part):\n                if not allow_path_parameters:\n                    raise RuntimeError(\"Path parameters are not allowed here\")\n                parent_node = parent_node.parameter(\n                    _extract_path_parameter(part)\n                )\n            else:\n                parent_node = parent_node.next_node(part)\n\n        return parent_node\n\n    def push_route(self, view: RouteView, path: str, method: Method) -> Route:\n        \"\"\"\n        Register a view with the router.\n        \"\"\"\n\n        if __debug__ and not callable(view):\n            raise InvalidTypeError(view, Callable)\n\n        node = self._get_node_for_path(path, allow_path_parameters=True)\n        if node.routes.get(method) is not None:\n            raise DuplicateRouteError(\n                f\"The route {path!r} was already used for method {method.value}\"\n            )\n\n        route = Route(view=view, path=path, method=method)\n        node.routes[method] = route\n        return route\n\n    def push_subrouter(self, subrouter: SubRouter, path: str) -> None:\n        \"\"\"\n        Register a subrouter that will be used to delegate parsing when nothing\n        else is found.\n        \"\"\"\n\n        if __debug__ and not callable(subrouter):\n            raise InvalidTypeError(subrouter, Callable)\n\n        node = self._get_node_for_path(path, allow_path_parameters=False)\n        if node.subrouter is not None:\n            raise DuplicateRouteError(\n                f\"The route {path!r} already has a subrouter\"\n            )\n\n        node.subrouter = subrouter\n\n    def push_error(\n        self, error: int | type[HTTPError], view: RouteView\n    ) -> None:\n        \"\"\"\n        Register an error view with the router.\n        \"\"\"\n        error_type: type[HTTPError]\n        if isinstance(error, int):\n            error_type = status_exception(error)\n        elif issubclass(error, HTTPError):\n            error_type = error\n        else:\n            raise InvalidTypeError(error, int, type)\n\n        self.error_views[error_type] = view\n\n    def lookup_route(self, path: str, method: Method, /) -> FoundRoute | None:\n        \"\"\"\n        Look up the view for the route.\n        \"\"\"\n        path_parameters: dict[str, str] = {}\n        assert normalize_route(path) == path, (\n            \"Request() should've normalized the route\"\n        )\n\n        parent_node = self.parent_node\n        parts = path.split(\"/\")\n\n        for index, part in enumerate(parts):\n            node = parent_node.children.get(part)\n            if node is None:\n                node = parent_node.path_parameter\n                if node is None:\n                    if parent_node.subrouter is not None:\n                        remaining = \"/\".join(parts[index:])\n                        return FoundRoute(parent_node.subrouter(remaining))\n\n                    # This route doesn't exist\n                    return None\n\n                path_parameters[node.name] = part\n\n            parent_node = node\n\n        final_route: Route | None = parent_node.routes.get(method)\n        if final_route is None:\n            if parent_node.subrouter is not None:\n                return FoundRoute(parent_node.subrouter(\"/\"))\n            return None\n\n        return FoundRoute(final_route, path_parameters)\n\n    def lookup_error(self, error: type[HTTPError], /) -> RouteView | None:\n        \"\"\"\n        Look up the error view for the given HTTP error.\n        \"\"\"\n        return self.error_views.get(error)\n"
  },
  {
    "path": "src/view/core/status_codes.py",
    "content": "\"\"\"\nUtilities and data regarding all HTTP status codes.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nimport traceback\nfrom enum import IntEnum\nfrom typing import ClassVar\n\nfrom view.core.response import TextResponse\n\n__all__ = \"HTTPError\", \"Success\", \"status_exception\"\n\nSTATUS_EXCEPTIONS: dict[int, type[HTTPError]] = {}\nSTATUS_STRINGS: dict[int, str] = {\n    100: \"Continue\",\n    101: \"Switching protocols\",\n    102: \"Processing\",\n    103: \"Early Hints\",\n    200: \"OK\",\n    201: \"Created\",\n    202: \"Accepted\",\n    203: \"Non-Authoritative Information\",\n    204: \"No Content\",\n    205: \"Reset Content\",\n    206: \"Partial Content\",\n    207: \"Multi-Status\",\n    208: \"Already Reported\",\n    226: \"IM Used\",\n    300: \"Multiple Choices\",\n    301: \"Moved Permanently\",\n    302: \"Found\",\n    303: \"See Other\",\n    304: \"Not Modified\",\n    305: \"Use Proxy\",\n    306: \"Switch Proxy\",\n    307: \"Temporary Redirect\",\n    308: \"Permanent Redirect\",\n    400: \"Bad Request\",\n    401: \"Unauthorized\",\n    402: \"Payment Required\",\n    403: \"Forbidden\",\n    404: \"Not Found\",\n    405: \"Method Not Allowed\",\n    406: \"Not Acceptable\",\n    407: \"Proxy Authentication Required\",\n    408: \"Request Timeout\",\n    409: \"Conflict\",\n    410: \"Gone\",\n    411: \"Length Required\",\n    412: \"Precondition Failed\",\n    413: \"Payload Too Large\",\n    414: \"URI Too Long\",\n    415: \"Unsupported Media Type\",\n    416: \"Range Not Satisfiable\",\n    417: \"Expectation Failed\",\n    418: \"I'm a Teapot\",\n    421: \"Misdirected Request\",\n    422: \"Unprocessable Entity\",\n    423: \"Locked\",\n    424: \"Failed Dependency\",\n    425: \"Too Early\",\n    426: \"Upgrade Required\",\n    428: \"Precondition Required\",\n    429: \"Too Many Requests\",\n    431: \"Request Header Fields Too Large\",\n    451: \"Unavailable For Legal Reasons\",\n    500: \"Internal Server Error\",\n    501: \"Not Implemented\",\n    502: \"Bad Gateway\",\n    503: \"Service Unavailable\",\n    504: \"Gateway Timeout\",\n    505: \"HTTP Version Not Supported\",\n    506: \"Variant Also Negotiates\",\n    507: \"Insufficient Storage\",\n    508: \"Loop Detected\",\n    510: \"Not Extended\",\n    511: \"Network Authentication Required\",\n}\n\n\nclass Success(IntEnum):\n    OK = 200\n    \"\"\"\n    The request succeeded. The result and meaning of \"success\" depends on\n    the HTTP method:\n\n    GET: The resource has been fetched and transmitted in the message body.\n    HEAD: Representation headers are included in the response without any\n          message body.\n    PUT or POST: The resource describing the result of the action is\n                 transmitted in the message body.\n    TRACE: The message body contains the request as received by the server.\n    \"\"\"\n\n    CREATED = 201\n    \"\"\"\n    The request succeeded, and a new resource was created as a result. This is\n    typically the response sent after POST requests, or some PUT requests.\n    \"\"\"\n\n    ACCEPTED = 202\n    \"\"\"\n    The request has been received but not yet acted upon. It is noncommittal,\n    since there is no way in HTTP to later send an asynchronous response\n    indicating the outcome of the request. It is intended for cases where\n    another process or server handles the request, or for batch processing.\n    \"\"\"\n\n    NONAUTHORITATIVE_INFORMATION = 203\n    \"\"\"\n    This response code means the returned metadata is not exactly the same as\n    is available from the origin server, but is collected from a local or a\n    third-party copy. This is mostly used for mirrors or backups of another\n    resource. Except for that specific case, the 200 OK response is preferred\n    to this status.\n    \"\"\"\n\n    NO_CONTENT = 204\n    \"\"\"\n    There is no content to send for this request, but the headers are useful.\n    The user agent may update its cached headers for this resource with the\n    new ones.\n    \"\"\"\n\n    RESET_CONTENT = 205\n    \"\"\"\n    Tells the user agent to reset the document which sent this request.\n    \"\"\"\n\n    PARTIAL_CONTENT = 206\n    \"\"\"\n    This response code is used in response to a range request when the client\n    has requested a part or parts of a resource.\n    \"\"\"\n\n    MULTISTATUS = 207\n    \"\"\"\n    Conveys information about multiple resources, for situations where\n    multiple status codes might be appropriate.\n    \"\"\"\n\n    ALREADY_REPORTED = 208\n    \"\"\"\n    Used inside a <dav:propstat> response element to avoid repeatedly\n    enumerating the internal members of multiple bindings to the same\n    collection.\n    \"\"\"\n\n    IM_USED = 226\n    \"\"\"\n    The server has fulfilled a GET request for the resource, and the response\n    is a representation of the result of one or more instance-manipulations\n    applied to the current instance.\n    \"\"\"\n\n\nHTTP_ERROR_TRACEBACK_NOTE = \"\"\"\n-----\n\nIf you're seeing this message, then something has gone horribly wrong.\nHTTP errors should never be in a real traceback, and instead only\nbe used for indicating something to a caller. If you meant to\naccess the message included with this HTTP error, use the\n.message attribute.\n\n-----\n\"\"\"\n\n\nclass HTTPError(Exception):\n    \"\"\"\n    Base class for all HTTP errors.\n\n    Raising this type, or a subclass of this type, will be converted\n    to a status code at runtime.\n    \"\"\"\n\n    status_code: ClassVar[int] = 0\n    description: ClassVar[str] = \"\"\n\n    def __init__(self, *msg: object) -> None:\n        if msg:\n            self.message: str | None = \" \".join([str(item) for item in msg])\n        else:\n            self.message = None\n\n        if sys.version_info < (3, 11):\n            super().__init__(*msg, HTTP_ERROR_TRACEBACK_NOTE)\n        else:\n            super().__init__(*msg)\n            super().add_note(HTTP_ERROR_TRACEBACK_NOTE)\n\n    def __init_subclass__(cls, *, ignore: bool = False) -> None:\n        if not ignore:\n            assert cls.status_code != 0, cls\n            STATUS_EXCEPTIONS[cls.status_code] = cls\n            cls.description = STATUS_STRINGS[cls.status_code]\n\n        # It's too much of a hassle to add an explicit __all__ with every status code.\n        global __all__  # noqa: PLW0603\n        __all__ += (cls.__name__,)\n\n    def as_response(self) -> TextResponse[str]:\n        cls = type(self)\n        if cls.status_code == 0:\n            raise TypeError(f\"{cls} is not a real response\")\n\n        if self.message is None:\n            message = f\"{cls.status_code} {cls.description}\"\n        else:\n            message = self.message\n\n        return TextResponse.from_content(message, status_code=cls.status_code)\n\n\ndef status_exception(status: int) -> type[HTTPError]:\n    \"\"\"\n    Get an exception for the given status.\n    \"\"\"\n    try:\n        status_type: type[HTTPError] = STATUS_EXCEPTIONS[status]\n    except KeyError as error:\n        raise ValueError(\n            f\"{status} is not a valid HTTP error status code\"\n        ) from error\n\n    return status_type\n\n\nclass ClientSideError(HTTPError, ignore=True):\n    \"\"\"\n    Base class for all HTTP errors between 400 and 500.\n    \"\"\"\n\n\nclass ServerSideError(HTTPError, ignore=True):\n    \"\"\"\n    Base class for all HTTP errors between 500 and 600.\n    \"\"\"\n\n\nclass BadRequest(ClientSideError):\n    \"\"\"\n    The server cannot or will not process the request due to something\n    that is perceived to be a client error (e.g., malformed request syntax,\n    invalid request message framing, or deceptive request routing).\n    \"\"\"\n\n    status_code = 400\n\n\nclass Unauthorized(ClientSideError):\n    \"\"\"\n    Although the HTTP standard specifies \"unauthorized\", semantically this\n    response means \"unauthenticated\". That is, the client must authenticate\n    itself to get the requested response.\n    \"\"\"\n\n    status_code = 401\n\n\nclass PaymentRequired(ClientSideError):\n    \"\"\"\n    The initial purpose of this code was for digital payment systems,\n    however this status code is rarely used and no standard convention exists.\n    \"\"\"\n\n    status_code = 402\n\n\nclass Forbidden(ClientSideError):\n    \"\"\"\n    The client does not have access rights to the content; that is, it is\n    unauthorized, so the server is refusing to give the requested resource.\n    Unlike 401 Unauthorized, the client's identity is known to the server.\n    \"\"\"\n\n    status_code = 403\n\n\nclass NotFound(ClientSideError):\n    \"\"\"\n    The server cannot find the requested resource. In the browser, this means\n    the URL is not recognized. In an API, this can also mean that the endpoint\n    is valid but the resource itself does not exist. Servers may also send this\n    response instead of 403 Forbidden to hide the existence of a resource from\n    an unauthorized client. This response code is probably the most well known\n    due to its frequent occurrence on the web.\n    \"\"\"\n\n    status_code = 404\n\n\nclass MethodNotAllowed(ClientSideError):\n    \"\"\"\n    The request method is known by the server but is not supported by the\n    target resource. For example, an API may not allow DELETE on a resource,\n    or the TRACE method entirely.\n    \"\"\"\n\n    status_code = 405\n\n\nclass NotAcceptable(ClientSideError):\n    \"\"\"\n    This response is sent when the web server, after performing server-driven\n    content negotiation, doesn't find any content that conforms to the\n    criteria given by the user agent.\n    \"\"\"\n\n    status_code = 406\n\n\nclass ProxyAuthenticationRequired(ClientSideError):\n    \"\"\"\n    This is similar to 401 Unauthorized but authentication is needed to be\n    done by a proxy.\n    \"\"\"\n\n    status_code = 407\n\n\nclass RequestTimeout(ClientSideError):\n    \"\"\"\n    This response is sent on an idle connection by some servers, even without\n    any previous request by the client. It means that the server would like to\n    shut down this unused connection. This response is used much more since\n    some browsers use HTTP pre-connection mechanisms to speed up browsing.\n    Some servers may shut down a connection without sending this message.\n    \"\"\"\n\n    status_code = 408\n\n\nclass Conflict(ClientSideError):\n    \"\"\"\n    This response is sent when a request conflicts with the current state of\n    the server. In WebDAV remote web authoring, 409 responses are errors sent\n    to the client so that a user might be able to resolve a conflict and\n    resubmit the request.\n    \"\"\"\n\n    status_code = 409\n\n\nclass Gone(ClientSideError):\n    \"\"\"\n    This response is sent when the requested content has been permanently\n    deleted from server, with no forwarding address. Clients are expected to\n    remove their caches and links to the resource. The HTTP specification\n    intends this status code to be used for \"limited-time, promotional\n    services\". APIs should not feel compelled to indicate resources that have\n    been deleted with this status code.\n    \"\"\"\n\n    status_code = 410\n\n\nclass LengthRequired(ClientSideError):\n    \"\"\"\n    Server rejected the request because the Content-Length header field is not\n    defined and the server requires it.\n    \"\"\"\n\n    status_code = 411\n\n\nclass PreconditionFailed(ClientSideError):\n    \"\"\"\n    In conditional requests, the client has indicated preconditions in its\n    headers which the server does not meet.\n    \"\"\"\n\n    status_code = 412\n\n\nclass ContentTooLarge(ClientSideError):\n    \"\"\"\n    The request body is larger than limits defined by server. The server might\n    close the connection or return an Retry-After header field.\n    \"\"\"\n\n    status_code = 413\n\n\nclass URITooLong(ClientSideError):\n    \"\"\"\n    The URI requested by the client is longer than the server is willing to\n    interpret.\n    \"\"\"\n\n    status_code = 414\n\n\nclass UnsupportedMediaType(ClientSideError):\n    \"\"\"\n    The media format of the requested data is not supported by the server,\n    so the server is rejecting the request.\n    \"\"\"\n\n    status_code = 415\n\n\nclass RangeNotSatisfiable(ClientSideError):\n    \"\"\"\n    The ranges specified by the Range header field in the request cannot be\n    fulfilled. It's possible that the range is outside the size of the target\n    resource's data.\n    \"\"\"\n\n    status_code = 416\n\n\nclass ExpectationFailed(ClientSideError):\n    \"\"\"\n    This response code means the expectation indicated by the Expect request\n    header field cannot be met by the server.\n    \"\"\"\n\n    status_code = 417\n\n\nclass IAmATeapot(ClientSideError):\n    \"\"\"\n    The server refuses the attempt to brew coffee with a teapot.\n    \"\"\"\n\n    status_code = 418\n\n\nclass MisdirectedRequest(ClientSideError):\n    \"\"\"\n    The request was directed at a server that is not able to produce a\n    response. This can be sent by a server that is not configured to produce\n    responses for the combination of scheme and authority that are included\n    in the request URI.\n    \"\"\"\n\n    status_code = 421\n\n\nclass UnprocessableContent(ClientSideError):\n    \"\"\"\n    The request was well-formed but was unable to be followed due to semantic errors.\n    \"\"\"\n\n    status_code = 422\n\n\nclass Locked(ClientSideError):\n    \"\"\"\n    The resource that is being accessed is locked.\n    \"\"\"\n\n    status_code = 423\n\n\nclass FailedDependency(ClientSideError):\n    \"\"\"\n    The request failed due to failure of a previous request.\n    \"\"\"\n\n    status_code = 424\n\n\nclass TooEarly(ClientSideError):\n    \"\"\"\n    Indicates that the server is unwilling to risk processing a request\n    that might be replayed.\n    \"\"\"\n\n    status_code = 425\n\n\nclass UpgradeRequired(ClientSideError):\n    \"\"\"\n    The server refuses to perform the request using the current protocol but\n    might be willing to do so after the client upgrades to a different\n    protocol. The server sends an Upgrade header in a 426 response to indicate\n    the required protocol(s).\n    \"\"\"\n\n    status_code = 426\n\n\nclass PreconditionRequired(ClientSideError):\n    \"\"\"\n    The origin server requires the request to be conditional. This response is\n    intended to prevent the 'lost update' problem, where a client GETs a\n    resource's state, modifies it and PUTs it back to the server, when\n    meanwhile a third party has modified the state on the server, leading to\n    a conflict.\n    \"\"\"\n\n    status_code = 428\n\n\nclass TooManyRequests(ClientSideError):\n    \"\"\"\n    The user has sent too many requests in a given amount of\n    time (rate limiting).\n    \"\"\"\n\n    status_code = 429\n\n\nclass RequestHeaderFieldsTooLarge(ClientSideError):\n    \"\"\"\n    The server is unwilling to process the request because its header fields\n    are too large. The request may be resubmitted after reducing the size of\n    the request header fields.\n    \"\"\"\n\n    status_code = 431\n\n\nclass UnavailableForLegalReasons(ClientSideError):\n    \"\"\"\n    The user agent requested a resource that cannot legally be provided,\n    such as a web page censored by a government.\n    \"\"\"\n\n    status_code = 451\n\n\nclass InternalServerError(ServerSideError):\n    \"\"\"\n    The server has encountered a situation it does not know how to handle.\n    This error is generic, indicating that the server cannot find a more\n    appropriate 5XX status code to respond with.\n    \"\"\"\n\n    status_code = 500\n\n    @classmethod\n    def from_current_exception(cls) -> InternalServerError:\n        message = traceback.format_exc()\n        return cls(message)\n\n\nclass NotImplemented(ServerSideError):  # noqa: A001\n    \"\"\"\n    The request method is not supported by the server and cannot be handled.\n    The only methods that servers are required to support (and therefore that\n    must not return this code) are GET and HEAD.\n    \"\"\"\n\n    status_code = 501\n\n\nclass BadGateway(ServerSideError):\n    \"\"\"\n    This error response means that the server, while working as a gateway to\n    get a response needed to handle the request, got an invalid response.\n    \"\"\"\n\n    status_code = 502\n\n\nclass ServiceUnavailable(ServerSideError):\n    \"\"\"\n    The server is not ready to handle the request. Common causes are a server\n    that is down for maintenance or that is overloaded. Note that together\n    with this response, a user-friendly page explaining the problem should be\n    sent. This response should be used for temporary conditions and the\n    Retry-After HTTP header should, if possible, contain the estimated time\n    before the recovery of the service. The webmaster must also take care\n    about the caching-related headers that are sent along with this response,\n    as these temporary condition responses should usually not be cached.\n    \"\"\"\n\n    status_code = 503\n\n\nclass GatewayTimeout(ServerSideError):\n    \"\"\"\n    This error response is given when the server is acting as a gateway and\n    cannot get a response in time.\n    \"\"\"\n\n    status_code = 504\n\n\nclass HTTPVersionNotSupported(ServerSideError):\n    \"\"\"\n    The HTTP version used in the request is not supported by the server.\n    \"\"\"\n\n    status_code = 505\n\n\nclass VariantAlsoNegotiates(ServerSideError):\n    \"\"\"\n    The server has an internal configuration error: during content\n    negotiation, the chosen variant is configured to engage in content\n    negotiation itself, which results in circular references when creating\n    responses.\n    \"\"\"\n\n    status_code = 506\n\n\nclass InsufficientStorage(ServerSideError):\n    \"\"\"\n    The method could not be performed on the resource because the server is\n    unable to store the representation needed to successfully complete the\n    request.\n    \"\"\"\n\n    status_code = 507\n\n\nclass LoopDetected(ServerSideError):\n    \"\"\"\n    The server detected an infinite loop while processing the request.\n    \"\"\"\n\n    status_code = 508\n\n\nclass NotExtended(ServerSideError):\n    \"\"\"\n    The client request declares an HTTP Extension (RFC 2774) that should be\n    used to process the request, but the extension is not supported.\n    \"\"\"\n\n    status_code = 510\n\n\nclass NetworkAuthenticationRequired(ServerSideError):\n    \"\"\"\n    Indicates that the client needs to authenticate to gain network access.\n    \"\"\"\n\n    status_code = 511\n"
  },
  {
    "path": "src/view/dom/__init__.py",
    "content": "\"\"\"\nA Document Object Model (DOM) API for Python, allowing users to write HTML in\ntheir Python code.\n\"\"\"\n\nfrom view.dom import components as components\nfrom view.dom import core as core\nfrom view.dom import primitives as primitives\n"
  },
  {
    "path": "src/view/dom/components.py",
    "content": "\"\"\"\nImplementation of \"components\" -- DOM nodes defined by the user.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom functools import wraps\nfrom typing import TYPE_CHECKING, NoReturn, ParamSpec\n\nfrom view.dom.core import HTMLNode, HTMLTree\nfrom view.dom.primitives import base, body, html, link, meta, script\nfrom view.dom.primitives import title as title_node\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, Iterable\n\n__all__ = \"Children\", \"Component\", \"component\"\n\n\nclass Children(HTMLNode):\n    \"\"\"\n    Sentinel class marking where to inject the body in a component.\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\"_children_node\", is_real=False)\n\n    def __enter__(self) -> NoReturn:\n        raise RuntimeError(\"Children() cannot be used in a 'with' block\")\n\n    def as_html(self) -> str:\n        raise RuntimeError(\n            \"Children() cannot be turned into HTML -- this is likely a bug with view.py\"\n        )\n\n\n@dataclass(slots=True, frozen=True)\nclass Component:\n    \"\"\"\n    A node with an \"injectable\" body.\n    \"\"\"\n\n    generator: HTMLTree\n\n    def __enter__(self) -> None:\n        stack = HTMLNode.node_stack.get()\n        for node in self.generator:\n            if isinstance(node, Children):\n                capture_node = HTMLNode.virtual(\"capture\")\n                stack.put_nowait(capture_node)\n                return\n\n    def __exit__(self, *_) -> None:\n        stack = HTMLNode.node_stack.get()\n        capture_node = stack.get_nowait()\n        assert not capture_node.is_real\n\n        parent_node = stack.queue[-1]\n        parent_node.children.extend(capture_node.children)\n\n        for node in self.generator:\n            if __debug__ and isinstance(node, Children):\n                raise RuntimeError(\n                    \"Cannot use Children() multiple times for the same component\"\n                )\n\n\nP = ParamSpec(\"P\")\n\n\ndef component(function: Callable[P, HTMLTree]) -> Callable[P, Component]:\n    \"\"\"\n    Make a function usable as an HTML node.\n    \"\"\"\n\n    @wraps(function)\n    def inner(*args: P.args, **kwargs: P.kwargs) -> Component:\n        return Component(function(*args, **kwargs))\n\n    return inner\n\n\n@component\ndef page(\n    title: str,\n    *,\n    language: str = \"en\",\n    stylesheets: Iterable[str] | None = None,\n    scripts: Iterable[str] | None = [],\n    description: str | None = None,\n    keywords: Iterable[str] | None = None,\n    author: str | None = None,\n    page_url: str | None = None,\n) -> HTMLTree:\n    \"\"\"\n    Common layout for an HTML page.\n    \"\"\"\n    with html(lang=language):\n        yield meta(charset=\"utf-8\")\n        yield meta(\n            name=\"viewport\", content=\"width=device-width, initial-scale=1.0\"\n        )\n\n        if description is not None:\n            yield meta(name=\"description\", content=description)\n\n        if keywords is not None:\n            yield meta(name=\"keywords\", content=\",\".join(keywords))\n\n        if author is not None:\n            yield meta(name=\"author\", content=author)\n\n        if page_url is not None:\n            yield link(rel=\"canonical\", href=page_url)\n            yield base(href=page_url)\n\n        for stylesheet in stylesheets or []:\n            yield link(rel=\"stylesheet\", href=stylesheet)\n\n        yield title_node(title)\n        for script_url in scripts or []:\n            yield script(src=script_url, defer=True)\n    with body():\n        yield Children()\n"
  },
  {
    "path": "src/view/dom/core.py",
    "content": "\"\"\"\nThe implementation of the DOM API.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport uuid\nfrom collections.abc import (\n    AsyncIterator,\n    Callable,\n    Iterator,\n    MutableMapping,\n    MutableSequence,\n)\nfrom contextlib import contextmanager\nfrom contextvars import ContextVar\nfrom dataclasses import dataclass, field\nfrom io import StringIO\nfrom queue import LifoQueue\nfrom typing import TYPE_CHECKING, ClassVar, ParamSpec, TypeAlias\n\nfrom view.core.headers import as_real_headers\nfrom view.core.response import Response\nfrom view.exceptions import InvalidTypeError\nfrom view.javascript import SupportsJavaScript\n\nif TYPE_CHECKING:\n    from view.core.router import RouteView\n    from view.dom.components import Component\n\n__all__ = (\"HTMLNode\", \"html_response\")\n\nHTMLTree: TypeAlias = Iterator[\"HTMLNode\"]\n\n\ndef _indent_iterator(iterator: Iterator[str]) -> Iterator[str]:\n    for line in iterator:\n        try:\n            yield \"    \" + line\n        except TypeError as error:\n            raise TypeError(f\"unexpected line: {line!r}\") from error\n\n\n@dataclass(slots=True)\nclass HTMLNode(SupportsJavaScript):\n    \"\"\"\n    Data class representing an HTML node in the tree.\n    \"\"\"\n\n    node_stack: ClassVar[ContextVar[LifoQueue[HTMLNode]]] = ContextVar(\n        \"node_stack\"\n    )\n\n    node_name: str\n    \"\"\"\n    Name of the node as it will appear in the HTML. For example, in an <html>\n    node, this will be the string 'html'.\n    \"\"\"\n\n    is_real: bool = True\n    \"\"\"\n    Whether this node will actually be included in the output. Generally, most\n    nodes will be rendered, but there are a few special types of nodes that\n    are only used during the rendering process.\n    \"\"\"\n\n    text: str = \"\"\n    \"\"\"\n    The direct text of this node, not including any other children.\n    \"\"\"\n\n    attributes: MutableMapping[str, str] = field(default_factory=dict)\n    \"\"\"\n    Dictionary containing attribute names and values as they will be rendered\n    in the final output.\n    \"\"\"\n\n    children: MutableSequence[HTMLNode] = field(default_factory=list)\n    \"\"\"\n    All nodes that are a direct descendant of this node.\n    \"\"\"\n\n    @classmethod\n    def virtual(cls, name: str) -> HTMLNode:\n        \"\"\"\n        Create a new \"fake\" node.\n        \"\"\"\n\n        return cls(f\"__view_internal_{name}_node\", is_real=False)\n\n    @classmethod\n    def new(\n        cls,\n        name: str,\n        *,\n        child_text: str | None = None,\n        attributes: MutableMapping[str, str] | None = None,\n    ) -> HTMLNode:\n        \"\"\"\n        Create a new node that will be included in the final HTML.\n        \"\"\"\n        return cls(\n            name,\n            is_real=True,\n            text=child_text or \"\",\n            attributes=attributes or {},\n            children=[],\n        )\n\n    def __enter__(self) -> None:\n        stack = self.node_stack.get()\n        stack.put_nowait(self)\n\n    def __exit__(self, *_) -> None:\n        stack = self.node_stack.get()\n        popped = stack.get_nowait()\n        assert popped is self, popped\n\n    def _html_body(self) -> Iterator[str]:\n        if self.text != \"\":\n            yield self.text\n\n        for child in self.children:\n            yield from child.as_html_stream()\n\n    def as_html_stream(self) -> Iterator[str]:\n        \"\"\"\n        Convert this node to actual HTML code, streaming each line individually.\n        \"\"\"\n\n        if self.is_real:\n            if self.attributes == {}:\n                yield f\"<{self.node_name}>\"\n            else:\n                yield f\"<{self.node_name}\"\n                for name, value in self.attributes.items():\n                    yield f'    {name}=\"{value}\"'\n                yield \">\"\n            yield from _indent_iterator(self._html_body())\n            yield f\"</{self.node_name}>\"\n        else:\n            assert self.attributes == {}, self.attributes\n            yield from self._html_body()\n\n    def as_html(self) -> str:\n        \"\"\"\n        Convert this node to HTML code.\n        \"\"\"\n\n        buffer = StringIO()\n        for line in self.as_html_stream():\n            buffer.write(line + \"\\n\")\n\n        return buffer.getvalue()\n\n    def as_javascript(self) -> str:\n        element_id = self.attributes.setdefault(\"id\", uuid.uuid4().hex)\n        return f\"document.getElementById({element_id!r})\"\n\n\n@contextmanager\ndef html_context() -> HTMLTree:\n    \"\"\"\n    Enter a context in which HTML nodes can be created under a fresh tree.\n    \"\"\"\n    stack = LifoQueue()\n    token = HTMLNode.node_stack.set(stack)\n\n    tree = HTMLNode.virtual(\"tree_top\")\n    stack.put_nowait(tree)\n\n    try:\n        yield tree\n    finally:\n        HTMLNode.node_stack.reset(token)\n\n\nP = ParamSpec(\"P\")\nHTMLViewResponseItem: TypeAlias = \"HTMLNode | int | Component\"\nHTMLViewResult = (\n    AsyncIterator[HTMLViewResponseItem] | Iterator[HTMLViewResponseItem]\n)\nHTMLView: TypeAlias = Callable[P, HTMLViewResult]\n\n\ndef html_response(\n    function: HTMLView,\n) -> RouteView:\n    \"\"\"\n    Return a :class:`~view.core.response.Response` object from a function\n    returning HTML.\n    \"\"\"\n\n    async def wrapper(*args: P.args, **kwargs: P.kwargs) -> Response:\n        with html_context() as parent:\n            iterator = function(*args, **kwargs)\n            status_code: int | None = None\n\n            def try_item(item: HTMLViewResponseItem) -> None:\n                nonlocal status_code\n\n                if isinstance(item, int):\n                    if __debug__ and status_code is not None:\n                        raise RuntimeError(\"Status code was already set\")\n                    status_code = item\n\n            if isinstance(iterator, AsyncIterator):\n                async for item in iterator:\n                    try_item(item)\n            else:\n                if __debug__ and not isinstance(iterator, Iterator):\n                    raise InvalidTypeError(iterator, AsyncIterator, Iterator)\n\n                for item in iterator:\n                    try_item(item)\n\n            async def stream() -> AsyncIterator[bytes]:\n                yield b\"<!DOCTYPE html>\\n\"\n                for line in parent.as_html_stream():\n                    yield line.encode(\"utf-8\") + b\"\\n\"\n\n        return Response(\n            stream(),\n            status_code or 200,\n            as_real_headers({\"content-type\": \"text/html\"}),\n        )\n\n    return wrapper\n"
  },
  {
    "path": "src/view/dom/primitives.py",
    "content": "\"\"\"\nConstructor functions for all HTTP elements.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any, Literal, TypedDict\n\nfrom typing_extensions import NotRequired, Unpack\n\nfrom view.dom.core import HTMLNode\nfrom view.exceptions import InvalidTypeError\n\nif TYPE_CHECKING:\n    from collections.abc import Callable\n\n\nclass ImplicitDefault(str):\n    \"\"\"\n    Sentinel class to mark a default value in an HTML node as \"implicit\", and\n    thus does not need to be included in the rendered output.\n    \"\"\"\n\n    __slots__ = ()\n\n\ndef _construct_node(\n    name: str,\n    child_text: str | None = None,\n    *,\n    attributes: dict[str, Any],\n    global_attributes: GlobalAttributes,\n    data: dict[str, str],\n) -> HTMLNode:\n    if __debug__ and (\n        (child_text is not None) and not isinstance(child_text, str)\n    ):\n        raise InvalidTypeError(child_text, str)\n\n    for attribute_name, value in attributes.copy().items():\n        if value in {None, False}:\n            attributes.pop(attribute_name)\n        elif value is True:\n            attributes[attribute_name] = \"\"\n\n    attributes = {**attributes, **global_attributes}\n    for data_name, value in data.items():\n        if __debug__ and not isinstance(value, str):\n            raise InvalidTypeError(value, str)\n\n        attributes[f\"data-{data_name}\"] = value\n\n    stack = HTMLNode.node_stack.get()\n    top = stack.queue[-1]\n\n    # Since \"class\" is a reserved Python keyword, we have to use cls instead\n    if \"cls\" in attributes:\n        attributes[\"class\"] = attributes.pop(\"cls\")\n\n    for attribute_name, value in attributes.copy().items():\n        if isinstance(value, ImplicitDefault):\n            attributes.pop(attribute_name)\n            continue\n\n        if \"_\" in attribute_name:\n            attributes[attribute_name.replace(\"_\", \"-\")] = str(\n                attributes.pop(attribute_name)\n            )\n\n    new_node = HTMLNode.new(name, child_text=child_text, attributes=attributes)\n    top.children.append(new_node)\n    return new_node\n\n\nclass GlobalAttributes(TypedDict):\n    accesskey: NotRequired[str]\n    \"\"\"Specifies a keyboard shortcut to activate or focus the element\"\"\"\n\n    cls: NotRequired[str]\n    \"\"\"Specifies one or more class names for an element (refers to a class in a style sheet)\"\"\"\n\n    contenteditable: NotRequired[Literal[\"true\", \"false\", \"plaintext-only\"]]\n    \"\"\"Specifies whether the content of an element is editable or not\"\"\"\n\n    dir: NotRequired[Literal[\"ltr\", \"rtl\", \"auto\"]]\n    \"\"\"Specifies the text direction for the content in an element\"\"\"\n\n    draggable: NotRequired[Literal[\"true\", \"false\", \"auto\"]]\n    \"\"\"Specifies whether an element is draggable or not\"\"\"\n\n    hidden: NotRequired[bool]\n    \"\"\"Specifies that an element is not yet, or is no longer, relevant\"\"\"\n\n    id: NotRequired[str]\n    \"\"\"Specifies a unique id for an element\"\"\"\n\n    lang: NotRequired[str]\n    \"\"\"Specifies the language of the element's content\"\"\"\n\n    spellcheck: NotRequired[Literal[\"true\", \"false\"]]\n    \"\"\"Specifies whether the element is to have its spelling and grammar checked or not\"\"\"\n\n    style: NotRequired[str]\n    \"\"\"Specifies an inline CSS style for an element\"\"\"\n\n    tabindex: NotRequired[int]\n    \"\"\"Specifies the tabbing order of an element\"\"\"\n\n    title: NotRequired[str]\n    \"\"\"Specifies extra information about an element (displayed as a tooltip)\"\"\"\n\n    translate: NotRequired[Literal[\"yes\", \"no\"]]\n    \"\"\"Specifies whether the content of an element should be translated or not\"\"\"\n\n    onabort: NotRequired[str]\n    \"\"\"Script to be run on abort\"\"\"\n\n    onblur: NotRequired[str]\n    \"\"\"Script to be run when an element loses focus\"\"\"\n\n    oncancel: NotRequired[str]\n    \"\"\"Script to be run when a dialog is canceled\"\"\"\n\n    oncanplay: NotRequired[str]\n    \"\"\"Script to be run when a file is ready to start playing\"\"\"\n\n    oncanplaythrough: NotRequired[str]\n    \"\"\"Script to be run when a file can be played all the way through without pausing\"\"\"\n\n    onchange: NotRequired[str]\n    \"\"\"Script to be run when the value of an element is changed\"\"\"\n\n    onclick: NotRequired[str]\n    \"\"\"Script to be run on a mouse click\"\"\"\n\n    onclose: NotRequired[str]\n    \"\"\"Script to be run when a dialog is closed\"\"\"\n\n    oncontextmenu: NotRequired[str]\n    \"\"\"Script to be run when a context menu is triggered\"\"\"\n\n    oncopy: NotRequired[str]\n    \"\"\"Script to be run when the content of an element is copied\"\"\"\n\n    oncuechange: NotRequired[str]\n    \"\"\"Script to be run when the cue changes in a track element\"\"\"\n\n    oncut: NotRequired[str]\n    \"\"\"Script to be run when the content of an element is cut\"\"\"\n\n    ondblclick: NotRequired[str]\n    \"\"\"Script to be run on a mouse double-click\"\"\"\n\n    ondrag: NotRequired[str]\n    \"\"\"Script to be run when an element is dragged\"\"\"\n\n    ondragend: NotRequired[str]\n    \"\"\"Script to be run at the end of a drag operation\"\"\"\n\n    ondragenter: NotRequired[str]\n    \"\"\"Script to be run when an element has been dragged to a valid drop target\"\"\"\n\n    ondragleave: NotRequired[str]\n    \"\"\"Script to be run when an element leaves a valid drop target\"\"\"\n\n    ondragover: NotRequired[str]\n    \"\"\"Script to be run when an element is being dragged over a valid drop target\"\"\"\n\n    ondragstart: NotRequired[str]\n    \"\"\"Script to be run at the start of a drag operation\"\"\"\n\n    ondrop: NotRequired[str]\n    \"\"\"Script to be run when dragged element is being dropped\"\"\"\n\n    ondurationchange: NotRequired[str]\n    \"\"\"Script to be run when the length of the media changes\"\"\"\n\n    onemptied: NotRequired[str]\n    \"\"\"Script to be run when media resource is suddenly unavailable\"\"\"\n\n    onended: NotRequired[str]\n    \"\"\"Script to be run when the media has reach the end\"\"\"\n\n    onerror: NotRequired[str]\n    \"\"\"Script to be run when an error occurs\"\"\"\n\n    onfocus: NotRequired[str]\n    \"\"\"Script to be run when an element gets focus\"\"\"\n\n    oninput: NotRequired[str]\n    \"\"\"Script to be run when an element gets user input\"\"\"\n\n    oninvalid: NotRequired[str]\n    \"\"\"Script to be run when an element is invalid\"\"\"\n\n    onkeydown: NotRequired[str]\n    \"\"\"Script to be run when a user is pressing a key\"\"\"\n\n    onkeypress: NotRequired[str]\n    \"\"\"Script to be run when a user presses a key\"\"\"\n\n    onkeyup: NotRequired[str]\n    \"\"\"Script to be run when a user releases a key\"\"\"\n\n    onload: NotRequired[str]\n    \"\"\"Script to be run when the element has finished loading\"\"\"\n\n    onloadeddata: NotRequired[str]\n    \"\"\"Script to be run when media data is loaded\"\"\"\n\n    onloadedmetadata: NotRequired[str]\n    \"\"\"Script to be run when meta data is loaded\"\"\"\n\n    onloadstart: NotRequired[str]\n    \"\"\"Script to be run just as the file begins to load\"\"\"\n\n    onmousedown: NotRequired[str]\n    \"\"\"Script to be run when a mouse button is pressed down on an element\"\"\"\n\n    onmouseenter: NotRequired[str]\n    \"\"\"Script to be run when the mouse pointer enters an element\"\"\"\n\n    onmouseleave: NotRequired[str]\n    \"\"\"Script to be run when the mouse pointer leaves an element\"\"\"\n\n    onmousemove: NotRequired[str]\n    \"\"\"Script to be run when the mouse pointer is moving over an element\"\"\"\n\n    onmouseout: NotRequired[str]\n    \"\"\"Script to be run when the mouse pointer moves out of an element\"\"\"\n\n    onmouseover: NotRequired[str]\n    \"\"\"Script to be run when the mouse pointer moves over an element\"\"\"\n\n    onmouseup: NotRequired[str]\n    \"\"\"Script to be run when a mouse button is released over an element\"\"\"\n\n    onpaste: NotRequired[str]\n    \"\"\"Script to be run when content is pasted into an element\"\"\"\n\n    onpause: NotRequired[str]\n    \"\"\"Script to be run when the media is paused\"\"\"\n\n    onplay: NotRequired[str]\n    \"\"\"Script to be run when the media starts playing\"\"\"\n\n    onplaying: NotRequired[str]\n    \"\"\"Script to be run when the media actually has started playing\"\"\"\n\n    onprogress: NotRequired[str]\n    \"\"\"Script to be run when the browser is in the process of getting the media data\"\"\"\n\n    onratechange: NotRequired[str]\n    \"\"\"Script to be run each time the playback rate changes\"\"\"\n\n    onreset: NotRequired[str]\n    \"\"\"Script to be run when a form is reset\"\"\"\n\n    onresize: NotRequired[str]\n    \"\"\"Script to be run when the browser window is being resized\"\"\"\n\n    onscroll: NotRequired[str]\n    \"\"\"Script to be run when an element's scrollbar is being scrolled\"\"\"\n\n    onseeked: NotRequired[str]\n    \"\"\"Script to be run when seeking has ended\"\"\"\n\n    onseeking: NotRequired[str]\n    \"\"\"Script to be run when seeking begins\"\"\"\n\n    onselect: NotRequired[str]\n    \"\"\"Script to be run when the element gets selected\"\"\"\n\n    onshow: NotRequired[str]\n    \"\"\"Script to be run when a context menu is shown\"\"\"\n\n    onstalled: NotRequired[str]\n    \"\"\"Script to be run when the browser is unable to fetch the media data\"\"\"\n\n    onsubmit: NotRequired[str]\n    \"\"\"Script to be run when a form is submitted\"\"\"\n\n    onsuspend: NotRequired[str]\n    \"\"\"Script to be run when fetching the media data is stopped\"\"\"\n\n    ontimeupdate: NotRequired[str]\n    \"\"\"Script to be run when the playing position has changed\"\"\"\n\n    ontoggle: NotRequired[str]\n    \"\"\"Script to be run when the user opens or closes a details element\"\"\"\n\n    onvolumechange: NotRequired[str]\n    \"\"\"Script to be run each time the volume is changed\"\"\"\n\n    onwaiting: NotRequired[str]\n    \"\"\"Script to be run when the media has paused but is expected to resume\"\"\"\n\n    onwheel: NotRequired[str]\n    \"\"\"Script to be run when the mouse wheel rolls up or down over an element\"\"\"\n\n\ndef a(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    href: str | None = None,\n    target: (\n        Literal[\"_blank\", \"_self\", \"_parent\", \"_top\"] | ImplicitDefault\n    ) = ImplicitDefault(\"_self\"),\n    download: str | None = None,\n    rel: str | None = None,\n    hreflang: str | None = None,\n    type: str | None = None,\n    referrerpolicy: (\n        Literal[\n            \"no-referrer\",\n            \"no-referrer-when-downgrade\",\n            \"origin\",\n            \"origin-when-cross-origin\",\n            \"same-origin\",\n            \"strict-origin\",\n            \"strict-origin-when-cross-origin\",\n            \"unsafe-url\",\n        ]\n        | None\n    ) = None,\n    ping: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a hyperlink that links to another page or location within the same page\"\"\"\n    return _construct_node(\n        \"a\",\n        child_text=child_text,\n        attributes={\n            \"href\": href,\n            \"target\": target,\n            \"download\": download,\n            \"rel\": rel,\n            \"hreflang\": hreflang,\n            \"type\": type,\n            \"referrerpolicy\": referrerpolicy,\n            \"ping\": ping,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef abbr(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines an abbreviation or acronym, optionally with its expansion\"\"\"\n    return _construct_node(\n        \"abbr\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef address(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines contact information for the author/owner of a document or article\"\"\"\n    return _construct_node(\n        \"address\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef span(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines an inline container with no semantic meaning, used for styling or scripting\"\"\"\n    return _construct_node(\n        \"span\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef strong(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines important text with strong importance (typically bold)\"\"\"\n    return _construct_node(\n        \"strong\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef style(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    media: str | None = None,\n    type: str = ImplicitDefault(\"text/css\"),\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Contains style information (CSS) for a document\"\"\"\n    return _construct_node(\n        \"style\",\n        child_text=child_text,\n        attributes={\"media\": media, \"type\": type},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef sub(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines subscript text\"\"\"\n    return _construct_node(\n        \"sub\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef summary(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a visible heading for a details element\"\"\"\n    return _construct_node(\n        \"summary\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef sup(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines superscript text\"\"\"\n    return _construct_node(\n        \"sup\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef table(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a table\"\"\"\n    return _construct_node(\n        \"table\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef tbody(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Groups the body content in a table\"\"\"\n    return _construct_node(\n        \"tbody\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef td(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    colspan: int | ImplicitDefault = ImplicitDefault(1),\n    rowspan: int | ImplicitDefault = ImplicitDefault(1),\n    headers: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a standard data cell in a table\"\"\"\n    return _construct_node(\n        \"td\",\n        child_text=child_text,\n        attributes={\n            \"colspan\": colspan,\n            \"rowspan\": rowspan,\n            \"headers\": headers,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef template(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a container for content that should not be rendered when the page loads\"\"\"\n    return _construct_node(\n        \"template\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef textarea(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    name: str | None = None,\n    rows: int | None = None,\n    cols: int | None = None,\n    placeholder: str | None = None,\n    required: bool = False,\n    readonly: bool = False,\n    disabled: bool = False,\n    maxlength: int | None = None,\n    minlength: int | None = None,\n    wrap: Literal[\"hard\", \"soft\"] | ImplicitDefault = ImplicitDefault(\"soft\"),\n    autocomplete: Literal[\"on\", \"off\"] | None = None,\n    autofocus: bool = False,\n    form: str | None = None,\n    dirname: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a multi-line text input control\"\"\"\n    return _construct_node(\n        \"textarea\",\n        child_text=child_text,\n        attributes={\n            \"name\": name,\n            \"rows\": rows,\n            \"cols\": cols,\n            \"placeholder\": placeholder,\n            \"required\": required,\n            \"readonly\": readonly,\n            \"disabled\": disabled,\n            \"maxlength\": maxlength,\n            \"minlength\": minlength,\n            \"wrap\": wrap,\n            \"autocomplete\": autocomplete,\n            \"autofocus\": autofocus,\n            \"form\": form,\n            \"dirname\": dirname,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef tfoot(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Groups the footer content in a table\"\"\"\n    return _construct_node(\n        \"tfoot\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef th(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    colspan: int | ImplicitDefault = ImplicitDefault(1),\n    rowspan: int | ImplicitDefault = ImplicitDefault(1),\n    headers: str | None = None,\n    scope: Literal[\"col\", \"row\", \"colgroup\", \"rowgroup\"] | None = None,\n    abbr: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a header cell in a table\"\"\"\n    return _construct_node(\n        \"th\",\n        child_text=child_text,\n        attributes={\n            \"colspan\": colspan,\n            \"rowspan\": rowspan,\n            \"headers\": headers,\n            \"scope\": scope,\n            \"abbr\": abbr,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef thead(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Groups the header content in a table\"\"\"\n    return _construct_node(\n        \"thead\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef time(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    datetime: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a specific time (or datetime)\"\"\"\n    return _construct_node(\n        \"time\",\n        child_text=child_text,\n        attributes={\"datetime\": datetime},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef title(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines the title of the document (shown in browser's title bar or tab)\"\"\"\n    return _construct_node(\n        \"title\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef tr(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a row in a table\"\"\"\n    return _construct_node(\n        \"tr\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef track(\n    *,\n    data: dict[str, str] | None = None,\n    kind: (\n        Literal[\n            \"subtitles\", \"captions\", \"descriptions\", \"chapters\", \"metadata\"\n        ]\n        | ImplicitDefault\n    ) = ImplicitDefault(\"subtitles\"),\n    src: str | None,\n    srclang: str | None = None,\n    label: str | None = None,\n    default: bool = False,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines text tracks for media elements (video and audio)\"\"\"\n    return _construct_node(\n        \"track\",\n        attributes={\n            \"kind\": kind,\n            \"src\": src,\n            \"srclang\": srclang,\n            \"label\": label,\n            \"default\": default,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef u(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines text with an unarticulated, non-textual annotation (typically underlined)\"\"\"\n    return _construct_node(\n        \"u\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef ul(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines an unordered (bulleted) list\"\"\"\n    return _construct_node(\n        \"ul\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef var(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a variable in programming or mathematical contexts\"\"\"\n    return _construct_node(\n        \"var\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef video(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    src: str | None = None,\n    controls: bool = False,\n    width: int | None = None,\n    height: int | None = None,\n    autoplay: bool = False,\n    loop: bool = False,\n    muted: bool = False,\n    preload: Literal[\"auto\", \"metadata\", \"none\"]\n    | ImplicitDefault = ImplicitDefault(\"auto\"),\n    poster: str | None = None,\n    playsinline: bool = False,\n    crossorigin: Literal[\"anonymous\", \"use-credentials\"] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Embeds video content in the document\"\"\"\n    return _construct_node(\n        \"video\",\n        child_text=child_text,\n        attributes={\n            \"src\": src,\n            \"controls\": controls,\n            \"width\": width,\n            \"height\": height,\n            \"autoplay\": autoplay,\n            \"loop\": loop,\n            \"muted\": muted,\n            \"preload\": preload,\n            \"poster\": poster,\n            \"playsinline\": playsinline,\n            \"crossorigin\": crossorigin,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef wbr(\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a possible line-break opportunity in text\"\"\"\n    return _construct_node(\n        \"wbr\",\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef area(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    alt: str | None,\n    coords: str | None = None,\n    shape: (\n        Literal[\"default\", \"rect\", \"circle\", \"poly\"] | ImplicitDefault\n    ) = ImplicitDefault(\"rect\"),\n    href: str | None = None,\n    target: Literal[\"_blank\", \"_self\", \"_parent\", \"_top\"] | None = None,\n    download: str | None = None,\n    rel: str | None = None,\n    referrerpolicy: (\n        Literal[\n            \"no-referrer\",\n            \"no-referrer-when-downgrade\",\n            \"origin\",\n            \"origin-when-cross-origin\",\n            \"same-origin\",\n            \"strict-origin\",\n            \"strict-origin-when-cross-origin\",\n            \"unsafe-url\",\n        ]\n        | None\n    ) = None,\n    ping: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a clickable area inside an image map\"\"\"\n    return _construct_node(\n        \"area\",\n        child_text=child_text,\n        attributes={\n            \"alt\": alt,\n            \"coords\": coords,\n            \"shape\": shape,\n            \"href\": href,\n            \"target\": target,\n            \"download\": download,\n            \"rel\": rel,\n            \"referrerpolicy\": referrerpolicy,\n            \"ping\": ping,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef article(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines independent, self-contained content that could be distributed independently\"\"\"\n    return _construct_node(\n        \"article\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef aside(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines content aside from the main content (like a sidebar)\"\"\"\n    return _construct_node(\n        \"aside\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef audio(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    src: str | None = None,\n    controls: bool = False,\n    autoplay: bool = False,\n    loop: bool = False,\n    muted: bool = False,\n    preload: Literal[\"auto\", \"metadata\", \"none\"]\n    | ImplicitDefault = ImplicitDefault(\"auto\"),\n    crossorigin: Literal[\"anonymous\", \"use-credentials\"] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Embeds sound content in documents\"\"\"\n    return _construct_node(\n        \"audio\",\n        child_text=child_text,\n        attributes={\n            \"src\": src,\n            \"controls\": controls,\n            \"autoplay\": autoplay,\n            \"loop\": loop,\n            \"muted\": muted,\n            \"preload\": preload,\n            \"crossorigin\": crossorigin,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef b(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines bold text without extra importance (use <strong> for importance)\"\"\"\n    return _construct_node(\n        \"b\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef base(\n    *,\n    data: dict[str, str] | None = None,\n    href: str | None = None,\n    target: Literal[\"_blank\", \"_self\", \"_parent\", \"_top\"] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Specifies the base URL and/or target for all relative URLs in a document\"\"\"\n    return _construct_node(\n        \"base\",\n        attributes={\"href\": href, \"target\": target},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef bdi(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Isolates text that might be formatted in a different direction from other text\"\"\"\n    return _construct_node(\n        \"bdi\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef bdo(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Overrides the current text direction\"\"\"\n    return _construct_node(\n        \"bdo\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef blockquote(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    cite: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a section that is quoted from another source\"\"\"\n    return _construct_node(\n        \"blockquote\",\n        child_text=child_text,\n        attributes={\"cite\": cite},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef body(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines the document's body, containing all visible contents\"\"\"\n    return _construct_node(\n        \"body\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef br(\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Inserts a single line break\"\"\"\n    return _construct_node(\n        \"br\",\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef button(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    type: Literal[\"button\", \"submit\", \"reset\"]\n    | ImplicitDefault = ImplicitDefault(\"submit\"),\n    name: str | None = None,\n    value: str | None = None,\n    disabled: bool = False,\n    form: str | None = None,\n    formaction: str | None = None,\n    formenctype: (\n        Literal[\n            \"application/x-www-form-urlencoded\",\n            \"multipart/form-data\",\n            \"text/plain\",\n        ]\n        | None\n    ) = None,\n    formmethod: Literal[\"get\", \"post\"] | None = None,\n    formnovalidate: bool = False,\n    formtarget: Literal[\"_blank\", \"_self\", \"_parent\", \"_top\"] | None = None,\n    autofocus: bool = False,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a clickable button\"\"\"\n    return _construct_node(\n        \"button\",\n        child_text=child_text,\n        attributes={\n            \"type\": type,\n            \"name\": name,\n            \"value\": value,\n            \"disabled\": disabled,\n            \"form\": form,\n            \"formaction\": formaction,\n            \"formenctype\": formenctype,\n            \"formmethod\": formmethod,\n            \"formnovalidate\": formnovalidate,\n            \"formtarget\": formtarget,\n            \"autofocus\": autofocus,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef canvas(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    width: int | ImplicitDefault = ImplicitDefault(300),\n    height: int | ImplicitDefault = ImplicitDefault(150),\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Provides a container for graphics that can be drawn using JavaScript\"\"\"\n    return _construct_node(\n        \"canvas\",\n        child_text=child_text,\n        attributes={\"width\": width, \"height\": height},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef caption(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a table caption\"\"\"\n    return _construct_node(\n        \"caption\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef cite(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines the title of a creative work (book, movie, song, etc.)\"\"\"\n    return _construct_node(\n        \"cite\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef code(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a piece of computer code\"\"\"\n    return _construct_node(\n        \"code\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef col(\n    *,\n    data: dict[str, str] | None = None,\n    span: int | ImplicitDefault = ImplicitDefault(1),\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Specifies column properties for each column within a <colgroup> element\"\"\"\n    return _construct_node(\n        \"col\",\n        attributes={\"span\": span},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef colgroup(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    span: int | ImplicitDefault = ImplicitDefault(1),\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Specifies a group of one or more columns in a table for formatting\"\"\"\n    return _construct_node(\n        \"colgroup\",\n        child_text=child_text,\n        attributes={\"span\": span},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef data(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    value: str | None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Links content with a machine-readable translation\"\"\"\n    return _construct_node(\n        \"data\",\n        child_text=child_text,\n        attributes={\"value\": value},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef datalist(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Contains a set of <option> elements that represent predefined options for input controls\"\"\"\n    return _construct_node(\n        \"datalist\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef dd(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a description/value of a term in a description list\"\"\"\n    return _construct_node(\n        \"dd\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef del_(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    cite: str | None = None,\n    datetime: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines text that has been deleted from a document\"\"\"\n    return _construct_node(\n        \"del\",\n        child_text=child_text,\n        attributes={\"cite\": cite, \"datetime\": datetime},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef details(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    open: bool = False,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines additional details that the user can view or hide\"\"\"\n    return _construct_node(\n        \"details\",\n        child_text=child_text,\n        attributes={\"open\": open},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef dfn(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Represents the defining instance of a term\"\"\"\n    return _construct_node(\n        \"dfn\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef dialog(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    open: bool = False,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a dialog box or window\"\"\"\n    return _construct_node(\n        \"dialog\",\n        child_text=child_text,\n        attributes={\"open\": open},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef div(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a generic container for flow content with no semantic meaning\"\"\"\n    return _construct_node(\n        \"div\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef dl(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a description list\"\"\"\n    return _construct_node(\n        \"dl\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef dt(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a term/name in a description list\"\"\"\n    return _construct_node(\n        \"dt\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef em(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines emphasized text (typically displayed in italic)\"\"\"\n    return _construct_node(\n        \"em\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef embed(\n    *,\n    data: dict[str, str] | None = None,\n    src: str | None,\n    type: str | None = None,\n    width: int | None = None,\n    height: int | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Embeds external content at the specified point in the document\"\"\"\n    return _construct_node(\n        \"embed\",\n        attributes={\n            \"src\": src,\n            \"type\": type,\n            \"width\": width,\n            \"height\": height,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef fieldset(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    disabled: bool = False,\n    form: str | None = None,\n    name: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Groups related elements in a form and draws a box around them\"\"\"\n    return _construct_node(\n        \"fieldset\",\n        child_text=child_text,\n        attributes={\"disabled\": disabled, \"form\": form, \"name\": name},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef figcaption(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a caption for a <figure> element\"\"\"\n    return _construct_node(\n        \"figcaption\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef figure(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Specifies self-contained content, like illustrations, diagrams, photos, code listings, etc.\"\"\"\n    return _construct_node(\n        \"figure\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef footer(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a footer for a document or section\"\"\"\n    return _construct_node(\n        \"footer\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef form(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    action: str | None = None,\n    method: Literal[\"get\", \"post\", \"dialog\"]\n    | ImplicitDefault = ImplicitDefault(\"get\"),\n    enctype: (\n        Literal[\n            \"application/x-www-form-urlencoded\",\n            \"multipart/form-data\",\n            \"text/plain\",\n        ]\n        | ImplicitDefault\n    ) = ImplicitDefault(\"application/x-www-form-urlencoded\"),\n    name: str | None = None,\n    target: Literal[\"_blank\", \"_self\", \"_parent\", \"_top\"] | None = None,\n    autocomplete: Literal[\"on\", \"off\"] | ImplicitDefault = ImplicitDefault(\n        \"on\"\n    ),\n    novalidate: bool = False,\n    accept_charset: str | None = None,\n    rel: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Creates an HTML form for user input\"\"\"\n    return _construct_node(\n        \"form\",\n        child_text=child_text,\n        attributes={\n            \"action\": action,\n            \"method\": method,\n            \"enctype\": enctype,\n            \"name\": name,\n            \"target\": target,\n            \"autocomplete\": autocomplete,\n            \"novalidate\": novalidate,\n            \"accept-charset\": accept_charset,\n            \"rel\": rel,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef h1(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines the most important heading (level 1)\"\"\"\n    return _construct_node(\n        \"h1\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef h2(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a level 2 heading\"\"\"\n    return _construct_node(\n        \"h2\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef h3(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a level 3 heading\"\"\"\n    return _construct_node(\n        \"h3\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef h4(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a level 4 heading\"\"\"\n    return _construct_node(\n        \"h4\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef h5(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a level 5 heading\"\"\"\n    return _construct_node(\n        \"h5\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef h6(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines the least important heading (level 6)\"\"\"\n    return _construct_node(\n        \"h6\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef head(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Contains metadata and information about the document\"\"\"\n    return _construct_node(\n        \"head\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef header(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a header for a document or section, typically containing introductory content\"\"\"\n    return _construct_node(\n        \"header\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef hgroup(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Groups a set of h1-h6 elements when a heading has multiple levels\"\"\"\n    return _construct_node(\n        \"hgroup\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef hr(\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a thematic break or horizontal rule in content\"\"\"\n    return _construct_node(\n        \"hr\",\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef html(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    xmlns: str = ImplicitDefault(\"http://www.w3.org/1999/xhtml\"),\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Represents the root element of an HTML document\"\"\"\n    return _construct_node(\n        \"html\",\n        child_text=child_text,\n        attributes={\"xmlns\": xmlns},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef i(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines italic text, typically used for technical terms, foreign phrases, etc.\"\"\"\n    return _construct_node(\n        \"i\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef iframe(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    src: str | None = None,\n    srcdoc: str | None = None,\n    name: str | None = None,\n    sandbox: str | None = None,\n    allow: str | None = None,\n    allowfullscreen: bool = False,\n    width: int | None = None,\n    height: int | None = None,\n    referrerpolicy: (\n        Literal[\n            \"no-referrer\",\n            \"no-referrer-when-downgrade\",\n            \"origin\",\n            \"origin-when-cross-origin\",\n            \"same-origin\",\n            \"strict-origin\",\n            \"strict-origin-when-cross-origin\",\n            \"unsafe-url\",\n        ]\n        | None\n    ) = None,\n    loading: Literal[\"eager\", \"lazy\"] | ImplicitDefault = ImplicitDefault(\n        \"eager\"\n    ),\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Embeds another HTML page within the current page\"\"\"\n    return _construct_node(\n        \"iframe\",\n        child_text=child_text,\n        attributes={\n            \"src\": src,\n            \"srcdoc\": srcdoc,\n            \"name\": name,\n            \"sandbox\": sandbox,\n            \"allow\": allow,\n            \"allowfullscreen\": allowfullscreen,\n            \"width\": width,\n            \"height\": height,\n            \"referrerpolicy\": referrerpolicy,\n            \"loading\": loading,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef img(\n    *,\n    data: dict[str, str] | None = None,\n    src: str | None,\n    alt: str | None,\n    width: int | None = None,\n    height: int | None = None,\n    srcset: str | None = None,\n    sizes: str | None = None,\n    crossorigin: Literal[\"anonymous\", \"use-credentials\"] | None = None,\n    usemap: str | None = None,\n    ismap: bool = False,\n    loading: Literal[\"eager\", \"lazy\"] | ImplicitDefault = ImplicitDefault(\n        \"eager\"\n    ),\n    decoding: Literal[\"sync\", \"async\", \"auto\"]\n    | ImplicitDefault = ImplicitDefault(\"auto\"),\n    referrerpolicy: (\n        Literal[\n            \"no-referrer\",\n            \"no-referrer-when-downgrade\",\n            \"origin\",\n            \"origin-when-cross-origin\",\n            \"same-origin\",\n            \"strict-origin\",\n            \"strict-origin-when-cross-origin\",\n            \"unsafe-url\",\n        ]\n        | None\n    ) = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Embeds an image in the document\"\"\"\n    return _construct_node(\n        \"img\",\n        attributes={\n            \"src\": src,\n            \"alt\": alt,\n            \"width\": width,\n            \"height\": height,\n            \"srcset\": srcset,\n            \"sizes\": sizes,\n            \"crossorigin\": crossorigin,\n            \"usemap\": usemap,\n            \"ismap\": ismap,\n            \"loading\": loading,\n            \"decoding\": decoding,\n            \"referrerpolicy\": referrerpolicy,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef input(\n    *,\n    data: dict[str, str] | None = None,\n    type: (\n        Literal[\n            \"button\",\n            \"checkbox\",\n            \"color\",\n            \"date\",\n            \"datetime-local\",\n            \"email\",\n            \"file\",\n            \"hidden\",\n            \"image\",\n            \"month\",\n            \"number\",\n            \"password\",\n            \"radio\",\n            \"range\",\n            \"reset\",\n            \"search\",\n            \"submit\",\n            \"tel\",\n            \"text\",\n            \"time\",\n            \"url\",\n            \"week\",\n        ]\n        | ImplicitDefault\n    ) = ImplicitDefault(\"text\"),\n    name: str | None = None,\n    value: str | None = None,\n    placeholder: str | None = None,\n    required: bool = False,\n    readonly: bool = False,\n    disabled: bool = False,\n    checked: bool = False,\n    autocomplete: (\n        Literal[\n            \"on\",\n            \"off\",\n            \"name\",\n            \"email\",\n            \"username\",\n            \"new-password\",\n            \"current-password\",\n            \"tel\",\n            \"url\",\n            \"street-address\",\n            \"postal-code\",\n            \"cc-number\",\n        ]\n        | None\n    ) = None,\n    autofocus: bool = False,\n    min: str | None = None,\n    max: str | None = None,\n    step: str | None = None,\n    minlength: int | None = None,\n    maxlength: int | None = None,\n    pattern: str | None = None,\n    size: int | None = None,\n    multiple: bool = False,\n    accept: str | None = None,\n    src: str | None = None,\n    alt: str | None = None,\n    width: int | None = None,\n    height: int | None = None,\n    list: str | None = None,\n    form: str | None = None,\n    formaction: str | None = None,\n    formenctype: (\n        Literal[\n            \"application/x-www-form-urlencoded\",\n            \"multipart/form-data\",\n            \"text/plain\",\n        ]\n        | None\n    ) = None,\n    formmethod: Literal[\"get\", \"post\"] | None = None,\n    formnovalidate: bool = False,\n    formtarget: Literal[\"_blank\", \"_self\", \"_parent\", \"_top\"] | None = None,\n    capture: Literal[\"user\", \"environment\"] | None = None,\n    dirname: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Creates an interactive control for user input within a form\"\"\"\n    return _construct_node(\n        \"input\",\n        attributes={\n            \"type\": type,\n            \"name\": name,\n            \"value\": value,\n            \"placeholder\": placeholder,\n            \"required\": required,\n            \"readonly\": readonly,\n            \"disabled\": disabled,\n            \"checked\": checked,\n            \"autocomplete\": autocomplete,\n            \"autofocus\": autofocus,\n            \"min\": min,\n            \"max\": max,\n            \"step\": step,\n            \"minlength\": minlength,\n            \"maxlength\": maxlength,\n            \"pattern\": pattern,\n            \"size\": size,\n            \"multiple\": multiple,\n            \"accept\": accept,\n            \"src\": src,\n            \"alt\": alt,\n            \"width\": width,\n            \"height\": height,\n            \"list\": list,\n            \"form\": form,\n            \"formaction\": formaction,\n            \"formenctype\": formenctype,\n            \"formmethod\": formmethod,\n            \"formnovalidate\": formnovalidate,\n            \"formtarget\": formtarget,\n            \"capture\": capture,\n            \"dirname\": dirname,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef ins(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    cite: str | None = None,\n    datetime: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines text that has been inserted into a document\"\"\"\n    return _construct_node(\n        \"ins\",\n        child_text=child_text,\n        attributes={\"cite\": cite, \"datetime\": datetime},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef kbd(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines keyboard input\"\"\"\n    return _construct_node(\n        \"kbd\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef label(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    for_: str | None = None,\n    form: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a label for an input element\"\"\"\n    return _construct_node(\n        \"label\",\n        child_text=child_text,\n        attributes={\"for\": for_, \"form\": form},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef legend(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a caption for a fieldset element\"\"\"\n    return _construct_node(\n        \"legend\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef li(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    value: int | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a list item\"\"\"\n    return _construct_node(\n        \"li\",\n        child_text=child_text,\n        attributes={\"value\": value},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef link(\n    *,\n    data: dict[str, str] | None = None,\n    href: str | None,\n    rel: str | None,\n    type: str | None = None,\n    media: str | None = None,\n    hreflang: str | None = None,\n    sizes: str | None = None,\n    crossorigin: Literal[\"anonymous\", \"use-credentials\"] | None = None,\n    referrerpolicy: (\n        Literal[\n            \"no-referrer\",\n            \"no-referrer-when-downgrade\",\n            \"origin\",\n            \"origin-when-cross-origin\",\n            \"same-origin\",\n            \"strict-origin\",\n            \"strict-origin-when-cross-origin\",\n            \"unsafe-url\",\n        ]\n        | None\n    ) = None,\n    integrity: str | None = None,\n    as_: (\n        Literal[\n            \"audio\",\n            \"document\",\n            \"embed\",\n            \"fetch\",\n            \"font\",\n            \"image\",\n            \"object\",\n            \"script\",\n            \"style\",\n            \"track\",\n            \"video\",\n            \"worker\",\n        ]\n        | None\n    ) = None,\n    disabled: bool = False,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines the relationship between the current document and an external resource\"\"\"\n    return _construct_node(\n        \"link\",\n        attributes={\n            \"href\": href,\n            \"rel\": rel,\n            \"type\": type,\n            \"media\": media,\n            \"hreflang\": hreflang,\n            \"sizes\": sizes,\n            \"crossorigin\": crossorigin,\n            \"referrerpolicy\": referrerpolicy,\n            \"integrity\": integrity,\n            \"as\": as_,\n            \"disabled\": disabled,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef main(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Specifies the main content of the document\"\"\"\n    return _construct_node(\n        \"main\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef map(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    name: str | None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a client-side image map\"\"\"\n    return _construct_node(\n        \"map\",\n        child_text=child_text,\n        attributes={\"name\": name},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef mark(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines marked/highlighted text\"\"\"\n    return _construct_node(\n        \"mark\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef meta(\n    *,\n    data: dict[str, str] | None = None,\n    name: str | None = None,\n    content: str | None = None,\n    charset: str | None = None,\n    http_equiv: (\n        Literal[\n            \"content-security-policy\",\n            \"content-type\",\n            \"default-style\",\n            \"refresh\",\n        ]\n        | None\n    ) = None,\n    property: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Provides metadata about the HTML document\"\"\"\n    return _construct_node(\n        \"meta\",\n        attributes={\n            \"name\": name,\n            \"content\": content,\n            \"charset\": charset,\n            \"http-equiv\": http_equiv,\n            \"property\": property,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef meter(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    value: int | None,\n    min: int = 0,\n    max: int = 1,\n    low: int | None = None,\n    high: int | None = None,\n    optimum: int | None = None,\n    form: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a scalar measurement within a known range (a gauge)\"\"\"\n    return _construct_node(\n        \"meter\",\n        child_text=child_text,\n        attributes={\n            \"value\": value,\n            \"min\": min,\n            \"max\": max,\n            \"low\": low,\n            \"high\": high,\n            \"optimum\": optimum,\n            \"form\": form,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef nav(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a section of navigation links\"\"\"\n    return _construct_node(\n        \"nav\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef noscript(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines alternate content for users that have disabled scripts or don't support scripting\"\"\"\n    return _construct_node(\n        \"noscript\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef object(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    data_: str | None = None,\n    type: str | None = None,\n    name: str | None = None,\n    usemap: str | None = None,\n    form: str | None = None,\n    width: int | None = None,\n    height: int | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Embeds an external resource such as an image, video, audio, PDF, or Flash\"\"\"\n    return _construct_node(\n        \"object\",\n        child_text=child_text,\n        attributes={\n            \"data\": data_,\n            \"type\": type,\n            \"name\": name,\n            \"usemap\": usemap,\n            \"form\": form,\n            \"width\": width,\n            \"height\": height,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef ol(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    reversed: bool = False,\n    start: int | None = None,\n    type: Literal[\"1\", \"A\", \"a\", \"I\", \"i\"] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines an ordered (numbered) list\"\"\"\n    return _construct_node(\n        \"ol\",\n        child_text=child_text,\n        attributes={\"reversed\": reversed, \"start\": start, \"type\": type},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef optgroup(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    label: str | None,\n    disabled: bool = False,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Groups related options in a drop-down list\"\"\"\n    return _construct_node(\n        \"optgroup\",\n        child_text=child_text,\n        attributes={\"label\": label, \"disabled\": disabled},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef option(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    value: str | None = None,\n    label: str | None = None,\n    selected: bool = False,\n    disabled: bool = False,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines an option in a drop-down list\"\"\"\n    return _construct_node(\n        \"option\",\n        child_text=child_text,\n        attributes={\n            \"value\": value,\n            \"label\": label,\n            \"selected\": selected,\n            \"disabled\": disabled,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef output(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    for_: str | None = None,\n    form: str | None = None,\n    name: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Represents the result of a calculation or user action\"\"\"\n    return _construct_node(\n        \"output\",\n        child_text=child_text,\n        attributes={\"for\": for_, \"form\": form, \"name\": name},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef p(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a paragraph\"\"\"\n    return _construct_node(\n        \"p\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef param(\n    *,\n    data: dict[str, str] | None = None,\n    name: str | None,\n    value: str | None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines parameters for an object element\"\"\"\n    return _construct_node(\n        \"param\",\n        attributes={\"name\": name, \"value\": value},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef picture(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Contains multiple image sources, allowing for different images in different scenarios\"\"\"\n    return _construct_node(\n        \"picture\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef pre(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines preformatted text that preserves spaces and line breaks\"\"\"\n    return _construct_node(\n        \"pre\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef progress(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    value: int | None = None,\n    max: int | ImplicitDefault = ImplicitDefault(1),\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Represents the progress of a task\"\"\"\n    return _construct_node(\n        \"progress\",\n        child_text=child_text,\n        attributes={\"value\": value, \"max\": max},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef q(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    cite: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a short inline quotation\"\"\"\n    return _construct_node(\n        \"q\",\n        child_text=child_text,\n        attributes={\"cite\": cite},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef rp(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines what to show in browsers that do not support ruby annotations\"\"\"\n    return _construct_node(\n        \"rp\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef rt(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines an explanation/pronunciation of characters (for East Asian typography)\"\"\"\n    return _construct_node(\n        \"rt\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef ruby(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a ruby annotation (for East Asian typography)\"\"\"\n    return _construct_node(\n        \"ruby\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef s(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines text that is no longer correct or relevant (strikethrough)\"\"\"\n    return _construct_node(\n        \"s\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef samp(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines sample output from a computer program\"\"\"\n    return _construct_node(\n        \"samp\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef script(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    src: str | None = None,\n    type: str = ImplicitDefault(\"text/javascript\"),\n    async_: bool = False,\n    defer: bool = False,\n    crossorigin: Literal[\"anonymous\", \"use-credentials\"] | None = None,\n    integrity: str | None = None,\n    referrerpolicy: (\n        Literal[\n            \"no-referrer\",\n            \"no-referrer-when-downgrade\",\n            \"origin\",\n            \"origin-when-cross-origin\",\n            \"same-origin\",\n            \"strict-origin\",\n            \"strict-origin-when-cross-origin\",\n            \"unsafe-url\",\n        ]\n        | None\n    ) = None,\n    nomodule: bool = False,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Embeds or references executable code, typically JavaScript\"\"\"\n    return _construct_node(\n        \"script\",\n        child_text=child_text,\n        attributes={\n            \"src\": src,\n            \"type\": type,\n            \"async\": async_,\n            \"defer\": defer,\n            \"crossorigin\": crossorigin,\n            \"integrity\": integrity,\n            \"referrerpolicy\": referrerpolicy,\n            \"nomodule\": nomodule,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef section(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a thematic grouping of content, typically with a heading\"\"\"\n    return _construct_node(\n        \"section\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef select(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    name: str | None = None,\n    multiple: bool = False,\n    required: bool = False,\n    disabled: bool = False,\n    size: int | None = None,\n    autofocus: bool = False,\n    form: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Creates a drop-down list\"\"\"\n    return _construct_node(\n        \"select\",\n        child_text=child_text,\n        attributes={\n            \"name\": name,\n            \"multiple\": multiple,\n            \"required\": required,\n            \"disabled\": disabled,\n            \"size\": size,\n            \"autofocus\": autofocus,\n            \"form\": form,\n        },\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef slot(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    name: str | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines a slot in a web component that can be filled with markup\"\"\"\n    return _construct_node(\n        \"slot\",\n        child_text=child_text,\n        attributes={\"name\": name},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\ndef small(\n    child_text: str = \"\",\n    /,\n    *,\n    data: dict[str, str] | None = None,\n    **global_attributes: Unpack[GlobalAttributes],\n) -> HTMLNode:\n    \"\"\"Defines smaller text (like copyright and other side-comments)\"\"\"\n    return _construct_node(\n        \"small\",\n        child_text=child_text,\n        attributes={},\n        global_attributes=global_attributes,\n        data=data or {},\n    )\n\n\nALL_PRIMITIVES: list[Callable[..., HTMLNode]] = [\n    a,\n    abbr,\n    address,\n    span,\n    strong,\n    style,\n    sub,\n    summary,\n    sup,\n    table,\n    tbody,\n    td,\n    template,\n    textarea,\n    tfoot,\n    th,\n    thead,\n    time,\n    title,\n    tr,\n    track,\n    u,\n    ul,\n    var,\n    video,\n    wbr,\n    area,\n    article,\n    aside,\n    audio,\n    b,\n    base,\n    bdi,\n    bdo,\n    blockquote,\n    body,\n    br,\n    button,\n    canvas,\n    caption,\n    cite,\n    code,\n    col,\n    colgroup,\n    data,\n    datalist,\n    dd,\n    del_,\n    details,\n    dfn,\n    dialog,\n    div,\n    dl,\n    dt,\n    em,\n    embed,\n    fieldset,\n    figcaption,\n    figure,\n    footer,\n    form,\n    h1,\n    h2,\n    h3,\n    h4,\n    h5,\n    h6,\n    head,\n    header,\n    hgroup,\n    hr,\n    html,\n    i,\n    iframe,\n    img,\n    input,\n    ins,\n    kbd,\n    label,\n    legend,\n    li,\n    link,\n    main,\n    map,\n    mark,\n    meta,\n    meter,\n    nav,\n    noscript,\n    object,\n    ol,\n    optgroup,\n    option,\n    output,\n    p,\n    param,\n    picture,\n    pre,\n    progress,\n    q,\n    rp,\n    rt,\n    ruby,\n    s,\n    samp,\n    script,\n    section,\n    select,\n    slot,\n    small,\n]\n"
  },
  {
    "path": "src/view/exceptions.py",
    "content": "\"\"\"\nCommon exceptions used throughout view.py.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\n__all__ = (\"ViewError\",)\n\n\nclass ViewError(Exception):\n    \"\"\"\n    Base class for all exceptions in view.py\n    \"\"\"\n\n    def __init__(self, *msg: str) -> None:\n        super().__init__(*msg)\n\n\nclass InvalidTypeError(ViewError, TypeError):\n    \"\"\"\n    Something got a type that it didn't expect. For example, passing a\n    :class:`str` object in a place where a :class:`bytes` object was\n    expected would raise this error.\n\n    In order to fix this, please review the documentation of the function\n    you're attempting to call and ensure that you are passing it the correct\n    types. view.py is completely type-safe, so if your editor/IDE is\n    complaining about something, it is very likely the culprit.\n    \"\"\"\n\n    def __init__(self, got: Any, *expected: type) -> None:\n        expected_string = \", \".join(\n            [exception.__name__ for exception in expected]\n        )\n        super().__init__(f\"Expected {expected_string}, but got {got!r}\")\n"
  },
  {
    "path": "src/view/javascript.py",
    "content": "\"\"\"\nUtilities for using JavaScript in view.py applications.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom io import StringIO\nfrom typing import TYPE_CHECKING, ParamSpec, Protocol, runtime_checkable\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, Iterator\n\nfrom view.exceptions import InvalidTypeError\n\n__all__ = (\n    \"SupportsJavaScript\",\n    \"as_javascript_expression\",\n    \"javascript_compiler\",\n)\n\nP = ParamSpec(\"P\")\n\n\n@runtime_checkable\nclass SupportsJavaScript(Protocol):\n    \"\"\"\n    Protocol for objects that want to allow use in :func:`as_javascript_expression`.\n    \"\"\"\n\n    def as_javascript(self) -> str:\n        \"\"\"\n        Convert this object into a single JavaScript expression.\n        \"\"\"\n        ...\n\n\ndef as_javascript_expression(data: object) -> str:\n    \"\"\"\n    Convert an object into a single JavaScript expression.\n    \"\"\"\n\n    if isinstance(data, str):\n        return repr(data)\n\n    if isinstance(data, int):\n        return str(data)\n\n    if isinstance(data, bool):\n        if data is True:\n            return \"true\"\n\n        assert data is False\n        return \"false\"\n\n    if isinstance(data, dict):\n        result = StringIO()\n        result.write(\"{\")\n        for key, value in data.items():\n            key_expression = as_javascript_expression(key)\n            value_expression = as_javascript_expression(value)\n            result.write(f\"{key_expression}: {value_expression},\")\n        result.write(\"}\")\n        return result.getvalue()\n\n    if isinstance(data, SupportsJavaScript):\n        result = data.as_javascript()\n        if __debug__ and not isinstance(result, str):\n            raise InvalidTypeError(result, str)\n\n        return result\n\n    raise TypeError(\n        f\"Don't know how to convert {data!r} to a JavaScript expression\"\n    )\n\n\ndef javascript_compiler(\n    function: Callable[P, Iterator[str]],\n) -> Callable[P, str]:\n    \"\"\"\n    Decorator that converts a function yielding lines of JavaScript code into\n    a function that returns the entire source code.\n    \"\"\"\n\n    def decorator(*args: P.args, **kwargs: P.kwargs) -> str:\n        buffer = StringIO()\n\n        for line in function(*args, **kwargs):\n            if __debug__ and not isinstance(line, str):\n                raise InvalidTypeError(line, str)\n            buffer.write(f\"{line};\\n\")\n\n        return buffer.getvalue()\n\n    return decorator\n"
  },
  {
    "path": "src/view/responses.py",
    "content": "\"\"\"\nCommon response types.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport mimetypes\nimport sys\nfrom os import PathLike\nfrom typing import TYPE_CHECKING, Any, TypeAlias\n\nif TYPE_CHECKING:\n    from collections.abc import AsyncGenerator, AsyncIterator, Callable\n\nimport asyncio\nimport json\nfrom dataclasses import dataclass\n\nfrom view.core.headers import HeadersLike, LowerStr, as_real_headers\nfrom view.core.response import Response\nfrom view.core.response import TextResponse as TextResponse  # noqa: PLC0414\nfrom view.exceptions import InvalidTypeError\n\n__all__ = \"FileResponse\", \"JSONResponse\", \"TextResponse\"\n\nStrPath: TypeAlias = str | PathLike[str]\n\n\ndef _guess_file_type(path: StrPath, /) -> str:\n    if sys.version_info >= (3, 13):\n        return mimetypes.guess_file_type(path)[0] or \"text/plain\"\n\n    return mimetypes.guess_type(path)[0] or \"text/plain\"\n\n\nasync def _read_stream(\n    path: StrPath, *, chunk_size: int\n) -> AsyncIterator[bytes]:\n    file = await asyncio.to_thread(open, path, \"rb\")\n    length = chunk_size\n    while length == chunk_size:\n        data = await asyncio.to_thread(file.read, chunk_size)\n        length = len(data)\n        yield data\n\n\n@dataclass(slots=True)\nclass FileResponse(Response):\n    \"\"\"\n    Response containing a file, streamed asynchronously.\n    \"\"\"\n\n    path: StrPath\n\n    @classmethod\n    def from_file(\n        cls,\n        path: StrPath,\n        /,\n        *,\n        status_code: int = 200,\n        headers: HeadersLike | None = None,\n        chunk_size: int = 512,  # This probably needs tuning\n        content_type: str | None = None,\n    ) -> FileResponse:\n        \"\"\"\n        Generate a :class:`FileResponse` from a file path.\n        \"\"\"\n        if __debug__ and not isinstance(chunk_size, int):\n            raise InvalidTypeError(chunk_size, int)\n\n        multi_map = as_real_headers(headers)\n        if \"content-type\" not in multi_map:\n            content_type = content_type or _guess_file_type(path)\n            multi_map = multi_map.with_new_value(\n                LowerStr(\"content-type\"), content_type\n            )\n\n        return cls(\n            _read_stream(path, chunk_size=chunk_size),\n            status_code,\n            multi_map,\n            path,\n        )\n\n\n@dataclass(slots=True)\nclass JSONResponse(Response):\n    \"\"\"\n    Response containing JSON data.\n    \"\"\"\n\n    content: dict[str, Any]\n    parsed_data: str\n\n    @classmethod\n    def from_content(\n        cls,\n        content: dict[str, Any],\n        *,\n        parse_function: Callable[[dict[str, Any]], str] = json.dumps,\n        status_code: int = 200,\n        headers: HeadersLike | None = None,\n    ) -> JSONResponse:\n        data = parse_function(content)\n\n        async def stream() -> AsyncGenerator[bytes]:\n            yield data.encode(\"utf-8\")\n\n        return cls(\n            content=content,\n            parsed_data=data,\n            headers=as_real_headers(headers),\n            status_code=status_code,\n            receive_data=stream(),\n        )\n"
  },
  {
    "path": "src/view/run/__init__.py",
    "content": "\"\"\"\nUtilities for running view.py web applications.\n\"\"\"\n\nfrom view.run import asgi as asgi\nfrom view.run import servers as servers\nfrom view.run import wsgi as wsgi\n"
  },
  {
    "path": "src/view/run/asgi.py",
    "content": "\"\"\"\nImplementation and utilities for running view.py applications on an ASGI server.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import AsyncIterator, Awaitable, Callable, Iterable\nfrom typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypedDict\n\nfrom typing_extensions import NotRequired\n\nfrom view.core.headers import asgi_to_headers, headers_to_asgi\nfrom view.core.request import Method, Request, extract_query_parameters\n\nif TYPE_CHECKING:\n    from view.core.app import BaseApp\n\n__all__ = (\"asgi_for_app\",)\n\n\nclass ASGIScopeData(TypedDict):\n    version: str\n    spec_version: NotRequired[str]\n\n\nASGIHeaders: TypeAlias = Iterable[tuple[bytes, bytes]]\n\n\nclass ASGIHttpScope(TypedDict):\n    type: Literal[\"http\"]\n    asgi: ASGIScopeData\n    http_version: str\n    method: str\n    scheme: str\n    path: str\n    raw_path: bytes\n    query_string: bytes\n    root_path: str\n    headers: ASGIHeaders\n    client: Iterable[tuple[str, int]] | None\n    server: Iterable[tuple[str, int | None]] | None\n    state: NotRequired[dict[str, Any] | None]\n\n\nclass ASGIBodyMixin(TypedDict):\n    body: NotRequired[bytes]\n    more_body: NotRequired[bool]\n\n\nclass ASGIHttpReceiveResult(ASGIBodyMixin, TypedDict):\n    type: Literal[\"http.request\"]\n\n\nclass ASGIHttpSendStart(TypedDict):\n    type: Literal[\"http.response.start\"]\n    status: int\n    headers: ASGIHeaders\n    trailers: NotRequired[bool]\n\n\nclass ASGIHttpSendBody(ASGIBodyMixin, TypedDict):\n    type: Literal[\"http.response.body\"]\n\n\nASGIHttpReceive: TypeAlias = Callable[[], Awaitable[ASGIHttpReceiveResult]]\nASGIHttpSend: TypeAlias = Callable[\n    [ASGIHttpSendStart | ASGIHttpSendBody], Awaitable[None]\n]\nASGIProtocol: TypeAlias = Callable[\n    [ASGIHttpScope, ASGIHttpReceive, ASGIHttpSend], Awaitable[None]\n]\n\n\ndef asgi_for_app(app: BaseApp, /) -> ASGIProtocol:\n    \"\"\"\n    Generate an ASGI-compliant callable for a given app, allowing\n    it to be executed in an ASGI server.\n\n    Don't use this directly; prefer the :meth:`view.core.app.BaseApp.wsgi`\n    method instead.\n    \"\"\"\n\n    async def asgi(\n        scope: ASGIHttpScope, receive: ASGIHttpReceive, send: ASGIHttpSend\n    ) -> None:\n        assert scope[\"type\"] == \"http\"\n        method = Method(scope[\"method\"])\n        headers = asgi_to_headers(scope[\"headers\"])\n\n        async def receive_data() -> AsyncIterator[bytes]:\n            more_body = True\n            while more_body:\n                data = await receive()\n                assert data[\"type\"] == \"http.request\"\n                yield data.get(\"body\", b\"\")\n                more_body = data.get(\"more_body\", False)\n\n        parameters = extract_query_parameters(scope[\"query_string\"])\n        request = Request(\n            receive_data(), app, scope[\"path\"], method, headers, parameters\n        )\n\n        response = await app.process_request(request)\n        await send(\n            {\n                \"type\": \"http.response.start\",\n                \"status\": response.status_code,\n                \"headers\": headers_to_asgi(response.headers),\n            }\n        )\n        async for data in response.stream_body():\n            await send(\n                {\"type\": \"http.response.body\", \"body\": data, \"more_body\": True}\n            )\n\n        await send(\n            {\"type\": \"http.response.body\", \"body\": b\"\", \"more_body\": False}\n        )\n\n    return asgi\n"
  },
  {
    "path": "src/view/run/servers.py",
    "content": "\"\"\"\nMagically run applications on some common servers.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Callable, MutableMapping\nfrom contextlib import suppress\nfrom dataclasses import dataclass\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    NotRequired,\n    TypeAlias,\n    TypedDict,\n    Unpack,\n)\n\nif TYPE_CHECKING:\n    from view.core.app import BaseApp\n    from view.run.wsgi import WSGIProtocol\n\nfrom view.exceptions import ViewError\n\n__all__ = (\"run_app_on_any_server\",)\n\n\nclass BadServerError(ViewError):\n    \"\"\"\n    Something is wrong with the selected server.\n\n    This generally means that the target server isn't installed or doesn't\n    exist (either not supported by view.py or there's a typo present).\n    \"\"\"\n\n\nclass ServerConfigArgs(TypedDict):\n    host: NotRequired[str]\n    port: NotRequired[int]\n    production: NotRequired[bool]\n    server_hint: NotRequired[str]\n\n\n@dataclass(slots=True, frozen=True)\nclass ServerSettings:\n    host: str\n    port: int\n    production: bool\n\n    @classmethod\n    def from_kwargs(cls, kwargs: ServerConfigArgs, /) -> ServerSettings:\n        return cls(\n            kwargs.get(\"host\") or \"localhost\",\n            kwargs.get(\"port\") or 5000,\n            kwargs.get(\"production\") or False,\n        )\n\n\ndef run_uvicorn(app: BaseApp, settings: ServerSettings) -> None:\n    \"\"\"\n    Run the app using the ``uvicorn`` library.\n    \"\"\"\n    import uvicorn\n\n    uvicorn.run(app.asgi(), host=settings.host, port=settings.port)\n\n\ndef run_hypercorn(app: BaseApp, settings: ServerSettings) -> None:\n    \"\"\"\n    Run the app using the ``hypercorn`` library.\n    \"\"\"\n    import asyncio\n\n    import hypercorn\n    from hypercorn.asyncio import serve\n\n    config = hypercorn.Config()\n    config.bind = [f\"{settings.host}:{settings.port}\"]\n    asyncio.run(serve(app.asgi(), config))  # type: ignore\n\n\ndef run_daphne(app: BaseApp, settings: ServerSettings) -> None:\n    \"\"\"\n    Run the app using the ``daphne`` library.\n    \"\"\"\n    from daphne.endpoints import build_endpoint_description_strings\n    from daphne.server import Server\n\n    endpoints = build_endpoint_description_strings(\n        host=settings.host,\n        port=settings.port,\n    )\n    server = Server(app.asgi(), endpoints=endpoints)\n    server.run()\n\n\ndef run_gunicorn(app: BaseApp, settings: ServerSettings) -> None:\n    \"\"\"\n    Run the app using the ``gunicorn`` library.\n    \"\"\"\n    from gunicorn.app.base import BaseApplication\n\n    class GunicornRunner(BaseApplication):\n        def __init__(\n            self, app: WSGIProtocol, options: dict[str, Any] | None = None\n        ) -> None:\n            self.options = options or {}\n            self.application = app\n            super().__init__()\n\n        def load_config(self):\n            assert self.cfg is not None\n            for key, value in self.options.items():\n                if key in self.cfg.settings and value is not None:\n                    self.cfg.set(key, value)\n\n        def load(self):\n            return self.application\n\n    runner = GunicornRunner(\n        app.wsgi(), {\"bind\": f\"{settings.host}:{settings.port}\"}\n    )\n    runner.run()\n\n\ndef run_werkzeug(app: BaseApp, settings: ServerSettings) -> None:\n    \"\"\"\n    Run the app using the ``werkzeug`` library.\n    \"\"\"\n    from werkzeug.serving import run_simple\n\n    run_simple(settings.host, settings.port, app.wsgi())\n\n\ndef run_wsgiref(app: BaseApp, settings: ServerSettings) -> None:\n    \"\"\"\n    Run the app using the built-in :mod:`wsgiref` module.\n    \"\"\"\n    from wsgiref.simple_server import make_server\n\n    with make_server(settings.host, settings.port, app.wsgi()) as server:\n        server.serve_forever()\n\n\nStartServer: TypeAlias = Callable[[\"BaseApp\", ServerSettings], None]\n\nALL_SERVERS: MutableMapping[str, StartServer] = {\n    \"uvicorn\": run_uvicorn,\n    \"hypercorn\": run_hypercorn,\n    \"daphne\": run_daphne,\n    \"gunicorn\": run_gunicorn,\n    \"werkzeug\": run_werkzeug,\n    \"wsgiref\": run_wsgiref,\n}\n\n\ndef run_app_on_any_server(\n    app: BaseApp, **kwargs: Unpack[ServerConfigArgs]\n) -> None:\n    \"\"\"\n    Run the app on the nearest available ASGI or WSGI server.\n\n    This will always succeed, as it will fall back to the standard\n    :mod:`wsgiref` module if no other server is installed.\n    \"\"\"\n    settings = ServerSettings.from_kwargs(kwargs)\n    hint = kwargs.get(\"server_hint\")\n    if hint is not None:\n        try:\n            start_server = ALL_SERVERS[hint]\n        except KeyError as key_error:\n            raise BadServerError(\n                f\"{hint!r} is not a known server\"\n            ) from key_error\n\n        try:\n            return start_server(app, settings)\n        except ImportError as error:\n            raise BadServerError(f\"{hint} is not installed\") from error\n\n    # I'm not sure what Ruff is complaining about here\n    for start_server in ALL_SERVERS.values():\n        with suppress(ImportError):\n            return start_server(app, settings)\n"
  },
  {
    "path": "src/view/run/wsgi.py",
    "content": "\"\"\"\nImplementation and utilities for running view.py applications on an ASGI server.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc import Callable, Iterable\nfrom typing import IO, TYPE_CHECKING, Any, TypeAlias\n\nfrom view.core.headers import headers_to_wsgi, wsgi_to_headers\nfrom view.core.request import Method, Request, extract_query_parameters\nfrom view.core.status_codes import STATUS_STRINGS\n\nif TYPE_CHECKING:\n    from view.core.app import BaseApp\n\n__all__ = (\"wsgi_for_app\",)\n\nWSGIHeaders: TypeAlias = Iterable[tuple[str, str]]\n# We can't use a TypedDict for the environment because it has arbitrary keys\n# for the headers.\nWSGIEnvironment: TypeAlias = dict[str, Any]\nWSGIStartResponse = Callable[[str, WSGIHeaders], Callable[[bytes], object]]\nWSGIProtocol: TypeAlias = Callable[\n    [WSGIEnvironment, WSGIStartResponse], Iterable[bytes]\n]\n\n\ndef wsgi_for_app(\n    app: BaseApp,\n    /,\n    loop: asyncio.AbstractEventLoop | None = None,\n    chunk_size: int = 512,\n) -> WSGIProtocol:\n    \"\"\"\n    Generate a WSGI-compliant callable for a given app, allowing\n    it to be executed in an ASGI server.\n\n    Don't use this directly; prefer the :meth:`view.core.app.BaseApp.wsgi`\n    method instead.\n    \"\"\"\n    loop = loop or asyncio.new_event_loop()\n\n    def wsgi(\n        environ: WSGIEnvironment, start_response: WSGIStartResponse\n    ) -> Iterable[bytes]:\n        method = Method(environ[\"REQUEST_METHOD\"])\n\n        async def stream():\n            request_body: str | IO[bytes] = environ[\"wsgi.input\"]\n            assert isinstance(request_body, IO)\n            length = chunk_size\n\n            while length == chunk_size:\n                data = await asyncio.to_thread(request_body.read, chunk_size)\n                length = len(data)\n                yield data\n\n        path = environ[\"PATH_INFO\"]\n        assert isinstance(path, str)\n        headers = wsgi_to_headers(environ)\n        parameters = extract_query_parameters(environ[\"QUERY_STRING\"])\n        request = Request(stream(), app, path, method, headers, parameters)\n        response = loop.run_until_complete(app.process_request(request))\n\n        wsgi_headers: WSGIHeaders = headers_to_wsgi(response.headers)\n\n        # WSGI is such a weird spec\n        status_str = (\n            f\"{response.status_code} {STATUS_STRINGS[response.status_code]}\"\n        )\n        start_response(status_str, wsgi_headers)\n        return [loop.run_until_complete(response.body())]\n\n    return wsgi\n"
  },
  {
    "path": "src/view/testing.py",
    "content": "\"\"\"\nUtilities for testing a view.py application without the use of I/O.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom view.core.headers import HeadersLike, as_real_headers\nfrom view.core.request import Method, Request, extract_query_parameters\nfrom view.core.status_codes import STATUS_STRINGS\n\nif TYPE_CHECKING:\n    from collections.abc import AsyncGenerator, Awaitable\n\n    from view.core.app import BaseApp\n    from view.core.headers import HTTPHeaders\n    from view.core.response import Response\n\n__all__ = (\"AppTestClient\",)\n\n\nasync def into_tuple(\n    response_coro: Awaitable[Response], /\n) -> tuple[bytes, int, HTTPHeaders]:\n    \"\"\"\n    Convenience function for transferring a test client call into a tuple\n    through a single :keyword:`await`.\n    \"\"\"\n    response = await response_coro\n    body = await response.body()\n    return (body, response.status_code, response.headers)\n\n\ndef ok(body: str | bytes) -> tuple[bytes, int, dict[str, str]]:\n    \"\"\"\n    Utility function for an OK response from :func:`into_tuple`.\n    \"\"\"\n\n    if isinstance(body, str):\n        body = body.encode(\"utf-8\")\n    return (body, 200, {})\n\n\ndef bad(status_code: int) -> tuple[bytes, int, dict[str, str]]:\n    \"\"\"\n    Utility function for an error response from :func:`into_tuple`.\n    \"\"\"\n    body = STATUS_STRINGS[status_code]\n    return (f\"{status_code} {body}\".encode(), status_code, {})\n\n\nclass AppTestClient:\n    \"\"\"\n    Client to test an app.\n\n    This makes no actual HTTP requests, and instead should be used to\n    exercise correctness of responses.\n    \"\"\"\n\n    def __init__(self, app: BaseApp) -> None:\n        self.app = app\n\n    async def request(\n        self,\n        route: str,\n        *,\n        method: Method,\n        headers: HeadersLike | None = None,\n        body: bytes | None = None,\n    ) -> Response:\n        async def stream() -> AsyncGenerator[bytes]:\n            yield body or b\"\"\n\n        path, _, query_string = route.partition(\"?\")\n\n        request_data = Request(\n            receive_data=stream(),\n            app=self.app,\n            path=path,\n            method=method,\n            headers=as_real_headers(headers),\n            query_parameters=extract_query_parameters(query_string),\n        )\n        return await self.app.process_request(request_data)\n\n    async def get(\n        self,\n        route: str,\n        *,\n        headers: HeadersLike | None = None,\n        body: bytes | None = None,\n    ) -> Response:\n        return await self.request(\n            route, method=Method.GET, headers=headers, body=body\n        )\n\n    async def post(\n        self,\n        route: str,\n        *,\n        headers: HeadersLike | None = None,\n        body: bytes | None = None,\n    ) -> Response:\n        return await self.request(\n            route, method=Method.POST, headers=headers, body=body\n        )\n\n    async def put(\n        self,\n        route: str,\n        *,\n        headers: HeadersLike | None = None,\n        body: bytes | None = None,\n    ) -> Response:\n        return await self.request(\n            route, method=Method.PUT, headers=headers, body=body\n        )\n\n    async def patch(\n        self,\n        route: str,\n        *,\n        headers: HeadersLike | None = None,\n        body: bytes | None = None,\n    ) -> Response:\n        return await self.request(\n            route, method=Method.PATCH, headers=headers, body=body\n        )\n\n    async def delete(\n        self,\n        route: str,\n        *,\n        headers: HeadersLike | None = None,\n        body: bytes | None = None,\n    ) -> Response:\n        return await self.request(\n            route, method=Method.DELETE, headers=headers, body=body\n        )\n\n    async def connect(\n        self,\n        route: str,\n        *,\n        headers: HeadersLike | None = None,\n        body: bytes | None = None,\n    ) -> Response:\n        return await self.request(\n            route, method=Method.CONNECT, headers=headers, body=body\n        )\n\n    async def options(\n        self,\n        route: str,\n        *,\n        headers: HeadersLike | None = None,\n        body: bytes | None = None,\n    ) -> Response:\n        return await self.request(\n            route, method=Method.OPTIONS, headers=headers, body=body\n        )\n\n    async def trace(\n        self,\n        route: str,\n        *,\n        headers: HeadersLike | None = None,\n        body: bytes | None = None,\n    ) -> Response:\n        return await self.request(\n            route, method=Method.TRACE, headers=headers, body=body\n        )\n\n    async def head(\n        self,\n        route: str,\n        *,\n        headers: HeadersLike | None = None,\n        body: bytes | None = None,\n    ) -> Response:\n        return await self.request(\n            route, method=Method.HEAD, headers=headers, body=body\n        )\n"
  },
  {
    "path": "src/view/utils.py",
    "content": "\"\"\"\nGeneral utilities for view.py users.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom contextlib import contextmanager\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from collections.abc import Iterator\n\n__all__ = (\"reraise\",)\n\n\n@contextmanager\ndef reraise(\n    new_exception: type[BaseException] | BaseException,\n    *exceptions: type[BaseException],\n) -> Iterator[None]:\n    \"\"\"\n    Context manager to reraise one or many exceptions as a single exception.\n\n    This is primarily useful for reraising exceptions into HTTP errors, such\n    as a :class:`~view.core.status_codes.BadRequest`.\n    \"\"\"\n    target = exceptions or Exception\n\n    try:\n        yield\n    except target as error:\n        raise new_exception from error\n"
  },
  {
    "path": "tests/test_cache.py",
    "content": "import time\nfrom unittest.mock import patch\n\nimport pytest\nfrom view.cache import InMemoryCache, in_memory_cache, minutes\nfrom view.core.app import App\nfrom view.core.response import ResponseLike\nfrom view.testing import AppTestClient\n\n\n@pytest.mark.asyncio\nasync def test_in_memory_cache():\n    app = App()\n    called = 0\n\n    @app.get(\"/\")\n    @in_memory_cache()\n    async def index() -> ResponseLike:\n        nonlocal called\n        called += 1\n        return \"test\"\n\n    client = AppTestClient(app)\n    await client.get(\"/\")\n    assert called == 1\n    for _ in range(3):\n        await client.get(\"/\")\n        assert called == 1\n\n    assert isinstance(index.view, InMemoryCache)\n    index.view.invalidate()\n    await client.get(\"/\")\n    assert called == 2\n\n\n@pytest.mark.asyncio\nasync def test_in_memory_cache_timeout():\n    app = App()\n\n    called = 0\n\n    @app.get(\"/\")\n    @in_memory_cache(minutes(2))\n    async def index() -> ResponseLike:\n        nonlocal called\n        called += 1\n        return \"test\"\n\n    client = AppTestClient(app)\n    await client.get(\"/\")\n    assert called == 1\n\n    for _ in range(100):\n        await client.get(\"/\")\n\n    assert called == 1\n    now = time.time()\n    with patch(\"time.time\", return_value=now + minutes(2)):\n        await client.get(\"/\")\n        assert called == 2\n"
  },
  {
    "path": "tests/test_dom.py",
    "content": "import inspect\nfrom collections.abc import AsyncIterator, Callable, Iterator\n\nimport pytest\nfrom view.core.app import App\nfrom view.dom.components import Children, component\nfrom view.dom.core import HTMLNode, html_context, html_response\nfrom view.dom.primitives import ALL_PRIMITIVES, div, html, p\nfrom view.testing import AppTestClient\nfrom view.javascript import SupportsJavaScript\n\n\ndef html_function(\n    node: Callable[..., HTMLNode], *, has_body: bool\n) -> Iterator[HTMLNode]:\n    with html(lang=\"en\"):\n        with div(data={\"foo\": \"bar\"}):\n            if has_body:\n                the_node = node(\"gotcha\", data={\"silly\": \"a\"})\n            else:\n                the_node = node(data={\"silly\": \"a\"})\n\n            assert isinstance(the_node, SupportsJavaScript)\n            yield the_node\n\n\n@pytest.mark.parametrize(\"dom_node\", ALL_PRIMITIVES)\ndef test_dom_primitives(dom_node: Callable[..., HTMLNode]):\n    with html_context() as parent:\n        parameters = inspect.signature(dom_node).parameters\n        has_body = parameters.get(\"child_text\") is not None\n        has_required = False\n        for parameter in parameters.values():\n            if parameter.name == \"global_attributes\":\n                continue\n\n            if parameter.default is inspect.Signature.empty:\n                has_required = True\n\n        if not has_required:\n            for _ in html_function(dom_node, has_body=has_body):\n                pass\n        else:\n            with pytest.raises(TypeError):\n                for _ in html_function(dom_node, has_body=has_body):\n                    pass\n\n            return\n\n        iterator = parent.as_html_stream()\n        assert next(iterator) == \"<html\"\n        assert 'lang=\"en\"' in next(iterator)\n        assert \">\" in next(iterator)\n        assert \"<div\" in next(iterator)\n        assert 'data-foo=\"bar\"' in next(iterator)\n        assert \">\" in next(iterator)\n        real_node_name = dom_node.__name__.removesuffix(\"_\")\n        assert f\"<{real_node_name}\" in next(iterator)\n        assert 'data-silly=\"a\"' in next(iterator)\n        assert \">\" in next(iterator)\n        if has_body:\n            assert \"gotcha\" in next(iterator)\n        assert f\"</{real_node_name}>\" in next(iterator)\n        assert \"</div>\" in next(iterator)\n        assert next(iterator) == \"</html>\"\n        with pytest.raises(StopIteration):\n            next(iterator)\n\n\n@pytest.mark.asyncio\nasync def test_html_response():\n    app = App()\n\n    @app.get(\"/\")\n    @html_response\n    async def index() -> AsyncIterator[HTMLNode | int]:\n        yield 201\n        with html():\n            with div():\n                yield p(\"test\")\n\n    client = AppTestClient(app)\n    response = await client.get(\"/\")\n    assert response.status_code == 201\n    body = (await response.body()).decode(\"utf-8\")\n    formatted = body.replace(\" \", \"\").replace(\"\\n\", \"\")\n    assert formatted == \"<!DOCTYPEhtml><html><div><p>test</p></div></html>\"\n\n\ndef test_components():\n    @component\n    def my_component():\n        with html():\n            yield p(\"1\")\n            yield Children()\n            yield p(\"3\")\n\n    def use_component():\n        with my_component():\n            yield p(\"2\")\n            with div():\n                yield p(\"2.5\")\n\n    with html_context() as top:\n        for _ in use_component():\n            pass\n\n        data = top.as_html()\n\n    formatted = data.replace(\" \", \"\").replace(\"\\n\", \"\")\n    assert formatted == \"<html><p>1</p><p>2</p><div><p>2.5</p></div><p>3</p></html>\"\n\n\ndef test_component_multiple_children():\n    @component\n    def my_component():\n        yield Children()\n        yield Children()\n\n    def use_component():\n        with my_component():\n            yield p()\n\n    with html_context():\n        with pytest.raises(RuntimeError):\n            for _ in use_component():\n                pass\n"
  },
  {
    "path": "tests/test_misc.py",
    "content": "import pytest\nfrom view.core.app import App, as_app\nfrom view.exceptions import InvalidTypeError\nfrom view.core.multi_map import HasMultipleValuesError, MultiMap\n\n\ndef test_as_app_invalid():\n    with pytest.raises(InvalidTypeError):\n        as_app(object())  # type: ignore\n\n\ndef test_invalid_type_route():\n    app = App()\n\n    with pytest.raises(InvalidTypeError):\n        app.get(object())  # type: ignore\n\n    with pytest.raises(InvalidTypeError):\n        app.get(\"/\")(object())  # type: ignore\n\n\ndef test_empty_multi_map():\n    multi_map = MultiMap()\n    assert multi_map == {}\n\n    with pytest.raises(KeyError):\n        multi_map[\"a\"]\n\n    with pytest.raises(KeyError):\n        multi_map[object()]\n\n    with pytest.raises(KeyError):\n        multi_map[None]\n\n    assert len(multi_map) == 0\n    assert multi_map.as_sequence() == []\n\n    called = False\n    for _ in multi_map.keys():\n        called = True\n\n    assert called is False\n\n    for _ in multi_map.values():\n        called = True\n\n    assert called is False\n\n    for _ in multi_map.items():\n        called = True\n\n    assert called is False\n\n    for _ in multi_map:\n        called = True\n\n    assert called is False\n\n\ndef test_multi_map_no_duplicates():\n    data = [(\"a\", 1), (\"b\", 2), (\"c\", 3)]\n    multi_map = MultiMap(data)\n\n    assert multi_map == {\"a\": 1, \"b\": 2, \"c\": 3}\n    assert len(multi_map) == 3\n    assert multi_map.as_sequence() == data\n\n    for key, value in data:\n        assert key in multi_map\n        assert multi_map[key] == value\n        assert multi_map.get_many(key) == [value]\n        assert multi_map.get(key) == value\n        assert multi_map.get_exactly_one(key) == value\n        assert key in multi_map.keys()\n        assert value in multi_map.values()\n\n    called = 0\n    for key in multi_map:\n        called += 1\n        assert key in (\"a\", \"b\", \"c\")\n\n    assert called == 3\n\n\ndef test_multi_map_with_duplicates():\n    data = [(\"a\", 1), (\"a\", 2), (\"a\", 3), (\"b\", 4)]\n    multi_map = MultiMap(data)\n    assert len(multi_map) == 2\n    assert multi_map.as_sequence() == data\n\n    assert multi_map == {\"a\": 1, \"b\": 4}\n    assert multi_map[\"a\"] == 1\n    assert multi_map.get_many(\"a\") == [1, 2, 3]\n\n    assert \"a\" in multi_map\n    assert \"b\" in multi_map\n    assert list(multi_map.keys()) == [\"a\", \"b\"]\n    assert list(multi_map.values()) == [1, 4]\n    assert list(multi_map.items()) == [(\"a\", 1), (\"b\", 4)]\n    assert list(multi_map.many_values()) == [[1, 2, 3], [4]]\n    assert list(multi_map.many_items()) == [(\"a\", [1, 2, 3]), (\"b\", [4])]\n\n    with pytest.raises(HasMultipleValuesError):\n        multi_map.get_exactly_one(\"a\")\n\n    assert multi_map.get_exactly_one(\"b\") == 4\n\n    called = 0\n    for key in multi_map:\n        called += 1\n        assert key in (\"a\", \"b\")\n\n    assert called == 2\n\n\ndef test_multi_map_with_new_value():\n    data = [(\"a\", 1), (\"b\", 2), (\"b\", 3)]\n    multi_map = MultiMap(data)\n    assert len(multi_map) == 2\n\n    new_map = multi_map.with_new_value(\"b\", 4)\n    assert len(new_map) == 2\n    assert \"b\" in new_map\n    assert multi_map != new_map\n    assert new_map.get_many(\"b\") == [2, 3, 4]\n\n    new_map = new_map.with_new_value(\"c\", 4)\n    assert len(new_map) == 3\n    assert \"c\" in new_map\n    assert new_map != multi_map\n    assert new_map[\"c\"] == 4\n    assert new_map.get_exactly_one(\"c\") == 4\n    assert new_map.get_many(\"b\") == [2, 3, 4]\n"
  },
  {
    "path": "tests/test_requests.py",
    "content": "import json\nfrom collections.abc import AsyncIterator\n\nfrom hypothesis import given, strategies\nimport pytest\nfrom view.core.app import App, as_app\nfrom view.core.body import InvalidJSONError\nfrom view.core.headers import as_real_headers\nfrom view.core.request import Method, Request\nfrom view.core.response import ResponseLike\nfrom view.core.router import DuplicateRouteError\nfrom view.core.status_codes import BadRequest\nfrom view.core.multi_map import MultiMap\nfrom view.testing import AppTestClient, bad, into_tuple, ok\n\n\n@pytest.mark.asyncio\nasync def test_request_data():\n    @as_app\n    def app(request: Request) -> ResponseLike:\n        assert request.app == app\n        assert request.app.current_request() is request\n        assert isinstance(request.path, str)\n        assert request.method is Method.GET\n\n        if request.path == \"/\":\n            assert request.headers == {}\n            return \"Hello\"\n        elif request.path == \"/1\":\n            assert request.headers == {\"test-something\": \"42\"}\n            return \"World\"\n        else:\n            raise BadRequest()\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == ok(\"Hello\")\n    assert (await into_tuple(client.get(\"/1\", headers={\"test-something\": \"42\"}))) == ok(\n        \"World\"\n    )\n\n\n@pytest.mark.asyncio\n@given(strategies.text(), strategies.binary(), strategies.text())\nasync def test_manual_request(path: str, body: bytes, content: str):\n    @as_app\n    async def app(request: Request) -> ResponseLike:\n        assert request.app == app\n        assert request.app.current_request() is request\n        assert isinstance(request.path, str)\n        assert request.method is Method.POST\n        assert request.headers[\"test\"] == \"42\"\n        assert (await request.body()) == body\n\n        return content\n\n    async def stream_body() -> AsyncIterator[bytes]:\n        yield body\n\n    with pytest.raises(LookupError):\n        app.current_request()\n\n    manual_request = Request(\n        receive_data=stream_body(),\n        app=app,\n        path=path,\n        method=Method.POST,\n        headers=as_real_headers({\"test\": \"42\"}),\n        query_parameters=MultiMap(),\n    )\n    response = await app.process_request(manual_request)\n    assert (await response.body()) == content.encode(\"utf-8\")\n\n\n@pytest.mark.asyncio\nasync def test_request_body():\n    @as_app\n    async def app(request: Request) -> ResponseLike:\n        body = await request.body()\n        if request.path == \"/\":\n            assert body == b\"test\"\n            return \"1\"\n        elif request.path == \"/large\":\n            assert body == b\"A\" * 10000\n            return \"2\"\n        else:\n            raise BadRequest()\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\", body=b\"test\"))) == ok(\"1\")\n    assert (await into_tuple(client.get(\"/large\", body=b\"A\" * 10000))) == ok(\"2\")\n\n\n@pytest.mark.asyncio\nasync def test_request_headers():\n    @as_app\n    async def app(request: Request) -> ResponseLike:\n        if request.path == \"/\":\n            assert request.headers[\"foo\"] == \"42\"\n            return \"1\"\n        elif request.path == \"/many\":\n            assert request.headers[\"Bar\"] == \"24\"\n            assert request.headers[\"bar\"] == \"24\"\n            assert request.headers[\"baR\"] == \"24\"\n            assert request.headers[\"test\"] == \"123\"\n            return \"2\"\n        else:\n            raise BadRequest()\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\", headers={\"foo\": \"42\"}))) == ok(\"1\")\n    assert (\n        await into_tuple(\n            client.get(\"/many\", headers={\"Bar\": \"24\", \"bAr\": \"42\", \"test\": \"123\"})\n        )\n    ) == ok(\"2\")\n\n\n@pytest.mark.asyncio\nasync def test_request_router():\n    app = App()\n\n    @app.get(\"/\")\n    def index():\n        return \"Index\"\n\n    @app.get(\"/hello\")\n    def hello():\n        return \"Hello\"\n\n    @app.get(\"/hello/world\")\n    def world():\n        return \"World\"\n\n    @app.get(\"/goodbye/world\")\n    def goodbye():\n        return \"Goodbye\"\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == ok(\"Index\")\n    assert (await into_tuple(client.get(\"/hello\"))) == ok(\"Hello\")\n    assert (await into_tuple(client.get(\"/hello/world\"))) == ok(\"World\")\n    assert (await into_tuple(client.get(\"/\"))) == ok(\"Index\")\n    assert (await into_tuple(client.get(\"/goodbye/world\"))) == ok(\"Goodbye\")\n\n\n@pytest.mark.asyncio\n@given(\n    # This is super ugly but don't worry about it\n    a=strategies.text().map(lambda x: x.replace(\"/\", \"\")).filter(lambda x: (x not in {'', 'a'}) and ('?' not in x)),\n    b=strategies.text().map(lambda x: x.replace(\"/\", \"\")).filter(lambda x: (x != '') and ('?' not in x)),\n)\nasync def test_request_path_parameters(a: str, b: str):\n    app = App()\n\n    @app.get(\"/\")\n    def index():\n        return \"Index\"\n\n    @app.get(\"/oneparam/{a}\")\n    async def path_param():\n        request = app.current_request()\n        assert request.path_parameters[\"a\"] == a\n        return \"0\"\n\n    @app.get(\"/oneparam/a\")\n    def overwrite_path_param():\n        return \"1\"\n\n    @app.get(\"/nested/param/{b}\")\n    def sub_path_param():\n        request = app.current_request()\n        assert request.path_parameters[\"b\"] == b\n        return \"2\"\n\n    @app.get(\"/twoparam/{a}/{b}\")\n    def double_path_param():\n        request = app.current_request()\n        assert request.path_parameters[\"a\"] == a\n        assert request.path_parameters[\"b\"] == b\n        return \"3\"\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(f\"/oneparam/{a}\"))) == ok(\"0\")\n    assert (await into_tuple(client.get(\"/oneparam/a\"))) == ok(\"1\")\n    assert (await into_tuple(client.get(f\"/nested/param/{b}\"))) == ok(\"2\")\n    assert (await into_tuple(client.get(f\"/twoparam/{a}/{b}\"))) == ok(\"3\")\n\n\n@pytest.mark.asyncio\nasync def test_request_method():\n    app = App()\n\n    @app.get(\"/\")\n    async def index_get():\n        return \"get\"\n\n    @app.post(\"/\")\n    async def index_post():\n        return \"post\"\n\n    @app.patch(\"/\")\n    async def index_patch():\n        return \"patch\"\n\n    @app.put(\"/\")\n    async def index_put():\n        return \"put\"\n\n    @app.delete(\"/\")\n    async def index_delete():\n        return \"delete\"\n\n    @app.connect(\"/\")\n    async def index_connect():\n        return \"connect\"\n\n    @app.options(\"/\")\n    async def index_options():\n        return \"options\"\n\n    @app.trace(\"/\")\n    async def index_trace():\n        return \"trace\"\n\n    @app.head(\"/\")\n    async def index_head():\n        return \"head\"\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == ok(\"get\")\n    assert (await into_tuple(client.post(\"/\"))) == ok(\"post\")\n    assert (await into_tuple(client.patch(\"/\"))) == ok(\"patch\")\n    assert (await into_tuple(client.put(\"/\"))) == ok(\"put\")\n    assert (await into_tuple(client.delete(\"/\"))) == ok(\"delete\")\n    assert (await into_tuple(client.connect(\"/\"))) == ok(\"connect\")\n    assert (await into_tuple(client.options(\"/\"))) == ok(\"options\")\n    assert (await into_tuple(client.trace(\"/\"))) == ok(\"trace\")\n    assert (await into_tuple(client.head(\"/\"))) == ok(\"head\")\n\n\n@pytest.mark.asyncio\nasync def test_normalized_routes():\n    app = App()\n\n    @app.get(\"\")\n    async def index():\n        return \"1\"\n\n    @app.get(\"hello/\")\n    async def hello():\n        return \"2\"\n\n    @app.get(\"/test/\")\n    async def test():\n        return \"3\"\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == ok(\"1\")\n    assert (await into_tuple(client.get(\"\"))) == ok(\"1\")\n    assert (await into_tuple(client.get(\"/hello\"))) == ok(\"2\")\n    assert (await into_tuple(client.get(\"/hello/\"))) == ok(\"2\")\n    assert (await into_tuple(client.get(\"hello/\"))) == ok(\"2\")\n    assert (await into_tuple(client.get(\"/test\"))) == ok(\"3\")\n    assert (await into_tuple(client.get(\"/test/\"))) == ok(\"3\")\n    assert (await into_tuple(client.get(\"test/\"))) == ok(\"3\")\n\n\n@pytest.mark.asyncio\nasync def test_current_app():\n    app = App()\n\n    @app.get(\"/\")\n    async def index():\n        assert App.current_app() is app\n        return \"1\"\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == ok(\"1\")\n\n\n@pytest.mark.asyncio\nasync def test_route_division():\n    app = App()\n\n    @app.get(\"/test/main\")\n    async def main():\n        return \"1\"\n\n    @app.get(main / \"foo\")\n    async def foo():\n        return \"2\"\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/test/main/\"))) == ok(\"1\")\n    assert (await into_tuple(client.get(\"/test/main/foo\"))) == ok(\"2\")\n\n\n@pytest.mark.asyncio\nasync def test_request_json():\n    app = App()\n\n    @app.get(\"/\")\n    async def main():\n        request = app.current_request()\n        try:\n            data = await request.json()\n        except InvalidJSONError as error:\n            raise BadRequest() from error\n        return data[\"test\"]\n\n    json_body = json.dumps({\"test\": \"123\"}).encode(\"utf-8\")\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\", body=json_body))) == ok(\"123\")\n    assert (await into_tuple(client.get(\"/\", body=b\"...\"))) == (\n        b\"400 Bad Request\",\n        400,\n        {},\n    )\n\n\n@pytest.mark.asyncio\nasync def test_request_query_parameters():\n    app = App()\n\n    @app.get(\"/\")\n    async def main():\n        request = app.current_request()\n        assert request.query_parameters[\"foo\"] == \"bar\"\n        assert request.query_parameters[\"test\"] == \"1\"\n        assert request.query_parameters.get_many(\"test\") == [\"1\", \"2\", \"3\"]\n        assert \"noexist\" not in request.query_parameters\n\n        return \"ok\"\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/?foo=bar&test=1&test=2&test=3\"))) == ok(\"ok\")\n\n\n@pytest.mark.asyncio\nasync def test_subrouters():\n    app = App()\n\n    @app.subrouter(\"/foo/bar\")\n    async def main(path: str) -> ResponseLike:\n        return path\n\n    @app.get(\"/foo/bar\")\n    async def conflict() -> ResponseLike:\n        return \"test\"\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/foo/bar\"))) == ok(\"test\")\n    assert (await into_tuple(client.get(\"/foo/bar/baz\"))) == ok(\"baz\")\n    assert (await into_tuple(client.get(\"/foo/bar//baz\"))) == ok(\"/baz\")\n    assert (await into_tuple(client.get(\"/foo/bar/\"))) == ok(\"test\")\n    assert (await into_tuple(client.get(\"/foo/\"))) == bad(404)\n\n    with pytest.raises(DuplicateRouteError):\n        app.subrouter(\"/foo/bar\")(main)\n\n    with pytest.raises(DuplicateRouteError):\n        app.get(\"/foo/bar\")(conflict.view)\n\n    with pytest.raises(RuntimeError):\n        app.subrouter(\"/{test}/x\")(main)\n"
  },
  {
    "path": "tests/test_responses.py",
    "content": "import asyncio\nimport tempfile\nfrom pathlib import Path\n\nimport pytest\nfrom view.core.app import App, as_app\nfrom view.core.headers import as_real_headers\nfrom view.core.request import Request\nfrom view.core.response import Response, ResponseLike\nfrom view.core.status_codes import (\n    STATUS_EXCEPTIONS,\n    STATUS_STRINGS,\n    BadRequest,\n    HTTPError,\n    Success,\n)\nfrom view.responses import JSONResponse, FileResponse\nfrom view.testing import AppTestClient, bad, into_tuple, ok\n\nfrom hypothesis import given, strategies\n\n\n@pytest.mark.asyncio\nasync def test_str_or_bytes_response():\n    class MyString(str):\n        pass\n\n    @as_app\n    def app(request: Request) -> ResponseLike:\n        if request.path == \"/\":\n            return \"Hello\"\n        elif request.path == \"/bytes\":\n            return b\"World\"\n        elif request.path == \"/my-string\":\n            return MyString(\"My string\")\n        else:\n            raise BadRequest()\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == ok(\"Hello\")\n    assert (await into_tuple(client.get(\"/bytes\"))) == ok(\"World\")\n    assert (await into_tuple(client.get(\"/my-string\"))) == ok(\"My string\")\n\n\n@pytest.mark.asyncio\nasync def test_raw_response():\n    @as_app\n    def app(request: Request) -> ResponseLike:\n        async def stream():\n            yield b\"Test\"\n\n        return Response(\n            receive_data=stream(),\n            status_code=Success.CREATED,\n            headers=as_real_headers({\"hello\": \"world\"}),\n        )\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == (b\"Test\", 201, {\"hello\": \"world\"})\n\n\n@pytest.mark.asyncio\nasync def test_tuple_response():\n    @as_app\n    def app(request: Request) -> ResponseLike:\n        if request.path == \"/status\":\n            return \"Test\", Success.CREATED\n        elif request.path == \"/status-bytes\":\n            return b\"Bytes\", Success.CREATED\n        elif request.path == \"/headers\":\n            return \"Headers\", Success.CREATED, {\"hello\": \"world\"}\n        elif request.path == \"/headers-bytes\":\n            return b\"HBytes\", Success.OK, {b\"1\": b\"2\"}\n        else:\n            raise BadRequest()\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/status\"))) == (b\"Test\", 201, {})\n    assert (await into_tuple(client.get(\"/status-bytes\"))) == (b\"Bytes\", 201, {})\n    assert (await into_tuple(client.get(\"/headers\"))) == (\n        b\"Headers\",\n        201,\n        {\"hello\": \"world\"},\n    )\n    assert (await into_tuple(client.get(\"/headers-bytes\"))) == (\n        b\"HBytes\",\n        200,\n        {\"1\": \"2\"},\n    )\n\n\n@pytest.mark.asyncio\nasync def test_stream_response_async():\n    @as_app\n    async def app(request: Request) -> ResponseLike:\n        yield b\"This \"\n        await asyncio.sleep(0)\n        yield \"Is \"\n        await asyncio.sleep(0)\n        yield b\"A \"\n        await asyncio.sleep(0)\n        yield \"Test\"\n        await asyncio.sleep(0)\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == ok(\"This Is A Test\")\n\n\n@pytest.mark.asyncio\nasync def test_stream_response_sync():\n    @as_app\n    def app(request: Request) -> ResponseLike:\n        yield b\"This \"\n        yield \"Is \"\n        yield b\"A \"\n        yield \"Test\"\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == ok(\"This Is A Test\")\n\n\n@pytest.mark.asyncio\nasync def test_file_response():\n    with tempfile.NamedTemporaryFile(\"w\", delete=False) as file:\n        file.write(\"A\" * 10000)\n\n    @as_app\n    def app(request: Request) -> ResponseLike:\n        return FileResponse.from_file(\n            str(file.name), status_code=Success.CREATED, headers={\"hello\": \"world\"}\n        )\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == (\n        b\"A\" * 10000,\n        201,\n        {\"hello\": \"world\", \"content-type\": \"text/plain\"},\n    )\n    file.close()\n\n\n@pytest.mark.asyncio\nasync def test_status_codes():\n    @as_app\n    def app(request: Request) -> ResponseLike:\n        if request.path == \"/\":\n            raise BadRequest()\n        elif request.path == \"/message\":\n            raise BadRequest(\"Test\")\n        else:\n            raise RuntimeError\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == bad(400)\n    assert (await into_tuple(client.get(\"/message\"))) == (b\"Test\", 400, {})\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"status_exception\", list(STATUS_EXCEPTIONS.values()))\nasync def test_status_code_strings(status_exception: type[HTTPError]):\n    @as_app\n    async def app(_: Request) -> ResponseLike:\n        raise status_exception()\n\n    client = AppTestClient(app)\n    response = await client.get(\"/\")\n    assert status_exception.status_code == response.status_code\n    message = f\"{status_exception.status_code} {STATUS_STRINGS[response.status_code]}\"\n    assert (await response.body()) == message.encode(\"utf-8\")\n\n\n@pytest.mark.asyncio\nasync def test_internal_server_error():\n    @as_app\n    async def app(_: Request):\n        raise Exception(\"silly\")\n\n    client = AppTestClient(app)\n    response = await client.get(\"/\")\n    assert response.status_code == 500\n    output = (await response.body()).decode(\"utf-8\")\n    assert \"Traceback (most recent call last)\" in output\n    assert \"Exception: silly\" in output\n\n\n@pytest.mark.asyncio\nasync def test_json_response():\n    @as_app\n    async def app(_: Request):\n        return JSONResponse.from_content({\"foo\": \"bar\"})\n\n    client = AppTestClient(app)\n    response = await client.get(\"/\")\n    assert response.status_code == 200\n    assert response.headers == {}\n    assert (await response.json()) == {\"foo\": \"bar\"}\n\n\n@pytest.mark.asyncio\nasync def test_static_files():\n    app = App()\n\n    with tempfile.TemporaryDirectory() as temporary_directory:\n        file = Path(temporary_directory) / \"a.txt\"\n        file.touch(exist_ok=False)\n        file.write_text(\"hello\")\n\n        directory = Path(temporary_directory) / \"foo\"\n        directory.mkdir(exist_ok=False)\n        other_file = directory / \"b.txt\"\n        other_file.write_text(\"goodbye\")\n\n        app.static_files(\"/files\", temporary_directory)\n\n        client = AppTestClient(app)\n        assert (await into_tuple(client.get(\"/files/a.txt\"))) == (\n            b\"hello\",\n            200,\n            {\"content-type\": \"text/plain\"},\n        )\n        assert (await into_tuple(client.get(\"/files/\"))) == bad(404)\n        assert (await into_tuple(client.get(\"/files\"))) == bad(404)\n        assert (await into_tuple(client.get(\"/files/foo/bar\"))) == bad(404)\n        assert (await into_tuple(client.get(\"/files/foo/../bar\"))) == bad(404)\n        assert (await into_tuple(client.get(\"/files/../\"))) == bad(404)\n        assert (await into_tuple(client.get(\"/files/~/\"))) == bad(404)\n        assert (await into_tuple(client.get(\"/files/foo/\"))) == bad(404)\n        assert (await into_tuple(client.get(\"/files/foo/b.txt\"))) == (\n            b\"goodbye\",\n            200,\n            {\"content-type\": \"text/plain\"},\n        )\n\n@pytest.mark.asyncio\nasync def test_header_case_insensitivity():\n    @as_app\n    async def app(_: Request):\n        return \"a\", 200, {\"Foo\": \"bar\"}\n\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == (b\"a\", 200, {\"foo\": \"bar\"})\n    assert (await into_tuple(client.get(\"/\"))) == (b\"a\", 200, {\"FOO\": \"bar\"})\n\n\n@pytest.mark.asyncio\n@given(\n    strategies.text(),\n    strategies.integers(min_value=200, max_value=208),\n    strategies.dictionaries(strategies.text(), strategies.text()),\n)\nasync def test_hypothesis_with_responses(\n    response: str, status: int, headers: dict[str, str]\n):\n    @as_app\n    async def app(_: Request):\n        return response, status, headers\n\n    client = AppTestClient(app)\n    assert (await into_tuple(client.get(\"/\"))) == (\n        response.encode(\"utf-8\"),\n        status,\n        headers,\n    )\n"
  },
  {
    "path": "tests/test_servers.py",
    "content": "import subprocess\nimport sys\nimport time\nimport platform\n\nimport pytest\nimport requests\nfrom view.core.app import as_app\nfrom view.core.request import Request\nfrom view.core.response import ResponseLike\nfrom view.core.status_codes import Success\nfrom view.run.servers import ALL_SERVERS\n\nfrom threading import Lock\n\n_PORT_LOCK = Lock()\n_PORT: int = 5000\n\n@pytest.fixture(scope=\"function\")\ndef port() -> int:\n    with _PORT_LOCK:\n        global _PORT\n        _PORT += 1\n        return _PORT\n\n\ndef wait_for_server(port: int, timeout: float = 10.0, interval: float = 0.1) -> bool:\n    deadline = time.time() + timeout\n    while time.time() < deadline:\n        try:\n            requests.get(f\"http://localhost:{port}\", timeout=1)\n            return True\n        except requests.ConnectionError:\n            time.sleep(interval)\n    return False\n\n\n@pytest.mark.parametrize(\"server_name\", ALL_SERVERS)\n@pytest.mark.skipif(platform.system() != \"Linux\", reason=\"this has issues on non-Linux\")\ndef test_run_server(server_name: str, port: int):\n    try:\n        __import__(server_name)\n    except ImportError:\n        pytest.skip(f\"{server_name} is not installed\")\n\n    code = f\"\"\"if True:\n    from view.core.app import App\n\n    app = App()\n\n    @app.get('/')\n    async def index():\n        return 'ok'\n\n    app.run(server_hint={server_name!r}, port={port})\n    \"\"\"\n    process = subprocess.Popen([sys.executable, \"-c\", code])\n    try:\n        if not wait_for_server(port):\n            pytest.fail(\"Server did not start in time\")\n        response = requests.get(f\"http://localhost:{port}\")\n        assert response.text == \"ok\"\n    finally:\n        process.kill()\n\n\n@pytest.mark.parametrize(\"server_name\", ALL_SERVERS)\n@pytest.mark.skip(\"some multiprocessing problems at the moment\")\ndef test_run_server_detached(server_name: str):\n    @as_app\n    def app(request: Request) -> ResponseLike:\n        header = request.headers[\"test\"]\n        assert request.headers[\"user-agent\"].startswith(\"python-requests\")\n        return \"test\", Success.CREATED, {\"foo\": \"bar\", \"baz\": header}\n\n    try:\n        __import__(server_name)\n    except ImportError:\n        pytest.skip(f\"{server_name} is not installed\")\n\n    process = app.run_detached(server_hint=server_name)\n    try:\n        time.sleep(2)\n        response = requests.get(\"http://localhost:5000\", headers={\"test\": \"silly\"})\n        assert response.text == \"test\"\n        assert response.status_code == 201\n        assert response.headers[\"foo\"] == \"bar\"\n        assert response.headers[\"baz\"] == \"silly\"\n    finally:\n        process.kill()\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "import pytest\nfrom view.utils import reraise\n\n\ndef test_simple_reraise():\n    with pytest.raises(RuntimeError) as error:\n        with reraise(RuntimeError, TypeError):\n            raise TypeError(\"hello\")\n\n    assert str(error.value) == \"\"\n\n\ndef test_reraise_no_match():\n\n    with pytest.raises(ValueError) as error:\n        with reraise(RuntimeError, TypeError):\n            raise ValueError(\"silly\")\n\n    assert str(error.value) == \"silly\"\n\n\ndef test_reraise_all_exceptions():\n\n    with pytest.raises(RuntimeError) as error:\n        with reraise(RuntimeError):\n            raise ZeroDivisionError(\"123\")\n\n    assert str(error.value) == \"\"\n\n\ndef test_reraise_exception_value():\n\n    with pytest.raises(RuntimeError) as error:\n        with reraise(RuntimeError(\"something\")):\n            raise ZeroDivisionError(\"456\")\n\n    assert str(error.value) == \"something\"\n\n\ndef test_reraise_multiple():\n\n    with pytest.raises(RuntimeError):\n        with reraise(RuntimeError, TypeError, ValueError):\n            raise ValueError\n\n    with pytest.raises(RuntimeError):\n        with reraise(RuntimeError, TypeError, ValueError):\n            raise TypeError\n\n\ndef test_do_not_reraise_base_exceptions():\n\n    with pytest.raises(KeyboardInterrupt):\n        with reraise(RuntimeError):\n            raise KeyboardInterrupt\n\n\ndef test_simple_reraise_as_decorator():\n    @reraise(RuntimeError, TypeError)\n    def runtime_from_type() -> None:\n        raise TypeError(\"silly\")\n\n    with pytest.raises(RuntimeError):\n        runtime_from_type()\n\n\ndef test_reraise_unexpected_as_decorator():\n\n    @reraise(RuntimeError, TypeError)\n    def runtime_from_type_but_value() -> None:\n        raise ValueError(\"haha\")\n\n    with pytest.raises(ValueError):\n        runtime_from_type_but_value()\n\n\ndef test_reraise_all_exceptions_as_decorator():\n\n    @reraise(RuntimeError)\n    def runtime_from_all() -> None:\n        raise ZeroDivisionError(\"anything\")\n\n    with pytest.raises(RuntimeError):\n        runtime_from_all()\n\n\ndef test_reraise_exception_instance_as_decorator():\n\n    @reraise(RuntimeError(\"test\"))\n    def runtime_value_from_all() -> None:\n        raise ZeroDivisionError(\"anything\")\n\n    with pytest.raises(RuntimeError) as error:\n        runtime_value_from_all()\n\n    assert str(error.value) == \"test\"\n\n\ndef test_multi_reraise_as_decorator():\n\n    @reraise(RuntimeError, TypeError, ValueError)\n    def runtime_from_type_or_value(exception: BaseException) -> None:\n        raise exception\n\n    with pytest.raises(RuntimeError):\n        runtime_from_type_or_value(ValueError(\"foo\"))\n\n    with pytest.raises(RuntimeError):\n        runtime_from_type_or_value(TypeError(\"bar\"))\n\n    with pytest.raises(ZeroDivisionError):\n        runtime_from_type_or_value(ZeroDivisionError())\n\n\ndef test_do_not_reraise_base_exceptions_as_decorator():\n    @reraise(RuntimeError)\n    def runtime_from_all_but_interrupt() -> None:\n        raise KeyboardInterrupt\n\n    with pytest.raises(KeyboardInterrupt):\n        runtime_from_all_but_interrupt()\n"
  }
]