client.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import asyncio
  2. import json
  3. import time
  4. import uuid
  5. from pathlib import Path
  6. from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union
  7. from fastapi import Request
  8. from fastapi.responses import Response
  9. from fastapi.templating import Jinja2Templates
  10. from . import globals
  11. from .dependencies import generate_js_imports, generate_vue_content
  12. from .element import Element
  13. from .favicon import get_favicon_url
  14. from .task_logger import create_task
  15. if TYPE_CHECKING:
  16. from .page import page
  17. templates = Jinja2Templates(Path(__file__).parent / 'templates')
  18. class Client:
  19. def __init__(self, page: 'page', *, shared: bool = False) -> None:
  20. self.id = str(uuid.uuid4())
  21. self.created = time.time()
  22. globals.clients[self.id] = self
  23. self.elements: Dict[int, Element] = {}
  24. self.next_element_id: int = 0
  25. self.is_waiting_for_handshake: bool = False
  26. self.environ: Optional[Dict[str, Any]] = None
  27. self.shared = shared
  28. with Element('q-layout', _client=self).props('view="HHH LpR FFF"') as self.layout:
  29. with Element('q-page-container'):
  30. with Element('q-page'):
  31. self.content = Element('div').classes('q-pa-md column items-start gap-4')
  32. self.waiting_javascript_commands: Dict[str, str] = {}
  33. self.head_html = ''
  34. self.body_html = ''
  35. self.page = page
  36. self.connect_handlers: List[Union[Callable, Awaitable]] = []
  37. self.disconnect_handlers: List[Union[Callable, Awaitable]] = []
  38. @property
  39. def ip(self) -> Optional[str]:
  40. return self.environ.get('REMOTE_ADDR') if self.environ else None
  41. @property
  42. def room(self) -> str:
  43. return globals._current_socket_ids[-1] if globals._use_current_socket[-1] else self.id
  44. @property
  45. def has_socket_connection(self) -> bool:
  46. return self.environ is not None
  47. def __enter__(self):
  48. self.content.__enter__()
  49. return self
  50. def __exit__(self, *_):
  51. self.content.__exit__()
  52. def build_response(self, request: Request, status_code: int = 200) -> Response:
  53. prefix = request.headers.get('X-Forwarded-Prefix', '')
  54. vue_html, vue_styles, vue_scripts = generate_vue_content()
  55. elements = json.dumps({id: element.to_dict() for id, element in self.elements.items()})
  56. return templates.TemplateResponse('index.html', {
  57. 'request': request,
  58. 'client_id': str(self.id),
  59. 'elements': elements,
  60. 'head_html': self.head_html,
  61. 'body_html': f'{self.body_html}\n{vue_html}\n{vue_styles}',
  62. 'vue_scripts': vue_scripts,
  63. 'js_imports': generate_js_imports(prefix),
  64. 'title': self.page.resolve_title(),
  65. 'favicon_url': get_favicon_url(self.page, prefix),
  66. 'dark': str(self.page.resolve_dark()),
  67. 'prefix': prefix,
  68. 'socket_io_js_extra_headers': globals.socket_io_js_extra_headers,
  69. }, status_code, {'Cache-Control': 'no-store', 'X-NiceGUI-Content': 'page'})
  70. async def handshake(self, timeout: float = 3.0, check_interval: float = 0.1) -> None:
  71. self.is_waiting_for_handshake = True
  72. deadline = time.time() + timeout
  73. while not self.environ:
  74. if time.time() > deadline:
  75. raise TimeoutError(f'No handshake after {timeout} seconds')
  76. await asyncio.sleep(check_interval)
  77. self.is_waiting_for_handshake = False
  78. async def run_javascript(self, code: str, *,
  79. respond: bool = True, timeout: float = 1.0, check_interval: float = 0.01) -> Optional[str]:
  80. request_id = str(uuid.uuid4())
  81. command = {
  82. 'code': code,
  83. 'request_id': request_id if respond else None,
  84. }
  85. create_task(globals.sio.emit('run_javascript', command, room=self.room))
  86. if not respond:
  87. return None
  88. deadline = time.time() + timeout
  89. while request_id not in self.waiting_javascript_commands:
  90. if time.time() > deadline:
  91. raise TimeoutError('JavaScript did not respond in time')
  92. await asyncio.sleep(check_interval)
  93. return self.waiting_javascript_commands.pop(request_id)
  94. def open(self, target: Union[Callable, str]) -> None:
  95. path = target if isinstance(target, str) else globals.page_routes[target]
  96. create_task(globals.sio.emit('open', path, room=self.room))