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