jupyverse 0.10.1__tar.gz → 0.10.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. {jupyverse-0.10.1 → jupyverse-0.10.2}/CHANGELOG.md +4 -0
  2. {jupyverse-0.10.1 → jupyverse-0.10.2}/PKG-INFO +2 -2
  3. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_server/server.py +2 -2
  4. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/pyproject.toml +1 -1
  5. {jupyverse-0.10.1 → jupyverse-0.10.2}/pyproject.toml +2 -2
  6. {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_kernels.py +82 -75
  7. jupyverse-0.10.2/tests/utils.py +385 -0
  8. jupyverse-0.10.1/tests/utils.py +0 -141
  9. {jupyverse-0.10.1 → jupyverse-0.10.2}/.devcontainer/devcontainer.json +0 -0
  10. {jupyverse-0.10.1 → jupyverse-0.10.2}/.devcontainer/requirements.txt +0 -0
  11. {jupyverse-0.10.1 → jupyverse-0.10.2}/.github/workflows/publish-release.yml +0 -0
  12. {jupyverse-0.10.1 → jupyverse-0.10.2}/.github/workflows/test.yml +0 -0
  13. {jupyverse-0.10.1 → jupyverse-0.10.2}/.gitignore +0 -0
  14. {jupyverse-0.10.1 → jupyverse-0.10.2}/.pre-commit-config.yaml +0 -0
  15. {jupyverse-0.10.1 → jupyverse-0.10.2}/COPYING.md +0 -0
  16. {jupyverse-0.10.1 → jupyverse-0.10.2}/README.md +0 -0
  17. {jupyverse-0.10.1 → jupyverse-0.10.2}/binder/environment.yml +0 -0
  18. {jupyverse-0.10.1 → jupyverse-0.10.2}/binder/jupyter_notebook_config.py +0 -0
  19. {jupyverse-0.10.1 → jupyverse-0.10.2}/binder/postBuild +0 -0
  20. {jupyverse-0.10.1 → jupyverse-0.10.2}/binder/start +0 -0
  21. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/index.md +0 -0
  22. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/install.md +0 -0
  23. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/jupyter.svg +0 -0
  24. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/auth.md +0 -0
  25. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/contents.md +0 -0
  26. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/frontend.md +0 -0
  27. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/jupyterlab.md +0 -0
  28. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/kernels.md +0 -0
  29. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/lab.md +0 -0
  30. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/login.md +0 -0
  31. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/nbconvert.md +0 -0
  32. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/notebook.md +0 -0
  33. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/resource_usage.md +0 -0
  34. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/terminals.md +0 -0
  35. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/yjs.md +0 -0
  36. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/tutorials/jupyterhub_jupyverse_deployment.md +0 -0
  37. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/tutorials/standalone_jupyverse_deployment.md +0 -0
  38. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/usage/microservices.md +0 -0
  39. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/usage/multi_user.md +0 -0
  40. {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/usage/single_user.md +0 -0
  41. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse/__init__.py +0 -0
  42. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse/py.typed +0 -0
  43. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/COPYING.md +0 -0
  44. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/README.md +0 -0
  45. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/__init__.py +0 -0
  46. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/app/__init__.py +0 -0
  47. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/auth/__init__.py +0 -0
  48. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/auth/models.py +0 -0
  49. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/cli.py +0 -0
  50. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/contents/__init__.py +0 -0
  51. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/contents/models.py +0 -0
  52. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/exceptions.py +0 -0
  53. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/file_id/__init__.py +0 -0
  54. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/frontend/__init__.py +0 -0
  55. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/jupyterlab/__init__.py +0 -0
  56. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/kernel/__init__.py +0 -0
  57. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/kernels/__init__.py +0 -0
  58. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/kernels/models.py +0 -0
  59. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/lab/__init__.py +0 -0
  60. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/login/__init__.py +0 -0
  61. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/main/__init__.py +0 -0
  62. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/nbconvert/__init__.py +0 -0
  63. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/notebook/__init__.py +0 -0
  64. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/py.typed +0 -0
  65. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/resource_usage/__init__.py +0 -0
  66. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/terminals/__init__.py +0 -0
  67. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/terminals/models.py +0 -0
  68. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/yjs/__init__.py +0 -0
  69. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/yjs/models.py +0 -0
  70. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/pyproject.toml +0 -0
  71. {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/tests/test_resource_lock.py +0 -0
  72. {jupyverse-0.10.1 → jupyverse-0.10.2}/mkdocs.yml +0 -0
  73. {jupyverse-0.10.1 → jupyverse-0.10.2}/notebooks/admin_users.ipynb +0 -0
  74. {jupyverse-0.10.1 → jupyverse-0.10.2}/notebooks/admin_users.py +0 -0
  75. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/COPYING.md +0 -0
  76. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/README.md +0 -0
  77. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/__init__.py +0 -0
  78. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/backends.py +0 -0
  79. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/config.py +0 -0
  80. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/db.py +0 -0
  81. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/main.py +0 -0
  82. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/models.py +0 -0
  83. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/py.typed +0 -0
  84. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/routes.py +0 -0
  85. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/pyproject.toml +0 -0
  86. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/COPYING.md +0 -0
  87. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/README.md +0 -0
  88. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/__init__.py +0 -0
  89. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/backend.py +0 -0
  90. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/config.py +0 -0
  91. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/main.py +0 -0
  92. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/py.typed +0 -0
  93. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/routes.py +0 -0
  94. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/pyproject.toml +0 -0
  95. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/COPYING.md +0 -0
  96. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/README.md +0 -0
  97. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/__init__.py +0 -0
  98. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/config.py +0 -0
  99. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/db.py +0 -0
  100. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/launch.py +0 -0
  101. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/main.py +0 -0
  102. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/models.py +0 -0
  103. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/py.typed +0 -0
  104. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/routes.py +0 -0
  105. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/pyproject.toml +0 -0
  106. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/COPYING.md +0 -0
  107. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/README.md +0 -0
  108. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/fps_contents/__init__.py +0 -0
  109. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/fps_contents/main.py +0 -0
  110. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/fps_contents/py.typed +0 -0
  111. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/fps_contents/routes.py +0 -0
  112. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/pyproject.toml +0 -0
  113. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/COPYING.md +0 -0
  114. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/README.md +0 -0
  115. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/fps_file_id/__init__.py +0 -0
  116. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/fps_file_id/file_id.py +0 -0
  117. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/fps_file_id/main.py +0 -0
  118. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/fps_file_id/py.typed +0 -0
  119. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/pyproject.toml +0 -0
  120. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/COPYING.md +0 -0
  121. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/README.md +0 -0
  122. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/fps_frontend/__init__.py +0 -0
  123. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/fps_frontend/main.py +0 -0
  124. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/fps_frontend/py.typed +0 -0
  125. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/pyproject.toml +0 -0
  126. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/COPYING.md +0 -0
  127. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/README.md +0 -0
  128. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/fps_jupyterlab/__init__.py +0 -0
  129. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/fps_jupyterlab/index.py +0 -0
  130. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/fps_jupyterlab/main.py +0 -0
  131. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/fps_jupyterlab/py.typed +0 -0
  132. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/fps_jupyterlab/routes.py +0 -0
  133. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/pyproject.toml +0 -0
  134. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/COPYING.md +0 -0
  135. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/README.md +0 -0
  136. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/fps_kernel_subprocess/__init__.py +0 -0
  137. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/fps_kernel_subprocess/connect.py +0 -0
  138. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/fps_kernel_subprocess/kernel_subprocess.py +0 -0
  139. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/fps_kernel_subprocess/main.py +0 -0
  140. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/fps_kernel_subprocess/py.typed +0 -0
  141. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/pyproject.toml +0 -0
  142. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/COPYING.md +0 -0
  143. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/README.md +0 -0
  144. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/__init__.py +0 -0
  145. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_driver/__init__.py +0 -0
  146. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_driver/driver.py +0 -0
  147. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_driver/kernelspec.py +0 -0
  148. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_driver/message.py +0 -0
  149. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_driver/paths.py +0 -0
  150. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_server/__init__.py +0 -0
  151. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_server/message.py +0 -0
  152. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/main.py +0 -0
  153. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/py.typed +0 -0
  154. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/routes.py +0 -0
  155. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/tests/test_kernel_launcher.py +0 -0
  156. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/COPYING.md +0 -0
  157. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/README.md +0 -0
  158. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/fps_lab/__init__.py +0 -0
  159. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/fps_lab/main.py +0 -0
  160. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/fps_lab/py.typed +0 -0
  161. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/fps_lab/routes.py +0 -0
  162. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/fps_lab/static/favicon.ico +0 -0
  163. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/pyproject.toml +0 -0
  164. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/COPYING.md +0 -0
  165. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/README.md +0 -0
  166. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/__init__.py +0 -0
  167. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/main.py +0 -0
  168. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/py.typed +0 -0
  169. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/routes.py +0 -0
  170. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-busy-1.ico +0 -0
  171. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-busy-2.ico +0 -0
  172. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-busy-3.ico +0 -0
  173. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-file.ico +0 -0
  174. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-notebook.ico +0 -0
  175. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-terminal.ico +0 -0
  176. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon.ico +0 -0
  177. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/index.html +0 -0
  178. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/logo/github.svg +0 -0
  179. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/logo/logo.png +0 -0
  180. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/style/index.css +0 -0
  181. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/pyproject.toml +0 -0
  182. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/COPYING.md +0 -0
  183. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/README.md +0 -0
  184. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/fps_nbconvert/__init__.py +0 -0
  185. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/fps_nbconvert/main.py +0 -0
  186. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/fps_nbconvert/py.typed +0 -0
  187. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/fps_nbconvert/routes.py +0 -0
  188. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/pyproject.toml +0 -0
  189. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/COPYING.md +0 -0
  190. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/README.md +0 -0
  191. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/fps_noauth/__init__.py +0 -0
  192. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/fps_noauth/backends.py +0 -0
  193. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/fps_noauth/main.py +0 -0
  194. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/fps_noauth/py.typed +0 -0
  195. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/pyproject.toml +0 -0
  196. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/COPYING.md +0 -0
  197. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/README.md +0 -0
  198. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/fps_notebook/__init__.py +0 -0
  199. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/fps_notebook/main.py +0 -0
  200. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/fps_notebook/py.typed +0 -0
  201. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/fps_notebook/routes.py +0 -0
  202. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/pyproject.toml +0 -0
  203. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/COPYING.md +0 -0
  204. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/README.md +0 -0
  205. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/fps_resource_usage/__init__.py +0 -0
  206. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/fps_resource_usage/main.py +0 -0
  207. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/fps_resource_usage/py.typed +0 -0
  208. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/fps_resource_usage/routes.py +0 -0
  209. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/pyproject.toml +0 -0
  210. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/COPYING.md +0 -0
  211. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/README.md +0 -0
  212. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/__init__.py +0 -0
  213. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/main.py +0 -0
  214. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/py.typed +0 -0
  215. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/routes.py +0 -0
  216. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/server.py +0 -0
  217. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/win_server.py +0 -0
  218. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/pyproject.toml +0 -0
  219. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/COPYING.md +0 -0
  220. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/README.md +0 -0
  221. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/fps_webdav/__init__.py +0 -0
  222. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/fps_webdav/config.py +0 -0
  223. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/fps_webdav/main.py +0 -0
  224. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/fps_webdav/py.typed +0 -0
  225. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/fps_webdav/routes.py +0 -0
  226. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/pyproject.toml +0 -0
  227. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/tests/conftest.py +0 -0
  228. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/tests/test_webdav.py +0 -0
  229. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/COPYING.md +0 -0
  230. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/README.md +0 -0
  231. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/__init__.py +0 -0
  232. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/main.py +0 -0
  233. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/py.typed +0 -0
  234. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/routes.py +0 -0
  235. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/__init__.py +0 -0
  236. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/asgi_server.py +0 -0
  237. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/awareness.py +0 -0
  238. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/django_channels_consumer.py +0 -0
  239. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/websocket.py +0 -0
  240. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/websocket_provider.py +0 -0
  241. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/websocket_server.py +0 -0
  242. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/yroom.py +0 -0
  243. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/ystore.py +0 -0
  244. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/yutils.py +0 -0
  245. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywidgets/__init__.py +0 -0
  246. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywidgets/widgets.py +0 -0
  247. {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/pyproject.toml +0 -0
  248. {jupyverse-0.10.1 → jupyverse-0.10.2}/pytest.ini +0 -0
  249. {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/conftest.py +0 -0
  250. {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/data/notebook0.ipynb +0 -0
  251. {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/data/notebook1.ipynb +0 -0
  252. {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_app.py +0 -0
  253. {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_auth.py +0 -0
  254. {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_contents.py +0 -0
  255. {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_execute.py +0 -0
  256. {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_server.py +0 -0
  257. {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_settings.py +0 -0
@@ -1,5 +1,9 @@
1
1
  # Version history
2
2
 
3
+ ## 0.10.2
4
+
5
+ - Rework `test_kernels.py` to use ASGI transport.
6
+
3
7
  ## 0.10.1
4
8
 
5
9
  - Fix optional dependencies pinning.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jupyverse
3
- Version: 0.10.1
3
+ Version: 0.10.2
4
4
  Summary: A set of FPS plugins implementing a Jupyter server
5
5
  Project-URL: Homepage, https://jupyter.org
6
6
  Author-email: Jupyter Development Team <jupyter@googlegroups.com>
@@ -13,7 +13,7 @@ Requires-Dist: fps-contents<0.11.0,>=0.10.0
13
13
  Requires-Dist: fps-file-id<0.3.0,>=0.2.0
14
14
  Requires-Dist: fps-frontend<0.10.0,>=0.9.0
15
15
  Requires-Dist: fps-kernel-subprocess<0.2.0,>=0.1.0
16
- Requires-Dist: fps-kernels<0.10.0,>=0.9.0
16
+ Requires-Dist: fps-kernels<0.10.0,>=0.9.1
17
17
  Requires-Dist: fps-lab<0.10.0,>=0.9.0
18
18
  Requires-Dist: fps-nbconvert<0.10.0,>=0.9.0
19
19
  Requires-Dist: fps-terminals<0.10.0,>=0.9.0
@@ -6,7 +6,7 @@ from datetime import datetime, timezone
6
6
 
7
7
  from anyio import TASK_STATUS_IGNORED, Event, create_task_group, move_on_after
8
8
  from anyio.abc import TaskStatus
9
- from fastapi import WebSocket, WebSocketDisconnect
9
+ from fastapi import WebSocket
10
10
  from starlette.websockets import WebSocketState
11
11
 
12
12
  from jupyverse_api.kernel import DefaultKernelFactory, KernelFactory
@@ -160,7 +160,7 @@ class KernelServer:
160
160
  async def listen_web(self, websocket: AcceptedWebSocket, stop_event: Event):
161
161
  try:
162
162
  await self.send_to_kernel(websocket)
163
- except WebSocketDisconnect:
163
+ except BaseException:
164
164
  pass
165
165
  finally:
166
166
  stop_event.set()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fps_kernels"
7
- version = "0.9.0"
7
+ version = "0.9.1"
8
8
  description = "An FPS plugin for the kernels API"
9
9
  keywords = ["jupyter", "server", "fastapi", "plugins"]
10
10
  requires-python = ">=3.9"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "jupyverse"
7
- version = "0.10.1"
7
+ version = "0.10.2"
8
8
  description = "A set of FPS plugins implementing a Jupyter server"
9
9
  keywords = ["jupyter", "server", "fastapi", "plugins"]
10
10
  requires-python = ">=3.9"
@@ -13,7 +13,7 @@ dependencies = [
13
13
  "fps-contents >=0.10.0,<0.11.0",
14
14
  "fps-file-id >=0.2.0,<0.3.0",
15
15
  "fps-kernel-subprocess >=0.1.0,<0.2.0",
16
- "fps-kernels >=0.9.0,<0.10.0",
16
+ "fps-kernels >=0.9.1,<0.10.0",
17
17
  "fps-terminals >=0.9.0,<0.10.0",
18
18
  "fps-nbconvert >=0.9.0,<0.10.0",
19
19
  "fps-yjs >=0.10.0,<0.11.0",
@@ -8,6 +8,7 @@ from fps import get_root_module, merge_config
8
8
  from fps_kernels.kernel_server.server import KernelServer, kernels
9
9
  from httpx import AsyncClient
10
10
  from httpx_ws import aconnect_ws
11
+ from utils import ASGIWebSocketTransport
11
12
 
12
13
  from jupyverse_api.kernel import Kernel
13
14
 
@@ -16,6 +17,9 @@ os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
16
17
  CONFIG = {
17
18
  "jupyverse": {
18
19
  "type": "jupyverse",
20
+ "config": {
21
+ "start_server": False,
22
+ },
19
23
  "modules": {
20
24
  "app": {
21
25
  "type": "app",
@@ -29,9 +33,6 @@ CONFIG = {
29
33
  "contents": {
30
34
  "type": "contents",
31
35
  },
32
- "file_id": {
33
- "type": "file_id",
34
- },
35
36
  "frontend": {
36
37
  "type": "frontend",
37
38
  },
@@ -47,9 +48,6 @@ CONFIG = {
47
48
  "kernels": {
48
49
  "type": "kernels",
49
50
  },
50
- "yjs": {
51
- "type": "yjs",
52
- },
53
51
  },
54
52
  }
55
53
  }
@@ -57,87 +55,96 @@ CONFIG = {
57
55
 
58
56
  @pytest.mark.anyio
59
57
  @pytest.mark.parametrize("auth_mode", ("noauth",))
60
- async def test_kernel_messages(auth_mode, capfd, unused_tcp_port):
58
+ async def test_kernel_messages(auth_mode, capfd):
61
59
  kernel_id = "kernel_id_0"
62
60
  kernel_name = "python3"
63
61
  kernelspec_path = (
64
62
  Path(sys.prefix) / "share" / "jupyter" / "kernels" / kernel_name / "kernel.json"
65
63
  )
66
64
  assert kernelspec_path.exists()
67
- async with create_task_group() as tg:
68
- msg = {
69
- "channel": "shell",
70
- "parent_header": None,
71
- "content": None,
72
- "metadata": None,
73
- "header": {
74
- "msg_type": "msg_type_0",
75
- "msg_id": "0",
76
- },
77
- }
78
-
79
- config = merge_config(
80
- CONFIG,
81
- {
82
- "jupyverse": {
83
- "config": {"port": unused_tcp_port},
84
- "modules": {
85
- "auth": {
86
- "config": {
87
- "mode": auth_mode,
88
- }
65
+ msg = {
66
+ "channel": "shell",
67
+ "parent_header": None,
68
+ "content": None,
69
+ "metadata": None,
70
+ "header": {
71
+ "msg_type": "msg_type_0",
72
+ "msg_id": "0",
73
+ },
74
+ }
75
+
76
+ config = merge_config(
77
+ CONFIG,
78
+ {
79
+ "jupyverse": {
80
+ "modules": {
81
+ "auth": {
82
+ "config": {
83
+ "mode": auth_mode,
89
84
  }
90
- },
91
- }
92
- },
93
- )
94
- async with get_root_module(config), AsyncClient():
85
+ }
86
+ },
87
+ }
88
+ },
89
+ )
90
+ async with get_root_module(config) as root_module:
91
+ app = root_module.app
92
+ transport = ASGIWebSocketTransport(app=app)
93
+ async with AsyncClient(transport=transport, base_url="http://testserver") as client:
95
94
  kernel_server = KernelServer(
96
95
  kernelspec_path=kernelspec_path,
97
96
  capture_kernel_output=False,
98
97
  default_kernel_factory=KernelFactory(),
99
98
  )
100
- await tg.start(kernel_server.start)
101
- kernels[kernel_id] = {"server": kernel_server, "driver": None}
102
- # block msg_type_0
103
- kernel_server.block_messages("msg_type_0")
104
- async with aconnect_ws(
105
- f"http://127.0.0.1:{unused_tcp_port}/api/kernels/{kernel_id}/channels?session_id=session_id_0",
106
- ) as websocket:
107
- await websocket.send_json(msg)
108
- await sleep(0.1)
109
- out, err = capfd.readouterr()
110
- assert not err
111
-
112
- # allow only msg_type_0
113
- kernel_server.allow_messages("msg_type_0")
114
- async with aconnect_ws(
115
- f"http://127.0.0.1:{unused_tcp_port}/api/kernels/{kernel_id}/channels?session_id=session_id_0",
116
- ) as websocket:
117
- await websocket.send_json(msg)
118
- await sleep(0.1)
119
- out, err = capfd.readouterr()
120
- assert err.count("msg_type_0") == 1
121
-
122
- # block all messages
123
- kernel_server.allow_messages([])
124
- async with aconnect_ws(
125
- f"http://127.0.0.1:{unused_tcp_port}/api/kernels/{kernel_id}/channels?session_id=session_id_0",
126
- ) as websocket:
127
- await websocket.send_json(msg)
128
- await sleep(0.1)
129
- out, err = capfd.readouterr()
130
- assert not err
131
-
132
- # allow all messages
133
- kernel_server.allow_messages()
134
- async with aconnect_ws(
135
- f"http://127.0.0.1:{unused_tcp_port}/api/kernels/{kernel_id}/channels?session_id=session_id_0",
136
- ) as websocket:
137
- await websocket.send_json(msg)
138
- await sleep(0.1)
139
- out, err = capfd.readouterr()
140
- assert err.count("msg_type_0") >= 1
99
+ async with create_task_group() as tg:
100
+ await tg.start(kernel_server.start)
101
+ kernels[kernel_id] = {"server": kernel_server, "driver": None}
102
+
103
+ # block msg_type_0
104
+ kernel_server.block_messages("msg_type_0")
105
+ async with aconnect_ws(
106
+ f"http://testserver/api/kernels/{kernel_id}/channels?session_id=session_id_0",
107
+ client,
108
+ ) as websocket:
109
+ await websocket.send_json(msg)
110
+ await sleep(0.1)
111
+ out, err = capfd.readouterr()
112
+ assert not err
113
+
114
+ # allow only msg_type_0
115
+ kernel_server.allow_messages("msg_type_0")
116
+ async with aconnect_ws(
117
+ f"http://testserver/api/kernels/{kernel_id}/channels?session_id=session_id_0",
118
+ client,
119
+ ) as websocket:
120
+ await websocket.send_json(msg)
121
+ await sleep(0.1)
122
+ out, err = capfd.readouterr()
123
+ assert err.count("msg_type_0") == 1
124
+
125
+ # block all messages
126
+ kernel_server.allow_messages([])
127
+ async with aconnect_ws(
128
+ f"http://testserver/api/kernels/{kernel_id}/channels?session_id=session_id_0",
129
+ client,
130
+ ) as websocket:
131
+ await websocket.send_json(msg)
132
+ await sleep(0.1)
133
+ out, err = capfd.readouterr()
134
+ assert not err
135
+
136
+ # allow all messages
137
+ kernel_server.allow_messages()
138
+ async with aconnect_ws(
139
+ f"http://testserver/api/kernels/{kernel_id}/channels?session_id=session_id_0",
140
+ client,
141
+ ) as websocket:
142
+ await websocket.send_json(msg)
143
+ await sleep(0.1)
144
+ out, err = capfd.readouterr()
145
+ assert err.count("msg_type_0") >= 1
146
+
147
+ await kernel_server.stop()
141
148
 
142
149
 
143
150
  class KernelFactory:
@@ -0,0 +1,385 @@
1
+ import contextlib
2
+ import queue
3
+ import typing
4
+ from types import TracebackType
5
+ from typing import Optional
6
+ from uuid import uuid4
7
+
8
+ import anyio
9
+ import wsproto
10
+ from anyio import Lock
11
+ from httpcore import AsyncNetworkStream
12
+ from httpx import ASGITransport, AsyncByteStream, Request, Response
13
+ from httpx_ws._exceptions import WebSocketDisconnect
14
+ from wsproto.frame_protocol import CloseReason
15
+
16
+
17
+ async def authenticate_client(http, port, permissions={}):
18
+ # create a new user
19
+ username = uuid4().hex
20
+ # if logged in, log out
21
+ first_time = True
22
+ while True:
23
+ response = await http.get(f"http://127.0.0.1:{port}/api/me")
24
+ if response.status_code == 403:
25
+ break
26
+ assert first_time
27
+ response = await http.post(f"http://127.0.0.1:{port}/auth/logout")
28
+ assert response.status_code == 200
29
+ first_time = False
30
+
31
+ # register user
32
+ register_body = {
33
+ "email": f"{username}@example.com",
34
+ "password": username,
35
+ "username": username,
36
+ "permissions": permissions,
37
+ }
38
+ response = await http.post(f"http://127.0.0.1:{port}/auth/register", json=register_body)
39
+ # check that we cannot register if not logged in
40
+ assert response.status_code == 403
41
+ # log in as admin
42
+ login_body = {"username": "admin@jupyter.com", "password": "jupyverse"}
43
+ response = await http.post(f"http://127.0.0.1:{port}/auth/login", data=login_body)
44
+ assert response.status_code == 204
45
+ # register user
46
+ response = await http.post(f"http://127.0.0.1:{port}/auth/register", json=register_body)
47
+ assert response.status_code == 201
48
+
49
+ # log out
50
+ response = await http.post(f"http://127.0.0.1:{port}/auth/logout")
51
+ assert response.status_code == 204
52
+ # check that we can't get our identity, since we're not logged in
53
+ response = await http.get(f"http://127.0.0.1:{port}/api/me")
54
+ assert response.status_code == 403
55
+
56
+ # log in with registered user
57
+ login_body = {"username": f"{username}@example.com", "password": username}
58
+ response = await http.post(f"http://127.0.0.1:{port}/auth/login", data=login_body)
59
+ assert response.status_code == 204
60
+ # we should now have a cookie
61
+ assert "fastapiusersauth" in http.cookies
62
+ # check our identity, since we're logged in
63
+ response = await http.get(
64
+ f"http://127.0.0.1:{port}/api/me", params={"permissions": permissions}
65
+ )
66
+ assert response.status_code == 200
67
+ me = response.json()
68
+ assert me["identity"]["username"] == username
69
+ # check our permissions
70
+ assert me["permissions"] == permissions
71
+
72
+
73
+ def create_content(
74
+ content: Optional[list],
75
+ type: str,
76
+ size: Optional[int],
77
+ mimetype: Optional[str],
78
+ name: str,
79
+ path: str,
80
+ format: Optional[str],
81
+ ) -> dict:
82
+ return {
83
+ "content": content,
84
+ "created": None,
85
+ "format": format,
86
+ "last_modified": None,
87
+ "mimetype": mimetype,
88
+ "name": name,
89
+ "path": path,
90
+ "size": size,
91
+ "type": type,
92
+ "writable": True,
93
+ }
94
+
95
+
96
+ def clear_content_values(content: dict, keys: list[str] = []):
97
+ for k in content:
98
+ if k in keys:
99
+ content[k] = None
100
+ if k == "content" and isinstance(content[k], list):
101
+ for c in content[k]:
102
+ clear_content_values(c, keys)
103
+ return content
104
+
105
+
106
+ def sort_content_by_name(content: dict):
107
+ for k in content:
108
+ if k == "content" and isinstance(content[k], list):
109
+ # FIXME: this sorting algorithm is terrible!
110
+ names = [c["name"] for c in content[k]]
111
+ names.sort()
112
+ new_content = []
113
+ for name in names:
114
+ for i, c in enumerate(content[k]):
115
+ if c["name"] == name:
116
+ break
117
+ content[k].pop(i)
118
+ new_content.append(c)
119
+ content[k] = new_content
120
+ for c in content[k]:
121
+ sort_content_by_name(c)
122
+ return content
123
+
124
+
125
+ class Websocket:
126
+ def __init__(self, websocket, path: str):
127
+ self._websocket = websocket
128
+ self._path = path
129
+ self._send_lock = Lock()
130
+
131
+ @property
132
+ def path(self) -> str:
133
+ return self._path
134
+
135
+ def __aiter__(self):
136
+ return self
137
+
138
+ async def __anext__(self) -> bytes:
139
+ try:
140
+ message = await self.recv()
141
+ except Exception:
142
+ raise StopAsyncIteration()
143
+ return message
144
+
145
+ async def send(self, message: bytes):
146
+ async with self._send_lock:
147
+ await self._websocket.send_bytes(message)
148
+
149
+ async def recv(self) -> bytes:
150
+ b = await self._websocket.receive_bytes()
151
+ return bytes(b)
152
+
153
+
154
+ Scope = dict[str, typing.Any]
155
+ Message = dict[str, typing.Any]
156
+ Receive = typing.Callable[[], typing.Awaitable[Message]]
157
+ Send = typing.Callable[[Scope], typing.Coroutine[None, None, None]]
158
+ ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Coroutine[None, None, None]]
159
+
160
+
161
+ class ASGIWebSocketTransportError(Exception):
162
+ pass
163
+
164
+
165
+ class UnhandledASGIMessageType(ASGIWebSocketTransportError):
166
+ def __init__(self, message: Message) -> None:
167
+ self.message = message
168
+
169
+
170
+ class UnhandledWebSocketEvent(ASGIWebSocketTransportError):
171
+ def __init__(self, event: wsproto.events.Event) -> None:
172
+ self.event = event
173
+
174
+
175
+ class ASGIWebSocketAsyncNetworkStream(AsyncNetworkStream):
176
+ def __init__(
177
+ self, app: ASGIApp, scope: Scope, task_group: anyio.abc.TaskGroup
178
+ ) -> None:
179
+ self.app = app
180
+ self.scope = scope
181
+ self._task_group = task_group
182
+ self._receive_queue: queue.Queue[Message] = queue.Queue()
183
+ self._send_queue: queue.Queue[Message] = queue.Queue()
184
+ self.connection = wsproto.WSConnection(wsproto.ConnectionType.SERVER)
185
+ self.connection.initiate_upgrade_connection(scope["headers"], scope["path"])
186
+ self._aentered = False
187
+
188
+ async def __aenter__(
189
+ self,
190
+ ) -> tuple["ASGIWebSocketAsyncNetworkStream", bytes]:
191
+ if self._aentered:
192
+ raise RuntimeError(
193
+ "Cannot use ASGIWebSocketAsyncNetworkStream in a context manager twice"
194
+ )
195
+ self._aentered = True
196
+ self._task_group.start_soon(self._run)
197
+ async with contextlib.AsyncExitStack() as stack:
198
+ await self.send({"type": "websocket.connect"})
199
+ message = await self.receive()
200
+
201
+ stack.push_async_callback(self.aclose)
202
+
203
+ if message["type"] == "websocket.close":
204
+ await stack.pop_all().aclose()
205
+ raise WebSocketDisconnect(message["code"], message.get("reason"))
206
+
207
+ assert message["type"] == "websocket.accept"
208
+ retval = self, self._build_accept_response(message)
209
+ self._exit_stack = stack.pop_all()
210
+ return retval
211
+
212
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> typing.Union[bool, None]:
213
+ return await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
214
+
215
+ async def read(
216
+ self, max_bytes: int, timeout: typing.Optional[float] = None
217
+ ) -> bytes:
218
+ message: Message = await self.receive(timeout=timeout)
219
+ type = message["type"]
220
+
221
+ if type not in {"websocket.send", "websocket.close"}:
222
+ raise UnhandledASGIMessageType(message)
223
+
224
+ event: wsproto.events.Event
225
+ if type == "websocket.send":
226
+ data_str: typing.Optional[str] = message.get("text")
227
+ if data_str is not None:
228
+ event = wsproto.events.TextMessage(data_str)
229
+ data_bytes: typing.Optional[bytes] = message.get("bytes")
230
+ if data_bytes is not None:
231
+ event = wsproto.events.BytesMessage(data_bytes)
232
+ elif type == "websocket.close":
233
+ event = wsproto.events.CloseConnection(message["code"], message["reason"])
234
+
235
+ return self.connection.send(event)
236
+
237
+ async def write(
238
+ self, buffer: bytes, timeout: typing.Optional[float] = None
239
+ ) -> None:
240
+ self.connection.receive_data(buffer)
241
+ for event in self.connection.events():
242
+ if isinstance(event, wsproto.events.Request):
243
+ pass
244
+ elif isinstance(event, wsproto.events.CloseConnection):
245
+ await self.send(
246
+ {
247
+ "type": "websocket.close",
248
+ "code": event.code,
249
+ "reason": event.reason,
250
+ }
251
+ )
252
+ elif isinstance(event, wsproto.events.TextMessage):
253
+ await self.send({"type": "websocket.receive", "text": event.data})
254
+ elif isinstance(event, wsproto.events.BytesMessage):
255
+ await self.send({"type": "websocket.receive", "bytes": event.data})
256
+ else:
257
+ raise UnhandledWebSocketEvent(event)
258
+
259
+ async def aclose(self) -> None:
260
+ await self.send({"type": "websocket.close"})
261
+
262
+ async def send(self, message: Message) -> None:
263
+ self._receive_queue.put(message)
264
+
265
+ async def receive(self, timeout: typing.Optional[float] = None) -> Message:
266
+ while self._send_queue.empty():
267
+ await anyio.sleep(0)
268
+ return self._send_queue.get(timeout=timeout)
269
+
270
+ async def _run(self) -> None:
271
+ """
272
+ The sub-thread in which the websocket session runs.
273
+ """
274
+ scope = self.scope
275
+ receive = self._asgi_receive
276
+ send = self._asgi_send
277
+ try:
278
+ await self.app(scope, receive, send)
279
+ except Exception as e:
280
+ message = {
281
+ "type": "websocket.close",
282
+ "code": CloseReason.INTERNAL_ERROR,
283
+ "reason": str(e),
284
+ }
285
+ await self._asgi_send(message)
286
+
287
+ async def _asgi_receive(self) -> Message:
288
+ while self._receive_queue.empty():
289
+ await anyio.sleep(0)
290
+ return self._receive_queue.get()
291
+
292
+ async def _asgi_send(self, message: Message) -> None:
293
+ self._send_queue.put(message)
294
+
295
+ def _build_accept_response(self, message: Message) -> bytes:
296
+ subprotocol = message.get("subprotocol", None)
297
+ headers = message.get("headers", [])
298
+ return self.connection.send(
299
+ wsproto.events.AcceptConnection(
300
+ subprotocol=subprotocol,
301
+ extra_headers=headers,
302
+ )
303
+ )
304
+
305
+
306
+ class ASGIWebSocketTransport(ASGITransport):
307
+ def __init__(self, *args, **kwargs) -> None:
308
+ super().__init__(*args, **kwargs)
309
+ self.exit_stacks: list[contextlib.AsyncExitStack] = []
310
+
311
+ async def __aenter__(self) -> "ASGIWebSocketTransport":
312
+ async with contextlib.AsyncExitStack() as stack:
313
+ self._task_group = await stack.enter_async_context(
314
+ anyio.create_task_group()
315
+ )
316
+ self.exit_stack = stack.pop_all()
317
+
318
+ return self
319
+
320
+ async def __aexit__(
321
+ self,
322
+ exc_type: typing.Optional[type[BaseException]] = None,
323
+ exc_val: typing.Optional[BaseException] = None,
324
+ exc_tb: typing.Optional[TracebackType] = None,
325
+ ) -> None:
326
+ await super().__aexit__(exc_type, exc_val, exc_tb)
327
+ await self.exit_stack.__aexit__(exc_type, exc_val, exc_tb)
328
+
329
+ async def handle_async_request(self, request: Request) -> Response:
330
+ scheme = request.url.scheme
331
+ headers = request.headers
332
+
333
+ if scheme in {"ws", "wss"} or headers.get("upgrade") == "websocket":
334
+ subprotocols: list[str] = []
335
+ if (
336
+ subprotocols_header := headers.get("sec-websocket-protocol")
337
+ ) is not None:
338
+ subprotocols = subprotocols_header.split(",")
339
+
340
+ scope = {
341
+ "type": "websocket",
342
+ "path": request.url.path,
343
+ "raw_path": request.url.raw_path,
344
+ "root_path": self.root_path,
345
+ "scheme": scheme,
346
+ "query_string": request.url.query,
347
+ "headers": [(k.lower(), v) for (k, v) in request.headers.raw],
348
+ "client": self.client,
349
+ "server": (request.url.host, request.url.port),
350
+ "subprotocols": subprotocols,
351
+ }
352
+ return await self._handle_ws_request(request, scope)
353
+
354
+ return await super().handle_async_request(request)
355
+
356
+ async def _handle_ws_request(
357
+ self,
358
+ request: Request,
359
+ scope: Scope,
360
+ ) -> Response:
361
+ assert isinstance(request.stream, AsyncByteStream)
362
+
363
+ self.scope = scope
364
+ async with contextlib.AsyncExitStack() as stack:
365
+ stream, accept_response = await stack.enter_async_context(
366
+ ASGIWebSocketAsyncNetworkStream(self.app, self.scope, self._task_group) # type: ignore[arg-type]
367
+ )
368
+ self.exit_stacks.append(stack.pop_all())
369
+
370
+ accept_response_lines = accept_response.decode("utf-8").splitlines()
371
+ headers = [
372
+ typing.cast(tuple[str, str], line.split(": ", 1))
373
+ for line in accept_response_lines[1:]
374
+ if line.strip() != ""
375
+ ]
376
+
377
+ return Response(
378
+ status_code=101,
379
+ headers=headers,
380
+ extensions={"network_stream": stream},
381
+ )
382
+
383
+ async def aclose(self) -> None:
384
+ for stack in self.exit_stacks[::-1]:
385
+ await stack.aclose()