ui_run.py 10 KB

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