page.py 4.6 KB

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