page.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  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 pygments.formatters import HtmlFormatter
  8. from starlette.requests import Request
  9. from ..globals import config, connect_handlers, page_stack, view_stack
  10. from ..helpers import is_coroutine
  11. class Page(jp.QuasarPage):
  12. def __init__(self,
  13. route: str,
  14. title: Optional[str] = None,
  15. favicon: Optional[str] = None,
  16. dark: Optional[bool] = ...,
  17. classes: str = 'q-ma-md column items-start',
  18. css: str = HtmlFormatter().get_style_defs('.codehilite'),
  19. on_connect: Optional[Callable] = None,
  20. ):
  21. """Page
  22. Creates a new page at the given path.
  23. :param route: route of the new page (path must start with '/')
  24. :param title: optional page title
  25. :param favicon: optional favicon
  26. :param dark: whether to use Quasar's dark mode (defaults to `dark` argument of `run` command)
  27. :param classes: tailwind classes for the container div (default: `'q-ma-md column items-start'`)
  28. :param css: CSS definitions
  29. :param on_connect: optional function or coroutine which is called for each new client connection
  30. """
  31. super().__init__()
  32. self.delete_flag = False
  33. self.title = title or config.title
  34. self.favicon = favicon or config.favicon
  35. self.dark = dark if dark is not ... else config.dark
  36. self.tailwind = True # use Tailwind classes instead of Quasars
  37. self.css = css
  38. self.on_connect = on_connect
  39. self.waiting_javascript_commands: dict[str, str] = {}
  40. self.on('result_ready', self.handle_javascript_result)
  41. self.view = jp.Div(a=self, classes=classes, style='row-gap: 1em', temp=False)
  42. self.view.add_page(self)
  43. self.route = route
  44. jp.Route(route, self._route_function)
  45. async def _route_function(self, request: Request):
  46. for connect_handler in connect_handlers + ([self.on_connect] if self.on_connect else []):
  47. arg_count = len(inspect.signature(connect_handler).parameters)
  48. is_coro = is_coroutine(connect_handler)
  49. if arg_count == 1:
  50. await connect_handler(request) if is_coro else connect_handler(request)
  51. elif arg_count == 0:
  52. await connect_handler() if is_coro else connect_handler()
  53. else:
  54. raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
  55. return self
  56. def __enter__(self):
  57. page_stack.append(self)
  58. view_stack.append(self.view)
  59. return self
  60. def __exit__(self, *_):
  61. page_stack.pop()
  62. view_stack.pop()
  63. async def await_javascript(self, code: str, check_interval: float = 0.01, timeout: float = 1.0) -> str:
  64. start_time = time.time()
  65. request_id = str(uuid.uuid4())
  66. await self.run_javascript(code, request_id=request_id)
  67. while request_id not in self.waiting_javascript_commands:
  68. if time.time() > start_time + timeout:
  69. raise TimeoutError('JavaScript did not respond in time')
  70. await asyncio.sleep(check_interval)
  71. return self.waiting_javascript_commands.pop(request_id)
  72. def handle_javascript_result(self, msg) -> bool:
  73. self.waiting_javascript_commands[msg.request_id] = msg.result
  74. return False