page.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. import asyncio
  2. import inspect
  3. import time
  4. from typing import Callable, Optional
  5. from fastapi import Request, Response
  6. from . import globals
  7. from .async_updater import AsyncUpdater
  8. from .client import Client
  9. from .favicon import create_favicon_route
  10. from .task_logger import create_task
  11. class page:
  12. def __init__(self,
  13. path: str, *,
  14. title: Optional[str] = None,
  15. favicon: Optional[str] = None,
  16. dark: Optional[bool] = ...,
  17. response_timeout: float = 3.0,
  18. ) -> None:
  19. """Page
  20. Creates a new page at the given route.
  21. :param path: route of the new page (path must start with '/')
  22. :param title: optional page title
  23. :param favicon: optional relative filepath or absolute URL to a favicon (default: `None`, NiceGUI icon will be used)
  24. :param dark: whether to use Quasar's dark mode (defaults to `dark` argument of `run` command)
  25. :param response_timeout: maximum time for the decorated function to build the page (default: 3.0)
  26. """
  27. self.path = path
  28. self.title = title
  29. self.favicon = favicon
  30. self.dark = dark
  31. self.response_timeout = response_timeout
  32. create_favicon_route(self.path, favicon)
  33. def resolve_title(self) -> str:
  34. return self.title if self.title is not None else globals.title
  35. def resolve_dark(self) -> Optional[bool]:
  36. return self.dark if self.dark is not ... else globals.dark
  37. def __call__(self, func: Callable) -> Callable:
  38. # NOTE we need to remove existing routes for this path to make sure only the latest definition is used
  39. globals.app.routes[:] = [r for r in globals.app.routes if getattr(r, 'path', None) != self.path]
  40. parameters_of_decorated_func = list(inspect.signature(func).parameters.keys())
  41. async def decorated(*dec_args, **dec_kwargs) -> Response:
  42. request = dec_kwargs['request']
  43. # NOTE cleaning up the keyword args so the signature is consistent with "func" again
  44. dec_kwargs = {k: v for k, v in dec_kwargs.items() if k in parameters_of_decorated_func}
  45. with Client(self) as client:
  46. if any(p.name == 'client' for p in inspect.signature(func).parameters.values()):
  47. dec_kwargs['client'] = client
  48. result = func(*dec_args, **dec_kwargs)
  49. if inspect.isawaitable(result):
  50. async def wait_for_result() -> None:
  51. with client:
  52. await AsyncUpdater(result)
  53. task = create_task(wait_for_result())
  54. deadline = time.time() + self.response_timeout
  55. while task and not client.is_waiting_for_connection and not task.done():
  56. if time.time() > deadline:
  57. raise TimeoutError(f'Response not ready after {self.response_timeout} seconds')
  58. await asyncio.sleep(0.1)
  59. result = task.result() if task.done() else None
  60. if isinstance(result, Response): # NOTE if setup returns a response, we don't need to render the page
  61. return result
  62. return client.build_response(request)
  63. parameters = [p for p in inspect.signature(func).parameters.values() if p.name != 'client']
  64. # NOTE adding request as a parameter so we can pass it to the client in the decorated function
  65. if 'request' not in [p.name for p in parameters]:
  66. parameters.append(inspect.Parameter('request', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Request))
  67. decorated.__signature__ = inspect.Signature(parameters)
  68. globals.page_routes[decorated] = self.path
  69. return globals.app.get(self.path)(decorated)