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.
- {jupyverse-0.10.1 → jupyverse-0.10.2}/CHANGELOG.md +4 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/PKG-INFO +2 -2
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_server/server.py +2 -2
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/pyproject.toml +1 -1
- {jupyverse-0.10.1 → jupyverse-0.10.2}/pyproject.toml +2 -2
- {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_kernels.py +82 -75
- jupyverse-0.10.2/tests/utils.py +385 -0
- jupyverse-0.10.1/tests/utils.py +0 -141
- {jupyverse-0.10.1 → jupyverse-0.10.2}/.devcontainer/devcontainer.json +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/.devcontainer/requirements.txt +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/.github/workflows/publish-release.yml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/.github/workflows/test.yml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/.gitignore +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/.pre-commit-config.yaml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/binder/environment.yml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/binder/jupyter_notebook_config.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/binder/postBuild +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/binder/start +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/index.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/install.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/jupyter.svg +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/auth.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/contents.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/frontend.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/jupyterlab.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/kernels.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/lab.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/login.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/nbconvert.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/notebook.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/resource_usage.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/terminals.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/plugins/yjs.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/tutorials/jupyterhub_jupyverse_deployment.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/tutorials/standalone_jupyverse_deployment.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/usage/microservices.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/usage/multi_user.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/docs/usage/single_user.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/app/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/auth/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/auth/models.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/cli.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/contents/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/contents/models.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/exceptions.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/file_id/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/frontend/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/jupyterlab/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/kernel/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/kernels/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/kernels/models.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/lab/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/login/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/main/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/nbconvert/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/notebook/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/resource_usage/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/terminals/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/terminals/models.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/yjs/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/jupyverse_api/yjs/models.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/jupyverse_api/tests/test_resource_lock.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/mkdocs.yml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/notebooks/admin_users.ipynb +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/notebooks/admin_users.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/backends.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/config.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/db.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/models.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/fps_auth/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/backend.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/config.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/fps_auth_fief/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_fief/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/config.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/db.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/launch.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/models.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/fps_auth_jupyterhub/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/auth_jupyterhub/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/fps_contents/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/fps_contents/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/fps_contents/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/fps_contents/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/contents/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/fps_file_id/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/fps_file_id/file_id.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/fps_file_id/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/fps_file_id/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/file_id/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/fps_frontend/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/fps_frontend/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/fps_frontend/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/frontend/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/fps_jupyterlab/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/fps_jupyterlab/index.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/fps_jupyterlab/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/fps_jupyterlab/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/fps_jupyterlab/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/jupyterlab/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/fps_kernel_subprocess/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/fps_kernel_subprocess/connect.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/fps_kernel_subprocess/kernel_subprocess.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/fps_kernel_subprocess/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/fps_kernel_subprocess/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernel_subprocess/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_driver/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_driver/driver.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_driver/kernelspec.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_driver/message.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_driver/paths.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_server/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/kernel_server/message.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/fps_kernels/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/kernels/tests/test_kernel_launcher.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/fps_lab/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/fps_lab/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/fps_lab/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/fps_lab/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/fps_lab/static/favicon.ico +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/lab/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-busy-1.ico +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-busy-2.ico +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-busy-3.ico +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-file.ico +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-notebook.ico +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon-terminal.ico +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/favicons/favicon.ico +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/index.html +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/logo/github.svg +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/logo/logo.png +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/fps_login/static/style/index.css +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/login/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/fps_nbconvert/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/fps_nbconvert/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/fps_nbconvert/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/fps_nbconvert/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/nbconvert/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/fps_noauth/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/fps_noauth/backends.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/fps_noauth/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/fps_noauth/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/noauth/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/fps_notebook/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/fps_notebook/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/fps_notebook/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/fps_notebook/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/notebook/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/fps_resource_usage/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/fps_resource_usage/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/fps_resource_usage/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/fps_resource_usage/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/resource_usage/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/server.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/fps_terminals/win_server.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/terminals/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/fps_webdav/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/fps_webdav/config.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/fps_webdav/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/fps_webdav/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/fps_webdav/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/tests/conftest.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/webdav/tests/test_webdav.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/COPYING.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/README.md +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/main.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/py.typed +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/routes.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/asgi_server.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/awareness.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/django_channels_consumer.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/websocket.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/websocket_provider.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/websocket_server.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/yroom.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/ystore.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywebsocket/yutils.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywidgets/__init__.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/fps_yjs/ywidgets/widgets.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/plugins/yjs/pyproject.toml +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/pytest.ini +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/conftest.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/data/notebook0.ipynb +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/data/notebook1.ipynb +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_app.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_auth.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_contents.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_execute.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_server.py +0 -0
- {jupyverse-0.10.1 → jupyverse-0.10.2}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: jupyverse
|
3
|
-
Version: 0.10.
|
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.
|
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
|
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
|
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 = "jupyverse"
|
7
|
-
version = "0.10.
|
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.
|
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
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
"
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
{
|
82
|
-
"
|
83
|
-
"
|
84
|
-
|
85
|
-
|
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
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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()
|