advanced.rst 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. Advanced topic
  2. ===============
  3. This section will introduce the advanced features of PyWebIO.
  4. .. _multiple_app:
  5. Start multiple applications with start_server()
  6. -------------------------------------------------
  7. `start_server() <pywebio.platform.tornado.start_server>` accepts a function as PyWebIO application. In addition,
  8. `start_server() <pywebio.platform.tornado.start_server>` also accepts a list of application function or a dictionary
  9. of it to start multiple applications. You can use `pywebio.session.go_app() <pywebio.session.go_app>` or
  10. `put_link() <pywebio.output.put_link>` to jump between application::
  11. def task_1():
  12. put_text('task_1')
  13. put_buttons(['Go task 2'], [lambda: go_app('task_2')])
  14. def task_2():
  15. put_text('task_2')
  16. put_buttons(['Go task 1'], [lambda: go_app('task_1')])
  17. def index():
  18. put_link('Go task 1', app='task_1') # Use `app` parameter to specify the task name
  19. put_link('Go task 2', app='task_2')
  20. # equal to `start_server({'index': index, 'task_1': task_1, 'task_2': task_2})`
  21. start_server([index, task_1, task_2])
  22. When the first parameter of `start_server() <pywebio.platform.tornado.start_server>` is a dictionary, whose key is
  23. application name and value is application function. When it is a list, PyWebIO will use function name as application name.
  24. You can select which application to access through the ``app`` URL parameter
  25. (for example, visit ``http://host:port/?app=foo`` to access the ``foo`` application),
  26. By default, the ``index`` application is opened when no ``app`` URL parameter provided.
  27. When the ``index`` application doesn't exist, PyWebIO will provide a default index application.
  28. .. _integration_web_framework:
  29. Integration with web framework
  30. ---------------------------------
  31. The PyWebIO application can be integrated into an existing Python Web project, the PyWebIO application and the Web
  32. project share a web framework. PyWebIO currently supports integration with Flask, Tornado, Django, aiohttp and
  33. FastAPI(Starlette) web frameworks.
  34. The integration methods of those web frameworks are as follows:
  35. .. tabs::
  36. .. tab:: Tornado
  37. .. only:: latex
  38. **Tornado**
  39. Use `pywebio.platform.tornado.webio_handler()` to get the
  40. `WebSocketHandler <https://www.tornadoweb.org/en/stable/websocket.html#tornado.websocket.WebSocketHandler>`_
  41. class for running PyWebIO applications in Tornado::
  42. import tornado.ioloop
  43. import tornado.web
  44. from pywebio.platform.tornado import webio_handler
  45. class MainHandler(tornado.web.RequestHandler):
  46. def get(self):
  47. self.write("Hello, world")
  48. if __name__ == "__main__":
  49. application = tornado.web.Application([
  50. (r"/", MainHandler),
  51. (r"/tool", webio_handler(task_func)), # `task_func` is PyWebIO task function
  52. ])
  53. application.listen(port=80, address='localhost')
  54. tornado.ioloop.IOLoop.current().start()
  55. In above code, we add a routing rule to bind the ``WebSocketHandler`` of the PyWebIO application to the ``/tool`` path.
  56. After starting the Tornado server, you can visit ``http://localhost/tool`` to open the PyWebIO application.
  57. .. attention::
  58. PyWebIO uses the WebSocket protocol to communicate with the browser in Tornado. If your Tornado application
  59. is behind a reverse proxy (such as Nginx), you may need to configure the reverse proxy to support the
  60. WebSocket protocol. :ref:`Here <nginx_ws_config>` is an example of Nginx WebSocket configuration.
  61. .. tab:: Flask
  62. .. only:: latex
  63. **Flask**
  64. Use `pywebio.platform.flask.webio_view()` to get the view function for running PyWebIO applications in Flask::
  65. from pywebio.platform.flask import webio_view
  66. from flask import Flask
  67. app = Flask(__name__)
  68. # `task_func` is PyWebIO task function
  69. app.add_url_rule('/tool', 'webio_view', webio_view(task_func),
  70. methods=['GET', 'POST', 'OPTIONS']) # need GET,POST and OPTIONS methods
  71. app.run(host='localhost', port=80)
  72. In above code, we add a routing rule to bind the view function of the PyWebIO application to the ``/tool`` path.
  73. After starting the Flask application, visit ``http://localhost/tool`` to open the PyWebIO application.
  74. .. tab:: Django
  75. .. only:: latex
  76. **Django**
  77. Use `pywebio.platform.django.webio_view()` to get the view function for running PyWebIO applications in Django::
  78. # urls.py
  79. from django.urls import path
  80. from pywebio.platform.django import webio_view
  81. # `task_func` is PyWebIO task function
  82. webio_view_func = webio_view(task_func)
  83. urlpatterns = [
  84. path(r"tool", webio_view_func),
  85. ]
  86. In above code, we add a routing rule to bind the view function of the PyWebIO application to the ``/tool`` path.
  87. After starting the Django server, visit ``http://localhost/tool`` to open the PyWebIO application
  88. .. tab:: aiohttp
  89. .. only:: latex
  90. **aiohttp**
  91. Use `pywebio.platform.aiohttp.webio_handler()` to get the
  92. `Request Handler <https://docs.aiohttp.org/en/stable/web_quickstart.html#aiohttp-web-handler>`_ coroutine for
  93. running PyWebIO applications in aiohttp::
  94. from aiohttp import web
  95. from pywebio.platform.aiohttp import webio_handler
  96. app = web.Application()
  97. # `task_func` is PyWebIO task function
  98. app.add_routes([web.get('/tool', webio_handler(task_func))])
  99. web.run_app(app, host='localhost', port=80)
  100. After starting the aiohttp server, visit ``http://localhost/tool`` to open the PyWebIO application
  101. .. attention::
  102. PyWebIO uses the WebSocket protocol to communicate with the browser in aiohttp. If your aiohttp server is
  103. behind a reverse proxy (such as Nginx), you may need to configure the reverse proxy to support the WebSocket
  104. protocol. :ref:`Here <nginx_ws_config>` is an example of Nginx WebSocket configuration.
  105. .. tab:: FastAPI/Starlette
  106. .. only:: latex
  107. **FastAPI/Starlette**
  108. Use `pywebio.platform.fastapi.webio_routes()` to get the FastAPI/Starlette routes for running PyWebIO applications.
  109. You can mount the routes to your FastAPI/Starlette app.
  110. FastAPI::
  111. from fastapi import FastAPI
  112. from pywebio.platform.fastapi import webio_routes
  113. app = FastAPI()
  114. @app.get("/app")
  115. def read_main():
  116. return {"message": "Hello World from main app"}
  117. # `task_func` is PyWebIO task function
  118. app.mount("/tool", FastAPI(routes=webio_routes(task_func)))
  119. Starlette::
  120. from starlette.applications import Starlette
  121. from starlette.responses import JSONResponse
  122. from starlette.routing import Route, Mount
  123. from pywebio.platform.fastapi import webio_routes
  124. async def homepage(request):
  125. return JSONResponse({'hello': 'world'})
  126. app = Starlette(routes=[
  127. Route('/', homepage),
  128. Mount('/tool', routes=webio_routes(task_func)) # `task_func` is PyWebIO task function
  129. ])
  130. After starting the server by using ``uvicorn <module>:app`` , visit ``http://localhost:8000/tool/`` to open the PyWebIO application
  131. See also: `FastAPI doc <https://fastapi.tiangolo.com/advanced/sub-applications/>`_ , `Starlette doc <https://www.starlette.io/routing/#submounting-routes>`_
  132. .. attention::
  133. PyWebIO uses the WebSocket protocol to communicate with the browser in FastAPI/Starlette. If your server is
  134. behind a reverse proxy (such as Nginx), you may need to configure the reverse proxy to support the WebSocket
  135. protocol. :ref:`Here <nginx_ws_config>` is an example of Nginx WebSocket configuration.
  136. .. _integration_web_framework_note:
  137. Notes
  138. ^^^^^^^^^^^
  139. **Deployment in production**
  140. In your production system, you may want to deploy the web applications with some WSGI/ASGI servers such as uWSGI, Gunicorn, and Uvicorn.
  141. Since PyWebIO applications store session state in memory of process, when you use HTTP-based sessions (Flask and Django)
  142. and spawn multiple workers to handle requests, the request may be dispatched to a process that does not hold the session
  143. to which the request belongs. So you can only start one worker to handle requests when using Flask or Django backend.
  144. If you still want to use multiple processes to increase concurrency, one way is to use Uvicorn+FastAPI, or you can also
  145. start multiple Tornado/aiohttp processes and add external load balancer (such as HAProxy or nginx) before them.
  146. Those backends use the WebSocket protocol to communicate with the browser in PyWebIO, so there is no the issue as described above.
  147. **Static resources Hosting**
  148. By default, the front-end of PyWebIO gets required static resources from CDN. If you want to deploy PyWebIO applications
  149. in an offline environment, you need to host static files by yourself, and set the ``cdn`` parameter of ``webio_view()``
  150. or ``webio_handler()`` to ``False``.
  151. When setting ``cdn=False`` , you need to host the static resources in the same directory as the PyWebIO application.
  152. In addition, you can also pass a string to ``cdn`` parameter to directly set the URL of PyWebIO static resources directory.
  153. The path of the static file of PyWebIO is stored in ``pywebio.STATIC_PATH``, you can use the command
  154. ``python3 -c "import pywebio; print(pywebio.STATIC_PATH)"`` to print it out.
  155. .. note::
  156. ``start_server()`` and ``path_deploy()`` also support ``cdn`` parameter, if it is set to ``False``, the static
  157. resource will be hosted in local server automatically, without manual hosting.
  158. .. _coroutine_based_session:
  159. Coroutine-based session
  160. -------------------------------
  161. In most cases, you don’t need the coroutine-based session. All functions or methods in PyWebIO that are only used for
  162. coroutine sessions are specifically noted in the document.
  163. PyWebIO's session is based on thread by default. Each time a user opens a session connection to the server, PyWebIO will
  164. start a thread to run the task function. In addition to thread-based sessions, PyWebIO also provides coroutine-based sessions.
  165. Coroutine-based sessions accept coroutine functions as task functions.
  166. The session based on the coroutine is a single-thread model, which means that all sessions run in a single thread.
  167. For IO-bound tasks, coroutines take up fewer resources than threads and have performance comparable to threads.
  168. In addition, the context switching of the coroutine is predictable, which can reduce the need for program synchronization
  169. and locking, and can effectively avoid most critical section problems.
  170. Using coroutine session
  171. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  172. To use coroutine-based session, you need to use the ``async`` keyword to declare the task function as a coroutine
  173. function, and use the ``await`` syntax to call the PyWebIO input function:
  174. .. code-block:: python
  175. :emphasize-lines: 5,6
  176. from pywebio.input import *
  177. from pywebio.output import *
  178. from pywebio import start_server
  179. async def say_hello():
  180. name = await input("what's your name?")
  181. put_text('Hello, %s' % name)
  182. start_server(say_hello, auto_open_webbrowser=True)
  183. In the coroutine task function, you can also use ``await`` to call other coroutines or
  184. (`awaitable objects <https://docs.python.org/3/library/asyncio-task.html#asyncio-awaitables>`_) in the standard
  185. library `asyncio <https://docs.python.org/3/library/asyncio.html>`_:
  186. .. code-block:: python
  187. :emphasize-lines: 6,10
  188. import asyncio
  189. from pywebio import start_server
  190. async def hello_word():
  191. put_text('Hello ...')
  192. await asyncio.sleep(1) # await awaitable objects in asyncio
  193. put_text('... World!')
  194. async def main():
  195. await hello_word() # await coroutine
  196. put_text('Bye, bye')
  197. start_server(main, auto_open_webbrowser=True)
  198. .. attention::
  199. In coroutine-based session, all input functions defined in the :doc:`pywebio.input </input>` module need to use
  200. ``await`` syntax to get the return value. Forgetting to use ``await`` will be a common error when using coroutine-based session.
  201. Other functions that need to use ``await`` syntax in the coroutine session are:
  202. * `pywebio.session.run_asyncio_coroutine(coro_obj) <pywebio.session.run_asyncio_coroutine>`
  203. * `pywebio.session.eval_js(expression) <pywebio.session.eval_js>`
  204. .. warning::
  205. Although the PyWebIO coroutine session is compatible with the ``awaitable objects`` in the standard library ``asyncio``,
  206. the ``asyncio`` library is not compatible with the ``awaitable objects`` in the PyWebIO coroutine session.
  207. That is to say, you can't pass PyWebIO ``awaitable objects`` to the ``asyncio`` functions that accept ``awaitable objects``.
  208. For example, the following calls are **not supported** ::
  209. await asyncio.shield(pywebio.input())
  210. await asyncio.gather(asyncio.sleep(1), pywebio.session.eval_js('1+1'))
  211. task = asyncio.create_task(pywebio.input())
  212. .. _coroutine_based_concurrency:
  213. Concurrency in coroutine-based sessions
  214. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  215. In coroutine-based session, you can start new thread, but you cannot call PyWebIO interactive functions in it
  216. (`register_thread() <pywebio.session.register_thread>` is not available in coroutine session). But you can use
  217. `run_async(coro) <pywebio.session.run_async>` to execute a coroutine object asynchronously, and PyWebIO interactive
  218. functions can be used in the new coroutine:
  219. .. code-block:: python
  220. :emphasize-lines: 10
  221. from pywebio import start_server
  222. from pywebio.session import run_async
  223. async def counter(n):
  224. for i in range(n):
  225. put_text(i)
  226. await asyncio.sleep(1)
  227. async def main():
  228. run_async(counter(10))
  229. put_text('Main coroutine function exited.')
  230. start_server(main, auto_open_webbrowser=True)
  231. `run_async(coro) <pywebio.session.run_async>` returns a `TaskHandler <pywebio.session.coroutinebased.TaskHandler>`,
  232. which can be used to query the running status of the coroutine or close the coroutine.
  233. Close of session
  234. ^^^^^^^^^^^^^^^^^^^
  235. Similar to thread-based session, when user close the browser page, the session will be closed.
  236. After the browser page closed, PyWebIO input function calls that have not yet returned in the current session will
  237. cause `SessionClosedException <pywebio.exceptions.SessionClosedException>`, and subsequent calls to PyWebIO interactive
  238. functions will cause `SessionNotFoundException <pywebio.exceptions.SessionNotFoundException>` or
  239. `SessionClosedException <pywebio.exceptions.SessionClosedException>`.
  240. `defer_call(func) <pywebio.session.defer_call>` also available in coroutine session.
  241. .. _coroutine_web_integration:
  242. Integration with Web Framework
  243. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  244. The PyWebIO application that using coroutine-based session can also be integrated to the web framework.
  245. However, there are some limitations when using coroutine-based sessions to integrate into Flask or Django:
  246. First, when ``await`` the coroutine objects/awaitable objects in the ``asyncio`` module, you need to use
  247. `run_asyncio_coroutine() <pywebio.session.run_asyncio_coroutine>` to wrap the coroutine object.
  248. Secondly, you need to start a new thread to run the event loop before starting a Flask/Django server.
  249. Example of coroutine-based session integration into Flask:
  250. .. code-block:: python
  251. :emphasize-lines: 12,20
  252. import asyncio
  253. import threading
  254. from flask import Flask, send_from_directory
  255. from pywebio import STATIC_PATH
  256. from pywebio.output import *
  257. from pywebio.platform.flask import webio_view
  258. from pywebio.platform import run_event_loop
  259. from pywebio.session import run_asyncio_coroutine
  260. async def hello_word():
  261. put_text('Hello ...')
  262. await run_asyncio_coroutine(asyncio.sleep(1)) # can't just "await asyncio.sleep(1)"
  263. put_text('... World!')
  264. app = Flask(__name__)
  265. app.add_url_rule('/hello', 'webio_view', webio_view(hello_word),
  266. methods=['GET', 'POST', 'OPTIONS'])
  267. # thread to run event loop
  268. threading.Thread(target=run_event_loop, daemon=True).start()
  269. app.run(host='localhost', port=80)
  270. Finally, coroutine-based session is not available in the script mode. You always need to use ``start_server()`` to
  271. run coroutine task function or integrate it to a web framework.