stario 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stario-0.1.0/PKG-INFO +133 -0
- stario-0.1.0/README.md +120 -0
- stario-0.1.0/pyproject.toml +27 -0
- stario-0.1.0/src/stario/__init__.py +26 -0
- stario-0.1.0/src/stario/application.py +150 -0
- stario-0.1.0/src/stario/datastar.py +281 -0
- stario-0.1.0/src/stario/dependencies.py +260 -0
- stario-0.1.0/src/stario/middlewares.py +73 -0
- stario-0.1.0/src/stario/parameters.py +119 -0
- stario-0.1.0/src/stario/py.typed +0 -0
- stario-0.1.0/src/stario/routes.py +69 -0
- stario-0.1.0/src/stario/routing.py +198 -0
- stario-0.1.0/src/stario/toys.py +65 -0
- stario-0.1.0/src/stario/types.py +4 -0
- stario-0.1.0/src/stario/wrappers.py +72 -0
stario-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: stario
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Stario brings back the joy of building web apps
|
|
5
|
+
Author: Adam Bobowski
|
|
6
|
+
Author-email: Adam Bobowski <adam.bobowski@wratilabs.com>
|
|
7
|
+
Requires-Dist: brotli-asgi>=1.4.0
|
|
8
|
+
Requires-Dist: pydantic>=2.11.7
|
|
9
|
+
Requires-Dist: starlette>=0.47.2
|
|
10
|
+
Requires-Dist: uvicorn>=0.35.0
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<picture>
|
|
16
|
+
<img alt="stario-logo" src="https://raw.githubusercontent.com/bobowski/stario/main/docs/img/stario.png">
|
|
17
|
+
</picture>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<em>✨ Binging back joy and exploration to building web apps ✨</em>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
**Documentation**: Coming soon
|
|
27
|
+
|
|
28
|
+
**Source Code**: <a href="https://github.com/bobowski/stario/tree/main/stario" target="_blank">https://github.com/bobowski/stario</a>
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### Stario
|
|
33
|
+
|
|
34
|
+
Stario is a lightweight ASGI framework built on top of [Starlette](https://www.starlette.io/)
|
|
35
|
+
that brings back the joy of building web apps.
|
|
36
|
+
Focus on building apps rather than passing data between components.
|
|
37
|
+
|
|
38
|
+
### Why Stario?
|
|
39
|
+
|
|
40
|
+
- HTML-first - dropping intermediary data layers (e.g. JSON) and using HTML as the primary data format. "Just throw html at user and make them happy."
|
|
41
|
+
- Rapid prototyping - a simple framework to build and test fullstack ideas with minimal setup. Build, see if it works, optimize later (if ever).
|
|
42
|
+
- No-nonsense - no ceremony, no complexity, just build.
|
|
43
|
+
- Learning experience - Learn about web, rather than X or Y language framework. I've learned a lot building this framework. I hope you will too.
|
|
44
|
+
|
|
45
|
+
It focuses on:
|
|
46
|
+
|
|
47
|
+
- **HTML-first DX**: return strings or HTML providers, no ceremony.
|
|
48
|
+
- **Realtime by default**: seamless Server-Sent Events (SSE) with [DataStar](https://data-star.dev/) patches/signals.
|
|
49
|
+
- **Great defaults**: Brotli compression out of the box, fast dependency resolution, and caching.
|
|
50
|
+
- **Starlette compatibility**: interop with routes, middleware, and tools you already know.
|
|
51
|
+
|
|
52
|
+
What you get:
|
|
53
|
+
|
|
54
|
+
- **A tiny, low-complexity HTTP framework** powered by [Starlette](https://www.starlette.io/) and [DataStar](https://data-star.dev/).
|
|
55
|
+
- **SSE streaming** by simply yielding patches, HTML, or signals (dicts).
|
|
56
|
+
- **Brotli compression** middleware enabled by default (GZip fallback).
|
|
57
|
+
- **Simple DI** via `Annotated` with request params (`QueryParam`, `PathParam`, `Header`, `Cookie`) and `Inject`.
|
|
58
|
+
- **Per-request/app/ttl caching** and configurable run modes (`auto`, `sync`, `thread`).
|
|
59
|
+
- **Header-constrained routes** and a lightweight `StarRouter`.
|
|
60
|
+
- **Few dependencies** and a clear, typed codebase.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
From PyPI (when available):
|
|
67
|
+
|
|
68
|
+
```shell
|
|
69
|
+
pip install stario
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or from source in this repository:
|
|
73
|
+
|
|
74
|
+
```shell
|
|
75
|
+
cd stario
|
|
76
|
+
pip install -e .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You'll also want an ASGI server such as [uvicorn](https://www.uvicorn.org/):
|
|
80
|
+
|
|
81
|
+
```shell
|
|
82
|
+
pip install uvicorn
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Quick start
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# main.py
|
|
89
|
+
import asyncio
|
|
90
|
+
from stario import Stario, Query, Command
|
|
91
|
+
from stario.toys import ToyPage
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def home():
|
|
95
|
+
return ToyPage(
|
|
96
|
+
"""
|
|
97
|
+
<h2>Realtime responses!</h2>
|
|
98
|
+
<div data-on-load="@get('/online-counter')">
|
|
99
|
+
This shows how long the connection has been open.
|
|
100
|
+
</div>
|
|
101
|
+
<div id="online-counter"></div>
|
|
102
|
+
"""
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def online_counter():
|
|
107
|
+
duration = 0
|
|
108
|
+
interval = 0.01
|
|
109
|
+
while True:
|
|
110
|
+
# Yielding strings/patches streams as SSE with DataStar-compatible events
|
|
111
|
+
yield f"<div id='online-counter'>Online since: {duration:.1f}s</div>"
|
|
112
|
+
duration += interval
|
|
113
|
+
await asyncio.sleep(interval)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# Building the app
|
|
117
|
+
app = Stario(
|
|
118
|
+
Query("/", home),
|
|
119
|
+
Query("/online-counter", online_counter),
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Run with Uvicorn:
|
|
124
|
+
|
|
125
|
+
```shell
|
|
126
|
+
uvicorn main:app --reload
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Open `http://127.0.0.1:8000/` in your browser.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
<p align="center"><i>Stario code is designed & crafted for joy.</i><br/>— ⭐️ —</p>
|
stario-0.1.0/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<img alt="stario-logo" src="https://raw.githubusercontent.com/bobowski/stario/main/docs/img/stario.png">
|
|
4
|
+
</picture>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<em>✨ Binging back joy and exploration to building web apps ✨</em>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
**Documentation**: Coming soon
|
|
14
|
+
|
|
15
|
+
**Source Code**: <a href="https://github.com/bobowski/stario/tree/main/stario" target="_blank">https://github.com/bobowski/stario</a>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
### Stario
|
|
20
|
+
|
|
21
|
+
Stario is a lightweight ASGI framework built on top of [Starlette](https://www.starlette.io/)
|
|
22
|
+
that brings back the joy of building web apps.
|
|
23
|
+
Focus on building apps rather than passing data between components.
|
|
24
|
+
|
|
25
|
+
### Why Stario?
|
|
26
|
+
|
|
27
|
+
- HTML-first - dropping intermediary data layers (e.g. JSON) and using HTML as the primary data format. "Just throw html at user and make them happy."
|
|
28
|
+
- Rapid prototyping - a simple framework to build and test fullstack ideas with minimal setup. Build, see if it works, optimize later (if ever).
|
|
29
|
+
- No-nonsense - no ceremony, no complexity, just build.
|
|
30
|
+
- Learning experience - Learn about web, rather than X or Y language framework. I've learned a lot building this framework. I hope you will too.
|
|
31
|
+
|
|
32
|
+
It focuses on:
|
|
33
|
+
|
|
34
|
+
- **HTML-first DX**: return strings or HTML providers, no ceremony.
|
|
35
|
+
- **Realtime by default**: seamless Server-Sent Events (SSE) with [DataStar](https://data-star.dev/) patches/signals.
|
|
36
|
+
- **Great defaults**: Brotli compression out of the box, fast dependency resolution, and caching.
|
|
37
|
+
- **Starlette compatibility**: interop with routes, middleware, and tools you already know.
|
|
38
|
+
|
|
39
|
+
What you get:
|
|
40
|
+
|
|
41
|
+
- **A tiny, low-complexity HTTP framework** powered by [Starlette](https://www.starlette.io/) and [DataStar](https://data-star.dev/).
|
|
42
|
+
- **SSE streaming** by simply yielding patches, HTML, or signals (dicts).
|
|
43
|
+
- **Brotli compression** middleware enabled by default (GZip fallback).
|
|
44
|
+
- **Simple DI** via `Annotated` with request params (`QueryParam`, `PathParam`, `Header`, `Cookie`) and `Inject`.
|
|
45
|
+
- **Per-request/app/ttl caching** and configurable run modes (`auto`, `sync`, `thread`).
|
|
46
|
+
- **Header-constrained routes** and a lightweight `StarRouter`.
|
|
47
|
+
- **Few dependencies** and a clear, typed codebase.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
From PyPI (when available):
|
|
54
|
+
|
|
55
|
+
```shell
|
|
56
|
+
pip install stario
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or from source in this repository:
|
|
60
|
+
|
|
61
|
+
```shell
|
|
62
|
+
cd stario
|
|
63
|
+
pip install -e .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
You'll also want an ASGI server such as [uvicorn](https://www.uvicorn.org/):
|
|
67
|
+
|
|
68
|
+
```shell
|
|
69
|
+
pip install uvicorn
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick start
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# main.py
|
|
76
|
+
import asyncio
|
|
77
|
+
from stario import Stario, Query, Command
|
|
78
|
+
from stario.toys import ToyPage
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def home():
|
|
82
|
+
return ToyPage(
|
|
83
|
+
"""
|
|
84
|
+
<h2>Realtime responses!</h2>
|
|
85
|
+
<div data-on-load="@get('/online-counter')">
|
|
86
|
+
This shows how long the connection has been open.
|
|
87
|
+
</div>
|
|
88
|
+
<div id="online-counter"></div>
|
|
89
|
+
"""
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def online_counter():
|
|
94
|
+
duration = 0
|
|
95
|
+
interval = 0.01
|
|
96
|
+
while True:
|
|
97
|
+
# Yielding strings/patches streams as SSE with DataStar-compatible events
|
|
98
|
+
yield f"<div id='online-counter'>Online since: {duration:.1f}s</div>"
|
|
99
|
+
duration += interval
|
|
100
|
+
await asyncio.sleep(interval)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Building the app
|
|
104
|
+
app = Stario(
|
|
105
|
+
Query("/", home),
|
|
106
|
+
Query("/online-counter", online_counter),
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Run with Uvicorn:
|
|
111
|
+
|
|
112
|
+
```shell
|
|
113
|
+
uvicorn main:app --reload
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Open `http://127.0.0.1:8000/` in your browser.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
<p align="center"><i>Stario code is designed & crafted for joy.</i><br/>— ⭐️ —</p>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "stario"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Stario brings back the joy of building web apps"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Adam Bobowski", email = "adam.bobowski@wratilabs.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"brotli-asgi>=1.4.0",
|
|
12
|
+
"pydantic>=2.11.7",
|
|
13
|
+
"starlette>=0.47.2",
|
|
14
|
+
"uvicorn>=0.35.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.8.6,<0.9.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"pyright>=1.1.403",
|
|
24
|
+
"pytest>=8.4.1",
|
|
25
|
+
"ruff>=0.12.8",
|
|
26
|
+
"ty>=0.0.1a19",
|
|
27
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .application import Stario
|
|
2
|
+
from .datastar import LOAD_DATASTAR, Signals
|
|
3
|
+
from .dependencies import Inject
|
|
4
|
+
from .middlewares import BrotliMiddleware
|
|
5
|
+
from .parameters import Body, Cookie, Header, PathParam, QueryParam
|
|
6
|
+
from .routes import Command, Query
|
|
7
|
+
from .routing import StarRoute
|
|
8
|
+
from .types import CacheScope, RunMode
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Stario",
|
|
12
|
+
"StarRoute",
|
|
13
|
+
"Query",
|
|
14
|
+
"Command",
|
|
15
|
+
"PathParam",
|
|
16
|
+
"QueryParam",
|
|
17
|
+
"Body",
|
|
18
|
+
"Header",
|
|
19
|
+
"Cookie",
|
|
20
|
+
"Inject",
|
|
21
|
+
"Signals",
|
|
22
|
+
"LOAD_DATASTAR",
|
|
23
|
+
"BrotliMiddleware",
|
|
24
|
+
"CacheScope",
|
|
25
|
+
"RunMode",
|
|
26
|
+
]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Any,
|
|
3
|
+
Callable,
|
|
4
|
+
Mapping,
|
|
5
|
+
ParamSpec,
|
|
6
|
+
Protocol,
|
|
7
|
+
Sequence,
|
|
8
|
+
TypeVar,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from starlette.applications import Starlette
|
|
12
|
+
from starlette.datastructures import State, URLPath
|
|
13
|
+
from starlette.middleware import Middleware
|
|
14
|
+
from starlette.routing import BaseRoute
|
|
15
|
+
from starlette.types import ASGIApp, ExceptionHandler, Lifespan, Receive, Scope, Send
|
|
16
|
+
|
|
17
|
+
from stario.middlewares import BrotliMiddleware
|
|
18
|
+
from stario.routing import StarRouter
|
|
19
|
+
|
|
20
|
+
AppType = TypeVar("AppType", bound="Stario")
|
|
21
|
+
|
|
22
|
+
P = ParamSpec("P")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _MiddlewareFactory(Protocol[P]):
|
|
26
|
+
def __call__(
|
|
27
|
+
self, app: ASGIApp, /, *args: P.args, **kwargs: P.kwargs
|
|
28
|
+
) -> ASGIApp: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Stario:
|
|
32
|
+
"""
|
|
33
|
+
Creates a Stario application.
|
|
34
|
+
It's 'almost' Starlette app, but we push on some of the details.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self: AppType,
|
|
39
|
+
*routes: BaseRoute,
|
|
40
|
+
middleware: Sequence[Middleware] | None = None,
|
|
41
|
+
compression_middleware: Middleware | None = BrotliMiddleware.as_middleware(),
|
|
42
|
+
exception_handlers: Mapping[Any, ExceptionHandler] | None = None,
|
|
43
|
+
lifespan: Lifespan[AppType] | None = None,
|
|
44
|
+
debug: bool = False,
|
|
45
|
+
router_class: type[StarRouter] = StarRouter,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initializes the application.
|
|
48
|
+
|
|
49
|
+
Parameters:
|
|
50
|
+
routes: A list of routes to serve incoming HTTP and WebSocket requests.
|
|
51
|
+
middleware: A list of middleware to run for every request. A starlette
|
|
52
|
+
application will always automatically include two middleware classes.
|
|
53
|
+
`ServerErrorMiddleware` is added as the very outermost middleware, to handle
|
|
54
|
+
any uncaught errors occurring anywhere in the entire stack.
|
|
55
|
+
`ExceptionMiddleware` is added as the very innermost middleware, to deal
|
|
56
|
+
with handled exception cases occurring in the routing or endpoints.
|
|
57
|
+
compression_middleware: A middleware class to compress the responses.
|
|
58
|
+
By default we opt for brotli compression with gzip fallback.
|
|
59
|
+
Parameters are what we think are reasonable for most use cases.
|
|
60
|
+
If you need tweaking those try `BrotliMiddleware.as_middleware()`.
|
|
61
|
+
exception_handlers: A mapping of either integer status codes,
|
|
62
|
+
or exception class types onto callables which handle the exceptions.
|
|
63
|
+
Exception handler callables should be of the form
|
|
64
|
+
`handler(request, exc) -> response` and may be either standard functions, or
|
|
65
|
+
async functions.
|
|
66
|
+
lifespan: A lifespan context function, which can be used to perform
|
|
67
|
+
startup and shutdown tasks. This is a newer style that replaces the
|
|
68
|
+
`on_startup` and `on_shutdown` handlers. Use one or the other, not both.
|
|
69
|
+
debug: Boolean indicating if debug tracebacks should be returned on errors.
|
|
70
|
+
router_class: A class to use for the router. By default we use `StarRouter`.
|
|
71
|
+
You can use this to customize the behaviour of the app, just consider
|
|
72
|
+
what are the implications :)
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
self.debug = debug
|
|
76
|
+
self.state = State()
|
|
77
|
+
self.router = router_class(*routes, lifespan=lifespan)
|
|
78
|
+
self.exception_handlers = (
|
|
79
|
+
{} if exception_handlers is None else dict(exception_handlers)
|
|
80
|
+
)
|
|
81
|
+
if compression_middleware is not None:
|
|
82
|
+
middleware = [] if middleware is None else list(middleware)
|
|
83
|
+
middleware.insert(0, compression_middleware)
|
|
84
|
+
self.user_middleware = [] if middleware is None else list(middleware)
|
|
85
|
+
self.middleware_stack: ASGIApp | None = None
|
|
86
|
+
|
|
87
|
+
self.cache: dict[Callable, Any] = {}
|
|
88
|
+
|
|
89
|
+
# self.dependency_overrides: dict[Callable, Callable] = {}
|
|
90
|
+
# """
|
|
91
|
+
# A dictionary with overrides for the dependencies.
|
|
92
|
+
|
|
93
|
+
# Each key is the original dependency callable, and the value is the
|
|
94
|
+
# actual dependency that should be called.
|
|
95
|
+
|
|
96
|
+
# This is for testing, to replace expensive dependencies with testing
|
|
97
|
+
# versions.
|
|
98
|
+
# """
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def routes(self) -> list[BaseRoute]:
|
|
102
|
+
return self.router.routes
|
|
103
|
+
|
|
104
|
+
def url_path_for(self, name: str, /, **path_params: Any) -> URLPath:
|
|
105
|
+
return self.router.url_path_for(name, **path_params)
|
|
106
|
+
|
|
107
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
108
|
+
scope["app"] = self
|
|
109
|
+
if self.middleware_stack is None:
|
|
110
|
+
# Delegate to Starlette to build the middleware stack
|
|
111
|
+
# - we like it so there's no need to re-implement it
|
|
112
|
+
# Starlette expects its own instance; use the instance method to
|
|
113
|
+
# construct the stack with our attributes
|
|
114
|
+
self.middleware_stack = Starlette.build_middleware_stack(self) # type: ignore[arg-type]
|
|
115
|
+
await self.middleware_stack(scope, receive, send)
|
|
116
|
+
|
|
117
|
+
def add(self, route: BaseRoute) -> None:
|
|
118
|
+
# We diverge from Starlette here because I think having more control over
|
|
119
|
+
# the process of adding routes is more important for us in context of this library
|
|
120
|
+
self.router.add(route)
|
|
121
|
+
|
|
122
|
+
def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None:
|
|
123
|
+
self.router.mount(path, app=app, name=name)
|
|
124
|
+
|
|
125
|
+
def host(self, host: str, app: ASGIApp, name: str | None = None) -> None:
|
|
126
|
+
self.router.host(host, app=app, name=name)
|
|
127
|
+
|
|
128
|
+
def add_middleware(
|
|
129
|
+
self,
|
|
130
|
+
middleware_class: _MiddlewareFactory[P],
|
|
131
|
+
*args: P.args,
|
|
132
|
+
**kwargs: P.kwargs,
|
|
133
|
+
) -> None:
|
|
134
|
+
if self.middleware_stack is not None:
|
|
135
|
+
raise RuntimeError("Cannot add middleware after an application has started")
|
|
136
|
+
self.user_middleware.insert(0, Middleware(middleware_class, *args, **kwargs))
|
|
137
|
+
|
|
138
|
+
def add_exception_handler(
|
|
139
|
+
self,
|
|
140
|
+
exc_class_or_status_code: int | type[Exception],
|
|
141
|
+
handler: ExceptionHandler,
|
|
142
|
+
) -> None:
|
|
143
|
+
self.exception_handlers[exc_class_or_status_code] = handler
|
|
144
|
+
|
|
145
|
+
def add_event_handler(
|
|
146
|
+
self,
|
|
147
|
+
event_type: str,
|
|
148
|
+
func: Callable,
|
|
149
|
+
) -> None:
|
|
150
|
+
self.router.add_event_handler(event_type, func)
|