client.py 4.3 KB

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