routes.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import asyncio
  2. import inspect
  3. import os.path
  4. from functools import wraps
  5. from typing import List
  6. import justpy as jp
  7. from starlette import requests, routing
  8. from starlette.responses import FileResponse
  9. from starlette.routing import BaseRoute, Mount, Route
  10. from starlette.staticfiles import StaticFiles
  11. from . import globals
  12. from .helpers import is_coroutine
  13. from .page import Page
  14. from .task_logger import create_task
  15. def add_route(self, route: BaseRoute) -> None:
  16. """
  17. :param route: starlette route including a path and a function to be called
  18. :return:
  19. """
  20. globals.app.routes.insert(0, route)
  21. def add_static_files(self, path: str, directory: str) -> None:
  22. """
  23. :param path: string that starts with a '/'
  24. :param directory: folder with static files to serve under the given path
  25. """
  26. add_route(None, Mount(path, app=StaticFiles(directory=directory)))
  27. def get(self, path: str):
  28. """
  29. Use as a decorator for a function like @ui.get('/another/route/{id}').
  30. :param path: string that starts with a '/'
  31. :return:
  32. """
  33. *_, converters = routing.compile_path(path)
  34. def decorator(func):
  35. @wraps(func)
  36. async def decorated(request: requests.Request):
  37. args = {name: converter.convert(request.path_params.get(name)) for name, converter in converters.items()}
  38. parameters = inspect.signature(func).parameters
  39. for key in parameters:
  40. if parameters[key].annotation.__name__ == 'bool':
  41. args[key] = bool(args[key])
  42. if parameters[key].annotation.__name__ == 'int':
  43. args[key] = int(args[key])
  44. elif parameters[key].annotation.__name__ == 'float':
  45. args[key] = float(args[key])
  46. elif parameters[key].annotation.__name__ == 'complex':
  47. args[key] = complex(args[key])
  48. if 'request' in parameters and 'request' not in args:
  49. args['request'] = request
  50. return await func(**args) if is_coroutine(func) else func(**args)
  51. self.add_route(routing.Route(path, decorated))
  52. return decorated
  53. return decorator
  54. def add_dependencies(py_filepath: str, dependencies: List[str] = []) -> None:
  55. if py_filepath in globals.dependencies:
  56. return
  57. globals.dependencies[py_filepath] = dependencies
  58. vue_filepath = os.path.splitext(os.path.realpath(py_filepath))[0] + '.js'
  59. for dependency in dependencies:
  60. is_remote = dependency.startswith('http://') or dependency.startswith('https://')
  61. src = dependency if is_remote else f'lib/{dependency}'
  62. if src not in jp.component_file_list:
  63. jp.component_file_list += [src]
  64. if not is_remote:
  65. filepath = f'{os.path.dirname(vue_filepath)}/{src}'
  66. route = Route(f'/{src}', lambda _, filepath=filepath: FileResponse(filepath))
  67. jp.app.routes.insert(0, route)
  68. if vue_filepath not in jp.component_file_list:
  69. filename = os.path.basename(vue_filepath)
  70. jp.app.routes.insert(0, Route(f'/{filename}', lambda _: FileResponse(vue_filepath)))
  71. jp.component_file_list += [filename]
  72. if globals.loop and globals.loop.is_running():
  73. # NOTE: if new dependencies are added after starting the server, we need to reload the page on connected clients
  74. async def reload(view: jp.HTMLBaseComponent) -> None:
  75. for page in view.pages.values():
  76. assert isinstance(page, Page)
  77. await page.run_javascript('location.reload()')
  78. create_task(reload(globals.view_stack[-1]))