diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index fb89f8193..33be8bab4 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -124,11 +124,23 @@ jobs:
fetch-depth: 0
- uses: lsst-sqre/build-and-push-to-ghcr@v1
- id: build
+ id: build1
with:
+ dockerfile: Dockerfile.controller
image: ${{ github.repository }}-controller
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Report result
run: |
- echo Pushed ghcr.io/${{ github.repository }}-controller:${{ steps.build.outputs.tag }}
+ echo Pushed ghcr.io/${{ github.repository }}-controller:${{ steps.build1.outputs.tag }}
+
+ - uses: lsst-sqre/build-and-push-to-ghcr@v1
+ id: build2
+ with:
+ dockerfile: Dockerfile.controller
+ image: ${{ github.repository }}-jupyterhub
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Report result
+ run: |
+ echo Pushed ghcr.io/${{ github.repository }}-jupyterhub:${{ steps.build2.outputs.tag }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2b8a7575d..41b72b534 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,7 +8,7 @@ repos:
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.3
+ rev: v0.1.4
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
@@ -17,6 +17,7 @@ repos:
rev: 23.10.1
hooks:
- id: black
+ args: [--config, pyproject.toml]
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.16.0
diff --git a/Dockerfile b/Dockerfile.controller
similarity index 81%
rename from Dockerfile
rename to Dockerfile.controller
index c1e1b403b..cf1875e64 100644
--- a/Dockerfile
+++ b/Dockerfile.controller
@@ -1,12 +1,14 @@
+# Docker build instructions for the Nublado controller.
+#
# This Dockerfile has four stages:
#
# base-image
# Updates the base Python image with security patches and common system
# packages. This image becomes the base of all other images.
# dependencies-image
-# Installs third-party dependencies (requirements/main.txt) into a virtual
-# environment. This virtual environment is ideal for copying across build
-# stages.
+# Installs third-party dependencies (controller/requirements/main.txt)
+# into a virtual environment. This virtual environment is ideal for
+# copying across build stages.
# install-image
# Installs the app into the virtual environment.
# runtime-image
@@ -17,13 +19,13 @@
FROM python:3.11.5-slim-bullseye as base-image
# Update system packages
-COPY scripts/install-base-packages.sh .
+COPY controller/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 scripts/install-dependency-packages.sh .
+COPY controller/scripts/install-dependency-packages.sh .
RUN ./install-dependency-packages.sh
# Create a Python virtual environment
@@ -56,7 +58,7 @@ RUN useradd --create-home appuser
COPY --from=install-image /opt/venv /opt/venv
# Copy the startup script
-COPY scripts/start.sh /start.sh
+COPY controller/scripts/start.sh /start.sh
# Make sure we use the virtualenv
ENV PATH="/opt/venv/bin:$PATH"
diff --git a/Dockerfile.hub b/Dockerfile.hub
new file mode 100644
index 000000000..012b75675
--- /dev/null
+++ b/Dockerfile.hub
@@ -0,0 +1,49 @@
+# Docker build instructions for the custom Nublado JupyterHub build.
+#
+# This Dockerfile has three stages:
+#
+# base-image
+# Updates the base Python image with security patches and common system
+# packages. This image becomes the base of all other images.
+# dependencies-image
+# Installs third-party dependencies (spawner/requirements/main.txt) into
+# a virtual environment. This virtual environment is ideal for copying
+# across build stages.
+# runtime-image
+# - Copies the virtual environment into place.
+# - Installs the additional JupyterHub plugins.
+# - Runs a non-root user.
+
+FROM jupyterhub/jupyterhub:4.0.2 as base-image
+
+# Update system packages
+COPY spawner/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 .
+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
+RUN pip install --quiet --no-cache-dir -r requirements.txt
+
+FROM dependencies-image as runtime-image
+
+# Install the extra JupyterHub modules.
+COPY . /app
+WORKDIR /app
+RUN pip install --no-cache-dir 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
+# application values.yaml.
+RUN groupadd --gid 768 jovyan
+RUN useradd --create-home jovyan --uid 768 --gid 768
+WORKDIR /home/jovyan
+
+USER jovyan
+EXPOSE 8000
+EXPOSE 8081
diff --git a/controller/LICENSE b/controller/LICENSE
deleted file mode 100644
index 1152a5eeb..000000000
--- a/controller/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2022-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/controller/LICENSE b/controller/LICENSE
new file mode 120000
index 000000000..ea5b60640
--- /dev/null
+++ b/controller/LICENSE
@@ -0,0 +1 @@
+../LICENSE
\ No newline at end of file
diff --git a/controller/requirements/dev.txt b/controller/requirements/dev.txt
index 5035bdcf9..c2b6f249f 100644
--- a/controller/requirements/dev.txt
+++ b/controller/requirements/dev.txt
@@ -19,7 +19,7 @@ anyio==3.7.1 \
--hash=sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5
# via
# -c controller/requirements/main.txt
- # httpcore
+ # httpx
asgi-lifespan==2.1.0 \
--hash=sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308 \
--hash=sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f
@@ -247,15 +247,15 @@ h11==0.14.0 \
# via
# -c controller/requirements/main.txt
# httpcore
-httpcore==0.18.0 \
- --hash=sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9 \
- --hash=sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced
+httpcore==1.0.1 \
+ --hash=sha256:c5e97ef177dca2023d0b9aad98e49507ef5423e9f1d94ffe2cfe250aa28e63b0 \
+ --hash=sha256:fce1ddf9b606cfb98132ab58865c3728c52c8e4c3c46e2aabb3674464a186e92
# via
# -c controller/requirements/main.txt
# httpx
-httpx==0.25.0 \
- --hash=sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100 \
- --hash=sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875
+httpx==0.25.1 \
+ --hash=sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a \
+ --hash=sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0
# via
# -c controller/requirements/main.txt
# respx
@@ -802,7 +802,6 @@ sniffio==1.3.0 \
# -c controller/requirements/main.txt
# anyio
# asgi-lifespan
- # httpcore
# httpx
snowballstemmer==2.2.0 \
--hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
diff --git a/controller/requirements/main.txt b/controller/requirements/main.txt
index b39bc6fa9..a0119d679 100644
--- a/controller/requirements/main.txt
+++ b/controller/requirements/main.txt
@@ -110,7 +110,7 @@ anyio==3.7.1 \
--hash=sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5
# via
# fastapi
- # httpcore
+ # httpx
# starlette
# watchfiles
async-timeout==4.0.3 \
@@ -402,9 +402,9 @@ google-auth==2.23.4 \
--hash=sha256:79905d6b1652187def79d491d6e23d0cbb3a21d3c7ba0dbaa9c8a01906b13ff3 \
--hash=sha256:d4bbc92fe4b8bfd2f3e8d88e5ba7085935da208ee38a134fc280e7ce682a05f2
# via google-api-core
-google-cloud-artifact-registry==1.8.4 \
- --hash=sha256:2deba5c9c0065cf8e2e63c547e93a4921867d57261d57d4a756be1031529e228 \
- --hash=sha256:a5066555fe8324cc6e8498014b054b1d3e87a8f9e1b0fa1e47ba3bbc3daaa581
+google-cloud-artifact-registry==1.9.0 \
+ --hash=sha256:3484d2a2c04fadfa480be16a554d01e47e4c4f4b5a123cd3725e865b4dd7f325 \
+ --hash=sha256:aeee14226b1636e7bc043476cff756876068872bda3246a3b124c39d3f4e3be8
# via -r controller/requirements/main.in
googleapis-common-protos[grpc]==1.61.0 \
--hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \
@@ -487,9 +487,9 @@ h11==0.14.0 \
# via
# httpcore
# uvicorn
-httpcore==0.18.0 \
- --hash=sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9 \
- --hash=sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced
+httpcore==1.0.1 \
+ --hash=sha256:c5e97ef177dca2023d0b9aad98e49507ef5423e9f1d94ffe2cfe250aa28e63b0 \
+ --hash=sha256:fce1ddf9b606cfb98132ab58865c3728c52c8e4c3c46e2aabb3674464a186e92
# via httpx
httptools==0.6.1 \
--hash=sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563 \
@@ -529,9 +529,9 @@ httptools==0.6.1 \
--hash=sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185 \
--hash=sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3
# via uvicorn
-httpx==0.25.0 \
- --hash=sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100 \
- --hash=sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875
+httpx==0.25.1 \
+ --hash=sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a \
+ --hash=sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0
# via
# -r controller/requirements/main.in
# safir
@@ -948,7 +948,6 @@ sniffio==1.3.0 \
--hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384
# via
# anyio
- # httpcore
# httpx
sse-starlette==1.6.5 \
--hash=sha256:68b6b7eb49be0c72a2af80a055994c13afcaa4761b29226beb208f954c25a642 \
diff --git a/scripts/install-base-packages.sh b/controller/scripts/install-base-packages.sh
similarity index 100%
rename from scripts/install-base-packages.sh
rename to controller/scripts/install-base-packages.sh
diff --git a/scripts/install-dependency-packages.sh b/controller/scripts/install-dependency-packages.sh
similarity index 100%
rename from scripts/install-dependency-packages.sh
rename to controller/scripts/install-dependency-packages.sh
diff --git a/scripts/start.sh b/controller/scripts/start.sh
similarity index 100%
rename from scripts/start.sh
rename to controller/scripts/start.sh
diff --git a/noxfile.py b/noxfile.py
index 1fb538cd9..bdd705b46 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -7,13 +7,15 @@
nox.options.default_venv_backend = "venv"
nox.options.reuse_existing_virtualenvs = True
-
-# pip-installable dependencies for the controller package.
+# pip-installable dependencies for all subpackages.
PIP_DEPENDENCIES = [
("--upgrade", "pip", "setuptools", "wheel"),
("-r", "controller/requirements/main.txt"),
("-r", "controller/requirements/dev.txt"),
("-e", "controller"),
+ ("-r", "spawner/requirements/main.txt"),
+ ("-r", "spawner/requirements/dev.txt"),
+ ("-e", "spawner"),
]
@@ -73,10 +75,20 @@ def typing(session: nox.Session) -> None:
session.install("mypy")
session.run(
"mypy",
+ *session.posargs,
"noxfile.py",
"controller/src",
+ "spawner/src",
+ )
+ session.run(
+ "mypy",
+ *session.posargs,
"controller/tests",
+ )
+ session.run(
+ "mypy",
*session.posargs,
+ "spawner/tests",
)
@@ -84,21 +96,34 @@ def typing(session: nox.Session) -> None:
def test(session: nox.Session) -> None:
"""Run tests."""
_install(session)
-
- # This will need to be more sophisticated once there are multiple
- # subdirectories with their own tests and we have to decide which
- # subdirectories to execute tests in based on the arguments.
with session.chdir("controller"):
controller_args = [
- a.removeprefix("controller/") for a in session.posargs
+ a.removeprefix("controller/")
+ for a in session.posargs
+ if a.startswith(("-", "controller/"))
]
- session.run(
- "pytest",
- "--cov=controller",
- "--cov-branch",
- "--cov-report=",
- *controller_args,
- )
+ 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,
+ )
@nox.session
@@ -149,28 +174,29 @@ def update_deps(session: nox.Session) -> None:
"--upgrade", "pip-tools", "pip", "setuptools", "wheel", "pre-commit"
)
session.run("pre-commit", "autoupdate")
- 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",
- )
+ 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",
+ )
print("\nTo refresh the development venv, run:\n\n\tnox -s init\n")
@@ -179,9 +205,5 @@ def update_deps(session: nox.Session) -> None:
def run(session: nox.Session) -> None:
"""Run the application in development mode."""
_install(session)
- with session.chdir("server"):
- session.run(
- "uvicorn",
- "controller.main:app",
- "--reload",
- )
+ with session.chdir("controller"):
+ session.run("uvicorn", "controller.main:app", "--reload")
diff --git a/pyproject.toml b/pyproject.toml
index c794867c3..d5612e2bc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -75,6 +75,7 @@ ignore = [
"PLR0911", # often many returns is clearer and simpler style
"PLR0913", # factory pattern uses constructors with many arguments
"PLR2004", # too aggressive about magic values
+ "PLW0603", # yes global is discouraged but if needed, it's needed
"S105", # good idea but too many false positives on non-passwords
"S106", # good idea but too many false positives on non-passwords
"S107", # good idea but too many false positives on non-passwords
@@ -88,6 +89,7 @@ ignore = [
"TD003", # we don't require issues be created for TODOs
"TID252", # if we're going to use relative imports, use them always
"TRY003", # good general advice but lint is way too aggressive
+ "TRY301", # sometimes raising exceptions inside try is the best flow
# TEMPORARY exclusions while refactoring is in progress. All of these will
# be re-enabled once refactoring is complete.
@@ -125,7 +127,7 @@ target-version = "py311"
]
[tool.ruff.isort]
-known-first-party = ["controller", "tests"]
+known-first-party = ["controller", "rsp_restspawner", "tests"]
split-on-trailing-comma = false
[tool.ruff.flake8-bugbear]
diff --git a/scripts/docker-tag.sh b/scripts/docker-tag.sh
deleted file mode 100755
index 080a9c9b3..000000000
--- a/scripts/docker-tag.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/bash
-
-# Determine the tag for Docker images based on GitHub Actions environment
-# variables.
-
-set -eo pipefail
-
-if [ -n "$GITHUB_HEAD_REF" ]; then
- # For pull requests
- echo ${GITHUB_HEAD_REF} | sed -E 's,/,-,g'
-else
- # For push events
- echo ${GITHUB_REF} | sed -E 's,refs/(heads|tags)/,,' | sed -E 's,/,-,g'
-fi
diff --git a/spawner/LICENSE b/spawner/LICENSE
new file mode 120000
index 000000000..ea5b60640
--- /dev/null
+++ b/spawner/LICENSE
@@ -0,0 +1 @@
+../LICENSE
\ No newline at end of file
diff --git a/spawner/README.md b/spawner/README.md
new file mode 100644
index 000000000..7563055be
--- /dev/null
+++ b/spawner/README.md
@@ -0,0 +1,10 @@
+# rsp-restspawner
+
+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
new file mode 100644
index 000000000..91c0f1515
--- /dev/null
+++ b/spawner/pyproject.toml
@@ -0,0 +1,71 @@
+[project]
+# https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
+name = "rsp-restspawner"
+description = "JupyterHub spawner using Nublado lab controller REST API."
+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 = []
+dynamic = ["version"]
+
+[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_scm]
+root = ".."
+
+[tool.coverage.run]
+parallel = true
+branch = true
+source = ["rsp_restspawner"]
+
+[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/spawner/requirements/dev.in b/spawner/requirements/dev.in
new file mode 100644
index 000000000..0be1685d0
--- /dev/null
+++ b/spawner/requirements/dev.in
@@ -0,0 +1,26 @@
+# -*- 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
new file mode 100644
index 000000000..e7902ce74
--- /dev/null
+++ b/spawner/requirements/dev.txt
@@ -0,0 +1,297 @@
+#
+# 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/requirements/main.in b/spawner/requirements/main.in
new file mode 100644
index 000000000..042a51805
--- /dev/null
+++ b/spawner/requirements/main.in
@@ -0,0 +1,36 @@
+# -*- conf -*-
+#
+# Editable runtime dependencies (equivalent to install_requires)
+# Add direct runtime dependencies here, as well as implicit dependencies
+# with constrained versions.
+#
+# After editing, update requirements/main.txt by running:
+# make update-deps
+
+# We want to explicitly control new versions of JupyterHub. Pin this to the
+# same version that Dockerfile uses.
+jupyterhub==4.0.2
+
+# Dependencies used directly by the code added by this package.
+httpx
+httpx-sse
+PyYAML
+traitlets
+
+# Additional dependencies required by our configuration of JupyterHub.
+jupyterhub-idle-culler
+psycopg2
+
+# Zero-to-JupyterHub unconditionally imports this, so we have to have it
+# installed even though we're using a different spawner class.
+kubernetes_asyncio
+
+# These dependencies aren't required in Python 3.11 but are required in 3.10.
+# Pin them so that we can regenerate dependencies with Python 3.11 and still
+# successfully build a Docker image using Python 3.10, which the base
+# JupyterHub image currently uses.
+exceptiongroup
+ruamel.yaml.clib
+
+# anyio 4 currently conflicts with FastAPI.
+anyio<4
diff --git a/spawner/requirements/main.txt b/spawner/requirements/main.txt
new file mode 100644
index 000000000..a393c3924
--- /dev/null
+++ b/spawner/requirements/main.txt
@@ -0,0 +1,1091 @@
+#
+# 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/main.txt spawner/requirements/main.in
+#
+aiohttp==3.8.6 \
+ --hash=sha256:002f23e6ea8d3dd8d149e569fd580c999232b5fbc601c48d55398fbc2e582e8c \
+ --hash=sha256:01770d8c04bd8db568abb636c1fdd4f7140b284b8b3e0b4584f070180c1e5c62 \
+ --hash=sha256:0912ed87fee967940aacc5306d3aa8ba3a459fcd12add0b407081fbefc931e53 \
+ --hash=sha256:0cccd1de239afa866e4ce5c789b3032442f19c261c7d8a01183fd956b1935349 \
+ --hash=sha256:0fa375b3d34e71ccccf172cab401cd94a72de7a8cc01847a7b3386204093bb47 \
+ --hash=sha256:13da35c9ceb847732bf5c6c5781dcf4780e14392e5d3b3c689f6d22f8e15ae31 \
+ --hash=sha256:14cd52ccf40006c7a6cd34a0f8663734e5363fd981807173faf3a017e202fec9 \
+ --hash=sha256:16d330b3b9db87c3883e565340d292638a878236418b23cc8b9b11a054aaa887 \
+ --hash=sha256:1bed815f3dc3d915c5c1e556c397c8667826fbc1b935d95b0ad680787896a358 \
+ --hash=sha256:1d84166673694841d8953f0a8d0c90e1087739d24632fe86b1a08819168b4566 \
+ --hash=sha256:1f13f60d78224f0dace220d8ab4ef1dbc37115eeeab8c06804fec11bec2bbd07 \
+ --hash=sha256:229852e147f44da0241954fc6cb910ba074e597f06789c867cb7fb0621e0ba7a \
+ --hash=sha256:253bf92b744b3170eb4c4ca2fa58f9c4b87aeb1df42f71d4e78815e6e8b73c9e \
+ --hash=sha256:255ba9d6d5ff1a382bb9a578cd563605aa69bec845680e21c44afc2670607a95 \
+ --hash=sha256:2817b2f66ca82ee699acd90e05c95e79bbf1dc986abb62b61ec8aaf851e81c93 \
+ --hash=sha256:2b8d4e166e600dcfbff51919c7a3789ff6ca8b3ecce16e1d9c96d95dd569eb4c \
+ --hash=sha256:2d5b785c792802e7b275c420d84f3397668e9d49ab1cb52bd916b3b3ffcf09ad \
+ --hash=sha256:3161ce82ab85acd267c8f4b14aa226047a6bee1e4e6adb74b798bd42c6ae1f80 \
+ --hash=sha256:33164093be11fcef3ce2571a0dccd9041c9a93fa3bde86569d7b03120d276c6f \
+ --hash=sha256:39a312d0e991690ccc1a61f1e9e42daa519dcc34ad03eb6f826d94c1190190dd \
+ --hash=sha256:3b2ab182fc28e7a81f6c70bfbd829045d9480063f5ab06f6e601a3eddbbd49a0 \
+ --hash=sha256:3c68330a59506254b556b99a91857428cab98b2f84061260a67865f7f52899f5 \
+ --hash=sha256:3f0e27e5b733803333bb2371249f41cf42bae8884863e8e8965ec69bebe53132 \
+ --hash=sha256:3f5c7ce535a1d2429a634310e308fb7d718905487257060e5d4598e29dc17f0b \
+ --hash=sha256:3fd194939b1f764d6bb05490987bfe104287bbf51b8d862261ccf66f48fb4096 \
+ --hash=sha256:41bdc2ba359032e36c0e9de5a3bd00d6fb7ea558a6ce6b70acedf0da86458321 \
+ --hash=sha256:41d55fc043954cddbbd82503d9cc3f4814a40bcef30b3569bc7b5e34130718c1 \
+ --hash=sha256:42c89579f82e49db436b69c938ab3e1559e5a4409eb8639eb4143989bc390f2f \
+ --hash=sha256:45ad816b2c8e3b60b510f30dbd37fe74fd4a772248a52bb021f6fd65dff809b6 \
+ --hash=sha256:4ac39027011414dbd3d87f7edb31680e1f430834c8cef029f11c66dad0670aa5 \
+ --hash=sha256:4d4cbe4ffa9d05f46a28252efc5941e0462792930caa370a6efaf491f412bc66 \
+ --hash=sha256:4fcf3eabd3fd1a5e6092d1242295fa37d0354b2eb2077e6eb670accad78e40e1 \
+ --hash=sha256:5d791245a894be071d5ab04bbb4850534261a7d4fd363b094a7b9963e8cdbd31 \
+ --hash=sha256:6c43ecfef7deaf0617cee936836518e7424ee12cb709883f2c9a1adda63cc460 \
+ --hash=sha256:6c5f938d199a6fdbdc10bbb9447496561c3a9a565b43be564648d81e1102ac22 \
+ --hash=sha256:6e2f9cc8e5328f829f6e1fb74a0a3a939b14e67e80832975e01929e320386b34 \
+ --hash=sha256:713103a8bdde61d13490adf47171a1039fd880113981e55401a0f7b42c37d071 \
+ --hash=sha256:71783b0b6455ac8f34b5ec99d83e686892c50498d5d00b8e56d47f41b38fbe04 \
+ --hash=sha256:76b36b3124f0223903609944a3c8bf28a599b2cc0ce0be60b45211c8e9be97f8 \
+ --hash=sha256:7bc88fc494b1f0311d67f29fee6fd636606f4697e8cc793a2d912ac5b19aa38d \
+ --hash=sha256:7ee912f7e78287516df155f69da575a0ba33b02dd7c1d6614dbc9463f43066e3 \
+ --hash=sha256:86f20cee0f0a317c76573b627b954c412ea766d6ada1a9fcf1b805763ae7feeb \
+ --hash=sha256:89341b2c19fb5eac30c341133ae2cc3544d40d9b1892749cdd25892bbc6ac951 \
+ --hash=sha256:8a9b5a0606faca4f6cc0d338359d6fa137104c337f489cd135bb7fbdbccb1e39 \
+ --hash=sha256:8d399dade330c53b4106160f75f55407e9ae7505263ea86f2ccca6bfcbdb4921 \
+ --hash=sha256:8e31e9db1bee8b4f407b77fd2507337a0a80665ad7b6c749d08df595d88f1cf5 \
+ --hash=sha256:90c72ebb7cb3a08a7f40061079817133f502a160561d0675b0a6adf231382c92 \
+ --hash=sha256:918810ef188f84152af6b938254911055a72e0f935b5fbc4c1a4ed0b0584aed1 \
+ --hash=sha256:93c15c8e48e5e7b89d5cb4613479d144fda8344e2d886cf694fd36db4cc86865 \
+ --hash=sha256:96603a562b546632441926cd1293cfcb5b69f0b4159e6077f7c7dbdfb686af4d \
+ --hash=sha256:99c5ac4ad492b4a19fc132306cd57075c28446ec2ed970973bbf036bcda1bcc6 \
+ --hash=sha256:9c19b26acdd08dd239e0d3669a3dddafd600902e37881f13fbd8a53943079dbc \
+ --hash=sha256:9de50a199b7710fa2904be5a4a9b51af587ab24c8e540a7243ab737b45844543 \
+ --hash=sha256:9e2ee0ac5a1f5c7dd3197de309adfb99ac4617ff02b0603fd1e65b07dc772e4b \
+ --hash=sha256:a2ece4af1f3c967a4390c284797ab595a9f1bc1130ef8b01828915a05a6ae684 \
+ --hash=sha256:a3628b6c7b880b181a3ae0a0683698513874df63783fd89de99b7b7539e3e8a8 \
+ --hash=sha256:ad1407db8f2f49329729564f71685557157bfa42b48f4b93e53721a16eb813ed \
+ --hash=sha256:b04691bc6601ef47c88f0255043df6f570ada1a9ebef99c34bd0b72866c217ae \
+ --hash=sha256:b0cf2a4501bff9330a8a5248b4ce951851e415bdcce9dc158e76cfd55e15085c \
+ --hash=sha256:b2fe42e523be344124c6c8ef32a011444e869dc5f883c591ed87f84339de5976 \
+ --hash=sha256:b30e963f9e0d52c28f284d554a9469af073030030cef8693106d918b2ca92f54 \
+ --hash=sha256:bb54c54510e47a8c7c8e63454a6acc817519337b2b78606c4e840871a3e15349 \
+ --hash=sha256:bd111d7fc5591ddf377a408ed9067045259ff2770f37e2d94e6478d0f3fc0c17 \
+ --hash=sha256:bdf70bfe5a1414ba9afb9d49f0c912dc524cf60141102f3a11143ba3d291870f \
+ --hash=sha256:ca80e1b90a05a4f476547f904992ae81eda5c2c85c66ee4195bb8f9c5fb47f28 \
+ --hash=sha256:caf486ac1e689dda3502567eb89ffe02876546599bbf915ec94b1fa424eeffd4 \
+ --hash=sha256:ccc360e87341ad47c777f5723f68adbb52b37ab450c8bc3ca9ca1f3e849e5fe2 \
+ --hash=sha256:d25036d161c4fe2225d1abff2bd52c34ed0b1099f02c208cd34d8c05729882f0 \
+ --hash=sha256:d52d5dc7c6682b720280f9d9db41d36ebe4791622c842e258c9206232251ab2b \
+ --hash=sha256:d67f8baed00870aa390ea2590798766256f31dc5ed3ecc737debb6e97e2ede78 \
+ --hash=sha256:d76e8b13161a202d14c9584590c4df4d068c9567c99506497bdd67eaedf36403 \
+ --hash=sha256:d95fc1bf33a9a81469aa760617b5971331cdd74370d1214f0b3109272c0e1e3c \
+ --hash=sha256:de6a1c9f6803b90e20869e6b99c2c18cef5cc691363954c93cb9adeb26d9f3ae \
+ --hash=sha256:e1d8cb0b56b3587c5c01de3bf2f600f186da7e7b5f7353d1bf26a8ddca57f965 \
+ --hash=sha256:e2a988a0c673c2e12084f5e6ba3392d76c75ddb8ebc6c7e9ead68248101cd446 \
+ --hash=sha256:e3f1e3f1a1751bb62b4a1b7f4e435afcdade6c17a4fd9b9d43607cebd242924a \
+ --hash=sha256:e6a00ffcc173e765e200ceefb06399ba09c06db97f401f920513a10c803604ca \
+ --hash=sha256:e827d48cf802de06d9c935088c2924e3c7e7533377d66b6f31ed175c1620e05e \
+ --hash=sha256:ebf3fd9f141700b510d4b190094db0ce37ac6361a6806c153c161dc6c041ccda \
+ --hash=sha256:ec00c3305788e04bf6d29d42e504560e159ccaf0be30c09203b468a6c1ccd3b2 \
+ --hash=sha256:ec4fd86658c6a8964d75426517dc01cbf840bbf32d055ce64a9e63a40fd7b771 \
+ --hash=sha256:efd2fcf7e7b9d7ab16e6b7d54205beded0a9c8566cb30f09c1abe42b4e22bdcb \
+ --hash=sha256:f0f03211fd14a6a0aed2997d4b1c013d49fb7b50eeb9ffdf5e51f23cfe2c77fa \
+ --hash=sha256:f628dbf3c91e12f4d6c8b3f092069567d8eb17814aebba3d7d60c149391aee3a \
+ --hash=sha256:f8ef51e459eb2ad8e7a66c1d6440c808485840ad55ecc3cafefadea47d1b1ba2 \
+ --hash=sha256:fc37e9aef10a696a5a4474802930079ccfc14d9f9c10b4662169671ff034b7df \
+ --hash=sha256:fdee8405931b0615220e5ddf8cd7edd8592c606a8e4ca2a00704883c396e4479
+ # via kubernetes-asyncio
+aiosignal==1.3.1 \
+ --hash=sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc \
+ --hash=sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17
+ # via aiohttp
+alembic==1.12.1 \
+ --hash=sha256:47d52e3dfb03666ed945becb723d6482e52190917fdb47071440cfdba05d92cb \
+ --hash=sha256:bca5877e9678b454706347bc10b97cb7d67f300320fa5c3a94423e8266e2823f
+ # via jupyterhub
+anyio==3.7.1 \
+ --hash=sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780 \
+ --hash=sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5
+ # via
+ # -r spawner/requirements/main.in
+ # httpx
+async-generator==1.10 \
+ --hash=sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b \
+ --hash=sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144
+ # via jupyterhub
+async-timeout==4.0.3 \
+ --hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \
+ --hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028
+ # via aiohttp
+attrs==23.1.0 \
+ --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
+ --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
+ # via
+ # aiohttp
+ # jsonschema
+ # referencing
+certifi==2023.7.22 \
+ --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \
+ --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9
+ # via
+ # httpcore
+ # httpx
+ # kubernetes-asyncio
+ # requests
+certipy==0.1.3 \
+ --hash=sha256:695704b7716b033375c9a1324d0d30f27110a28895c40151a90ec07ff1032859 \
+ --hash=sha256:f272c13bfa9af6b2f3f746329d08adb66af7dd0bbb08fc81175597f25a7284b5
+ # via jupyterhub
+cffi==1.16.0 \
+ --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \
+ --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \
+ --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \
+ --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \
+ --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \
+ --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \
+ --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \
+ --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \
+ --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \
+ --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \
+ --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \
+ --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \
+ --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \
+ --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \
+ --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \
+ --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \
+ --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \
+ --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \
+ --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \
+ --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \
+ --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \
+ --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \
+ --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \
+ --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \
+ --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \
+ --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \
+ --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \
+ --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \
+ --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \
+ --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \
+ --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \
+ --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \
+ --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \
+ --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \
+ --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \
+ --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \
+ --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \
+ --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \
+ --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \
+ --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \
+ --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \
+ --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \
+ --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \
+ --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \
+ --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \
+ --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \
+ --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \
+ --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \
+ --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \
+ --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \
+ --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \
+ --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357
+ # via cryptography
+charset-normalizer==3.3.2 \
+ --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \
+ --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \
+ --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \
+ --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \
+ --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \
+ --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \
+ --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \
+ --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \
+ --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \
+ --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \
+ --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \
+ --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \
+ --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \
+ --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \
+ --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \
+ --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \
+ --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \
+ --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \
+ --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \
+ --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \
+ --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \
+ --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \
+ --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \
+ --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \
+ --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \
+ --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \
+ --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \
+ --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \
+ --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \
+ --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \
+ --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \
+ --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \
+ --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \
+ --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \
+ --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \
+ --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \
+ --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \
+ --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \
+ --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \
+ --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \
+ --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \
+ --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \
+ --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \
+ --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \
+ --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \
+ --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \
+ --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \
+ --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \
+ --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \
+ --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \
+ --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \
+ --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \
+ --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \
+ --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \
+ --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \
+ --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \
+ --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \
+ --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \
+ --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \
+ --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \
+ --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \
+ --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \
+ --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \
+ --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \
+ --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \
+ --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \
+ --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \
+ --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \
+ --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \
+ --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \
+ --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \
+ --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \
+ --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \
+ --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \
+ --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \
+ --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \
+ --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \
+ --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \
+ --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \
+ --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \
+ --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \
+ --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \
+ --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \
+ --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \
+ --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \
+ --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \
+ --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \
+ --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \
+ --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
+ --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
+ # via
+ # aiohttp
+ # requests
+cryptography==41.0.5 \
+ --hash=sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf \
+ --hash=sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84 \
+ --hash=sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e \
+ --hash=sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8 \
+ --hash=sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7 \
+ --hash=sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1 \
+ --hash=sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88 \
+ --hash=sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86 \
+ --hash=sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179 \
+ --hash=sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81 \
+ --hash=sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20 \
+ --hash=sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548 \
+ --hash=sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d \
+ --hash=sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d \
+ --hash=sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5 \
+ --hash=sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1 \
+ --hash=sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147 \
+ --hash=sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936 \
+ --hash=sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797 \
+ --hash=sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696 \
+ --hash=sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72 \
+ --hash=sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da \
+ --hash=sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723
+ # via pyopenssl
+exceptiongroup==1.1.3 \
+ --hash=sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9 \
+ --hash=sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3
+ # via -r spawner/requirements/main.in
+frozenlist==1.4.0 \
+ --hash=sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6 \
+ --hash=sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01 \
+ --hash=sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251 \
+ --hash=sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9 \
+ --hash=sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b \
+ --hash=sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87 \
+ --hash=sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf \
+ --hash=sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f \
+ --hash=sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0 \
+ --hash=sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2 \
+ --hash=sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b \
+ --hash=sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc \
+ --hash=sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c \
+ --hash=sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467 \
+ --hash=sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9 \
+ --hash=sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1 \
+ --hash=sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a \
+ --hash=sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79 \
+ --hash=sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167 \
+ --hash=sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300 \
+ --hash=sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf \
+ --hash=sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea \
+ --hash=sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2 \
+ --hash=sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab \
+ --hash=sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3 \
+ --hash=sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb \
+ --hash=sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087 \
+ --hash=sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc \
+ --hash=sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8 \
+ --hash=sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62 \
+ --hash=sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f \
+ --hash=sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326 \
+ --hash=sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c \
+ --hash=sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431 \
+ --hash=sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963 \
+ --hash=sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7 \
+ --hash=sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef \
+ --hash=sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3 \
+ --hash=sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956 \
+ --hash=sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781 \
+ --hash=sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472 \
+ --hash=sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc \
+ --hash=sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839 \
+ --hash=sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672 \
+ --hash=sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3 \
+ --hash=sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503 \
+ --hash=sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d \
+ --hash=sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8 \
+ --hash=sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b \
+ --hash=sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc \
+ --hash=sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f \
+ --hash=sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559 \
+ --hash=sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b \
+ --hash=sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95 \
+ --hash=sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb \
+ --hash=sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963 \
+ --hash=sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919 \
+ --hash=sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f \
+ --hash=sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3 \
+ --hash=sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1 \
+ --hash=sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e
+ # via
+ # aiohttp
+ # aiosignal
+greenlet==3.0.1 \
+ --hash=sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174 \
+ --hash=sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd \
+ --hash=sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa \
+ --hash=sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a \
+ --hash=sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec \
+ --hash=sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565 \
+ --hash=sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d \
+ --hash=sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c \
+ --hash=sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234 \
+ --hash=sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d \
+ --hash=sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546 \
+ --hash=sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2 \
+ --hash=sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74 \
+ --hash=sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de \
+ --hash=sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd \
+ --hash=sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9 \
+ --hash=sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3 \
+ --hash=sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846 \
+ --hash=sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2 \
+ --hash=sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353 \
+ --hash=sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8 \
+ --hash=sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166 \
+ --hash=sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206 \
+ --hash=sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b \
+ --hash=sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d \
+ --hash=sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe \
+ --hash=sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997 \
+ --hash=sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445 \
+ --hash=sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0 \
+ --hash=sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96 \
+ --hash=sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884 \
+ --hash=sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6 \
+ --hash=sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1 \
+ --hash=sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619 \
+ --hash=sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94 \
+ --hash=sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4 \
+ --hash=sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1 \
+ --hash=sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63 \
+ --hash=sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd \
+ --hash=sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a \
+ --hash=sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376 \
+ --hash=sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57 \
+ --hash=sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16 \
+ --hash=sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e \
+ --hash=sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc \
+ --hash=sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a \
+ --hash=sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c \
+ --hash=sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5 \
+ --hash=sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a \
+ --hash=sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72 \
+ --hash=sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9 \
+ --hash=sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9 \
+ --hash=sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e \
+ --hash=sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8 \
+ --hash=sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65 \
+ --hash=sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064 \
+ --hash=sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36
+ # via sqlalchemy
+h11==0.14.0 \
+ --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
+ --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
+ # via httpcore
+httpcore==1.0.1 \
+ --hash=sha256:c5e97ef177dca2023d0b9aad98e49507ef5423e9f1d94ffe2cfe250aa28e63b0 \
+ --hash=sha256:fce1ddf9b606cfb98132ab58865c3728c52c8e4c3c46e2aabb3674464a186e92
+ # via httpx
+httpx==0.25.1 \
+ --hash=sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a \
+ --hash=sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0
+ # via -r spawner/requirements/main.in
+httpx-sse==0.3.1 \
+ --hash=sha256:3bb3289b2867f50cbdb2fee3eeeefecb1e86653122e164faac0023f1ffc88aea \
+ --hash=sha256:7376dd88732892f9b6b549ac0ad05a8e2341172fe7dcf9f8f9c8050934297316
+ # via -r spawner/requirements/main.in
+idna==3.4 \
+ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+ # via
+ # anyio
+ # httpx
+ # requests
+ # yarl
+jinja2==3.1.2 \
+ --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
+ --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
+ # via jupyterhub
+jsonschema==4.19.2 \
+ --hash=sha256:c9ff4d7447eed9592c23a12ccee508baf0dd0d59650615e847feb6cdca74f392 \
+ --hash=sha256:eee9e502c788e89cb166d4d37f43084e3b64ab405c795c03d343a4dbc2c810fc
+ # via jupyter-telemetry
+jsonschema-specifications==2023.7.1 \
+ --hash=sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1 \
+ --hash=sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb
+ # via jsonschema
+jupyter-telemetry==0.1.0 \
+ --hash=sha256:1de3e423b23aa40ca4a4238d65c56dda544061ff5aedc3f7647220ed7e3b9589 \
+ --hash=sha256:445c613ae3df70d255fe3de202f936bba8b77b4055c43207edf22468ac875314
+ # via jupyterhub
+jupyterhub==4.0.2 \
+ --hash=sha256:2f389e7d3067e1b11bb4091719048eedecee161039fd2e5b025d031f7ab23c62 \
+ --hash=sha256:d4e450eed8d90dfbcf0eca08f00f2093a0bce74dc51f7cfc0b7057f602341a1d
+ # via -r spawner/requirements/main.in
+jupyterhub-idle-culler==1.2.1 \
+ --hash=sha256:c84e45a51932a34bd95c08b3b6e8330fb0ee3391fa4d0f1ac0d4458a40492fd9 \
+ --hash=sha256:d80822982d2590cb876d01849cee028943fdbdcc991c0f02fce45af3f40dd415
+ # via -r spawner/requirements/main.in
+kubernetes-asyncio==28.2.0 \
+ --hash=sha256:31d36b86fa151cb656b8c8de28f1f869fb05b0d7cfbc3a08e60f541ec7077b30 \
+ --hash=sha256:9399790ea4a41fafd5cef4017e1d8dc3e16502fe16825be0bb4f07a6deb05345
+ # via -r spawner/requirements/main.in
+mako==1.2.4 \
+ --hash=sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818 \
+ --hash=sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34
+ # via alembic
+markupsafe==2.1.3 \
+ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
+ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
+ --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
+ --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
+ --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
+ --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
+ --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
+ --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
+ --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
+ --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
+ --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
+ --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
+ --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
+ --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
+ --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
+ --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
+ --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
+ --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
+ --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
+ --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
+ --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
+ --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
+ --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
+ --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
+ --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
+ --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
+ --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
+ --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
+ --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
+ --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
+ --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
+ --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
+ --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
+ --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
+ --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
+ --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
+ --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
+ --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
+ --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
+ --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
+ --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
+ --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
+ --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
+ --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
+ --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
+ --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
+ --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
+ --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
+ --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
+ --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
+ --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
+ --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
+ --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
+ --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
+ --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
+ --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
+ --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
+ --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
+ --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
+ --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
+ # via
+ # jinja2
+ # mako
+multidict==6.0.4 \
+ --hash=sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9 \
+ --hash=sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8 \
+ --hash=sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03 \
+ --hash=sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710 \
+ --hash=sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161 \
+ --hash=sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664 \
+ --hash=sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569 \
+ --hash=sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067 \
+ --hash=sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313 \
+ --hash=sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706 \
+ --hash=sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2 \
+ --hash=sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636 \
+ --hash=sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49 \
+ --hash=sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93 \
+ --hash=sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603 \
+ --hash=sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0 \
+ --hash=sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60 \
+ --hash=sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4 \
+ --hash=sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e \
+ --hash=sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1 \
+ --hash=sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60 \
+ --hash=sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951 \
+ --hash=sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc \
+ --hash=sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe \
+ --hash=sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95 \
+ --hash=sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d \
+ --hash=sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8 \
+ --hash=sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed \
+ --hash=sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2 \
+ --hash=sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775 \
+ --hash=sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87 \
+ --hash=sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c \
+ --hash=sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2 \
+ --hash=sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98 \
+ --hash=sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3 \
+ --hash=sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe \
+ --hash=sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78 \
+ --hash=sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660 \
+ --hash=sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176 \
+ --hash=sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e \
+ --hash=sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988 \
+ --hash=sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c \
+ --hash=sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c \
+ --hash=sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0 \
+ --hash=sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449 \
+ --hash=sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f \
+ --hash=sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde \
+ --hash=sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5 \
+ --hash=sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d \
+ --hash=sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac \
+ --hash=sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a \
+ --hash=sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9 \
+ --hash=sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca \
+ --hash=sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11 \
+ --hash=sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35 \
+ --hash=sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063 \
+ --hash=sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b \
+ --hash=sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982 \
+ --hash=sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258 \
+ --hash=sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1 \
+ --hash=sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52 \
+ --hash=sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480 \
+ --hash=sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7 \
+ --hash=sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461 \
+ --hash=sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d \
+ --hash=sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc \
+ --hash=sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779 \
+ --hash=sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a \
+ --hash=sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547 \
+ --hash=sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0 \
+ --hash=sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171 \
+ --hash=sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf \
+ --hash=sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d \
+ --hash=sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba
+ # via
+ # aiohttp
+ # yarl
+oauthlib==3.2.2 \
+ --hash=sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca \
+ --hash=sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918
+ # via jupyterhub
+packaging==23.2 \
+ --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \
+ --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7
+ # via jupyterhub
+pamela==1.1.0 \
+ --hash=sha256:d4b139fe600e192e176a2a368059207a6bffa0e7879879b13f4fcba0163481be \
+ --hash=sha256:f4534bba9645665b01adfce0134772b0147faea72c278f67a1a732e7ebd46ec6
+ # via jupyterhub
+prometheus-client==0.18.0 \
+ --hash=sha256:35f7a8c22139e2bb7ca5a698e92d38145bc8dc74c1c0bf56f25cca886a764e17 \
+ --hash=sha256:8de3ae2755f890826f4b6479e5571d4f74ac17a81345fe69a6778fdb92579184
+ # via jupyterhub
+psycopg2==2.9.9 \
+ --hash=sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981 \
+ --hash=sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516 \
+ --hash=sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3 \
+ --hash=sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa \
+ --hash=sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a \
+ --hash=sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693 \
+ --hash=sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372 \
+ --hash=sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e \
+ --hash=sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59 \
+ --hash=sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156 \
+ --hash=sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024 \
+ --hash=sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913 \
+ --hash=sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c
+ # via -r spawner/requirements/main.in
+pycparser==2.21 \
+ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
+ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
+ # via cffi
+pyopenssl==23.3.0 \
+ --hash=sha256:6756834481d9ed5470f4a9393455154bc92fe7a64b7bc6ee2c804e78c52099b2 \
+ --hash=sha256:6b2cba5cc46e822750ec3e5a81ee12819850b11303630d575e98108a079c2b12
+ # via certipy
+python-dateutil==2.8.2 \
+ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
+ --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
+ # via
+ # jupyterhub
+ # jupyterhub-idle-culler
+ # kubernetes-asyncio
+python-json-logger==2.0.7 \
+ --hash=sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c \
+ --hash=sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd
+ # via jupyter-telemetry
+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
+ # -r spawner/requirements/main.in
+ # kubernetes-asyncio
+referencing==0.30.2 \
+ --hash=sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf \
+ --hash=sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0
+ # via
+ # jsonschema
+ # jsonschema-specifications
+requests==2.31.0 \
+ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
+ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
+ # via jupyterhub
+rpds-py==0.10.6 \
+ --hash=sha256:023574366002bf1bd751ebaf3e580aef4a468b3d3c216d2f3f7e16fdabd885ed \
+ --hash=sha256:031f76fc87644a234883b51145e43985aa2d0c19b063e91d44379cd2786144f8 \
+ --hash=sha256:052a832078943d2b2627aea0d19381f607fe331cc0eb5df01991268253af8417 \
+ --hash=sha256:0699ab6b8c98df998c3eacf51a3b25864ca93dab157abe358af46dc95ecd9801 \
+ --hash=sha256:0713631d6e2d6c316c2f7b9320a34f44abb644fc487b77161d1724d883662e31 \
+ --hash=sha256:0774a46b38e70fdde0c6ded8d6d73115a7c39d7839a164cc833f170bbf539116 \
+ --hash=sha256:0898173249141ee99ffcd45e3829abe7bcee47d941af7434ccbf97717df020e5 \
+ --hash=sha256:09586f51a215d17efdb3a5f090d7cbf1633b7f3708f60a044757a5d48a83b393 \
+ --hash=sha256:102eac53bb0bf0f9a275b438e6cf6904904908562a1463a6fc3323cf47d7a532 \
+ --hash=sha256:10f32b53f424fc75ff7b713b2edb286fdbfc94bf16317890260a81c2c00385dc \
+ --hash=sha256:150eec465dbc9cbca943c8e557a21afdcf9bab8aaabf386c44b794c2f94143d2 \
+ --hash=sha256:1d7360573f1e046cb3b0dceeb8864025aa78d98be4bb69f067ec1c40a9e2d9df \
+ --hash=sha256:1f36a9d751f86455dc5278517e8b65580eeee37d61606183897f122c9e51cef3 \
+ --hash=sha256:24656dc36f866c33856baa3ab309da0b6a60f37d25d14be916bd3e79d9f3afcf \
+ --hash=sha256:25860ed5c4e7f5e10c496ea78af46ae8d8468e0be745bd233bab9ca99bfd2647 \
+ --hash=sha256:26857f0f44f0e791f4a266595a7a09d21f6b589580ee0585f330aaccccb836e3 \
+ --hash=sha256:2bb2e4826be25e72013916eecd3d30f66fd076110de09f0e750163b416500721 \
+ --hash=sha256:2f6da6d842195fddc1cd34c3da8a40f6e99e4a113918faa5e60bf132f917c247 \
+ --hash=sha256:30adb75ecd7c2a52f5e76af50644b3e0b5ba036321c390b8e7ec1bb2a16dd43c \
+ --hash=sha256:3339eca941568ed52d9ad0f1b8eb9fe0958fa245381747cecf2e9a78a5539c42 \
+ --hash=sha256:34ad87a831940521d462ac11f1774edf867c34172010f5390b2f06b85dcc6014 \
+ --hash=sha256:3777cc9dea0e6c464e4b24760664bd8831738cc582c1d8aacf1c3f546bef3f65 \
+ --hash=sha256:3953c6926a63f8ea5514644b7afb42659b505ece4183fdaaa8f61d978754349e \
+ --hash=sha256:3c4eff26eddac49d52697a98ea01b0246e44ca82ab09354e94aae8823e8bda02 \
+ --hash=sha256:40578a6469e5d1df71b006936ce95804edb5df47b520c69cf5af264d462f2cbb \
+ --hash=sha256:40f93086eef235623aa14dbddef1b9fb4b22b99454cb39a8d2e04c994fb9868c \
+ --hash=sha256:4134aa2342f9b2ab6c33d5c172e40f9ef802c61bb9ca30d21782f6e035ed0043 \
+ --hash=sha256:442626328600bde1d09dc3bb00434f5374948838ce75c41a52152615689f9403 \
+ --hash=sha256:4a5ee600477b918ab345209eddafde9f91c0acd931f3776369585a1c55b04c57 \
+ --hash=sha256:4ce5a708d65a8dbf3748d2474b580d606b1b9f91b5c6ab2a316e0b0cf7a4ba50 \
+ --hash=sha256:516a611a2de12fbea70c78271e558f725c660ce38e0006f75139ba337d56b1f6 \
+ --hash=sha256:52c215eb46307c25f9fd2771cac8135d14b11a92ae48d17968eda5aa9aaf5071 \
+ --hash=sha256:53c43e10d398e365da2d4cc0bcaf0854b79b4c50ee9689652cdc72948e86f487 \
+ --hash=sha256:5752b761902cd15073a527b51de76bbae63d938dc7c5c4ad1e7d8df10e765138 \
+ --hash=sha256:5e8a78bd4879bff82daef48c14d5d4057f6856149094848c3ed0ecaf49f5aec2 \
+ --hash=sha256:5ed505ec6305abd2c2c9586a7b04fbd4baf42d4d684a9c12ec6110deefe2a063 \
+ --hash=sha256:5ee97c683eaface61d38ec9a489e353d36444cdebb128a27fe486a291647aff6 \
+ --hash=sha256:61fa268da6e2e1cd350739bb61011121fa550aa2545762e3dc02ea177ee4de35 \
+ --hash=sha256:64ccc28683666672d7c166ed465c09cee36e306c156e787acef3c0c62f90da5a \
+ --hash=sha256:66414dafe4326bca200e165c2e789976cab2587ec71beb80f59f4796b786a238 \
+ --hash=sha256:68fe9199184c18d997d2e4293b34327c0009a78599ce703e15cd9a0f47349bba \
+ --hash=sha256:6a555ae3d2e61118a9d3e549737bb4a56ff0cec88a22bd1dfcad5b4e04759175 \
+ --hash=sha256:6bdc11f9623870d75692cc33c59804b5a18d7b8a4b79ef0b00b773a27397d1f6 \
+ --hash=sha256:6cf4393c7b41abbf07c88eb83e8af5013606b1cdb7f6bc96b1b3536b53a574b8 \
+ --hash=sha256:6eef672de005736a6efd565577101277db6057f65640a813de6c2707dc69f396 \
+ --hash=sha256:734c41f9f57cc28658d98270d3436dba65bed0cfc730d115b290e970150c540d \
+ --hash=sha256:73e0a78a9b843b8c2128028864901f55190401ba38aae685350cf69b98d9f7c9 \
+ --hash=sha256:775049dfa63fb58293990fc59473e659fcafd953bba1d00fc5f0631a8fd61977 \
+ --hash=sha256:7854a207ef77319ec457c1eb79c361b48807d252d94348305db4f4b62f40f7f3 \
+ --hash=sha256:78ca33811e1d95cac8c2e49cb86c0fb71f4d8409d8cbea0cb495b6dbddb30a55 \
+ --hash=sha256:79edd779cfc46b2e15b0830eecd8b4b93f1a96649bcb502453df471a54ce7977 \
+ --hash=sha256:7bf347b495b197992efc81a7408e9a83b931b2f056728529956a4d0858608b80 \
+ --hash=sha256:7fde6d0e00b2fd0dbbb40c0eeec463ef147819f23725eda58105ba9ca48744f4 \
+ --hash=sha256:81de24a1c51cfb32e1fbf018ab0bdbc79c04c035986526f76c33e3f9e0f3356c \
+ --hash=sha256:879fb24304ead6b62dbe5034e7b644b71def53c70e19363f3c3be2705c17a3b4 \
+ --hash=sha256:8e7f2219cb72474571974d29a191714d822e58be1eb171f229732bc6fdedf0ac \
+ --hash=sha256:9164ec8010327ab9af931d7ccd12ab8d8b5dc2f4c6a16cbdd9d087861eaaefa1 \
+ --hash=sha256:945eb4b6bb8144909b203a88a35e0a03d22b57aefb06c9b26c6e16d72e5eb0f0 \
+ --hash=sha256:99a57006b4ec39dbfb3ed67e5b27192792ffb0553206a107e4aadb39c5004cd5 \
+ --hash=sha256:9e9184fa6c52a74a5521e3e87badbf9692549c0fcced47443585876fcc47e469 \
+ --hash=sha256:9ff93d3aedef11f9c4540cf347f8bb135dd9323a2fc705633d83210d464c579d \
+ --hash=sha256:a360cfd0881d36c6dc271992ce1eda65dba5e9368575663de993eeb4523d895f \
+ --hash=sha256:a5d7ed104d158c0042a6a73799cf0eb576dfd5fc1ace9c47996e52320c37cb7c \
+ --hash=sha256:ac17044876e64a8ea20ab132080ddc73b895b4abe9976e263b0e30ee5be7b9c2 \
+ --hash=sha256:ad857f42831e5b8d41a32437f88d86ead6c191455a3499c4b6d15e007936d4cf \
+ --hash=sha256:b2039f8d545f20c4e52713eea51a275e62153ee96c8035a32b2abb772b6fc9e5 \
+ --hash=sha256:b455492cab07107bfe8711e20cd920cc96003e0da3c1f91297235b1603d2aca7 \
+ --hash=sha256:b4a9fe992887ac68256c930a2011255bae0bf5ec837475bc6f7edd7c8dfa254e \
+ --hash=sha256:b5a53f5998b4bbff1cb2e967e66ab2addc67326a274567697379dd1e326bded7 \
+ --hash=sha256:b788276a3c114e9f51e257f2a6f544c32c02dab4aa7a5816b96444e3f9ffc336 \
+ --hash=sha256:bddd4f91eede9ca5275e70479ed3656e76c8cdaaa1b354e544cbcf94c6fc8ac4 \
+ --hash=sha256:c0503c5b681566e8b722fe8c4c47cce5c7a51f6935d5c7012c4aefe952a35eed \
+ --hash=sha256:c1b3cd23d905589cb205710b3988fc8f46d4a198cf12862887b09d7aaa6bf9b9 \
+ --hash=sha256:c48f3fbc3e92c7dd6681a258d22f23adc2eb183c8cb1557d2fcc5a024e80b094 \
+ --hash=sha256:c63c3ef43f0b3fb00571cff6c3967cc261c0ebd14a0a134a12e83bdb8f49f21f \
+ --hash=sha256:c6c45a2d2b68c51fe3d9352733fe048291e483376c94f7723458cfd7b473136b \
+ --hash=sha256:caa1afc70a02645809c744eefb7d6ee8fef7e2fad170ffdeacca267fd2674f13 \
+ --hash=sha256:cc435d059f926fdc5b05822b1be4ff2a3a040f3ae0a7bbbe672babb468944722 \
+ --hash=sha256:cf693eb4a08eccc1a1b636e4392322582db2a47470d52e824b25eca7a3977b53 \
+ --hash=sha256:cf71343646756a072b85f228d35b1d7407da1669a3de3cf47f8bbafe0c8183a4 \
+ --hash=sha256:d08f63561c8a695afec4975fae445245386d645e3e446e6f260e81663bfd2e38 \
+ --hash=sha256:d29ddefeab1791e3c751e0189d5f4b3dbc0bbe033b06e9c333dca1f99e1d523e \
+ --hash=sha256:d7f5e15c953ace2e8dde9824bdab4bec50adb91a5663df08d7d994240ae6fa31 \
+ --hash=sha256:d858532212f0650be12b6042ff4378dc2efbb7792a286bee4489eaa7ba010586 \
+ --hash=sha256:d97dd44683802000277bbf142fd9f6b271746b4846d0acaf0cefa6b2eaf2a7ad \
+ --hash=sha256:dcdc88b6b01015da066da3fb76545e8bb9a6880a5ebf89e0f0b2e3ca557b3ab7 \
+ --hash=sha256:dd609fafdcdde6e67a139898196698af37438b035b25ad63704fd9097d9a3482 \
+ --hash=sha256:defa2c0c68734f4a82028c26bcc85e6b92cced99866af118cd6a89b734ad8e0d \
+ --hash=sha256:e22260a4741a0e7a206e175232867b48a16e0401ef5bce3c67ca5b9705879066 \
+ --hash=sha256:e225a6a14ecf44499aadea165299092ab0cba918bb9ccd9304eab1138844490b \
+ --hash=sha256:e3df0bc35e746cce42579826b89579d13fd27c3d5319a6afca9893a9b784ff1b \
+ --hash=sha256:e6fcc026a3f27c1282c7ed24b7fcac82cdd70a0e84cc848c0841a3ab1e3dea2d \
+ --hash=sha256:e782379c2028a3611285a795b89b99a52722946d19fc06f002f8b53e3ea26ea9 \
+ --hash=sha256:e8cdd52744f680346ff8c1ecdad5f4d11117e1724d4f4e1874f3a67598821069 \
+ --hash=sha256:e9616f5bd2595f7f4a04b67039d890348ab826e943a9bfdbe4938d0eba606971 \
+ --hash=sha256:e98c4c07ee4c4b3acf787e91b27688409d918212dfd34c872201273fdd5a0e18 \
+ --hash=sha256:ebdab79f42c5961682654b851f3f0fc68e6cc7cd8727c2ac4ffff955154123c1 \
+ --hash=sha256:f0f17f2ce0f3529177a5fff5525204fad7b43dd437d017dd0317f2746773443d \
+ --hash=sha256:f4e56860a5af16a0fcfa070a0a20c42fbb2012eed1eb5ceeddcc7f8079214281
+ # via
+ # jsonschema
+ # referencing
+ruamel-yaml==0.18.5 \
+ --hash=sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e \
+ --hash=sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada
+ # via jupyter-telemetry
+ruamel-yaml-clib==0.2.8 \
+ --hash=sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d \
+ --hash=sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001 \
+ --hash=sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462 \
+ --hash=sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9 \
+ --hash=sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b \
+ --hash=sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b \
+ --hash=sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615 \
+ --hash=sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15 \
+ --hash=sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b \
+ --hash=sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9 \
+ --hash=sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675 \
+ --hash=sha256:3fcc54cb0c8b811ff66082de1680b4b14cf8a81dce0d4fbf665c2265a81e07a1 \
+ --hash=sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899 \
+ --hash=sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7 \
+ --hash=sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7 \
+ --hash=sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312 \
+ --hash=sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa \
+ --hash=sha256:665f58bfd29b167039f714c6998178d27ccd83984084c286110ef26b230f259f \
+ --hash=sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91 \
+ --hash=sha256:7048c338b6c86627afb27faecf418768acb6331fc24cfa56c93e8c9780f815fa \
+ --hash=sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b \
+ --hash=sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3 \
+ --hash=sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334 \
+ --hash=sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5 \
+ --hash=sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3 \
+ --hash=sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe \
+ --hash=sha256:9eb5dee2772b0f704ca2e45b1713e4e5198c18f515b52743576d196348f374d3 \
+ --hash=sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed \
+ --hash=sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337 \
+ --hash=sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880 \
+ --hash=sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d \
+ --hash=sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248 \
+ --hash=sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d \
+ --hash=sha256:b5edda50e5e9e15e54a6a8a0070302b00c518a9d32accc2346ad6c984aacd279 \
+ --hash=sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf \
+ --hash=sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512 \
+ --hash=sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069 \
+ --hash=sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb \
+ --hash=sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942 \
+ --hash=sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d \
+ --hash=sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31 \
+ --hash=sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92 \
+ --hash=sha256:d92f81886165cb14d7b067ef37e142256f1c6a90a65cd156b063a43da1708cfd \
+ --hash=sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5 \
+ --hash=sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28 \
+ --hash=sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d \
+ --hash=sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1 \
+ --hash=sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2 \
+ --hash=sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875 \
+ --hash=sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412
+ # via
+ # -r spawner/requirements/main.in
+ # ruamel-yaml
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+ # via
+ # kubernetes-asyncio
+ # python-dateutil
+sniffio==1.3.0 \
+ --hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \
+ --hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384
+ # via
+ # anyio
+ # httpx
+sqlalchemy==2.0.23 \
+ --hash=sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3 \
+ --hash=sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884 \
+ --hash=sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74 \
+ --hash=sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d \
+ --hash=sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc \
+ --hash=sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca \
+ --hash=sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d \
+ --hash=sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf \
+ --hash=sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846 \
+ --hash=sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306 \
+ --hash=sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221 \
+ --hash=sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5 \
+ --hash=sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89 \
+ --hash=sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55 \
+ --hash=sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72 \
+ --hash=sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea \
+ --hash=sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8 \
+ --hash=sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577 \
+ --hash=sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df \
+ --hash=sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4 \
+ --hash=sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d \
+ --hash=sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34 \
+ --hash=sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4 \
+ --hash=sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24 \
+ --hash=sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6 \
+ --hash=sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965 \
+ --hash=sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35 \
+ --hash=sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b \
+ --hash=sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab \
+ --hash=sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22 \
+ --hash=sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4 \
+ --hash=sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204 \
+ --hash=sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855 \
+ --hash=sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d \
+ --hash=sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab \
+ --hash=sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69 \
+ --hash=sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693 \
+ --hash=sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e \
+ --hash=sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8 \
+ --hash=sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0 \
+ --hash=sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45 \
+ --hash=sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab \
+ --hash=sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1 \
+ --hash=sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d \
+ --hash=sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda \
+ --hash=sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b \
+ --hash=sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18 \
+ --hash=sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac \
+ --hash=sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60
+ # via
+ # alembic
+ # jupyterhub
+tornado==6.3.3 \
+ --hash=sha256:1bd19ca6c16882e4d37368e0152f99c099bad93e0950ce55e71daed74045908f \
+ --hash=sha256:22d3c2fa10b5793da13c807e6fc38ff49a4f6e1e3868b0a6f4164768bb8e20f5 \
+ --hash=sha256:502fba735c84450974fec147340016ad928d29f1e91f49be168c0a4c18181e1d \
+ --hash=sha256:65ceca9500383fbdf33a98c0087cb975b2ef3bfb874cb35b8de8740cf7f41bd3 \
+ --hash=sha256:71a8db65160a3c55d61839b7302a9a400074c9c753040455494e2af74e2501f2 \
+ --hash=sha256:7ac51f42808cca9b3613f51ffe2a965c8525cb1b00b7b2d56828b8045354f76a \
+ --hash=sha256:7d01abc57ea0dbb51ddfed477dfe22719d376119844e33c661d873bf9c0e4a16 \
+ --hash=sha256:805d507b1f588320c26f7f097108eb4023bbaa984d63176d1652e184ba24270a \
+ --hash=sha256:9dc4444c0defcd3929d5c1eb5706cbe1b116e762ff3e0deca8b715d14bf6ec17 \
+ --hash=sha256:ceb917a50cd35882b57600709dd5421a418c29ddc852da8bcdab1f0db33406b0 \
+ --hash=sha256:e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe
+ # via
+ # jupyterhub
+ # jupyterhub-idle-culler
+traitlets==5.13.0 \
+ --hash=sha256:9b232b9430c8f57288c1024b34a8f0251ddcc47268927367a0dd3eeaca40deb5 \
+ --hash=sha256:baf991e61542da48fe8aef8b779a9ea0aa38d8a54166ee250d5af5ecf4486619
+ # via
+ # -r spawner/requirements/main.in
+ # jupyter-telemetry
+ # jupyterhub
+typing-extensions==4.8.0 \
+ --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \
+ --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef
+ # via
+ # alembic
+ # sqlalchemy
+urllib3==2.0.7 \
+ --hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \
+ --hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e
+ # via
+ # kubernetes-asyncio
+ # requests
+yarl==1.9.2 \
+ --hash=sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571 \
+ --hash=sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3 \
+ --hash=sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3 \
+ --hash=sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c \
+ --hash=sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7 \
+ --hash=sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04 \
+ --hash=sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191 \
+ --hash=sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea \
+ --hash=sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4 \
+ --hash=sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4 \
+ --hash=sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095 \
+ --hash=sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e \
+ --hash=sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74 \
+ --hash=sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef \
+ --hash=sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33 \
+ --hash=sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde \
+ --hash=sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45 \
+ --hash=sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf \
+ --hash=sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b \
+ --hash=sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac \
+ --hash=sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0 \
+ --hash=sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528 \
+ --hash=sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716 \
+ --hash=sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb \
+ --hash=sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18 \
+ --hash=sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72 \
+ --hash=sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6 \
+ --hash=sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582 \
+ --hash=sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5 \
+ --hash=sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368 \
+ --hash=sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc \
+ --hash=sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9 \
+ --hash=sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be \
+ --hash=sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a \
+ --hash=sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80 \
+ --hash=sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8 \
+ --hash=sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6 \
+ --hash=sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417 \
+ --hash=sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574 \
+ --hash=sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59 \
+ --hash=sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608 \
+ --hash=sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82 \
+ --hash=sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1 \
+ --hash=sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3 \
+ --hash=sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d \
+ --hash=sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8 \
+ --hash=sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc \
+ --hash=sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac \
+ --hash=sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8 \
+ --hash=sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955 \
+ --hash=sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0 \
+ --hash=sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367 \
+ --hash=sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb \
+ --hash=sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a \
+ --hash=sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623 \
+ --hash=sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2 \
+ --hash=sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6 \
+ --hash=sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7 \
+ --hash=sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4 \
+ --hash=sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051 \
+ --hash=sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938 \
+ --hash=sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8 \
+ --hash=sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9 \
+ --hash=sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3 \
+ --hash=sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5 \
+ --hash=sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9 \
+ --hash=sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333 \
+ --hash=sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185 \
+ --hash=sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3 \
+ --hash=sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560 \
+ --hash=sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b \
+ --hash=sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7 \
+ --hash=sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78 \
+ --hash=sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7
+ # via aiohttp
+
+# The following packages are considered to be unsafe in a requirements file:
+setuptools==68.2.2 \
+ --hash=sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87 \
+ --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a
+ # via kubernetes-asyncio
diff --git a/spawner/scripts/install-base-packages.sh b/spawner/scripts/install-base-packages.sh
new file mode 100755
index 000000000..620781c1f
--- /dev/null
+++ b/spawner/scripts/install-base-packages.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# This script updates packages in the base Docker image that's used by both the
+# build and runtime images, and gives us a place to install additional
+# system-level packages with apt-get.
+#
+# Based on the blog post:
+# https://pythonspeed.com/articles/system-packages-docker/
+
+# Bash "strict mode", to help catch problems and bugs in the shell
+# script. Every bash script you write should include this. See
+# http://redsymbol.net/articles/unofficial-bash-strict-mode/ for
+# details.
+set -euo pipefail
+
+# Display each command as it's run.
+set -x
+
+# Tell apt-get we're never going to be able to give manual
+# feedback:
+export DEBIAN_FRONTEND=noninteractive
+
+# Update the package listing, so we know what packages exist:
+apt-get update
+
+# Install security updates:
+apt-get -y upgrade
+
+# Example of installing a new package, without unnecessary packages:
+apt-get -y install --no-install-recommends git
+
+# Delete cached files we don't need anymore:
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/spawner/scripts/install-dependency-packages.sh b/spawner/scripts/install-dependency-packages.sh
new file mode 100755
index 000000000..3706c9042
--- /dev/null
+++ b/spawner/scripts/install-dependency-packages.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+# This script installs additional packages used by the dependency image but
+# not needed by the runtime image, such as additional packages required to
+# build Python dependencies.
+#
+# Since the base image wipes all the apt caches to clean up the image that
+# will be reused by the runtime image, we unfortunately have to do another
+# apt-get update here, which wastes some time and network.
+
+# Bash "strict mode", to help catch problems and bugs in the shell
+# script. Every bash script you write should include this. See
+# http://redsymbol.net/articles/unofficial-bash-strict-mode/ for
+# details.
+set -euo pipefail
+
+# Display each command as it's run.
+set -x
+
+# Tell apt-get we're never going to be able to give manual
+# feedback:
+export DEBIAN_FRONTEND=noninteractive
+
+# Update the package listing, so we know what packages exist:
+apt-get update
+
+# Install build-essential because sometimes Python dependencies need to build
+# C modules, particularly when upgrading to newer Python versions. libffi-dev
+# is sometimes needed to build cffi (a cryptography dependency). libpq-dev
+# and python3-dev are required to build psycopg2
+apt-get -y install --no-install-recommends build-essential libffi-dev \
+ libpq-dev python3-dev
+
+# postgresql-client is not *strictly* necessary, but if we're using
+# CloudSQL proxy against a Cloud SQL instance that has no public IP
+# and a network policy only allowing access to the proxy from the Hub
+# pod, this is a much easier way to inspect the DB than an interactive
+# python instance.
+
+apt-get -y install --no-install-recommends postgresql-client
+
+# Delete cached files we don't need anymore:
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/spawner/src/rsp_restspawner/__init__.py b/spawner/src/rsp_restspawner/__init__.py
new file mode 100644
index 000000000..b33101260
--- /dev/null
+++ b/spawner/src/rsp_restspawner/__init__.py
@@ -0,0 +1,4 @@
+from .auth import GafaelfawrAuthenticator
+from .spawner import RSPRestSpawner
+
+__all__ = ["GafaelfawrAuthenticator", "RSPRestSpawner"]
diff --git a/spawner/src/rsp_restspawner/auth.py b/spawner/src/rsp_restspawner/auth.py
new file mode 100644
index 000000000..eaabc2318
--- /dev/null
+++ b/spawner/src/rsp_restspawner/auth.py
@@ -0,0 +1,198 @@
+"""Gafaelfawr authenticator for JupyterHub."""
+
+from __future__ import annotations
+
+from typing import Any
+
+from jupyterhub.app import JupyterHub
+from jupyterhub.auth import Authenticator
+from jupyterhub.handlers import BaseHandler, LogoutHandler
+from jupyterhub.user import User
+from jupyterhub.utils import url_path_join
+from tornado.httputil import HTTPHeaders
+from tornado.web import HTTPError, RequestHandler
+
+AuthInfo = dict[str, str | dict[str, str]]
+Route = tuple[str, type[BaseHandler]]
+
+
+def _build_auth_info(headers: HTTPHeaders) -> AuthInfo:
+ """Construct the authentication information for a user.
+
+ Gafaelfawr puts the username in ``X-Auth-Request-User`` and the delegated
+ notebook token in ``X-Auth-Request-Token``. Use those headers to construct
+ a valid JupyterHub ``auth_state``.
+ """
+ username = headers.get("X-Auth-Request-User")
+ token = headers.get("X-Auth-Request-Token")
+ if not username or not token:
+ raise HTTPError(401, "User is not authenticated")
+ return {"name": username, "auth_state": {"token": token}}
+
+
+class GafaelfawrAuthenticator(Authenticator):
+ """JupyterHub authenticator using Gafaelfawr headers.
+
+ Rather than implement any authentication logic inside of JupyterHub,
+ authentication is done via an ``auth_request`` handler made by the NGINX
+ ingress controller. JupyterHub then only needs to read the authentication
+ results from the headers of the incoming request.
+
+ Normally, the authentication flow for JupyterHub is to send the user to
+ ``/hub/login`` and display a login form. The submitted values to the form
+ are then passed to the ``authenticate`` method of the authenticator, which
+ is responsible for returning authentication information for the user.
+ That information is then stored in an authentication session and the user
+ is redirected to whatever page they were trying to go to.
+
+ We however do not want to display an interactive form, since the
+ authentication information is already present in the headers. We just need
+ JupyterHub to read it.
+
+ The documented way to do this is to register a custom login handler on a
+ new route not otherwise used by JupyterHub, and then enable the
+ ``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.
+
+ In this model, the ``authenticate`` method is not used, since the login
+ handler never receives a form submission.
+
+ Notes
+ -----
+ A possible alternative implementation that seems to be supported by the
+ JupyterHub code would be to not override ``login_url``, set
+ ``auto_login``, and then override ``get_authenticated_user`` in the
+ authenticator to read authentication information directly from the request
+ headers. It looks like an authenticator configured in that way would
+ authenticate the user "in place" in the handler of whatever page the user
+ first went to, without any redirects. This would be slightly more
+ efficient and the code appears to handle it, but the current documentation
+ (as of 1.5.0) explicitly says to not override ``get_authenticated_user``.
+
+ This implementation therefore takes the well-documented path of a new
+ handler and a redirect from the built-in login handler, on the theory that
+ a few extra redirects is a small price to pay for staying within the
+ supported and expected interface.
+ """
+
+ def __init__(self, **kwargs: Any) -> None:
+ super().__init__(**kwargs)
+
+ # Automatically log in rather than prompting the user with a link.
+ self.auto_login = True
+
+ # Enable secure storage of auth state, which we'll use to stash the
+ # user's token and pass it to the spawned pod.
+ self.enable_auth_state = True
+
+ # Refresh the auth state before spawning to ensure we have the user's
+ # most recent token and group information.
+ self.refresh_pre_spawn = True
+
+ async def authenticate(
+ self, handler: RequestHandler, data: dict[str, str]
+ ) -> str | dict[str, Any] | None:
+ """Login form authenticator.
+
+ This is not used in our authentication scheme.
+ """
+ raise NotImplementedError
+
+ def get_handlers(self, app: JupyterHub) -> list[Route]:
+ """Register the header-only login and the logout handlers."""
+ return [
+ ("/gafaelfawr/login", GafaelfawrLoginHandler),
+ ("/logout", GafaelfawrLogoutHandler),
+ ]
+
+ def login_url(self, base_url: str) -> str:
+ """Override the login URL.
+
+ This must be changed to something other than ``/login`` to trigger
+ correct behavior when ``auto_login`` is set to true (as it is in our
+ case).
+ """
+ return url_path_join(base_url, "gafaelfawr/login")
+
+ async def refresh_user(
+ self, user: User, handler: RequestHandler | None = None
+ ) -> bool | AuthInfo:
+ """Optionally refresh the user's token."""
+ # If running outside of a Tornado handler, we can't refresh the auth
+ # state, so assume that it is okay.
+ if not handler:
+ return True
+
+ # If there is no X-Auth-Request-Token header, this request did not go
+ # through the Hub ingress and thus is coming from inside the cluster,
+ # such as requests to JupyterHub from a JupyterLab instance. Allow
+ # JupyterHub to use its normal authentication logic.
+ token = handler.request.headers.get("X-Auth-Request-Token")
+ if not token:
+ return True
+
+ # JupyterHub doesn't support changing the username of a user during
+ # refresh, so if the username doesn't match what we're expecting,
+ # raise a 401 error and force the user to reauthenticate. This can
+ # happen if the user's username was changed underneath us.
+ username = handler.request.headers.get("X-Auth-Request-User")
+ if not username or user.name != username:
+ raise HTTPError(401, "Username does not match expected identity")
+
+ # We have a new token. If it doesn't match the token we have stored,
+ # replace the stored auth state with the new auth state.
+ auth_state = await user.get_auth_state()
+ if token == auth_state.get("token"):
+ 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/spawner/src/rsp_restspawner/exceptions.py b/spawner/src/rsp_restspawner/exceptions.py
new file mode 100644
index 000000000..44f586ea2
--- /dev/null
+++ b/spawner/src/rsp_restspawner/exceptions.py
@@ -0,0 +1,110 @@
+"""Exceptions for the RSP REST spawner.
+
+JupyterHub catches all exceptions derived from `Exception` and treats them the
+same, so the distinction between exceptions is just for better error reporting
+and improved code readability.
+"""
+
+from __future__ import annotations
+
+from httpx import HTTPError, HTTPStatusError, RequestError
+
+__all__ = [
+ "ControllerWebError",
+ "InvalidAuthStateError",
+ "MissingFieldError",
+ "SpawnFailedError",
+]
+
+
+class ControllerWebError(Exception):
+ """Failure to talk to the lab controller API.
+
+ Parameters
+ ----------
+ message
+ Exception string value, which is the default Slack message.
+ method
+ Method of request.
+ url
+ URL of the request.
+ status
+ Status code of failure, if any.
+ reason
+ Reason string of failure, if any.
+ body
+ Body of failure message, if any.
+ """
+
+ @classmethod
+ def from_exception(cls, exc: HTTPError) -> ControllerWebError:
+ """Create an exception from an httpx exception.
+
+ Parameters
+ ----------
+ exc
+ Exception from httpx.
+
+ Returns
+ -------
+ ControllerWebError
+ Newly-constructed exception.
+ """
+ if isinstance(exc, HTTPStatusError):
+ status = exc.response.status_code
+ method = exc.request.method
+ message = f"Status {status} from {method} {exc.request.url}"
+ return cls(
+ message,
+ method=exc.request.method,
+ url=str(exc.request.url),
+ status=status,
+ reason=exc.response.reason_phrase,
+ body=exc.response.text,
+ )
+ else:
+ message = f"{type(exc).__name__}: {exc!s}"
+ if isinstance(exc, RequestError):
+ return cls(
+ message,
+ method=exc.request.method,
+ url=str(exc.request.url),
+ )
+ else:
+ return cls(message)
+
+ def __init__(
+ self,
+ message: str,
+ *,
+ method: str | None = None,
+ url: str | None = None,
+ status: int | None = None,
+ reason: str | None = None,
+ body: str | None = None,
+ ) -> None:
+ self.message = message
+ self.method = method
+ self.url = url
+ self.status = status
+ self.reason = reason
+ self.body = body
+ super().__init__(message)
+
+ def __str__(self) -> str:
+ result = self.message
+ if self.body:
+ result += f"\nBody:\n{self.body}\n"
+ return result
+
+
+class InvalidAuthStateError(Exception):
+ """The JupyterHub auth state for the user contains no token."""
+
+
+class MissingFieldError(Exception):
+ """The reply from the lab controller is missing a required field."""
+
+
+class SpawnFailedError(Exception):
+ """The lab controller reports that the spawn failed."""
diff --git a/spawner/src/rsp_restspawner/spawner.py b/spawner/src/rsp_restspawner/spawner.py
new file mode 100644
index 000000000..5cdecb0a7
--- /dev/null
+++ b/spawner/src/rsp_restspawner/spawner.py
@@ -0,0 +1,695 @@
+"""Spawner class that uses a REST API to a separate Kubernetes service."""
+
+from __future__ import annotations
+
+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
+
+from httpx import AsyncClient, HTTPError, Response
+from httpx_sse import ServerSentEvent, aconnect_sse
+from jupyterhub.spawner import Spawner
+from traitlets import Unicode, default
+
+from .exceptions import (
+ ControllerWebError,
+ InvalidAuthStateError,
+ MissingFieldError,
+ SpawnFailedError,
+)
+
+P = ParamSpec("P")
+T = TypeVar("T")
+
+__all__ = [
+ "LabStatus",
+ "RSPRestSpawner",
+]
+
+_CLIENT: AsyncClient | None = None
+"""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]]:
+ """Convert ``httpx`` exceptions to `ControllerWebError`."""
+
+ @wraps(f)
+ async def wrapper(
+ spawner: RSPRestSpawner, *args: P.args, **kwargs: P.kwargs
+ ) -> T:
+ try:
+ return await f(spawner, *args, **kwargs)
+ except HTTPError as e:
+ # JupyterHub appears to swallow the backtrace of the original
+ # exception even though we reference it in a from clause, so
+ # explicitly log the original exception before raising the
+ # translated one.
+ spawner.log.exception("Exception raised in REST spawner")
+ raise ControllerWebError.from_exception(e) from e
+
+ return wrapper
+
+
+class RSPRestSpawner(Spawner):
+ """Spawner class that sends requests to the RSP lab controller.
+
+ Rather than having JupyterHub spawn labs directly and therefore need
+ Kubernetes permissions to manage every resource that a user's lab
+ environment may need, the Rubin Science Platform manages all labs in a
+ separate privileged lab controller process. JupyterHub makes RESTful HTTP
+ requests to that service using either its own credentials or the
+ credentials of the user.
+
+ See `SQR-066
This is some lab form for {spawner.user.name}
" + assert await spawner.options_form(spawner) == expected + + +@pytest.mark.asyncio +async def test_progress(spawner: RSPRestSpawner) -> None: + await spawner.start() + user = spawner.user.name + expected = [ + {"progress": 2, "message": "[info] Lab creation initiated"}, + {"progress": 45, "message": "[info] Pod requested"}, + { + "progress": 75, + "message": f"[info] Pod successfully spawned for {user}", + }, + ] + index = 0 + async for message in spawner.progress(): + assert message == expected[index] + index += 1 + assert index == len(expected) + + +@pytest.mark.asyncio +async def test_progress_conflict(spawner: RSPRestSpawner) -> None: + await spawner.start() + + # Start it a second time, which should trigger deleting the old lab. + await spawner.start() + user = spawner.user.name + expected = [ + {"progress": 1, "message": "[warning] Deleting existing orphaned lab"}, + {"progress": 2, "message": "[info] Lab creation initiated"}, + {"progress": 45, "message": "[info] Pod requested"}, + { + "progress": 75, + "message": f"[info] Pod successfully spawned for {user}", + }, + ] + index = 0 + async for message in spawner.progress(): + assert message == expected[index] + index += 1 + assert index == len(expected) + + +@pytest.mark.asyncio +async def test_progress_multiple( + spawner: RSPRestSpawner, mock_lab_controller: MockLabController +) -> None: + """Test multiple progress listeners for the same spawn.""" + mock_lab_controller.delay = timedelta(milliseconds=750) + user = spawner.user.name + expected = [ + {"progress": 2, "message": "[info] Lab creation initiated"}, + {"progress": 45, "message": "[info] Pod requested"}, + { + "progress": 75, + "message": f"[info] Pod successfully spawned for {user}", + }, + ] + + results = await asyncio.gather( + spawner.start(), + collect_progress(spawner), + collect_progress(spawner), + collect_progress(spawner), + ) + url = results[0] + assert url == f"http://lab.nublado-{user}:8888" + for events in results[1:]: + assert events == expected + + +@pytest.mark.asyncio +async def test_spawn_failure( + spawner: RSPRestSpawner, mock_lab_controller: MockLabController +) -> None: + """Test error handling when a spawn fails. + + Also tests invalid JSON in the event body. In those cases, the raw event + body should be treated as the message. + """ + mock_lab_controller.delay = timedelta(milliseconds=750) + mock_lab_controller.fail_during_spawn = True + user = spawner.user.name + expected = [ + {"progress": 2, "message": "[info] Lab creation initiated"}, + {"progress": 45, "message": "[info] Pod requested"}, + {"progress": 45, "message": "[unknown] This is not JSON"}, + {"progress": 45, "message": "[error] Something is going wrong"}, + {"progress": 45, "message": '[info] {"invalid": "value"}'}, + { + "progress": 45, + "message": '[info] {"message": "Blah", "progress": "Happy!"}', + }, + { + "progress": 45, + "message": f"[error] Some random failure for {user}", + }, + ] + + results = await asyncio.gather( + spawner.start(), collect_progress(spawner), return_exceptions=True + ) + assert isinstance(results[0], SpawnFailedError) + assert results[1] == expected diff --git a/spawner/tests/support/__init__.py b/spawner/tests/support/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/spawner/tests/support/controller.py b/spawner/tests/support/controller.py new file mode 100644 index 000000000..130fd9ed9 --- /dev/null +++ b/spawner/tests/support/controller.py @@ -0,0 +1,227 @@ +"""Mock responses from jupyterlab-controller.""" + +from __future__ import annotations + +import asyncio +from collections.abc import AsyncIterator +from datetime import UTC, datetime, timedelta + +import respx +from httpx import AsyncByteStream, Request, Response + +from rsp_restspawner.spawner import LabStatus + +__all__ = [ + "MockLabController", + "register_mock_lab_controller", +] + + +class MockProgress(AsyncByteStream): + """Generator that produces progress events for a lab spawn. + + An instantiation of this object is suitable for passing as the stream + argument to an `httpx.Response`. + + Parameters + ---------- + user + Name of user for which progress events should be generated. + delay + Delay by this long between events. + fail_during_spawn + Whether to emit a failure message instead of a completion message. + """ + + def __init__( + self, user: str, delay: timedelta, *, fail_during_spawn: bool = False + ) -> None: + self._user = user + self._delay = delay + self._fail_during_spawn = fail_during_spawn + + async def __aiter__(self) -> AsyncIterator[bytes]: + yield b"event: info\r\n" + yield b'data: {"message": "Lab creation initiated", "progress": 2}\r\n' + yield b"\r\n" + + await asyncio.sleep(self._delay.total_seconds()) + + # sse-starlette sends these ping events periodically to keep the + # connection alive. We should just ignore them. + yield b"event: ping\r\n" + yield b"data: " + str(datetime.now(tz=UTC)).encode() + b"\r\n" + yield b"\r\n" + + yield b"event: info\r\n" + yield b'data: {"message": "Pod requested", "progress": 45}\r\n' + yield b"\r\n" + + await asyncio.sleep(self._delay.total_seconds()) + + if self._fail_during_spawn: + yield b"event: blahblah\r\n" + yield b"data: This is not JSON\r\n" + yield b"\r\n" + + yield b"event: error\r\n" + yield b'data: {"message": "Something is going wrong"}\r\n' + yield b"\r\n" + + yield b"event: info\r\n" + yield b'data: {"invalid": "value"}\r\n' + yield b"\r\n" + + yield b"event: info\r\n" + yield b'data: {"message": "Blah", "progress": "Happy!"}\r\n' + yield b"\r\n" + + yield b"event: failed\r\n" + msg = f"Some random failure for {self._user}" + yield b'data: {"message": "' + msg.encode() + b'"}\r\n' + yield b"\r\n" + else: + yield b"event: complete\r\n" + msg = f"Pod successfully spawned for {self._user}" + yield b'data: {"message": "' + msg.encode() + b'"}\r\n' + yield b"\r\n" + + +class MockLabController: + """Mock Nublado lab controller. + + This is an extremely simplified version of the lab controller API + specified in `SQR-066This is some lab form for {user}
" + ) + + def set_status(self, user: str, status: LabStatus) -> None: + """Set the lab status for a given user, called by tests.""" + self._lab_status[user] = status + + def status(self, request: Request, user: str) -> Response: + self._check_authorization(request, admin=True) + if not self._lab_status.get(user): + return Response(status_code=404) + return Response( + status_code=200, + json={ + "status": self._lab_status[user], + "internal_url": f"http://lab.nublado-{user}:8888", + }, + ) + + def _check_authorization( + self, request: Request, *, admin: bool = False + ) -> None: + authorization = request.headers["Authorization"] + auth_type, token = authorization.split(None, 1) + assert auth_type.lower() == "bearer" + if admin: + assert token == self._admin_token + else: + assert token == self._user_token + + +def register_mock_lab_controller( + respx_mock: respx.Router, + base_url: str, + *, + user_token: str, + admin_token: str, +) -> MockLabController: + """Mock out a Nublado lab controller. + + Parameters + ---------- + respx_mock + Mock router. + base_url + Base URL for the lab controller. + user_token + User token expected for routes requiring user authentication. + admin_token + JupyterHub token expected for routes only it can use. + + Returns + ------- + MockLabController + The mock JupyterlabController object. + """ + base_labs_url = f"{base_url}/spawner/v1/labs/(?P