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 FastAPI, 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 binding, globals
  12. from .client import Client
  13. from .dependencies import js_components, js_dependencies
  14. from .element import Element
  15. from .error import error_content
  16. from .helpers import safe_invoke
  17. from .page import page
  18. from .task_logger import create_task
  19. globals.app = app = FastAPI()
  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. create_task(binding.loop())
  42. create_task(prune_clients())
  43. create_task(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 globals.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]