From 3f8dc41aac37bdc48d3917ac128089e5613d7a20 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 3 Nov 2023 17:27:49 -0700 Subject: [PATCH] Reorganize the JupyterHub build Move the spawner and authenticator into separate Python modules with their own build systems and normal, unpinned dependencies. Hide the internal modules more effectively and use the new rubin.nublado.* namespace for both. Unpinning dependencies allows both modules and the controller to be tested in the same nox session. Move the infrastructure for the JupyterHub build, including its pinned dependencies, into a separate hub subdirectory. Only it pins dependencies, and it no longer needs development dependencies. This should fix dependency conflicts between the Hub and controller builds. --- Dockerfile.hub | 8 +- authenticator/LICENSE | 1 + authenticator/README.md | 6 + authenticator/pyproject.toml | 83 +++++ .../rubin/nublado/authenticator/__init__.py | 5 + .../rubin/nublado/authenticator/_internals.py | 101 +++--- authenticator/tests/__init__.py | 0 .../tests/authenticator_test.py | 12 +- hub/README.md | 11 + {spawner => hub}/requirements/main.in | 5 +- {spawner => hub}/requirements/main.txt | 0 .../scripts/install-base-packages.sh | 0 .../scripts/install-dependency-packages.sh | 0 noxfile.py | 124 ++++---- pyproject.toml | 7 +- spawner/README.md | 6 +- spawner/pyproject.toml | 25 +- spawner/requirements/dev.in | 26 -- spawner/requirements/dev.txt | 297 ------------------ spawner/src/rsp_restspawner/__init__.py | 4 - spawner/src/rubin/nublado/spawner/__init__.py | 3 + .../nublado/spawner/_exceptions.py} | 0 .../nublado/spawner/_internals.py} | 86 +---- spawner/src/rubin/nublado/spawner/_models.py | 89 ++++++ spawner/tests/conftest.py | 2 +- spawner/tests/spawner_test.py | 6 +- spawner/tests/support/controller.py | 2 +- 27 files changed, 362 insertions(+), 547 deletions(-) create mode 120000 authenticator/LICENSE create mode 100644 authenticator/README.md create mode 100644 authenticator/pyproject.toml create mode 100644 authenticator/src/rubin/nublado/authenticator/__init__.py rename spawner/src/rsp_restspawner/auth.py => authenticator/src/rubin/nublado/authenticator/_internals.py (93%) create mode 100644 authenticator/tests/__init__.py rename spawner/tests/auth_test.py => authenticator/tests/authenticator_test.py (92%) create mode 100644 hub/README.md rename {spawner => hub}/requirements/main.in (89%) rename {spawner => hub}/requirements/main.txt (100%) rename {spawner => hub}/scripts/install-base-packages.sh (100%) rename {spawner => hub}/scripts/install-dependency-packages.sh (100%) delete mode 100644 spawner/requirements/dev.in delete mode 100644 spawner/requirements/dev.txt delete mode 100644 spawner/src/rsp_restspawner/__init__.py create mode 100644 spawner/src/rubin/nublado/spawner/__init__.py rename spawner/src/{rsp_restspawner/exceptions.py => rubin/nublado/spawner/_exceptions.py} (100%) rename spawner/src/{rsp_restspawner/spawner.py => rubin/nublado/spawner/_internals.py} (89%) create mode 100644 spawner/src/rubin/nublado/spawner/_models.py diff --git a/Dockerfile.hub b/Dockerfile.hub index 012b75675..6e697eb91 100644 --- a/Dockerfile.hub +++ b/Dockerfile.hub @@ -17,17 +17,17 @@ FROM jupyterhub/jupyterhub:4.0.2 as base-image # Update system packages -COPY spawner/scripts/install-base-packages.sh . +COPY hub/scripts/install-base-packages.sh . RUN ./install-base-packages.sh && rm install-base-packages.sh FROM base-image as dependencies-image # Install system packages only needed for building dependencies. -COPY spawner/scripts/install-dependency-packages.sh . +COPY hub/scripts/install-dependency-packages.sh . RUN ./install-dependency-packages.sh && rm install-dependency-packages.sh # Install the dependencies of the extra JupyterHub modules. -COPY spawner/requirements/main.txt ./requirements.txt +COPY hub/requirements/main.txt ./requirements.txt RUN pip install --quiet --no-cache-dir -r requirements.txt FROM dependencies-image as runtime-image @@ -35,7 +35,7 @@ FROM dependencies-image as runtime-image # Install the extra JupyterHub modules. COPY . /app WORKDIR /app -RUN pip install --no-cache-dir spawner +RUN pip install --no-cache-dir ./authenticator ./spawner # Create a non-root user to run JupyterHub. Upstream uses 768 as the UID # and GID. This value must be kept in sync with the Phalanx nublado diff --git a/authenticator/LICENSE b/authenticator/LICENSE new file mode 120000 index 000000000..ea5b60640 --- /dev/null +++ b/authenticator/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/authenticator/README.md b/authenticator/README.md new file mode 100644 index 000000000..ab858d49e --- /dev/null +++ b/authenticator/README.md @@ -0,0 +1,6 @@ +# Gafaelfawr JupyterHub authenticator + +This is an implementation of the JupyterHub `Authenticator` class that authenticates a user using [Gafaelfawr](https://gafaelfawr.lsst.io), assuming authentication is configured using [Phalanx](https://phalanx.lsst.io). +It is, in theory, generic, and is maintained as a stand-alone Python module, but is normally installed in a Docker image with JupyterHub as part of the Rubin Science Platform JupyterHub build. + +For more details about this architecture, see [SQR-066](https://sqr-066.lsst.io/). diff --git a/authenticator/pyproject.toml b/authenticator/pyproject.toml new file mode 100644 index 000000000..4a8fb975a --- /dev/null +++ b/authenticator/pyproject.toml @@ -0,0 +1,83 @@ +[project] +# https://packaging.python.org/en/latest/specifications/declaring-project-metadata/ +name = "rubin.nublado.authenticator" +description = "JupyterHub Authenticator class supporting Gafaelfawr." +license = { file = "LICENSE" } +readme = "README.md" +keywords = ["rubin", "lsst"] +# https://pypi.org/classifiers/ +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Natural Language :: English", + "Operating System :: POSIX", + "Typing :: Typed", +] +requires-python = ">=3.10" +dependencies = ["jupyterhub<5", "tornado<7"] +dynamic = ["version"] + +[project.optional-dependencies] +dev = [ + # Testing + "pytest", + "pytest-asyncio", + "pytest-cov", + "pytest-sugar", +] + +[project.urls] +Homepage = "https://nublado.lsst.io" +Source = "https://github.com/lsst-sqre/nublado" + +[build-system] +requires = ["setuptools>=61", "wheel", "setuptools_scm[toml]>=6.2"] +build-backend = 'setuptools.build_meta' + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools_scm] +root = ".." + +[tool.coverage.run] +parallel = true +branch = true +source = ["rubin.nublado.authenticator"] + +[tool.coverage.paths] +source = ["src"] + +[tool.coverage.report] +show_missing = true +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:" +] + +[tool.pytest.ini_options] +asyncio_mode = "strict" +filterwarnings = [ + # Will probably be fixed with JupyterHub v4. + "ignore:'pipes' is deprecated:DeprecationWarning:jupyterhub.spawner" +] +# The python_files setting is not for test detection (pytest will pick up any +# test files named *_test.py without this setting) but to enable special +# assert processing in any non-test supporting files under tests. We +# conventionally put test support functions under tests.support and may +# sometimes use assert in test fixtures in conftest.py, and pytest only +# enables magical assert processing (showing a full diff on assert failures +# with complex data structures rather than only the assert message) in files +# listed in python_files. +python_files = ["tests/*.py", "tests/*/*.py"] diff --git a/authenticator/src/rubin/nublado/authenticator/__init__.py b/authenticator/src/rubin/nublado/authenticator/__init__.py new file mode 100644 index 000000000..7c69d56ef --- /dev/null +++ b/authenticator/src/rubin/nublado/authenticator/__init__.py @@ -0,0 +1,5 @@ +"""Gafaelfawr authenticator for JupyterHub.""" + +from ._internals import GafaelfawrAuthenticator + +__all__ = ["GafaelfawrAuthenticator"] diff --git a/spawner/src/rsp_restspawner/auth.py b/authenticator/src/rubin/nublado/authenticator/_internals.py similarity index 93% rename from spawner/src/rsp_restspawner/auth.py rename to authenticator/src/rubin/nublado/authenticator/_internals.py index eaabc2318..0d53858c6 100644 --- a/spawner/src/rsp_restspawner/auth.py +++ b/authenticator/src/rubin/nublado/authenticator/_internals.py @@ -30,6 +30,50 @@ def _build_auth_info(headers: HTTPHeaders) -> AuthInfo: return {"name": username, "auth_state": {"token": token}} +class _GafaelfawrLogoutHandler(LogoutHandler): + """Logout handler for Gafaelfawr authentication. + + A logout should always stop all running servers, and then redirect to the + RSP logout page. + """ + + @property + def shutdown_on_logout(self) -> bool: + """Unconditionally true for Gafaelfawr logout.""" + return True + + async def render_logout_page(self) -> None: + self.redirect("/logout", permanent=False) + + +class _GafaelfawrLoginHandler(BaseHandler): + """Login handler for Gafaelfawr authentication. + + This retrieves the authentication token from the headers, makes an API + call to get its metadata, constructs an authentication state, and then + redirects to the next URL. + """ + + async def get(self) -> None: + """Handle GET to the login page.""" + auth_info = _build_auth_info(self.request.headers) + + # Store the ancillary user information in the user database and create + # or return the user object. This call is unfortunately undocumented, + # but it's what BaseHandler calls to record the auth_state information + # after a form-based login. Hopefully this is a stable interface. + user = await self.auth_to_user(auth_info) + + # Tell JupyterHub to set its login cookie (also undocumented). + self.set_login_cookie(user) + + # Redirect to the next URL, which is under the control of JupyterHub + # and opaque to the authenticator. In practice, it will normally be + # whatever URL the user was trying to go to when JupyterHub decided + # they needed to be authenticated. + self.redirect(self.get_next_url(user)) + + class GafaelfawrAuthenticator(Authenticator): """JupyterHub authenticator using Gafaelfawr headers. @@ -54,11 +98,10 @@ class GafaelfawrAuthenticator(Authenticator): ``auto_login`` setting on the configured authenticator. This setting tells the built-in login page to, instead of presenting a login form, redirect the user to whatever URL is returned by ``login_url``. In our case, this - will be ``/hub/gafaelfawr/login``, served by `GafaelfawrLoginHandler`. - This simple handler will read the token from the header, retrieve its - metadata, create the session and cookie, and then make the same redirect - call the login form handler would normally have made after the - ``authenticate`` method returned. + will be ``/hub/gafaelfawr/login``. This simple handler will read the token + from the header, retrieve its metadata, create the session and cookie, and + then make the same redirect call the login form handler would normally + have made after the ``authenticate`` method returned. In this model, the ``authenticate`` method is not used, since the login handler never receives a form submission. @@ -107,8 +150,8 @@ async def authenticate( def get_handlers(self, app: JupyterHub) -> list[Route]: """Register the header-only login and the logout handlers.""" return [ - ("/gafaelfawr/login", GafaelfawrLoginHandler), - ("/logout", GafaelfawrLogoutHandler), + ("/gafaelfawr/login", _GafaelfawrLoginHandler), + ("/logout", _GafaelfawrLogoutHandler), ] def login_url(self, base_url: str) -> str: @@ -152,47 +195,3 @@ async def refresh_user( return True else: return _build_auth_info(handler.request.headers) - - -class GafaelfawrLogoutHandler(LogoutHandler): - """Logout handler for Gafaelfawr authentication. - - A logout should always stop all running servers, and then redirect to the - RSP logout page. - """ - - @property - def shutdown_on_logout(self) -> bool: - """Unconditionally true for Gafaelfawr logout.""" - return True - - async def render_logout_page(self) -> None: - self.redirect("/logout", permanent=False) - - -class GafaelfawrLoginHandler(BaseHandler): - """Login handler for Gafaelfawr authentication. - - This retrieves the authentication token from the headers, makes an API - call to get its metadata, constructs an authentication state, and then - redirects to the next URL. - """ - - async def get(self) -> None: - """Handle GET to the login page.""" - auth_info = _build_auth_info(self.request.headers) - - # Store the ancillary user information in the user database and create - # or return the user object. This call is unfortunately undocumented, - # but it's what BaseHandler calls to record the auth_state information - # after a form-based login. Hopefully this is a stable interface. - user = await self.auth_to_user(auth_info) - - # Tell JupyterHub to set its login cookie (also undocumented). - self.set_login_cookie(user) - - # Redirect to the next URL, which is under the control of JupyterHub - # and opaque to the authenticator. In practice, it will normally be - # whatever URL the user was trying to go to when JupyterHub decided - # they needed to be authenticated. - self.redirect(self.get_next_url(user)) diff --git a/authenticator/tests/__init__.py b/authenticator/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/spawner/tests/auth_test.py b/authenticator/tests/authenticator_test.py similarity index 92% rename from spawner/tests/auth_test.py rename to authenticator/tests/authenticator_test.py index 0a34357e3..0eb6ae0c0 100644 --- a/spawner/tests/auth_test.py +++ b/authenticator/tests/authenticator_test.py @@ -13,11 +13,11 @@ from tornado import web from tornado.httputil import HTTPHeaders -from rsp_restspawner import GafaelfawrAuthenticator -from rsp_restspawner.auth import ( - GafaelfawrLoginHandler, - GafaelfawrLogoutHandler, +from rubin.nublado.authenticator import GafaelfawrAuthenticator +from rubin.nublado.authenticator._internals import ( _build_auth_info, + _GafaelfawrLoginHandler, + _GafaelfawrLogoutHandler, ) @@ -25,8 +25,8 @@ async def test_authenticator() -> None: authenticator = GafaelfawrAuthenticator() assert authenticator.get_handlers(MagicMock()) == [ - ("/gafaelfawr/login", GafaelfawrLoginHandler), - ("/logout", GafaelfawrLogoutHandler), + ("/gafaelfawr/login", _GafaelfawrLoginHandler), + ("/logout", _GafaelfawrLogoutHandler), ] assert authenticator.login_url("/hub") == "/hub/gafaelfawr/login" diff --git a/hub/README.md b/hub/README.md new file mode 100644 index 000000000..4ee0bd899 --- /dev/null +++ b/hub/README.md @@ -0,0 +1,11 @@ +# RSP JupyterHub build + +This directory holds the build machinery for the modified JupyterHub build used by the Notebook Aspect of the Rubin Science Platform. + +The RSP JupyterHub is a combination of the standard JupyterHub build plus a custom authenticator and a custom spawner. +The authenticator and spawner are maintained as separate Python library packages with normal floating dependencies. +However, for a reproducible JupyterHub image build, we want to pin all Python dependencies and add some supplemental scripts. + +This directory contains the pinned Python dependencies and the supplemental scripts. +It is used by `Dockerfile.hub` when building the JupyterHub image. +The pinned dependencies here are only used for that image, and may differ from the pinned dependencies for the Nublado controller. diff --git a/spawner/requirements/main.in b/hub/requirements/main.in similarity index 89% rename from spawner/requirements/main.in rename to hub/requirements/main.in index 042a51805..28ce196a4 100644 --- a/spawner/requirements/main.in +++ b/hub/requirements/main.in @@ -11,7 +11,7 @@ # same version that Dockerfile uses. jupyterhub==4.0.2 -# Dependencies used directly by the code added by this package. +# Dependencies used directly by the authenticator and spawner. httpx httpx-sse PyYAML @@ -31,6 +31,3 @@ kubernetes_asyncio # JupyterHub image currently uses. exceptiongroup ruamel.yaml.clib - -# anyio 4 currently conflicts with FastAPI. -anyio<4 diff --git a/spawner/requirements/main.txt b/hub/requirements/main.txt similarity index 100% rename from spawner/requirements/main.txt rename to hub/requirements/main.txt diff --git a/spawner/scripts/install-base-packages.sh b/hub/scripts/install-base-packages.sh similarity index 100% rename from spawner/scripts/install-base-packages.sh rename to hub/scripts/install-base-packages.sh diff --git a/spawner/scripts/install-dependency-packages.sh b/hub/scripts/install-dependency-packages.sh similarity index 100% rename from spawner/scripts/install-dependency-packages.sh rename to hub/scripts/install-dependency-packages.sh diff --git a/noxfile.py b/noxfile.py index bdd705b46..987a39ee6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -12,10 +12,9 @@ ("--upgrade", "pip", "setuptools", "wheel"), ("-r", "controller/requirements/main.txt"), ("-r", "controller/requirements/dev.txt"), + ("-e", "authenticator[dev]"), ("-e", "controller"), - ("-r", "spawner/requirements/main.txt"), - ("-r", "spawner/requirements/dev.txt"), - ("-e", "spawner"), + ("-e", "spawner[dev]"), ] @@ -41,6 +40,24 @@ def _install_dev(session: nox.Session, bin_prefix: str = "") -> None: session.run(precommit, "install", external=True) +def _pytest(session: nox.Session, directory: str, module: str) -> None: + """Run pytest for the given directory and module, if needed.""" + args = [ + a.removeprefix(f"{directory}/") + for a in session.posargs + if a.startswith(("-", f"{directory}/")) + ] + if not session.posargs or args: + with session.chdir(directory): + session.run( + "pytest", + f"--cov={module}", + "--cov-branch", + "--cov-report=", + *args, + ) + + @nox.session(name="venv-init") def venv_init(session: nox.Session) -> None: """Set up a development venv. @@ -76,19 +93,27 @@ def typing(session: nox.Session) -> None: session.run( "mypy", *session.posargs, - "noxfile.py", - "controller/src", - "spawner/src", + "--namespace-packages", + "--explicit-package-bases", + "authenticator/src", + "authenticator/tests", + env={"MYPYPATH": "authenticator/src:authenticator"}, ) session.run( "mypy", *session.posargs, + "noxfile.py", + "controller/src", "controller/tests", ) session.run( "mypy", *session.posargs, + "--namespace-packages", + "--explicit-package-bases", + "spawner/src", "spawner/tests", + env={"MYPYPATH": "spawner/src:spawner"}, ) @@ -96,34 +121,9 @@ def typing(session: nox.Session) -> None: def test(session: nox.Session) -> None: """Run tests.""" _install(session) - with session.chdir("controller"): - controller_args = [ - a.removeprefix("controller/") - for a in session.posargs - if a.startswith(("-", "controller/")) - ] - if not session.posargs or controller_args: - session.run( - "pytest", - "--cov=controller", - "--cov-branch", - "--cov-report=", - *controller_args, - ) - with session.chdir("spawner"): - spawner_args = [ - a.removeprefix("spawner/") - for a in session.posargs - if a.startswith(("-", "spawner/")) - ] - if not session.posargs or spawner_args: - session.run( - "pytest", - "--cov=rsp_restspawner", - "--cov-branch", - "--cov-report=", - *spawner_args, - ) + _pytest(session, "authenticator", "rubin.nublado.authenticator") + _pytest(session, "controller", "controller") + _pytest(session, "spawner", "rubin.nublado.spawner") @nox.session @@ -174,29 +174,39 @@ def update_deps(session: nox.Session) -> None: "--upgrade", "pip-tools", "pip", "setuptools", "wheel", "pre-commit" ) session.run("pre-commit", "autoupdate") - for subdir in ("controller", "spawner"): - session.run( - "pip-compile", - "--upgrade", - "--resolver=backtracking", - "--build-isolation", - "--allow-unsafe", - "--generate-hashes", - "--output-file", - f"{subdir}/requirements/main.txt", - f"{subdir}/requirements/main.in", - ) - session.run( - "pip-compile", - "--upgrade", - "--resolver=backtracking", - "--build-isolation", - "--allow-unsafe", - "--generate-hashes", - "--output-file", - f"{subdir}/requirements/dev.txt", - f"{subdir}/requirements/dev.in", - ) + session.run( + "pip-compile", + "--upgrade", + "--resolver=backtracking", + "--build-isolation", + "--allow-unsafe", + "--generate-hashes", + "--output-file", + "controller/requirements/main.txt", + "controller/requirements/main.in", + ) + session.run( + "pip-compile", + "--upgrade", + "--resolver=backtracking", + "--build-isolation", + "--allow-unsafe", + "--generate-hashes", + "--output-file", + "controller/requirements/dev.txt", + "controller/requirements/dev.in", + ) + session.run( + "pip-compile", + "--upgrade", + "--resolver=backtracking", + "--build-isolation", + "--allow-unsafe", + "--generate-hashes", + "--output-file", + "hub/requirements/main.txt", + "hub/requirements/main.in", + ) print("\nTo refresh the development venv, run:\n\n\tnox -s init\n") diff --git a/pyproject.toml b/pyproject.toml index d5612e2bc..f72e68914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,12 @@ target-version = "py311" ] [tool.ruff.isort] -known-first-party = ["controller", "rsp_restspawner", "tests"] +known-first-party = [ + "controller", + "rubin.nublado.authenticator", + "rubin.nublado.spawner", + "tests", +] split-on-trailing-comma = false [tool.ruff.flake8-bugbear] diff --git a/spawner/README.md b/spawner/README.md index 7563055be..e7ecccdd5 100644 --- a/spawner/README.md +++ b/spawner/README.md @@ -1,10 +1,6 @@ -# rsp-restspawner +# REST spawner for JupyterHub Provides an implementation of the JupyterHub `Spawner` class that makes REST API calls to a Nublado lab controller to manage user lab Kubernetes pods. This is a client of the Nublado controller and an implementation of the [spawner API](https://jupyterhub.readthedocs.io/en/stable/api/spawner.html). -Currently, this repository also provides an implementation of the JupyterHub `Authenticator` class that authenticates a user using [Gafaelfawr](https://gafaelfawr.lsst.io/), assuming authentication is configured using [Phalanx](https://phalanx.lsst.io/). -It also builds the Docker image that is used as JupyterHub in a Rubin Science Platform installation. -In the future, these components will be broken into separate directories. - For more details about this architecture, see [SQR-066](https://sqr-066.lsst.io/). diff --git a/spawner/pyproject.toml b/spawner/pyproject.toml index 91c0f1515..0d3256df2 100644 --- a/spawner/pyproject.toml +++ b/spawner/pyproject.toml @@ -1,6 +1,6 @@ [project] # https://packaging.python.org/en/latest/specifications/declaring-project-metadata/ -name = "rsp-restspawner" +name = "rubin.nublado.spawner" description = "JupyterHub spawner using Nublado lab controller REST API." license = { file = "LICENSE" } readme = "README.md" @@ -18,9 +18,28 @@ classifiers = [ "Typing :: Typed", ] requires-python = ">=3.10" -dependencies = [] +dependencies = [ + "httpx<0.26", + "httpx-sse<0.4", + "jupyterhub<4", + "PyYAML<7", + "traitlets<6", +] dynamic = ["version"] +[project.optional-dependencies] +dev = [ + # Testing + "pytest", + "pytest-asyncio", + "pytest-cov", + "pytest-sugar", + "respx", + + # Type checking + "types-PyYAML", +] + [project.urls] Homepage = "https://nublado.lsst.io" Source = "https://github.com/lsst-sqre/nublado" @@ -35,7 +54,7 @@ root = ".." [tool.coverage.run] parallel = true branch = true -source = ["rsp_restspawner"] +source = ["rubin.nublado.spawner"] [tool.coverage.paths] source = ["src"] diff --git a/spawner/requirements/dev.in b/spawner/requirements/dev.in deleted file mode 100644 index 0be1685d0..000000000 --- a/spawner/requirements/dev.in +++ /dev/null @@ -1,26 +0,0 @@ -# -*- conf -*- -# -# Editable development dependencies -# Add direct development, test, and documentation dependencies here, as well -# as implicit dev dependencies with constrained versions. -# -# After editing, update requirements/dev.txt by running: -# make update-deps - --c main.txt - -coverage[toml] -mypy -pre-commit -pytest -pytest-asyncio -pytest-cov -pytest-sugar -respx -types-PyYAML - -# These dependencies aren't required in Python 3.11 but are required in 3.10. -# Pin them so that we can run tests with Python 3.10 when the dependencies -# have been regenerated with Python 3.11. -exceptiongroup -tomli diff --git a/spawner/requirements/dev.txt b/spawner/requirements/dev.txt deleted file mode 100644 index e7902ce74..000000000 --- a/spawner/requirements/dev.txt +++ /dev/null @@ -1,297 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --allow-unsafe --generate-hashes --output-file=spawner/requirements/dev.txt spawner/requirements/dev.in -# -anyio==3.7.1 \ - --hash=sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780 \ - --hash=sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5 - # via - # -c spawner/requirements/main.txt - # httpx -certifi==2023.7.22 \ - --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ - --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 - # via - # -c spawner/requirements/main.txt - # httpcore - # httpx -cfgv==3.4.0 \ - --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ - --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 - # via pre-commit -coverage[toml]==7.3.2 \ - --hash=sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1 \ - --hash=sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63 \ - --hash=sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9 \ - --hash=sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312 \ - --hash=sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3 \ - --hash=sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb \ - --hash=sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25 \ - --hash=sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92 \ - --hash=sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda \ - --hash=sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148 \ - --hash=sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6 \ - --hash=sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216 \ - --hash=sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a \ - --hash=sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640 \ - --hash=sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836 \ - --hash=sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c \ - --hash=sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f \ - --hash=sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2 \ - --hash=sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901 \ - --hash=sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed \ - --hash=sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a \ - --hash=sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074 \ - --hash=sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc \ - --hash=sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84 \ - --hash=sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083 \ - --hash=sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f \ - --hash=sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c \ - --hash=sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c \ - --hash=sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637 \ - --hash=sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2 \ - --hash=sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82 \ - --hash=sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f \ - --hash=sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce \ - --hash=sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef \ - --hash=sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f \ - --hash=sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611 \ - --hash=sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c \ - --hash=sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76 \ - --hash=sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9 \ - --hash=sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce \ - --hash=sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9 \ - --hash=sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf \ - --hash=sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf \ - --hash=sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9 \ - --hash=sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6 \ - --hash=sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2 \ - --hash=sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a \ - --hash=sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a \ - --hash=sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf \ - --hash=sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738 \ - --hash=sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a \ - --hash=sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4 - # via - # -r spawner/requirements/dev.in - # pytest-cov -distlib==0.3.7 \ - --hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 \ - --hash=sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8 - # via virtualenv -exceptiongroup==1.1.3 \ - --hash=sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9 \ - --hash=sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3 - # via - # -c spawner/requirements/main.txt - # -r spawner/requirements/dev.in -filelock==3.13.1 \ - --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \ - --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c - # via virtualenv -h11==0.14.0 \ - --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ - --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 - # via - # -c spawner/requirements/main.txt - # httpcore -httpcore==1.0.1 \ - --hash=sha256:c5e97ef177dca2023d0b9aad98e49507ef5423e9f1d94ffe2cfe250aa28e63b0 \ - --hash=sha256:fce1ddf9b606cfb98132ab58865c3728c52c8e4c3c46e2aabb3674464a186e92 - # via - # -c spawner/requirements/main.txt - # httpx -httpx==0.25.1 \ - --hash=sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a \ - --hash=sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0 - # via - # -c spawner/requirements/main.txt - # respx -identify==2.5.31 \ - --hash=sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75 \ - --hash=sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d - # via pre-commit -idna==3.4 \ - --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ - --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 - # via - # -c spawner/requirements/main.txt - # anyio - # httpx -iniconfig==2.0.0 \ - --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ - --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 - # via pytest -mypy==1.6.1 \ - --hash=sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7 \ - --hash=sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e \ - --hash=sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c \ - --hash=sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169 \ - --hash=sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208 \ - --hash=sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0 \ - --hash=sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1 \ - --hash=sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1 \ - --hash=sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7 \ - --hash=sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45 \ - --hash=sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143 \ - --hash=sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5 \ - --hash=sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f \ - --hash=sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd \ - --hash=sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245 \ - --hash=sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f \ - --hash=sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332 \ - --hash=sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30 \ - --hash=sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183 \ - --hash=sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f \ - --hash=sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85 \ - --hash=sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46 \ - --hash=sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71 \ - --hash=sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660 \ - --hash=sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb \ - --hash=sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c \ - --hash=sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a - # via -r spawner/requirements/dev.in -mypy-extensions==1.0.0 \ - --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ - --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 - # via mypy -nodeenv==1.8.0 \ - --hash=sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2 \ - --hash=sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec - # via pre-commit -packaging==23.2 \ - --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ - --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 - # via - # -c spawner/requirements/main.txt - # pytest - # pytest-sugar -platformdirs==3.11.0 \ - --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ - --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e - # via virtualenv -pluggy==1.3.0 \ - --hash=sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12 \ - --hash=sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7 - # via pytest -pre-commit==3.5.0 \ - --hash=sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32 \ - --hash=sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660 - # via -r spawner/requirements/dev.in -pytest==7.4.3 \ - --hash=sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac \ - --hash=sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5 - # via - # -r spawner/requirements/dev.in - # pytest-asyncio - # pytest-cov - # pytest-sugar -pytest-asyncio==0.21.1 \ - --hash=sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d \ - --hash=sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b - # via -r spawner/requirements/dev.in -pytest-cov==4.1.0 \ - --hash=sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6 \ - --hash=sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a - # via -r spawner/requirements/dev.in -pytest-sugar==0.9.7 \ - --hash=sha256:8cb5a4e5f8bbcd834622b0235db9e50432f4cbd71fef55b467fe44e43701e062 \ - --hash=sha256:f1e74c1abfa55f7241cf7088032b6e378566f16b938f3f08905e2cf4494edd46 - # via -r spawner/requirements/dev.in -pyyaml==6.0.1 \ - --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ - --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ - --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ - --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ - --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ - --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ - --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ - --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ - --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ - --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ - --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ - --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ - --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ - --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ - --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ - --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ - --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ - --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ - --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ - --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ - --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ - --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ - --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ - --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ - --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ - --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ - --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ - --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ - --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ - --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ - --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ - --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ - --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ - --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ - --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ - --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ - --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ - --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ - --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ - --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ - --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ - --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ - --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ - --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ - --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ - --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ - --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ - --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ - --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ - --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f - # via - # -c spawner/requirements/main.txt - # pre-commit -respx==0.20.2 \ - --hash=sha256:07cf4108b1c88b82010f67d3c831dae33a375c7b436e54d87737c7f9f99be643 \ - --hash=sha256:ab8e1cf6da28a5b2dd883ea617f8130f77f676736e6e9e4a25817ad116a172c9 - # via -r spawner/requirements/dev.in -sniffio==1.3.0 \ - --hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \ - --hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384 - # via - # -c spawner/requirements/main.txt - # anyio - # httpx -termcolor==2.3.0 \ - --hash=sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475 \ - --hash=sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a - # via pytest-sugar -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f - # via -r spawner/requirements/dev.in -types-pyyaml==6.0.12.12 \ - --hash=sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062 \ - --hash=sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24 - # via -r spawner/requirements/dev.in -typing-extensions==4.8.0 \ - --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ - --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef - # via - # -c spawner/requirements/main.txt - # mypy -virtualenv==20.24.6 \ - --hash=sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af \ - --hash=sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381 - # via pre-commit - -# The following packages are considered to be unsafe in a requirements file: -setuptools==68.2.2 \ - --hash=sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87 \ - --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a - # via - # -c spawner/requirements/main.txt - # nodeenv diff --git a/spawner/src/rsp_restspawner/__init__.py b/spawner/src/rsp_restspawner/__init__.py deleted file mode 100644 index b33101260..000000000 --- a/spawner/src/rsp_restspawner/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .auth import GafaelfawrAuthenticator -from .spawner import RSPRestSpawner - -__all__ = ["GafaelfawrAuthenticator", "RSPRestSpawner"] diff --git a/spawner/src/rubin/nublado/spawner/__init__.py b/spawner/src/rubin/nublado/spawner/__init__.py new file mode 100644 index 000000000..f031caaa3 --- /dev/null +++ b/spawner/src/rubin/nublado/spawner/__init__.py @@ -0,0 +1,3 @@ +from ._internals import RSPRestSpawner + +__all__ = ["RSPRestSpawner"] diff --git a/spawner/src/rsp_restspawner/exceptions.py b/spawner/src/rubin/nublado/spawner/_exceptions.py similarity index 100% rename from spawner/src/rsp_restspawner/exceptions.py rename to spawner/src/rubin/nublado/spawner/_exceptions.py diff --git a/spawner/src/rsp_restspawner/spawner.py b/spawner/src/rubin/nublado/spawner/_internals.py similarity index 89% rename from spawner/src/rsp_restspawner/spawner.py rename to spawner/src/rubin/nublado/spawner/_internals.py index 5cdecb0a7..a9a03d48d 100644 --- a/spawner/src/rsp_restspawner/spawner.py +++ b/spawner/src/rubin/nublado/spawner/_internals.py @@ -4,9 +4,7 @@ import asyncio from collections.abc import AsyncIterator, Callable, Coroutine -from dataclasses import dataclass from datetime import timedelta -from enum import Enum from functools import wraps from pathlib import Path from typing import Any, Concatenate, ParamSpec, TypeVar @@ -16,12 +14,13 @@ from jupyterhub.spawner import Spawner from traitlets import Unicode, default -from .exceptions import ( +from ._exceptions import ( ControllerWebError, InvalidAuthStateError, MissingFieldError, SpawnFailedError, ) +from ._models import LabStatus, SpawnEvent P = ParamSpec("P") T = TypeVar("T") @@ -35,87 +34,6 @@ """Cached global HTTP client so that we can share a connection pool.""" -class LabStatus(str, Enum): - """Possible status conditions of a user's pod per the lab controller. - - This is not directly equivalent to pod phases. It is instead intended to - capture the status of the lab from an infrastructure standpoint, - reflecting the current intent of the controller. Most notably, labs that - have stopped running for any reason (failure or success) use the - terminated status. The failed status is reserved for failed Kubernetes - operations or missing or invalid Kubernetes objects. - - Keep this in sync with the status values reported by the status endpoint - of the lab controller. - """ - - PENDING = "pending" - RUNNING = "running" - TERMINATING = "terminating" - TERMINATED = "terminated" - FAILED = "failed" - - -@dataclass(frozen=True, slots=True) -class SpawnEvent: - """JupyterHub spawning event.""" - - progress: int - """Percentage of progress, from 0 to 100.""" - - message: str - """Event description.""" - - severity: str - """Log message severity.""" - - complete: bool = False - """Whether the event indicated spawning is done.""" - - failed: bool = False - """Whether the event indicated spawning failed.""" - - @classmethod - def from_sse(cls, sse: ServerSentEvent, progress: int) -> SpawnEvent: - """Convert from a server-sent event from the lab controller. - - Parameters - ---------- - sse - Event from the lab controller. - progress - Current progress percentage, if the event doesn't specify one. - """ - try: - data = sse.json() - if not (set(data.keys()) <= {"message", "progress"}): - raise ValueError("Invalid key in SSE data") - if "progress" in data: - progress = int(data["progress"]) - if progress < 0 or progress > 100: - raise ValueError(f"Invalid progress value {progress}") - except Exception: - data = {"message": sse.data} - data["progress"] = progress - - if sse.event == "complete": - data["progress"] = 75 - return cls(**data, severity="info", complete=True) - elif sse.event in ("info", "error"): - return cls(**data, severity=sse.event) - elif sse.event == "failed": - return cls(**data, severity="error", failed=True) - else: - return cls(**data, severity="unknown") - - def to_dict(self) -> dict[str, int | str]: - """Convert to the dictionary expected by JupyterHub.""" - return { - "progress": self.progress, - "message": f"[{self.severity}] {self.message}", - } - - def _convert_exception( f: Callable[Concatenate[RSPRestSpawner, P], Coroutine[None, None, T]] ) -> Callable[Concatenate[RSPRestSpawner, P], Coroutine[None, None, T]]: diff --git a/spawner/src/rubin/nublado/spawner/_models.py b/spawner/src/rubin/nublado/spawner/_models.py new file mode 100644 index 000000000..da5dbc630 --- /dev/null +++ b/spawner/src/rubin/nublado/spawner/_models.py @@ -0,0 +1,89 @@ +"""Models for the Nublado JupyterHub spawner class.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + +from httpx_sse import ServerSentEvent + + +class LabStatus(str, Enum): + """Possible status conditions of a user's pod per the lab controller. + + This is not directly equivalent to pod phases. It is instead intended to + capture the status of the lab from an infrastructure standpoint, + reflecting the current intent of the controller. Most notably, labs that + have stopped running for any reason (failure or success) use the + terminated status. The failed status is reserved for failed Kubernetes + operations or missing or invalid Kubernetes objects. + + Keep this in sync with the status values reported by the status endpoint + of the lab controller. + """ + + PENDING = "pending" + RUNNING = "running" + TERMINATING = "terminating" + TERMINATED = "terminated" + FAILED = "failed" + + +@dataclass(frozen=True, slots=True) +class SpawnEvent: + """JupyterHub spawning event.""" + + progress: int + """Percentage of progress, from 0 to 100.""" + + message: str + """Event description.""" + + severity: str + """Log message severity.""" + + complete: bool = False + """Whether the event indicated spawning is done.""" + + failed: bool = False + """Whether the event indicated spawning failed.""" + + @classmethod + def from_sse(cls, sse: ServerSentEvent, progress: int) -> SpawnEvent: + """Convert from a server-sent event from the lab controller. + + Parameters + ---------- + sse + Event from the lab controller. + progress + Current progress percentage, if the event doesn't specify one. + """ + try: + data = sse.json() + if not (set(data.keys()) <= {"message", "progress"}): + raise ValueError("Invalid key in SSE data") + if "progress" in data: + progress = int(data["progress"]) + if progress < 0 or progress > 100: + raise ValueError(f"Invalid progress value {progress}") + except Exception: + data = {"message": sse.data} + data["progress"] = progress + + if sse.event == "complete": + data["progress"] = 75 + return cls(**data, severity="info", complete=True) + elif sse.event in ("info", "error"): + return cls(**data, severity=sse.event) + elif sse.event == "failed": + return cls(**data, severity="error", failed=True) + else: + return cls(**data, severity="unknown") + + def to_dict(self) -> dict[str, int | str]: + """Convert to the dictionary expected by JupyterHub.""" + return { + "progress": self.progress, + "message": f"[{self.severity}] {self.message}", + } diff --git a/spawner/tests/conftest.py b/spawner/tests/conftest.py index 71cff3ab5..026d013ea 100644 --- a/spawner/tests/conftest.py +++ b/spawner/tests/conftest.py @@ -7,7 +7,7 @@ import pytest import respx -from rsp_restspawner.spawner import RSPRestSpawner +from rubin.nublado.spawner import RSPRestSpawner from .support.controller import MockLabController, register_mock_lab_controller from .support.jupyterhub import MockHub, MockUser diff --git a/spawner/tests/spawner_test.py b/spawner/tests/spawner_test.py index 6b5af20e4..c905d6954 100644 --- a/spawner/tests/spawner_test.py +++ b/spawner/tests/spawner_test.py @@ -7,9 +7,9 @@ import pytest -from rsp_restspawner import RSPRestSpawner -from rsp_restspawner.exceptions import SpawnFailedError -from rsp_restspawner.spawner import LabStatus +from rubin.nublado.spawner import RSPRestSpawner +from rubin.nublado.spawner._exceptions import SpawnFailedError +from rubin.nublado.spawner._models import LabStatus from .support.controller import MockLabController diff --git a/spawner/tests/support/controller.py b/spawner/tests/support/controller.py index 130fd9ed9..1020a9594 100644 --- a/spawner/tests/support/controller.py +++ b/spawner/tests/support/controller.py @@ -9,7 +9,7 @@ import respx from httpx import AsyncByteStream, Request, Response -from rsp_restspawner.spawner import LabStatus +from rubin.nublado.spawner._models import LabStatus __all__ = [ "MockLabController",