page.py 6.1 KB

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