1- import traceback
2-
3- from fastapi import FastAPI , Request , Response , APIRouter
4- from fastapi .responses import JSONResponse
5- from dash .exceptions import PreventUpdate , InvalidResourceError
6- from dash .server_factories import set_request_adapter , get_request_adapter
7- from .base_factory import BaseServerFactory
1+ import sys
2+ import mimetypes
3+ import hashlib
84import inspect
95import pkgutil
106from contextvars import copy_context
117import importlib .util
8+ import time
9+ import uvicorn
10+ from fastapi import FastAPI , Request , Response
11+ from fastapi .responses import JSONResponse , PlainTextResponse
12+ from fastapi .staticfiles import StaticFiles
13+ from starlette .responses import Response as StarletteResponse
14+ from starlette .datastructures import MutableHeaders
15+ from dash .fingerprint import check_fingerprint
16+ from dash import _validate
17+ from dash .exceptions import PreventUpdate , InvalidResourceError
18+ from dash .server_factories import set_request_adapter
19+ from .base_factory import BaseServerFactory
1220
1321
1422class FastAPIServerFactory (BaseServerFactory ):
@@ -32,8 +40,6 @@ def create_app(self, name="__main__", config=None):
3240 def register_assets_blueprint (
3341 self , app , blueprint_name , assets_url_path , assets_folder
3442 ):
35- from fastapi .staticfiles import StaticFiles
36-
3743 try :
3844 app .mount (
3945 assets_url_path ,
@@ -46,17 +52,21 @@ def register_assets_blueprint(
4652
4753 def register_error_handlers (self , app ):
4854 @app .exception_handler (PreventUpdate )
49- async def _handle_error (request : Request , exc : PreventUpdate ):
55+ async def _handle_error (_request , _exc ):
5056 return Response (status_code = 204 )
5157
5258 @app .exception_handler (InvalidResourceError )
53- async def _invalid_resources_handler (
54- request : Request , exc : InvalidResourceError
55- ):
59+ async def _invalid_resources_handler (_request , exc ):
5660 return Response (content = exc .args [0 ], status_code = 404 )
5761
62+ def register_prune_error_handler (self , app , secret , get_traceback_func ):
63+ @app .exception_handler (Exception )
64+ async def _wrap_errors (_error_request , error ):
65+ tb = get_traceback_func (secret , error )
66+ return PlainTextResponse (tb , status_code = 500 )
67+
5868 def _html_response_wrapper (self , view_func ):
59- async def wrapped (* args , ** kwargs ):
69+ async def wrapped (* _args , ** _kwargs ):
6070 # If view_func is a function, call it; if it's a string, use it directly
6171 html = view_func () if callable (view_func ) else view_func
6272 return Response (content = html , media_type = "text/html" )
@@ -75,10 +85,11 @@ async def index(request: Request):
7585 def setup_catchall (self , app , dash_app ):
7686 @dash_app .server .on_event ("startup" )
7787 def _setup_catchall ():
78- dash_app .enable_dev_tools (** self .config ) # do this to make sure dev tools are enabled
79- from fastapi import Request , Response
88+ dash_app .enable_dev_tools (
89+ ** self .config , first_run = False
90+ ) # do this to make sure dev tools are enabled
8091
81- async def catchall (path : str , request : Request ):
92+ async def catchall (_path : str , request : Request ):
8293 adapter = FastAPIRequestAdapter ()
8394 set_request_adapter (adapter )
8495 adapter .set_request (request )
@@ -88,8 +99,6 @@ async def catchall(path: str, request: Request):
8899 app , "/{path:path}" , catchall , endpoint = "catchall" , methods = ["GET" ]
89100 )
90101
91- pass # catchall needs to be last to not override other routes
92-
93102 def add_url_rule (self , app , rule , view_func , endpoint = None , methods = None ):
94103 if rule == "" :
95104 rule = "/"
@@ -114,11 +123,7 @@ def after_request(self, app, func):
114123
115124 def run (self , app , host , port , debug , ** kwargs ):
116125 frame = inspect .stack ()[2 ]
117- import uvicorn
118-
119- self .config = dict ({'debug' : debug } if debug else {}, ** kwargs )
120-
121-
126+ self .config = dict ({"debug" : debug } if debug else {}, ** kwargs )
122127 reload = debug
123128 if reload :
124129 # Dynamically determine the module name from the file path
@@ -149,8 +154,6 @@ def get_request_adapter(self):
149154 return FastAPIRequestAdapter
150155
151156 def _make_before_middleware (self , func ):
152- pass
153-
154157 async def middleware (request , call_next ):
155158 if func is not None :
156159 if inspect .iscoroutinefunction (func ):
@@ -163,24 +166,20 @@ async def middleware(request, call_next):
163166 return middleware
164167
165168 def _make_after_middleware (self , func ):
166- pass
167-
168169 async def middleware (request , call_next ):
169170 response = await call_next (request )
170- await func ()
171+ if func is not None :
172+ if inspect .iscoroutinefunction (func ):
173+ await func ()
174+ else :
175+ func ()
171176 return response
172177
173178 return middleware
174179
175180 def serve_component_suites (
176181 self , dash_app , package_name , fingerprinted_path , request
177182 ):
178- import sys
179- import mimetypes
180- import pkgutil
181- from dash .fingerprint import check_fingerprint
182- from dash import _validate
183-
184183 path_in_pkg , has_fingerprint = check_fingerprint (fingerprinted_path )
185184 _validate .validate_js_path (dash_app .registered_paths , package_name , path_in_pkg )
186185 extension = "." + path_in_pkg .split ("." )[- 1 ]
@@ -194,24 +193,17 @@ def serve_component_suites(
194193 package .__path__ ,
195194 )
196195 data = pkgutil .get_data (package_name , path_in_pkg )
197- from starlette .responses import Response as StarletteResponse
198-
199196 headers = {}
200197 if has_fingerprint :
201198 headers ["Cache-Control" ] = "public, max-age=31536000"
202199 return StarletteResponse (content = data , media_type = mimetype , headers = headers )
203- else :
204- import hashlib
205-
206- etag = hashlib .md5 (data ).hexdigest () if data else ""
207- headers ["ETag" ] = etag
208- if request .headers .get ("if-none-match" ) == etag :
209- return StarletteResponse (status_code = 304 )
210- return StarletteResponse (content = data , media_type = mimetype , headers = headers )
200+ etag = hashlib .md5 (data ).hexdigest () if data else ""
201+ headers ["ETag" ] = etag
202+ if request .headers .get ("if-none-match" ) == etag :
203+ return StarletteResponse (status_code = 304 )
204+ return StarletteResponse (content = data , media_type = mimetype , headers = headers )
211205
212206 def setup_component_suites (self , app , dash_app ):
213- from fastapi import Request
214-
215207 async def serve (request : Request , package_name : str , fingerprinted_path : str ):
216208 return self .serve_component_suites (
217209 dash_app , package_name , fingerprinted_path , request
@@ -223,17 +215,26 @@ async def serve(request: Request, package_name: str, fingerprinted_path: str):
223215 serve ,
224216 )
225217
226- def dispatch (self , app , dash_app , use_async ):
218+ def dispatch (self , _app , dash_app , _use_async ):
227219 async def _dispatch (request : Request ):
228220 adapter = FastAPIRequestAdapter ()
229221 set_request_adapter (adapter )
230222 adapter .set_request (request )
223+ # pylint: disable=protected-access
231224 body = await request .json ()
232- g = dash_app ._initialize_context (body , adapter )
233- func = dash_app ._prepare_callback (g , body )
234- args = dash_app ._inputs_to_vals (g .inputs_list + g .states_list )
225+ g = dash_app ._initialize_context (
226+ body , adapter
227+ ) # pylint: disable=protected-access
228+ func = dash_app ._prepare_callback (
229+ g , body
230+ ) # pylint: disable=protected-access
231+ args = dash_app ._inputs_to_vals (
232+ g .inputs_list + g .states_list
233+ ) # pylint: disable=protected-access
235234 ctx = copy_context ()
236- partial_func = dash_app ._execute_callback (func , args , g .outputs_list , g )
235+ partial_func = dash_app ._execute_callback (
236+ func , args , g .outputs_list , g
237+ ) # pylint: disable=protected-access
237238 response_data = ctx .run (partial_func )
238239 if inspect .iscoroutine (response_data ):
239240 response_data = await response_data
@@ -247,6 +248,33 @@ def _serve_default_favicon(self):
247248 content = pkgutil .get_data ("dash" , "favicon.ico" ), media_type = "image/x-icon"
248249 )
249250
251+ def register_timing_hooks (self , app , first_run ):
252+ if not first_run :
253+ return
254+
255+ @app .middleware ("http" )
256+ async def timing_middleware (request , call_next ):
257+ # Before request
258+ request .state .timing_information = {
259+ "__dash_server" : {"dur" : time .time (), "desc" : None }
260+ }
261+ response = await call_next (request )
262+ # After request
263+ timing_information = getattr (request .state , "timing_information" , None )
264+ if timing_information is not None :
265+ dash_total = timing_information .get ("__dash_server" , None )
266+ if dash_total is not None :
267+ dash_total ["dur" ] = round ((time .time () - dash_total ["dur" ]) * 1000 )
268+ headers = MutableHeaders (response .headers )
269+ for name , info in timing_information .items ():
270+ value = name
271+ if info .get ("desc" ) is not None :
272+ value += f';desc="{ info ["desc" ]} "'
273+ if info .get ("dur" ) is not None :
274+ value += f";dur={ info ['dur' ]} "
275+ headers .append ("Server-Timing" , value )
276+ return response
277+
250278
251279class FastAPIRequestAdapter :
252280 def __init__ (self ):
@@ -266,7 +294,7 @@ def is_json(self):
266294 "application/json"
267295 )
268296
269- def get_cookies (self , request = None ):
297+ def get_cookies (self , _request = None ):
270298 return self ._request .cookies
271299
272300 def get_headers (self ):
@@ -282,4 +310,4 @@ def get_origin(self):
282310 return self ._request .headers .get ("origin" )
283311
284312 def get_path (self ):
285- return self ._request .url .path # <-- Add this method
313+ return self ._request .url .path
0 commit comments