nicegui.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import asyncio
  2. import time
  3. import urllib.parse
  4. from pathlib import Path
  5. from typing import Dict, Optional
  6. from fastapi import HTTPException, Request
  7. from fastapi.middleware.gzip import GZipMiddleware
  8. from fastapi.responses import FileResponse, Response
  9. from fastapi.staticfiles import StaticFiles
  10. from fastapi_socketio import SocketManager
  11. from . import background_tasks, binding, globals
  12. from .app import App
  13. from .client import Client
  14. from .dependencies import js_components, js_dependencies
  15. from .element import Element
  16. from .error import error_content
  17. from .helpers import safe_invoke
  18. from .page import page
  19. globals.app = app = App()
  20. globals.sio = sio = SocketManager(app=app)._sio
  21. app.add_middleware(GZipMiddleware)
  22. app.mount('/_nicegui/static', StaticFiles(directory=Path(__file__).parent / 'static'), name='static')
  23. globals.index_client = Client(page('/'), shared=True).__enter__()
  24. @app.get('/')
  25. def index(request: Request) -> Response:
  26. return globals.index_client.build_response(request)
  27. @app.get('/_nicegui/dependencies/{id}/{name}')
  28. def get_dependencies(id: int, name: str):
  29. if id in js_dependencies and js_dependencies[id].path.exists() and js_dependencies[id].path.name == name:
  30. return FileResponse(js_dependencies[id].path, media_type='text/javascript')
  31. raise HTTPException(status_code=404, detail=f'dependency "{name}" with ID {id} not found')
  32. @app.get('/_nicegui/components/{name}')
  33. def get_components(name: str):
  34. return FileResponse(js_components[name].path, media_type='text/javascript')
  35. @app.on_event('startup')
  36. def handle_startup(with_welcome_message: bool = True) -> None:
  37. globals.state = globals.State.STARTING
  38. globals.loop = asyncio.get_running_loop()
  39. for t in globals.startup_handlers:
  40. safe_invoke(t)
  41. background_tasks.create(binding.loop())
  42. background_tasks.create(prune_clients())
  43. background_tasks.create(prune_slot_stacks())
  44. globals.state = globals.State.STARTED
  45. if with_welcome_message:
  46. print(f'NiceGUI ready to go on http://{globals.host}:{globals.port}')
  47. @app.on_event('shutdown')
  48. def handle_shutdown() -> None:
  49. globals.state = globals.State.STOPPING
  50. for t in globals.shutdown_handlers:
  51. safe_invoke(t)
  52. for t in background_tasks.running_tasks:
  53. t.cancel()
  54. globals.state = globals.State.STOPPED
  55. @app.exception_handler(404)
  56. async def exception_handler_404(request: Request, exception: Exception) -> Response:
  57. globals.log.warning(f'{request.url} not found')
  58. with Client(page('')) as client:
  59. error_content(404, exception)
  60. return client.build_response(request, 404)
  61. @app.exception_handler(Exception)
  62. async def exception_handler_500(request: Request, exception: Exception) -> Response:
  63. globals.log.exception(exception)
  64. with Client(page('')) as client:
  65. error_content(500, exception)
  66. return client.build_response(request, 500)
  67. @sio.on('handshake')
  68. async def handle_handshake(sid: str) -> bool:
  69. client = get_client(sid)
  70. if not client:
  71. return False
  72. client.environ = sio.get_environ(sid)
  73. sio.enter_room(sid, client.id)
  74. for t in client.connect_handlers:
  75. safe_invoke(t, client)
  76. for t in globals.connect_handlers:
  77. safe_invoke(t, client)
  78. return True
  79. @sio.on('disconnect')
  80. async def handle_disconnect(sid: str) -> None:
  81. client = get_client(sid)
  82. if not client:
  83. return
  84. if not client.shared:
  85. delete_client(client.id)
  86. for t in client.disconnect_handlers:
  87. safe_invoke(t, client)
  88. for t in globals.disconnect_handlers:
  89. safe_invoke(t, client)
  90. @sio.on('event')
  91. def handle_event(sid: str, msg: Dict) -> None:
  92. client = get_client(sid)
  93. if not client or not client.has_socket_connection:
  94. return
  95. with client:
  96. sender = client.elements.get(msg['id'])
  97. if sender:
  98. sender.handle_event(msg)
  99. @sio.on('javascript_response')
  100. def handle_javascript_response(sid: str, msg: Dict) -> None:
  101. client = get_client(sid)
  102. if not client:
  103. return
  104. client.waiting_javascript_commands[msg['request_id']] = msg['result']
  105. def get_client(sid: str) -> Optional[Client]:
  106. query_bytes: bytearray = sio.get_environ(sid)['asgi.scope']['query_string']
  107. query = urllib.parse.parse_qs(query_bytes.decode())
  108. client_id = query['client_id'][0]
  109. return globals.clients.get(client_id)
  110. async def prune_clients() -> None:
  111. while True:
  112. stale = [
  113. id
  114. for id, client in globals.clients.items()
  115. if not client.shared and not client.has_socket_connection and client.created < time.time() - 60.0
  116. ]
  117. for id in stale:
  118. delete_client(id)
  119. await asyncio.sleep(10)
  120. async def prune_slot_stacks() -> None:
  121. while True:
  122. running = [
  123. id(task)
  124. for task in asyncio.tasks.all_tasks()
  125. if not task.done() and not task.cancelled()
  126. ]
  127. stale = [
  128. id_
  129. for id_ in globals.slot_stacks
  130. if id_ not in running
  131. ]
  132. for id_ in stale:
  133. del globals.slot_stacks[id_]
  134. await asyncio.sleep(10)
  135. def delete_client(id: str) -> None:
  136. binding.remove(list(globals.clients[id].elements.values()), Element)
  137. del globals.clients[id]