page.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. from __future__ import annotations
  2. import asyncio
  3. import inspect
  4. import time
  5. from pathlib import Path
  6. from typing import TYPE_CHECKING, Any, Callable, Optional, Union
  7. from fastapi import Request, Response
  8. from . import background_tasks, binding, core, helpers
  9. from .client import Client
  10. from .favicon import create_favicon_route
  11. from .language import Language
  12. if TYPE_CHECKING:
  13. from .api_router import APIRouter
  14. class page:
  15. def __init__(self,
  16. path: str, *,
  17. title: Optional[str] = None,
  18. viewport: Optional[str] = None,
  19. favicon: Optional[Union[str, Path]] = None,
  20. dark: Optional[bool] = ..., # type: ignore
  21. language: Language = ..., # type: ignore
  22. response_timeout: float = 3.0,
  23. reconnect_timeout: Optional[float] = None,
  24. api_router: Optional[APIRouter] = None,
  25. **kwargs: Any,
  26. ) -> None:
  27. """Page
  28. This decorator marks a function to be a page builder.
  29. Each user accessing the given route will see a new instance of the page.
  30. This means it is private to the user and not shared with others
  31. (as it is done `when placing elements outside of a page decorator <https://nicegui.io/documentation#auto-index_page>`_).
  32. :param path: route of the new page (path must start with '/')
  33. :param title: optional page title
  34. :param viewport: optional viewport meta tag content
  35. :param favicon: optional relative filepath or absolute URL to a favicon (default: `None`, NiceGUI icon will be used)
  36. :param dark: whether to use Quasar's dark mode (defaults to `dark` argument of `run` command)
  37. :param language: language of the page (defaults to `language` argument of `run` command)
  38. :param response_timeout: maximum time for the decorated function to build the page (default: 3.0 seconds)
  39. :param reconnect_timeout: maximum time the server waits for the browser to reconnect (default: 0.0 seconds)
  40. :param api_router: APIRouter instance to use, can be left `None` to use the default
  41. :param kwargs: additional keyword arguments passed to FastAPI's @app.get method
  42. """
  43. self._path = path
  44. self.title = title
  45. self.viewport = viewport
  46. self.favicon = favicon
  47. self.dark = dark
  48. self.language = language
  49. self.response_timeout = response_timeout
  50. self.kwargs = kwargs
  51. self.api_router = api_router or core.app.router
  52. self.reconnect_timeout = reconnect_timeout
  53. create_favicon_route(self.path, favicon)
  54. @property
  55. def path(self) -> str:
  56. """The path of the page including the APIRouter's prefix."""
  57. return self.api_router.prefix + self._path
  58. def resolve_title(self) -> str:
  59. """Return the title of the page."""
  60. return self.title if self.title is not None else core.app.config.title
  61. def resolve_viewport(self) -> str:
  62. """Return the viewport of the page."""
  63. return self.viewport if self.viewport is not None else core.app.config.viewport
  64. def resolve_dark(self) -> Optional[bool]:
  65. """Return whether the page should use dark mode."""
  66. return self.dark if self.dark is not ... else core.app.config.dark
  67. def resolve_language(self) -> Optional[str]:
  68. """Return the language of the page."""
  69. return self.language if self.language is not ... else core.app.config.language
  70. def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
  71. core.app.remove_route(self.path) # NOTE make sure only the latest route definition is used
  72. parameters_of_decorated_func = list(inspect.signature(func).parameters.keys())
  73. async def decorated(*dec_args, **dec_kwargs) -> Response:
  74. request = dec_kwargs['request']
  75. # NOTE cleaning up the keyword args so the signature is consistent with "func" again
  76. dec_kwargs = {k: v for k, v in dec_kwargs.items() if k in parameters_of_decorated_func}
  77. with Client(self) as client:
  78. if any(p.name == 'client' for p in inspect.signature(func).parameters.values()):
  79. dec_kwargs['client'] = client
  80. result = func(*dec_args, **dec_kwargs)
  81. if helpers.is_coroutine_function(func):
  82. async def wait_for_result() -> None:
  83. with client:
  84. return await result
  85. task = background_tasks.create(wait_for_result())
  86. deadline = time.time() + self.response_timeout
  87. while task and not client.is_waiting_for_connection and not task.done():
  88. if time.time() > deadline:
  89. raise TimeoutError(f'Response not ready after {self.response_timeout} seconds')
  90. await asyncio.sleep(0.1)
  91. result = task.result() if task.done() else None
  92. if isinstance(result, Response): # NOTE if setup returns a response, we don't need to render the page
  93. return result
  94. binding._refresh_step() # pylint: disable=protected-access
  95. return client.build_response(request)
  96. parameters = [p for p in inspect.signature(func).parameters.values() if p.name != 'client']
  97. # NOTE adding request as a parameter so we can pass it to the client in the decorated function
  98. if 'request' not in {p.name for p in parameters}:
  99. request = inspect.Parameter('request', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Request)
  100. parameters.insert(0, request)
  101. decorated.__signature__ = inspect.Signature(parameters) # type: ignore
  102. if 'include_in_schema' not in self.kwargs:
  103. self.kwargs['include_in_schema'] = core.app.config.endpoint_documentation in {'page', 'all'}
  104. self.api_router.get(self._path, **self.kwargs)(decorated)
  105. Client.page_routes[func] = self.path
  106. return func