ui_run.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import multiprocessing
  2. import os
  3. import sys
  4. from pathlib import Path
  5. from typing import Any, List, Literal, Optional, Tuple, TypedDict, Union
  6. from fastapi.middleware.gzip import GZipMiddleware
  7. from starlette.routing import Route
  8. from uvicorn.main import STARTUP_FAILURE
  9. from uvicorn.supervisors import ChangeReload, Multiprocess
  10. import __main__
  11. from . import core, helpers
  12. from . import native as native_module
  13. from .air import Air
  14. from .client import Client
  15. from .language import Language
  16. from .logging import log
  17. from .middlewares import RedirectWithPrefixMiddleware
  18. from .server import CustomServerConfig, Server
  19. APP_IMPORT_STRING = 'nicegui:app'
  20. class ContactDict(TypedDict):
  21. name: Optional[str]
  22. url: Optional[str]
  23. email: Optional[str]
  24. class LicenseInfoDict(TypedDict):
  25. name: str
  26. identifier: Optional[str]
  27. url: Optional[str]
  28. class DocsConfig(TypedDict):
  29. title: Optional[str]
  30. summary: Optional[str]
  31. description: Optional[str]
  32. version: Optional[str]
  33. terms_of_service: Optional[str]
  34. contact: Optional[ContactDict]
  35. license_info: Optional[LicenseInfoDict]
  36. def run(*,
  37. host: Optional[str] = None,
  38. port: Optional[int] = None,
  39. title: str = 'NiceGUI',
  40. viewport: str = 'width=device-width, initial-scale=1',
  41. favicon: Optional[Union[str, Path]] = None,
  42. dark: Optional[bool] = False,
  43. language: Language = 'en-US',
  44. binding_refresh_interval: float = 0.1,
  45. reconnect_timeout: float = 3.0,
  46. message_history_length: int = 1000,
  47. fastapi_docs: Union[bool, DocsConfig] = False,
  48. show: bool = True,
  49. on_air: Optional[Union[str, Literal[True]]] = None,
  50. native: bool = False,
  51. window_size: Optional[Tuple[int, int]] = None,
  52. fullscreen: bool = False,
  53. frameless: bool = False,
  54. reload: bool = True,
  55. uvicorn_logging_level: str = 'warning',
  56. uvicorn_reload_dirs: str = '.',
  57. uvicorn_reload_includes: str = '*.py',
  58. uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
  59. tailwind: bool = True,
  60. prod_js: bool = True,
  61. endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none',
  62. storage_secret: Optional[str] = None,
  63. show_welcome_message: bool = True,
  64. **kwargs: Any,
  65. ) -> None:
  66. """ui.run
  67. You can call `ui.run()` with optional arguments.
  68. Most of them only apply after stopping and fully restarting the app and do not apply with auto-reloading.
  69. :param host: start server with this host (defaults to `'127.0.0.1` in native mode, otherwise `'0.0.0.0'`)
  70. :param port: use this port (default: 8080 in normal mode, and an automatically determined open port in native mode)
  71. :param title: page title (default: `'NiceGUI'`, can be overwritten per page)
  72. :param viewport: page meta viewport content (default: `'width=device-width, initial-scale=1'`, can be overwritten per page)
  73. :param favicon: relative filepath, absolute URL to a favicon (default: `None`, NiceGUI icon will be used) or emoji (e.g. `'🚀'`, works for most browsers)
  74. :param dark: whether to use Quasar's dark mode (default: `False`, use `None` for "auto" mode)
  75. :param language: language for Quasar elements (default: `'en-US'`)
  76. :param binding_refresh_interval: time between binding updates (default: `0.1` seconds, bigger is more CPU friendly)
  77. :param reconnect_timeout: maximum time the server waits for the browser to reconnect (default: 3.0 seconds)
  78. :param message_history_length: maximum number of messages that will be stored and resent after a connection interruption (default: 1000, use 0 to disable)
  79. :param fastapi_docs: enable FastAPI's automatic documentation with Swagger UI, ReDoc, and OpenAPI JSON (bool or dictionary as described `here<https://fastapi.tiangolo.com/tutorial/metadata/>`_, default: `False`)
  80. :param show: automatically open the UI in a browser tab (default: `True`)
  81. :param on_air: tech preview: `allows temporary remote access <https://nicegui.io/documentation/section_configuration_deployment#nicegui_on_air>`_ if set to `True` (default: disabled)
  82. :param native: open the UI in a native window of size 800x600 (default: `False`, deactivates `show`, automatically finds an open port)
  83. :param window_size: open the UI in a native window with the provided size (e.g. `(1024, 786)`, default: `None`, also activates `native`)
  84. :param fullscreen: open the UI in a fullscreen window (default: `False`, also activates `native`)
  85. :param frameless: open the UI in a frameless window (default: `False`, also activates `native`)
  86. :param reload: automatically reload the UI on file changes (default: `True`)
  87. :param uvicorn_logging_level: logging level for uvicorn server (default: `'warning'`)
  88. :param uvicorn_reload_dirs: string with comma-separated list for directories to be monitored (default is current working directory only)
  89. :param uvicorn_reload_includes: string with comma-separated list of glob-patterns which trigger reload on modification (default: `'*.py'`)
  90. :param uvicorn_reload_excludes: string with comma-separated list of glob-patterns which should be ignored for reload (default: `'.*, .py[cod], .sw.*, ~*'`)
  91. :param tailwind: whether to use Tailwind (experimental, default: `True`)
  92. :param prod_js: whether to use the production version of Vue and Quasar dependencies (default: `True`)
  93. :param endpoint_documentation: control what endpoints appear in the autogenerated OpenAPI docs (default: 'none', options: 'none', 'internal', 'page', 'all')
  94. :param storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.individual and ui.storage.browser)
  95. :param show_welcome_message: whether to show the welcome message (default: `True`)
  96. :param kwargs: additional keyword arguments are passed to `uvicorn.run`
  97. """
  98. core.app.config.add_run_config(
  99. reload=reload,
  100. title=title,
  101. viewport=viewport,
  102. favicon=favicon,
  103. dark=dark,
  104. language=language,
  105. binding_refresh_interval=binding_refresh_interval,
  106. reconnect_timeout=reconnect_timeout,
  107. message_history_length=message_history_length,
  108. tailwind=tailwind,
  109. prod_js=prod_js,
  110. show_welcome_message=show_welcome_message,
  111. )
  112. core.app.config.endpoint_documentation = endpoint_documentation
  113. if not helpers.is_pytest():
  114. core.app.add_middleware(GZipMiddleware)
  115. core.app.add_middleware(RedirectWithPrefixMiddleware)
  116. for route in core.app.routes:
  117. if not isinstance(route, Route):
  118. continue
  119. if route.path.startswith('/_nicegui') and hasattr(route, 'methods'):
  120. route.include_in_schema = endpoint_documentation in {'internal', 'all'}
  121. if route.path == '/' or route.path in Client.page_routes.values():
  122. route.include_in_schema = endpoint_documentation in {'page', 'all'}
  123. if fastapi_docs:
  124. if not core.app.docs_url:
  125. core.app.docs_url = '/docs'
  126. if not core.app.redoc_url:
  127. core.app.redoc_url = '/redoc'
  128. if not core.app.openapi_url:
  129. core.app.openapi_url = '/openapi.json'
  130. if isinstance(fastapi_docs, dict):
  131. core.app.title = fastapi_docs.get('title') or title
  132. core.app.summary = fastapi_docs.get('summary')
  133. core.app.description = fastapi_docs.get('description') or ''
  134. core.app.version = fastapi_docs.get('version') or '0.1.0'
  135. core.app.terms_of_service = fastapi_docs.get('terms_of_service')
  136. contact = fastapi_docs.get('contact')
  137. license_info = fastapi_docs.get('license_info')
  138. core.app.contact = dict(contact) if contact else None
  139. core.app.license_info = dict(license_info) if license_info else None
  140. core.app.setup()
  141. if on_air:
  142. core.air = Air('' if on_air is True else on_air)
  143. if multiprocessing.current_process().name != 'MainProcess':
  144. return
  145. if reload and not hasattr(__main__, '__file__'):
  146. log.warning('disabling auto-reloading because is is only supported when running from a file')
  147. core.app.config.reload = reload = False
  148. if fullscreen:
  149. native = True
  150. if frameless:
  151. native = True
  152. if window_size:
  153. native = True
  154. if native:
  155. show = False
  156. host = host or '127.0.0.1'
  157. port = port or native_module.find_open_port()
  158. width, height = window_size or (800, 600)
  159. native_module.activate(host, port, title, width, height, fullscreen, frameless)
  160. else:
  161. port = port or 8080
  162. host = host or '0.0.0.0'
  163. assert host is not None
  164. assert port is not None
  165. # NOTE: We save host and port in environment variables so the subprocess started in reload mode can access them.
  166. os.environ['NICEGUI_HOST'] = host
  167. os.environ['NICEGUI_PORT'] = str(port)
  168. if show:
  169. helpers.schedule_browser(host, port)
  170. def split_args(args: str) -> List[str]:
  171. return [a.strip() for a in args.split(',')]
  172. if kwargs.get('workers', 1) > 1:
  173. raise ValueError('NiceGUI does not support multiple workers yet.')
  174. # NOTE: The following lines are basically a copy of `uvicorn.run`, but keep a reference to the `server`.
  175. config = CustomServerConfig(
  176. APP_IMPORT_STRING if reload else core.app,
  177. host=host,
  178. port=port,
  179. reload=reload,
  180. reload_includes=split_args(uvicorn_reload_includes) if reload else None,
  181. reload_excludes=split_args(uvicorn_reload_excludes) if reload else None,
  182. reload_dirs=split_args(uvicorn_reload_dirs) if reload else None,
  183. log_level=uvicorn_logging_level,
  184. **kwargs,
  185. )
  186. config.storage_secret = storage_secret
  187. config.method_queue = native_module.method_queue if native else None
  188. config.response_queue = native_module.response_queue if native else None
  189. Server.create_singleton(config)
  190. if (reload or config.workers > 1) and not isinstance(config.app, str):
  191. log.warning('You must pass the application as an import string to enable "reload" or "workers".')
  192. sys.exit(1)
  193. if config.should_reload:
  194. sock = config.bind_socket()
  195. ChangeReload(config, target=Server.instance.run, sockets=[sock]).run()
  196. elif config.workers > 1:
  197. sock = config.bind_socket()
  198. Multiprocess(config, target=Server.instance.run, sockets=[sock]).run()
  199. else:
  200. Server.instance.run()
  201. if config.uds:
  202. os.remove(config.uds) # pragma: py-win32
  203. if not Server.instance.started and not config.should_reload and config.workers == 1:
  204. sys.exit(STARTUP_FAILURE)