nicegui.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import asyncio
  2. import mimetypes
  3. import urllib.parse
  4. from contextlib import asynccontextmanager
  5. from pathlib import Path
  6. from typing import Dict
  7. import socketio
  8. from fastapi import HTTPException, Request
  9. from fastapi.middleware.gzip import GZipMiddleware
  10. from fastapi.responses import FileResponse, Response
  11. from fastapi.staticfiles import StaticFiles
  12. from . import air, background_tasks, binding, core, favicon, helpers, json, run, welcome
  13. from .app import App
  14. from .client import Client
  15. from .dependencies import js_components, libraries, resources
  16. from .error import error_content
  17. from .json import NiceGUIJSONResponse
  18. from .logging import log
  19. from .middlewares import RedirectWithPrefixMiddleware
  20. from .page import page
  21. from .slot import Slot
  22. from .version import __version__
  23. @asynccontextmanager
  24. async def _lifespan(_: App):
  25. await _startup()
  26. yield
  27. await _shutdown()
  28. core.app = app = App(default_response_class=NiceGUIJSONResponse, lifespan=_lifespan)
  29. # NOTE we use custom json module which wraps orjson
  30. core.sio = sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*', json=json)
  31. sio_app = socketio.ASGIApp(socketio_server=sio, socketio_path='/_nicegui_ws/socket.io')
  32. app.mount('/_nicegui_ws/', sio_app)
  33. mimetypes.add_type('text/javascript', '.js')
  34. mimetypes.add_type('text/css', '.css')
  35. app.add_middleware(GZipMiddleware)
  36. app.add_middleware(RedirectWithPrefixMiddleware)
  37. static_files = StaticFiles(
  38. directory=(Path(__file__).parent / 'static').resolve(),
  39. follow_symlink=True,
  40. )
  41. app.mount(f'/_nicegui/{__version__}/static', static_files, name='static')
  42. Client.auto_index_client = Client(page('/'), shared=True).__enter__() # pylint: disable=unnecessary-dunder-call
  43. @app.get('/')
  44. def _get_index(request: Request) -> Response:
  45. return Client.auto_index_client.build_response(request)
  46. @app.get(f'/_nicegui/{__version__}' + '/libraries/{key:path}')
  47. def _get_library(key: str) -> FileResponse:
  48. is_map = key.endswith('.map')
  49. dict_key = key[:-4] if is_map else key
  50. if dict_key in libraries:
  51. path = libraries[dict_key].path
  52. if is_map:
  53. path = path.with_name(path.name + '.map')
  54. if path.exists():
  55. headers = {'Cache-Control': 'public, max-age=3600'}
  56. return FileResponse(path, media_type='text/javascript', headers=headers)
  57. raise HTTPException(status_code=404, detail=f'library "{key}" not found')
  58. @app.get(f'/_nicegui/{__version__}' + '/components/{key:path}')
  59. def _get_component(key: str) -> FileResponse:
  60. if key in js_components and js_components[key].path.exists():
  61. headers = {'Cache-Control': 'public, max-age=3600'}
  62. return FileResponse(js_components[key].path, media_type='text/javascript', headers=headers)
  63. raise HTTPException(status_code=404, detail=f'component "{key}" not found')
  64. @app.get(f'/_nicegui/{__version__}' + '/resources/{key}/{path:path}')
  65. def _get_resource(key: str, path: str) -> FileResponse:
  66. if key in resources:
  67. filepath = resources[key].path / path
  68. if filepath.exists():
  69. headers = {'Cache-Control': 'public, max-age=3600'}
  70. media_type, _ = mimetypes.guess_type(filepath)
  71. return FileResponse(filepath, media_type=media_type, headers=headers)
  72. raise HTTPException(status_code=404, detail=f'resource "{key}" not found')
  73. async def _startup() -> None:
  74. """Handle the startup event."""
  75. if not app.config.has_run_config:
  76. raise RuntimeError('\n\n'
  77. 'You must call ui.run() to start the server.\n'
  78. 'If ui.run() is behind a main guard\n'
  79. ' if __name__ == "__main__":\n'
  80. 'remove the guard or replace it with\n'
  81. ' if __name__ in {"__main__", "__mp_main__"}:\n'
  82. 'to allow for multiprocessing.')
  83. await welcome.collect_urls()
  84. # NOTE ping interval and timeout need to be lower than the reconnect timeout, but can't be too low
  85. sio.eio.ping_interval = max(app.config.reconnect_timeout * 0.8, 4)
  86. sio.eio.ping_timeout = max(app.config.reconnect_timeout * 0.4, 2)
  87. if core.app.config.favicon:
  88. if helpers.is_file(core.app.config.favicon):
  89. app.add_route('/favicon.ico', lambda _: FileResponse(core.app.config.favicon)) # type: ignore
  90. else:
  91. app.add_route('/favicon.ico', lambda _: favicon.get_favicon_response())
  92. else:
  93. app.add_route('/favicon.ico', lambda _: FileResponse(Path(__file__).parent / 'static' / 'favicon.ico'))
  94. core.loop = asyncio.get_running_loop()
  95. app.start()
  96. background_tasks.create(binding.refresh_loop(), name='refresh bindings')
  97. background_tasks.create(Client.prune_instances(), name='prune clients')
  98. background_tasks.create(Slot.prune_stacks(), name='prune slot stacks')
  99. air.connect()
  100. async def _shutdown() -> None:
  101. """Handle the shutdown event."""
  102. if app.native.main_window:
  103. app.native.main_window.signal_server_shutdown()
  104. air.disconnect()
  105. app.stop()
  106. run.tear_down()
  107. @app.exception_handler(404)
  108. async def _exception_handler_404(request: Request, exception: Exception) -> Response:
  109. log.warning(f'{request.url} not found')
  110. with Client(page('')) as client:
  111. error_content(404, exception)
  112. return client.build_response(request, 404)
  113. @app.exception_handler(Exception)
  114. async def _exception_handler_500(request: Request, exception: Exception) -> Response:
  115. log.exception(exception)
  116. with Client(page('')) as client:
  117. error_content(500, exception)
  118. return client.build_response(request, 500)
  119. @sio.on('handshake')
  120. async def _on_handshake(sid: str, client_id: str) -> bool:
  121. client = Client.instances.get(client_id)
  122. if not client:
  123. return False
  124. client.environ = sio.get_environ(sid)
  125. await sio.enter_room(sid, client.id)
  126. client.handle_handshake()
  127. return True
  128. @sio.on('disconnect')
  129. def _on_disconnect(sid: str) -> None:
  130. query_bytes: bytearray = sio.get_environ(sid)['asgi.scope']['query_string']
  131. query = urllib.parse.parse_qs(query_bytes.decode())
  132. client_id = query['client_id'][0]
  133. client = Client.instances.get(client_id)
  134. if client:
  135. client.handle_disconnect()
  136. @sio.on('event')
  137. def _on_event(_: str, msg: Dict) -> None:
  138. client = Client.instances.get(msg['client_id'])
  139. if not client or not client.has_socket_connection:
  140. return
  141. client.handle_event(msg)
  142. @sio.on('javascript_response')
  143. def _on_javascript_response(_: str, msg: Dict) -> None:
  144. client = Client.instances.get(msg['client_id'])
  145. if not client:
  146. return
  147. client.handle_javascript_response(msg)