6
6
from datetime import datetime , timezone
7
7
from email .utils import formatdate
8
8
from logging import getLogger
9
+ from typing import Callable , Literal , cast , overload
9
10
10
11
from asgiref import typing as asgi_types
11
12
from typing_extensions import Unpack
12
13
13
14
from reactpy import html
14
15
from reactpy .asgi .middleware import ReactPyMiddleware
15
- from reactpy .asgi .utils import dict_to_byte_list , http_response , vdom_head_to_html
16
- from reactpy .types import ReactPyConfig , RootComponentConstructor , VdomDict
16
+ from reactpy .asgi .utils import (
17
+ dict_to_byte_list ,
18
+ http_response ,
19
+ import_dotted_path ,
20
+ vdom_head_to_html ,
21
+ )
22
+ from reactpy .types import (
23
+ AsgiApp ,
24
+ AsgiHttpApp ,
25
+ AsgiLifespanApp ,
26
+ AsgiWebsocketApp ,
27
+ ReactPyConfig ,
28
+ RootComponentConstructor ,
29
+ VdomDict ,
30
+ )
17
31
from reactpy .utils import render_mount_template
18
32
19
33
_logger = getLogger (__name__ )
@@ -34,7 +48,7 @@ def __init__(
34
48
"""ReactPy's standalone ASGI application.
35
49
36
50
Parameters:
37
- root_component: The root component to render. This component is assumed to be a single page application.
51
+ root_component: The root component to render. This app is typically a single page application.
38
52
http_headers: Additional headers to include in the HTTP response for the base HTML document.
39
53
html_head: Additional head elements to include in the HTML response.
40
54
html_lang: The language of the HTML document.
@@ -51,6 +65,89 @@ def match_dispatch_path(self, scope: asgi_types.WebSocketScope) -> bool:
51
65
"""Method override to remove `dotted_path` from the dispatcher URL."""
52
66
return str (scope ["path" ]) == self .dispatcher_path
53
67
68
+ def match_extra_paths (self , scope : asgi_types .Scope ) -> AsgiApp | None :
69
+ """Method override to match user-provided HTTP/Websocket routes."""
70
+ if scope ["type" ] == "lifespan" :
71
+ return self .extra_lifespan_app
72
+
73
+ if scope ["type" ] == "http" :
74
+ routing_dictionary = self .extra_http_routes .items ()
75
+
76
+ if scope ["type" ] == "websocket" :
77
+ routing_dictionary = self .extra_ws_routes .items () # type: ignore
78
+
79
+ return next (
80
+ (
81
+ app
82
+ for route , app in routing_dictionary
83
+ if re .match (route , scope ["path" ])
84
+ ),
85
+ None ,
86
+ )
87
+
88
+ @overload
89
+ def route (
90
+ self ,
91
+ path : str ,
92
+ type : Literal ["http" ] = "http" ,
93
+ ) -> Callable [[AsgiHttpApp | str ], AsgiApp ]: ...
94
+
95
+ @overload
96
+ def route (
97
+ self ,
98
+ path : str ,
99
+ type : Literal ["websocket" ],
100
+ ) -> Callable [[AsgiWebsocketApp | str ], AsgiApp ]: ...
101
+
102
+ def route (
103
+ self ,
104
+ path : str ,
105
+ type : Literal ["http" , "websocket" ] = "http" ,
106
+ ) -> (
107
+ Callable [[AsgiHttpApp | str ], AsgiApp ]
108
+ | Callable [[AsgiWebsocketApp | str ], AsgiApp ]
109
+ ):
110
+ """Interface that allows user to define their own HTTP/Websocket routes
111
+ within the current ReactPy application.
112
+
113
+ Parameters:
114
+ path: The URL route to match, using regex format.
115
+ type: The protocol to route for. Can be 'http' or 'websocket'.
116
+ """
117
+
118
+ def decorator (
119
+ app : AsgiApp | str ,
120
+ ) -> AsgiApp :
121
+ re_path = path
122
+ if not re_path .startswith ("^" ):
123
+ re_path = f"^{ re_path } "
124
+ if not re_path .endswith ("$" ):
125
+ re_path = f"{ re_path } $"
126
+
127
+ asgi_app : AsgiApp = import_dotted_path (app ) if isinstance (app , str ) else app
128
+ if type == "http" :
129
+ self .extra_http_routes [re_path ] = cast (AsgiHttpApp , asgi_app )
130
+ elif type == "websocket" :
131
+ self .extra_ws_routes [re_path ] = cast (AsgiWebsocketApp , asgi_app )
132
+
133
+ return asgi_app
134
+
135
+ return decorator
136
+
137
+ def lifespan (self , app : AsgiLifespanApp | str ) -> None :
138
+ """Interface that allows user to define their own lifespan app
139
+ within the current ReactPy application.
140
+
141
+ Parameters:
142
+ app: The ASGI application to route to.
143
+ """
144
+ if self .extra_lifespan_app :
145
+ raise ValueError ("Only one lifespan app can be defined." )
146
+
147
+ self .extra_lifespan_app = (
148
+ import_dotted_path (app ) if isinstance (app , str ) else app
149
+ )
150
+
54
151
55
152
@dataclass
56
153
class ReactPyApp :
0 commit comments