page.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import asyncio
  2. import inspect
  3. import time
  4. import uuid
  5. from typing import Callable, Optional
  6. import justpy as jp
  7. from addict import Dict
  8. from pygments.formatters import HtmlFormatter
  9. from starlette.requests import Request
  10. from ..globals import config, connect_handlers, disconnect_handlers, page_stack, view_stack
  11. from ..helpers import is_coroutine
  12. class Page(jp.QuasarPage):
  13. def __init__(self,
  14. route: str,
  15. title: Optional[str] = None,
  16. *,
  17. favicon: Optional[str] = None,
  18. dark: Optional[bool] = ...,
  19. classes: str = 'q-ma-md column items-start',
  20. css: str = HtmlFormatter().get_style_defs('.codehilite'),
  21. on_connect: Optional[Callable] = None,
  22. on_page_ready: Optional[Callable] = None,
  23. on_disconnect: Optional[Callable] = None,
  24. ):
  25. """Page
  26. Creates a new page at the given path.
  27. :param route: route of the new page (path must start with '/')
  28. :param title: optional page title
  29. :param favicon: optional favicon
  30. :param dark: whether to use Quasar's dark mode (defaults to `dark` argument of `run` command)
  31. :param classes: tailwind classes for the container div (default: `'q-ma-md column items-start'`)
  32. :param css: CSS definitions
  33. :param on_connect: optional function or coroutine which is called for each new client connection
  34. :param on_page_ready: optional function or coroutine which is called when the websocket is connected
  35. :param on_disconnect: optional function or coroutine which is called when a client disconnects
  36. """
  37. super().__init__()
  38. self.delete_flag = False
  39. self.title = title or config.title
  40. self.favicon = favicon or config.favicon
  41. self.dark = dark if dark is not ... else config.dark
  42. self.tailwind = True # use Tailwind classes instead of Quasars
  43. self.css = css
  44. self.connect_handler = on_connect
  45. self.page_ready_handler = on_page_ready
  46. self.disconnect_handler = on_disconnect
  47. self.waiting_javascript_commands: dict[str, str] = {}
  48. self.on('result_ready', self.handle_javascript_result)
  49. self.on('page_ready', self.handle_page_ready)
  50. self.view = jp.Div(a=self, classes=classes, style='row-gap: 1em', temp=False)
  51. self.view.add_page(self)
  52. self.route = route
  53. jp.Route(route, self._route_function)
  54. async def _route_function(self, request: Request):
  55. for connect_handler in connect_handlers + ([self.connect_handler] if self.connect_handler else []):
  56. arg_count = len(inspect.signature(connect_handler).parameters)
  57. is_coro = is_coroutine(connect_handler)
  58. if arg_count == 1:
  59. await connect_handler(request) if is_coro else connect_handler(request)
  60. elif arg_count == 0:
  61. await connect_handler() if is_coro else connect_handler()
  62. else:
  63. raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
  64. return self
  65. async def handle_page_ready(self, msg: Dict) -> bool:
  66. if self.page_ready_handler:
  67. arg_count = len(inspect.signature(self.page_ready_handler).parameters)
  68. is_coro = is_coroutine(self.page_ready_handler)
  69. if arg_count == 1:
  70. await self.page_ready_handler(msg.websocket) if is_coro else self.page_ready_handler(msg.websocket)
  71. elif arg_count == 0:
  72. await self.page_ready_handler() if is_coro else self.page_ready_handler()
  73. else:
  74. raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
  75. return False
  76. async def on_disconnect(self, websocket=None) -> None:
  77. for disconnect_handler in ([self.disconnect_handler] if self.disconnect_handler else []) + disconnect_handlers:
  78. await disconnect_handler() if is_coroutine(disconnect_handler) else disconnect_handler()
  79. await super().on_disconnect(websocket)
  80. def __enter__(self):
  81. page_stack.append(self)
  82. view_stack.append(self.view)
  83. return self
  84. def __exit__(self, *_):
  85. page_stack.pop()
  86. view_stack.pop()
  87. async def await_javascript(self, code: str, check_interval: float = 0.01, timeout: float = 1.0) -> str:
  88. start_time = time.time()
  89. request_id = str(uuid.uuid4())
  90. await self.run_javascript(code, request_id=request_id)
  91. while request_id not in self.waiting_javascript_commands:
  92. if time.time() > start_time + timeout:
  93. raise TimeoutError('JavaScript did not respond in time')
  94. await asyncio.sleep(check_interval)
  95. return self.waiting_javascript_commands.pop(request_id)
  96. def handle_javascript_result(self, msg) -> bool:
  97. self.waiting_javascript_commands[msg.request_id] = msg.result
  98. return False
  99. def add_head_html(self, html: str) -> None:
  100. page_stack[-1].head_html += html
  101. def add_body_html(self, html: str) -> None:
  102. page_stack[-1].body_html += html