diff --git a/.github/ISSUE_TEMPLATE/new-package.md b/.github/ISSUE_TEMPLATE/new-package.md
new file mode 100644
index 0000000..6ed3097
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/new-package.md
@@ -0,0 +1,29 @@
+---
+name: New Package
+about: Request a new python package and/or system library be installed
+title: "Request a New Package"
+labels: 'new-package'
+
+---
+
+Please fill in the fields below to request new packages for the OpenSAFELY python image.
+
+
+### Python package(s) you wish to add to the image
+
+<!-- please add packages, including links to pypi.org page if possible -->
+
+
+### System Libraries
+
+<! --Any system libraries that this package may require. Leave blank if unsure -->
+
+
+### Requesting Project
+
+<!-- Link to the OpenSAFELY Project that will use these packages -->
+
+
+### Rationale
+
+<!-- Rationale for use in OpenSAFELY -->
diff --git a/.github/workflows/build_and_publish.yaml b/.github/workflows/build_and_publish.yaml
index 84349d8..ad4161e 100644
--- a/.github/workflows/build_and_publish.yaml
+++ b/.github/workflows/build_and_publish.yaml
@@ -1,33 +1,34 @@
 name: Build and publish
 on:
+  workflow_dispatch:
   push:
     branches: [main]
-  workflow_dispatch:
-permissions:
-  packages: write
-env:
-  IMAGE_NAME: python
+
 jobs:
-  build-and-publish:
-    runs-on: ubuntu-20.04
+  publish:
+    # note: this builds/tests all versions in serial for two reasons. Firstly we
+    # want all versions to release or none of them. Secondly, we will be able
+    # publish the exact images that were built and tested.
+    runs-on: ubuntu-22.04
     steps:
-    - name: Checkout
-      uses: actions/checkout@v3
-    - name: Build image
-      run: make build
+    - uses: actions/checkout@v3
+    - uses: "opensafely-core/setup-action@v1"
+      with:
+          install-just: true
+    - name: Build images
+      run: |
+        just build v1
+        just build v2
     - name: Run tests
-      run: make test functional-test
-    - name: Run lint
-      run: make lint 
+      run: |
+        just test v1
+        just test v2
+    - name: Run linters
+      run: just check 
+
     - name: Log into GitHub Container Registry
       run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
     - name: Push image to GitHub Container Registry
       run: |
-        IMAGE_ID="ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME"
-        docker tag "$IMAGE_NAME" "$IMAGE_ID:latest"
-        docker push "$IMAGE_ID:latest"
-
-        JUPYTER_ID="ghcr.io/${{ github.repository_owner }}/jupyter"
-        # also publish as jupyter image for backward compatibility
-        docker tag "$IMAGE_NAME" "$JUPYTER_ID:latest"
-        docker push "$JUPYTER_ID:latest"
+        just publish v1 true
+        just publish v2 true
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index b449bd5..f5a9ed6 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -1,17 +1,28 @@
 name: Run tests
 on:
   pull_request:
-env:
-  IMAGE_NAME: python
 jobs:
-  tests:
-    runs-on: ubuntu-20.04
+  version-tests:
+    runs-on: ubuntu-22.04
+    strategy:
+      matrix:
+        version: [v1, v2]
     steps:
-    - name: Checkout
-      uses: actions/checkout@master
+    - uses: actions/checkout@v3
+    - uses: "opensafely-core/setup-action@v1"
+      with:
+          install-just: true
     - name: Build image
-      run: make build
+      run: just build ${{ matrix.version }}
     - name: Run tests
-      run: make test functional-test
-    - name: Run lint
-      run: make lint 
+      run: just test ${{ matrix.version }}
+  lint:
+    runs-on: ubuntu-22.04
+    steps:
+    - uses: actions/checkout@v3
+    - uses: "opensafely-core/setup-action@v1"
+      with:
+          install-just: true
+    - name: Run linters
+      run: just check 
+
diff --git a/DEVELOPERS.md b/DEVELOPERS.md
new file mode 100644
index 0000000..1c9deea
--- /dev/null
+++ b/DEVELOPERS.md
@@ -0,0 +1,71 @@
+# Basics
+
+Each major version has its configuration in a subdirectory named after the
+version, e.g. ./v1/ has all the configuration for the `v1` image.
+
+Inside each version's directory there are 4 main files:
+
+- `env`: environment variables used to parameterise the Docker/docker-compose
+  files:
+    - `BASE`: the base Ubuntu version to build from, e.g. `22.04`
+    - `MAJOR_VERSION`: this shoud match the directory name.
+- `dependencies.txt`: the Ubuntu packages that need to be installed
+- `build-dependencies.txt`: the Ubuntu package needed to *build* any
+  dependencies (these will *not* be included in the final image).
+- `requirements.in`: the list of packages to install (*without* version
+  specfiers, unless needed for some reason).
+
+There will also be two autogenerated files:
+
+- `requirements.txt`: the fully pinned set of python dependences generated with
+  `pip-compile`.
+- `packages.md`: generated user facing documentation of package versions
+
+
+Use just to build and test image versions:
+
+```
+just build v2
+just test v2
+```
+
+
+## Add a new package to existing version
+
+* Add the new package without version specifier to all relevant version's
+  `requirement.in` files
+* For each version, do the following:
+    * Run `just update $VERSION`. This will update pacakges, then build and
+      test the new image.
+    * If the build fails, depending on the error message:
+        - you may need to add a new system package to `dependencies.txt`
+        - you may need to add a new build dependency package to
+          `build-dependencies.txt`
+        - you may need to finesse the tests for poorly packaged libraries: see
+          [`BAD_PACKAGES`](./tests/test_import.py)
+    * Inspect the changes to requirements.txt
+        - ensure no pre-existing package has been updated by this change.
+
+
+## Create a new version
+
+TODO, but basically, `cp -a v$N v${N+1}` and edit.
+
+
+## Publishing
+
+> ![WARNING]
+> By default, these images are published via CI, so only do this if you know
+> you need to, e.g. testing publishing a new version
+
+To publish a version locally, you will need to be logged in to ghcr.io with the
+right permissions (`docker login ghcr.io`)
+
+By default, this command is a dry run, and will show you the commands it *will* run:
+
+`just publish $version`
+
+To run for real, pass `true`:
+
+`just publish $version true`
+
diff --git a/Dockerfile b/Dockerfile
index 6de34b7..8bdf3af 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,61 +8,77 @@
 # and b) we specifically always want to build on the latest base image, by
 # design.
 #
+ARG BASE
 # hadolint ignore=DL3007
-FROM ghcr.io/opensafely-core/base-action:latest as base-python
-COPY dependencies.txt /root/dependencies.txt
+FROM ghcr.io/opensafely-core/base-action:$BASE as base-python
+
+RUN mkdir /workspace
+WORKDIR /workspace
+
+ARG MAJOR_VERSION
+ARG BASE
+# ACTION_EXEC sets the default executable for the entrypoint in the base-docker image
+ENV ACTION_EXEC=python MAJOR_VERSION=${MAJOR_VERSION} BASE=${BASE}
+
+COPY ${MAJOR_VERSION}/dependencies.txt /opt/dependencies.txt
 # use space efficient utility from base image
-RUN /root/docker-apt-install.sh /root/dependencies.txt
+RUN /root/docker-apt-install.sh /opt/dependencies.txt
+
+# now we have python, set up a venv to install packages to, for isolation from
+# system python libraries 
+# hadolint ignore=DL3059
+RUN python3 -m venv /opt/venv
+# "activate" the venv
+ENV VIRTUAL_ENV=/opt/venv/ PATH="/opt/venv/bin:$PATH"
+# We ensure up-to-date build tools (which why we ignore DL3013)
+# hadolint ignore=DL3013,DL3042
+RUN --mount=type=cache,target=/root/.cache python -m pip install -U pip setuptools wheel pip-tools
+
 
 #################################################
 #
 # Next, use the base-docker-plus-python image to create a build image
 FROM base-python as builder
+ARG MAJOR_VERSION
 
 # install build time dependencies 
-COPY build-dependencies.txt /root/build-dependencies.txt
-RUN /root/docker-apt-install.sh /root/build-dependencies.txt
-
-# install everything in venv for isolation from system python libraries
-# hadolint ignore=DL3059
-RUN python3 -m venv /opt/venv
-ENV VIRTUAL_ENV=/opt/venv/ PATH="/opt/venv/bin:$PATH" LLVM_CONFIG=/usr/bin/llvm-config-10
+COPY ${MAJOR_VERSION}/build-dependencies.txt /opt/build-dependencies.txt
+RUN /root/docker-apt-install.sh /opt/build-dependencies.txt
 
-COPY requirements.txt /root/requirements.txt
-# We ensure up-to-date build tools (which why we ignore DL3013)
+COPY ${MAJOR_VERSION}/requirements.txt /opt/requirements.txt
+COPY ${MAJOR_VERSION}/packages.md /opt/packages.md
 # Note: the mount command does two things: 1) caches across builds to speed up
 # local development and 2) ensures the pip cache does not get committed to the
 # layer (which is why we ignore DL3042).
-# hadolint ignore=DL3013,DL3042
+# hadolint ignore=DL3042
 RUN --mount=type=cache,target=/root/.cache \
-    python -m pip install -U pip setuptools wheel && \
-    python -m pip install --requirement /root/requirements.txt
+    python -m pip install --requirement /opt/requirements.txt
 
 ################################################
 #
 # Finally, build the actual image from the base-python image
 FROM base-python as python
 
+
+ARG MAJOR_VERSION
 # Some static metadata for this specific image, as defined by:
 # https://github.com/opencontainers/image-spec/blob/master/annotations.md#pre-defined-annotation-keys
 # The org.opensafely.action label is used by the jobrunner to indicate this is
 # an approved action image to run.
-LABEL org.opencontainers.image.title="python" \
+LABEL org.opencontainers.image.title="python:${MAJOR_VERSION}" \
       org.opencontainers.image.description="Python action for opensafely.org" \
       org.opencontainers.image.source="https://github.com/opensafely-core/python-docker" \
-      org.opensafely.action="python"
+      org.opensafely.action="python:${MAJOR_VERSION}"
 
 # copy venv over from builder image
-COPY --from=builder /opt/venv /opt/venv
-# ACTION_EXEC sets the default executable for the entrypoint in the base-docker image
-ENV VIRTUAL_ENV=/opt/venv/ PATH="/opt/venv/bin:$PATH" ACTION_EXEC=python
-
-RUN mkdir /workspace
-WORKDIR /workspace
+COPY --from=builder /opt/ /opt/
 
-# tag with build info as the very last step, as it will never be cached
+# tag with build info as the very last step, as it will never be cacheable
 ARG BUILD_DATE
 ARG REVISION
+ARG BUILD_NUMBER
 # RFC 3339.
 LABEL org.opencontainers.image.created=$BUILD_DATE \
-      org.opencontainers.image.revision=$REVISION
+      org.opencontainers.image.revision=$REVISION \
+      org.opencontainers.image.build=$BUILD_NUMBER \
+      org.opencontainers.image.version=$MAJOR_VERSION.$BUILD_NUMBER
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 3816bc8..0000000
--- a/Makefile
+++ /dev/null
@@ -1,35 +0,0 @@
-INTERACTIVE:=$(shell [ -t 0 ] && echo 1)
-export DOCKER_BUILDKIT=1
-export BUILD_DATE=$(shell date +'%y-%m-%dT%H:%M:%S.%3NZ')
-export REVISION=$(shell git rev-parse --short HEAD)
-
-.PHONY: build
-build:
-	docker-compose build --pull python
-
-
-.PHONY: test
-test:
-	docker-compose run --rm -v $(PWD):/workspace python pytest tests -v
-
-# test basic python invocation
-functional-test:
-	docker-compose run --rm python -c ''
-	docker-compose run --rm python python -c ''
-
-
-.PHONY: lint
-lint:
-	@docker pull hadolint/hadolint:v2.8.0
-	@docker run --rm -i hadolint/hadolint:v2.8.0 < Dockerfile
-
-requirements.txt: requirements.in venv/bin/pip-compile
-	venv/bin/pip-compile requirements.in
-
-venv/bin/pip-compile: | venv
-	venv/bin/pip install pip-tools
-
-venv:
-	virtualenv -p python3 venv
-
-
diff --git a/README.md b/README.md
index ba68d92..19fe6f9 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,57 @@
-# OpenSAFELY Python image
+# OpenSAFELY Python Runtime Image
 
-This is a dockerfile for running python in an OpenSAFELY
-environment.
+This repo manages the Docker image for the OpenSAFELY Python runtime. These
+images are based on a base Ubuntu LTS version, and come pre-installed with
+a set of standard scientific python packages.
 
-To make a new release, use conventional commits to bump version
-(i.e. subject lines starting `feat:` or `fix:`)
+The current latest version is `v2`, and you should use that unless you have
+a specific reason. You can use it in your `project.yaml` like so:
 
-See `requirements.txt` for current available packages and versions
+```
+actions:
+  my_action:
+    run: python:v2 my_script.py ...
+```
+
+## Version List
+
+Current available versions, in reverse chronological order:
+
+ - v2: Ubuntu 22.04 and Python 3.10 - [full package list](v2/packages.md)
+ - v1: Ubuntu 20.04 and Python 3.8 - [full package list](v1/packages.md)
+
+### Legacy version: `latest`
+
+Initially, OpenSAFELY only had one version of the python image. This is the
+`v1` image, but was originally published under the `:latest` tag. You can use
+either `v1` or `latest` - they are the same version.  In future, we may
+deprecate the `latest` tag and require users to update their `project.yaml` to
+use `v1` instead of `latest`.
+
+
+## Update Policy
+
+### Python Package Versions
+
+For each version of the python image, we do *not* upgade the python packages
+from their initially installed version. This is done in order to backwards
+compatiblity and thus ensure reproduciblity. We do [add new packages on user
+request](https://github.com/opensafely-core/python-dockerissues/new?template=new-package.md),
+as such a change will not break backwards incompatibilty.
+
+Occasionally, we will create a new major version of the image with all packages
+updated to their latest version. We may also possibly remove old and uneeded
+pacakges at this point.  A new major version is chance to make backwards
+incompatible changes, which is occasionally needed.
+
+Once this new version of the image is intially released, its set of package
+versions will be frozen and no longer updatable.
+
+### Operating System Packages
+
+We *do* update the underlying operating system packages on a regular basis, in
+order to apply security updates to the base system. It is very unlikely that
+this will break backwards compatibility, as these are a small set very
+conservative set of updates to address security issues.
+
+We also add additional operating system libraries on user request.
diff --git a/docker-compose.yml b/docker-compose.yml
index a9751fa..e9f4f17 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,18 +1,26 @@
 services:
-  # used to build the production image
-  python:
-    image: python
+  base:
+    init: true
+    image: python:${MAJOR_VERSION}-base
     build:
       context: .
-      target: python
+      target: base-python
       cache_from:  # should speed up the build in CI, where we have a cold cache
-        - ghcr.io/opensafely-core/base-docker
-        - ghcr.io/opensafely-core/python
+        - ghcr.io/opensafely-core/base-action:${BASE}
+        - ghcr.io/opensafely-core/python:${MAJOR_VERSION}
       args:
         # this makes the image work for later cache_from: usage
         - BUILDKIT_INLINE_CACHE=1
-        # env vars supplied by make/just
+        # env vars supplied by just
+        - BUILD_NUMBER
         - BUILD_DATE
         - REVISION
-        - VERSION
-    init: true
+        - BASE
+        - MAJOR_VERSION
+
+  python:
+    extends:
+      service: base
+    image: python:${MAJOR_VERSION}
+    build:
+      target: python
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..2b05e3f
--- /dev/null
+++ b/justfile
@@ -0,0 +1,38 @@
+export DOCKER_BUILDKIT := "1"
+# technically, these could differ by 1 seconds, but thats unlikely and doesn't matter
+# human readable, used as label in docker image
+export BUILD_DATE := `date +'%y-%m-%dT%H:%M:%S.%3NZ'`
+# monotonic, used as label in docker image *and* in docker tag
+export BUILD_NUMBER := `date +'%y%m%d%H%M%S'`
+export REVISION := `git rev-parse --short HEAD`
+
+# build docker image for version
+build version target="python" *args="":
+    docker compose --env-file {{ version }}/env build --pull {{ args }} {{ target }} 
+
+
+# test docker image for version
+test version *args="tests -v": (build version)
+    docker compose --env-file {{ version }}/env run --rm -v $PWD:/workspace python pytest {{ args }}
+
+
+# run pip-compile to add new dependencies, or update existing ones with --upgrade
+update version *args="":
+    docker compose --env-file {{ version }}/env run --rm -v $PWD:/workspace base pip-compile {{ args }} {{ version }}/requirements.in -o {{ version }}/requirements.txt
+    {{ just_executable() }} render {{ version }}
+    {{ just_executable() }} test {{ version }}
+
+# render package version information
+render version *args:
+    docker compose --env-file {{ version }}/env run --rm -v $PWD:/workspace python ./scripts/render.py {{ args }} > {{ version }}/packages.md
+
+
+# run linters
+check:
+    @docker pull hadolint/hadolint:v2.12.0
+    @docker run --rm -i hadolint/hadolint:v2.12.0 < Dockerfile
+
+
+# publish version (dry run by default - pass "true" to perform publish)
+publish version publish="false":
+    PUBLISH={{ publish }} ./scripts/publish.sh {{ version }}
diff --git a/scripts/packages.j2.md b/scripts/packages.j2.md
new file mode 100644
index 0000000..e9dd3e1
--- /dev/null
+++ b/scripts/packages.j2.md
@@ -0,0 +1,12 @@
+# Package Versions for {{ MAJOR_VERSION }}
+
+This python:{{ MAJOR_VERSION }} OpenSAFELY image is based on Ubuntu {{ BASE }} with Python {{ PYTHON_VERSION }}.
+
+## Packages
+
+It comes pre-installed with a standard set of python packages.
+
+{% for pkg in PACKAGES -%}
+ - [{{ pkg | replace("==", ": ")}}](https://pypi.org/project/{{pkg.name}}/{{pkg.specs[0][1]}}/)
+{% endfor -%}
+
diff --git a/scripts/publish.sh b/scripts/publish.sh
new file mode 100755
index 0000000..19c8027
--- /dev/null
+++ b/scripts/publish.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+set -euo pipefail
+
+version=$1
+registry=ghcr.io/opensafely-core
+
+run() {
+    echo "$@"
+    if test "${PUBLISH:-}" = "true"; then
+        # shellcheck disable=SC2068
+        $@
+    fi
+}
+
+publish() {
+    local local_tag=$1;
+    local remote_tag=$2;
+
+    run docker tag "$local_tag" "$remote_tag"
+    run docker push "$remote_tag"
+}
+
+full_version="$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.version"}}' "python:$version")"
+
+publish "python:$version" "$registry/python:$version"
+publish "python:$version" "$registry/python:${full_version}"
+
+if test "$version" = "v1"; then
+    # jupyter is only alias for v1
+    publish "python:$version" "$registry/jupyter:$version"
+
+    # v1 is also known as latest, at least until we transition fully
+    publish "python:$version" "$registry/python:latest"
+    publish "python:$version" "$registry/jupyter:latest"
+fi
diff --git a/scripts/render.py b/scripts/render.py
new file mode 100755
index 0000000..f036bfe
--- /dev/null
+++ b/scripts/render.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env -S python3 -W ignore
+from pathlib import Path
+import os
+import sys
+import pkg_resources
+
+from jinja2 import Environment, FileSystemLoader
+env = Environment(loader=FileSystemLoader("scripts"))
+
+version = os.environ["MAJOR_VERSION"]
+requirements = Path(version) / "requirements.txt"
+
+context = {
+    "MAJOR_VERSION": version,
+    "BASE": os.environ["BASE"],
+    "PYTHON_VERSION": "{}.{}.{}".format(*sys.version_info),
+}
+
+with requirements.open() as r:
+    context["PACKAGES"] = list(pkg_resources.parse_requirements(r))
+
+template = env.get_template("packages.j2.md")
+
+print(template.render(**context))
diff --git a/tests/test_import.py b/tests/test_import.py
index 09d4a4a..f877650 100644
--- a/tests/test_import.py
+++ b/tests/test_import.py
@@ -1,21 +1,44 @@
+import os
 import subprocess
 from importlib import import_module
 from pathlib import Path
+import re
 
 import pytest
 from pkg_resources import Requirement, get_provider
 
 
+# packages that have no way to detect their importable name
+BAD_PACKAGES = {
+    "beautifulsoup4": "bs4",
+    "protobuf": None, # AARRRRGG
+    "qtpy": None,  # required dependency of jupyter-lab
+}
+
 def get_module_names(pkg_name):
     """Load pkg metadata to find out its importable module name(s)."""
+    # remove any extras
+    pkg_name = re.sub(r'\[.*\]', '', pkg_name)
     modules = set()
     provider = get_provider(Requirement.parse(pkg_name))
     # top level package name is typically all we need
-    if provider.has_metadata("top_level.txt"):
-        modules |= set(provider.get_metadata_lines("top_level.txt"))
+    if pkg_name in BAD_PACKAGES:
+        name = BAD_PACKAGES[pkg_name]
+        if name is None:  # unimportably package
+            return []
+        modules.add(BAD_PACKAGES[pkg_name])
+    elif provider.has_metadata("top_level.txt"):
+        first_line = list(provider.get_metadata_lines("top_level.txt"))[0]
+        modules.add(first_line)
     else:
         # badly packaged dependency, make an educated guess
-        modules.add(pkg_name.replace("-", "_"))
+        name = pkg_name
+        if pkg_name.endswith("-cffi"):
+            name = pkg_name[:-5]
+        elif pkg_name.endswith("-py"):
+            name = pkg_name[:-3]
+        
+        modules.add(name.replace("-", "_"))
 
     if provider.has_metadata("namespace_packages.txt"):
         modules |= set(provider.get_metadata_lines("namespace_packages.txt"))
@@ -24,8 +47,9 @@ def get_module_names(pkg_name):
     return [n for n in modules if n[0] != "_"]
 
 
-def generate_import_names(req_path):
+def generate_import_names(major_version):
     """Generate list of expected modules to be able to import."""
+    req_path = Path(major_version) / "requirements.txt"
     with req_path.open() as fp:
         for line in fp:
             line = line.strip()
@@ -38,7 +62,7 @@ def generate_import_names(req_path):
 
 
 @pytest.mark.parametrize(
-    "name, module", generate_import_names(Path("requirements.txt"))
+    "name, module", generate_import_names(os.environ["MAJOR_VERSION"])
 )
 @pytest.mark.filterwarnings("ignore")
 def test_import_package(name, module):
diff --git a/build-dependencies.txt b/v1/build-dependencies.txt
similarity index 100%
rename from build-dependencies.txt
rename to v1/build-dependencies.txt
diff --git a/dependencies.txt b/v1/dependencies.txt
similarity index 100%
rename from dependencies.txt
rename to v1/dependencies.txt
diff --git a/v1/env b/v1/env
new file mode 100644
index 0000000..ea122d4
--- /dev/null
+++ b/v1/env
@@ -0,0 +1,2 @@
+MAJOR_VERSION=v1
+BASE=20.04
diff --git a/v1/packages.md b/v1/packages.md
new file mode 100644
index 0000000..e578dc2
--- /dev/null
+++ b/v1/packages.md
@@ -0,0 +1,145 @@
+# Package Versions for v1
+
+This python:v1 OpenSAFELY image is based on Ubuntu 20.04 with Python 3.8.10.
+
+## Packages
+
+It comes pre-installed with a standard set of python packages.
+
+- [astor: 0.8.1](https://pypi.org/project/astor/0.8.1/)
+- [attrs: 19.3.0](https://pypi.org/project/attrs/19.3.0/)
+- [autograd: 1.3](https://pypi.org/project/autograd/1.3/)
+- [autograd-gamma: 0.5.0](https://pypi.org/project/autograd-gamma/0.5.0/)
+- [backcall: 0.1.0](https://pypi.org/project/backcall/0.1.0/)
+- [bash-kernel: 0.7.2](https://pypi.org/project/bash-kernel/0.7.2/)
+- [bleach: 3.1.2](https://pypi.org/project/bleach/3.1.2/)
+- [cachetools: 4.0.0](https://pypi.org/project/cachetools/4.0.0/)
+- [cairocffi: 1.4.0](https://pypi.org/project/cairocffi/1.4.0/)
+- [cairosvg: 2.5.2](https://pypi.org/project/cairosvg/2.5.2/)
+- [certifi: 2019.11.28](https://pypi.org/project/certifi/2019.11.28/)
+- [cffi: 1.15.1](https://pypi.org/project/cffi/1.15.1/)
+- [chardet: 3.0.4](https://pypi.org/project/chardet/3.0.4/)
+- [click: 7.0](https://pypi.org/project/click/7.0/)
+- [click-plugins: 1.1.1](https://pypi.org/project/click-plugins/1.1.1/)
+- [cligj: 0.5.0](https://pypi.org/project/cligj/0.5.0/)
+- [coverage: 4.5.4](https://pypi.org/project/coverage/4.5.4/)
+- [cssselect2: 0.7.0](https://pypi.org/project/cssselect2/0.7.0/)
+- [cycler: 0.10.0](https://pypi.org/project/cycler/0.10.0/)
+- [decorator: 4.4.1](https://pypi.org/project/decorator/4.4.1/)
+- [defusedxml: 0.6.0](https://pypi.org/project/defusedxml/0.6.0/)
+- [descartes: 1.1.0](https://pypi.org/project/descartes/1.1.0/)
+- [ebmdatalab: 0.0.30](https://pypi.org/project/ebmdatalab/0.0.30/)
+- [entrypoints: 0.3](https://pypi.org/project/entrypoints/0.3/)
+- [fiona: 1.8.13](https://pypi.org/project/fiona/1.8.13/)
+- [formulaic: 0.2.4](https://pypi.org/project/formulaic/0.2.4/)
+- [future: 0.18.2](https://pypi.org/project/future/0.18.2/)
+- [geopandas: 0.6.3](https://pypi.org/project/geopandas/0.6.3/)
+- [google-api-core: 1.16.0](https://pypi.org/project/google-api-core/1.16.0/)
+- [google-auth: 1.11.0](https://pypi.org/project/google-auth/1.11.0/)
+- [google-auth-oauthlib: 0.4.1](https://pypi.org/project/google-auth-oauthlib/0.4.1/)
+- [google-cloud-bigquery: 1.24.0](https://pypi.org/project/google-cloud-bigquery/1.24.0/)
+- [google-cloud-core: 1.3.0](https://pypi.org/project/google-cloud-core/1.3.0/)
+- [google-resumable-media: 0.5.0](https://pypi.org/project/google-resumable-media/0.5.0/)
+- [googleapis-common-protos: 1.51.0](https://pypi.org/project/googleapis-common-protos/1.51.0/)
+- [idna: 2.8](https://pypi.org/project/idna/2.8/)
+- [interface-meta: 1.2.4](https://pypi.org/project/interface-meta/1.2.4/)
+- [ipykernel: 5.1.4](https://pypi.org/project/ipykernel/5.1.4/)
+- [ipython: 7.12.0](https://pypi.org/project/ipython/7.12.0/)
+- [ipython-genutils: 0.2.0](https://pypi.org/project/ipython-genutils/0.2.0/)
+- [ipywidgets: 7.5.1](https://pypi.org/project/ipywidgets/7.5.1/)
+- [jedi: 0.16.0](https://pypi.org/project/jedi/0.16.0/)
+- [jinja2: 2.11.1](https://pypi.org/project/jinja2/2.11.1/)
+- [joblib: 1.0.1](https://pypi.org/project/joblib/1.0.1/)
+- [json5: 0.9.0](https://pypi.org/project/json5/0.9.0/)
+- [jsonschema: 3.2.0](https://pypi.org/project/jsonschema/3.2.0/)
+- [jupyter: 1.0.0](https://pypi.org/project/jupyter/1.0.0/)
+- [jupyter-client: 5.3.4](https://pypi.org/project/jupyter-client/5.3.4/)
+- [jupyter-console: 6.1.0](https://pypi.org/project/jupyter-console/6.1.0/)
+- [jupyter-core: 4.6.1](https://pypi.org/project/jupyter-core/4.6.1/)
+- [jupyterlab: 1.2.6](https://pypi.org/project/jupyterlab/1.2.6/)
+- [jupyterlab-server: 1.0.6](https://pypi.org/project/jupyterlab-server/1.0.6/)
+- [jupytext: 1.3.3](https://pypi.org/project/jupytext/1.3.3/)
+- [kaleido: 0.2.1](https://pypi.org/project/kaleido/0.2.1/)
+- [kiwisolver: 1.1.0](https://pypi.org/project/kiwisolver/1.1.0/)
+- [lifelines: 0.26.4](https://pypi.org/project/lifelines/0.26.4/)
+- [llvmlite: 0.34.0](https://pypi.org/project/llvmlite/0.34.0/)
+- [lz4: 3.1.3](https://pypi.org/project/lz4/3.1.3/)
+- [markupsafe: 1.1.1](https://pypi.org/project/markupsafe/1.1.1/)
+- [matplotlib: 3.1.3](https://pypi.org/project/matplotlib/3.1.3/)
+- [mistune: 0.8.4](https://pypi.org/project/mistune/0.8.4/)
+- [more-itertools: 8.2.0](https://pypi.org/project/more-itertools/8.2.0/)
+- [munch: 2.5.0](https://pypi.org/project/munch/2.5.0/)
+- [nbconvert: 5.6.1](https://pypi.org/project/nbconvert/5.6.1/)
+- [nbformat: 5.0.4](https://pypi.org/project/nbformat/5.0.4/)
+- [nbval: 0.9.4](https://pypi.org/project/nbval/0.9.4/)
+- [notebook: 6.0.3](https://pypi.org/project/notebook/6.0.3/)
+- [numba: 0.51.2](https://pypi.org/project/numba/0.51.2/)
+- [numpy: 1.18.1](https://pypi.org/project/numpy/1.18.1/)
+- [oauthlib: 3.1.0](https://pypi.org/project/oauthlib/3.1.0/)
+- [opensafely-cohort-extractor: 1.88.0](https://pypi.org/project/opensafely-cohort-extractor/1.88.0/)
+- [opensafely-matching: 0.2.0](https://pypi.org/project/opensafely-matching/0.2.0/)
+- [packaging: 20.1](https://pypi.org/project/packaging/20.1/)
+- [pandas: 1.0.1](https://pypi.org/project/pandas/1.0.1/)
+- [pandas-gbq: 0.13.0](https://pypi.org/project/pandas-gbq/0.13.0/)
+- [pandocfilters: 1.4.2](https://pypi.org/project/pandocfilters/1.4.2/)
+- [parso: 0.6.1](https://pypi.org/project/parso/0.6.1/)
+- [patsy: 0.5.1](https://pypi.org/project/patsy/0.5.1/)
+- [pep517: 0.10.0](https://pypi.org/project/pep517/0.10.0/)
+- [pexpect: 4.8.0](https://pypi.org/project/pexpect/4.8.0/)
+- [pickleshare: 0.7.5](https://pypi.org/project/pickleshare/0.7.5/)
+- [pillow: 8.1.0](https://pypi.org/project/pillow/8.1.0/)
+- [pip-tools: 6.2.0](https://pypi.org/project/pip-tools/6.2.0/)
+- [plotly: 4.5.0](https://pypi.org/project/plotly/4.5.0/)
+- [pluggy: 0.13.1](https://pypi.org/project/pluggy/0.13.1/)
+- [prometheus-client: 0.7.1](https://pypi.org/project/prometheus-client/0.7.1/)
+- [prompt-toolkit: 3.0.3](https://pypi.org/project/prompt-toolkit/3.0.3/)
+- [protobuf: 3.11.3](https://pypi.org/project/protobuf/3.11.3/)
+- [ptyprocess: 0.6.0](https://pypi.org/project/ptyprocess/0.6.0/)
+- [py: 1.8.1](https://pypi.org/project/py/1.8.1/)
+- [pyarrow: 3.0.0](https://pypi.org/project/pyarrow/3.0.0/)
+- [pyasn1: 0.4.8](https://pypi.org/project/pyasn1/0.4.8/)
+- [pyasn1-modules: 0.2.8](https://pypi.org/project/pyasn1-modules/0.2.8/)
+- [pycparser: 2.21](https://pypi.org/project/pycparser/2.21/)
+- [pydata-google-auth: 0.3.0](https://pypi.org/project/pydata-google-auth/0.3.0/)
+- [pygments: 2.5.2](https://pypi.org/project/pygments/2.5.2/)
+- [pyparsing: 2.4.6](https://pypi.org/project/pyparsing/2.4.6/)
+- [pyproj: 2.4.2.post1](https://pypi.org/project/pyproj/2.4.2.post1/)
+- [pyrsistent: 0.15.7](https://pypi.org/project/pyrsistent/0.15.7/)
+- [pytest: 5.3.5](https://pypi.org/project/pytest/5.3.5/)
+- [python-dateutil: 2.8.1](https://pypi.org/project/python-dateutil/2.8.1/)
+- [pytz: 2019.3](https://pypi.org/project/pytz/2019.3/)
+- [pyyaml: 5.3](https://pypi.org/project/pyyaml/5.3/)
+- [pyzmq: 18.1.1](https://pypi.org/project/pyzmq/18.1.1/)
+- [qtconsole: 4.6.0](https://pypi.org/project/qtconsole/4.6.0/)
+- [requests: 2.22.0](https://pypi.org/project/requests/2.22.0/)
+- [requests-oauthlib: 1.3.0](https://pypi.org/project/requests-oauthlib/1.3.0/)
+- [retry: 0.9.2](https://pypi.org/project/retry/0.9.2/)
+- [retrying: 1.3.3](https://pypi.org/project/retrying/1.3.3/)
+- [rsa: 4.0](https://pypi.org/project/rsa/4.0/)
+- [scikit-learn: 0.24.1](https://pypi.org/project/scikit-learn/0.24.1/)
+- [scipy: 1.4.1](https://pypi.org/project/scipy/1.4.1/)
+- [seaborn: 0.10.0](https://pypi.org/project/seaborn/0.10.0/)
+- [send2trash: 1.5.0](https://pypi.org/project/send2trash/1.5.0/)
+- [shapely: 1.7.0](https://pypi.org/project/shapely/1.7.0/)
+- [six: 1.14.0](https://pypi.org/project/six/1.14.0/)
+- [sqlparse: 0.4.1](https://pypi.org/project/sqlparse/0.4.1/)
+- [statsmodels: 0.11.0](https://pypi.org/project/statsmodels/0.11.0/)
+- [structlog: 20.2.0](https://pypi.org/project/structlog/20.2.0/)
+- [tabulate: 0.8.7](https://pypi.org/project/tabulate/0.8.7/)
+- [terminado: 0.8.3](https://pypi.org/project/terminado/0.8.3/)
+- [testpath: 0.4.4](https://pypi.org/project/testpath/0.4.4/)
+- [threadpoolctl: 2.1.0](https://pypi.org/project/threadpoolctl/2.1.0/)
+- [tinycss2: 1.2.1](https://pypi.org/project/tinycss2/1.2.1/)
+- [toml: 0.10.2](https://pypi.org/project/toml/0.10.2/)
+- [tornado: 6.0.3](https://pypi.org/project/tornado/6.0.3/)
+- [tqdm: 4.42.1](https://pypi.org/project/tqdm/4.42.1/)
+- [traitlets: 4.3.3](https://pypi.org/project/traitlets/4.3.3/)
+- [upsetplot: 0.6.1](https://pypi.org/project/upsetplot/0.6.1/)
+- [urllib3: 1.25.8](https://pypi.org/project/urllib3/1.25.8/)
+- [venn: 0.1.3](https://pypi.org/project/venn/0.1.3/)
+- [wcwidth: 0.1.8](https://pypi.org/project/wcwidth/0.1.8/)
+- [webencodings: 0.5.1](https://pypi.org/project/webencodings/0.5.1/)
+- [wheel: 0.36.2](https://pypi.org/project/wheel/0.36.2/)
+- [widgetsnbextension: 3.5.1](https://pypi.org/project/widgetsnbextension/3.5.1/)
+- [wrapt: 1.13.3](https://pypi.org/project/wrapt/1.13.3/)
+
diff --git a/requirements.in b/v1/requirements.in
similarity index 100%
rename from requirements.in
rename to v1/requirements.in
diff --git a/requirements.txt b/v1/requirements.txt
similarity index 87%
rename from requirements.txt
rename to v1/requirements.txt
index 18935e4..4b4b48a 100644
--- a/requirements.txt
+++ b/v1/requirements.txt
@@ -1,8 +1,8 @@
 #
-# This file is autogenerated by pip-compile with python 3.8
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
 #
-#    pip-compile
+#    pip-compile --output-file=v1/requirements.txt v1/requirements.in
 #
 astor==0.8.1
     # via formulaic
@@ -20,7 +20,7 @@ autograd-gamma==0.5.0
 backcall==0.1.0
     # via ipython
 bash-kernel==0.7.2
-    # via -r requirements.in
+    # via -r v1/requirements.in
 bleach==3.1.2
     # via nbconvert
 cachetools==4.0.0
@@ -28,7 +28,7 @@ cachetools==4.0.0
 cairocffi==1.4.0
     # via cairosvg
 cairosvg==2.5.2
-    # via -r requirements.in
+    # via -r v1/requirements.in
 certifi==2019.11.28
     # via requests
 cffi==1.15.1
@@ -37,6 +37,8 @@ chardet==3.0.4
     # via requests
 click==7.0
     # via
+    #   click-plugins
+    #   cligj
     #   fiona
     #   pip-tools
 click-plugins==1.1.1
@@ -61,7 +63,7 @@ defusedxml==0.6.0
 descartes==1.1.0
     # via ebmdatalab
 ebmdatalab==0.0.30
-    # via -r requirements.in
+    # via -r v1/requirements.in
 entrypoints==0.3
     # via nbconvert
 fiona==1.8.13
@@ -120,7 +122,7 @@ ipython-genutils==0.2.0
     #   traitlets
 ipywidgets==7.5.1
     # via
-    #   -r requirements.in
+    #   -r v1/requirements.in
     #   jupyter
 jedi==0.16.0
     # via ipython
@@ -139,7 +141,7 @@ jsonschema==3.2.0
     #   jupyterlab-server
     #   nbformat
 jupyter==1.0.0
-    # via -r requirements.in
+    # via -r v1/requirements.in
 jupyter-client==5.3.4
     # via
     #   ipykernel
@@ -157,17 +159,17 @@ jupyter-core==4.6.1
     #   notebook
     #   qtconsole
 jupyterlab==1.2.6
-    # via -r requirements.in
+    # via -r v1/requirements.in
 jupyterlab-server==1.0.6
     # via jupyterlab
 jupytext==1.3.3
-    # via -r requirements.in
+    # via -r v1/requirements.in
 kaleido==0.2.1
-    # via -r requirements.in
+    # via -r v1/requirements.in
 kiwisolver==1.1.0
     # via matplotlib
 lifelines==0.26.4
-    # via -r requirements.in
+    # via -r v1/requirements.in
 llvmlite==0.34.0
     # via numba
 lz4==3.1.3
@@ -176,7 +178,7 @@ markupsafe==1.1.1
     # via jinja2
 matplotlib==3.1.3
     # via
-    #   -r requirements.in
+    #   -r v1/requirements.in
     #   descartes
     #   lifelines
     #   seaborn
@@ -200,7 +202,7 @@ nbformat==5.0.4
     #   nbval
     #   notebook
 nbval==0.9.4
-    # via -r requirements.in
+    # via -r v1/requirements.in
 notebook==6.0.3
     # via
     #   jupyter
@@ -208,10 +210,10 @@ notebook==6.0.3
     #   jupyterlab-server
     #   widgetsnbextension
 numba==0.51.2
-    # via -r requirements.in
+    # via -r v1/requirements.in
 numpy==1.18.1
     # via
-    #   -r requirements.in
+    #   -r v1/requirements.in
     #   autograd
     #   formulaic
     #   lifelines
@@ -227,14 +229,14 @@ numpy==1.18.1
 oauthlib==3.1.0
     # via requests-oauthlib
 opensafely-cohort-extractor==1.88.0
-    # via -r requirements.in
+    # via -r v1/requirements.in
 opensafely-matching==0.2.0
-    # via -r requirements.in
+    # via -r v1/requirements.in
 packaging==20.1
     # via pytest
 pandas==1.0.1
     # via
-    #   -r requirements.in
+    #   -r v1/requirements.in
     #   ebmdatalab
     #   formulaic
     #   geopandas
@@ -247,7 +249,7 @@ pandas==1.0.1
     #   upsetplot
 pandas-gbq==0.13.0
     # via
-    #   -r requirements.in
+    #   -r v1/requirements.in
     #   ebmdatalab
 pandocfilters==1.4.2
     # via nbconvert
@@ -265,12 +267,12 @@ pickleshare==0.7.5
     # via ipython
 pillow==8.1.0
     # via
-    #   -r requirements.in
+    #   -r v1/requirements.in
     #   cairosvg
 pip-tools==6.2.0
-    # via -r requirements.in
+    # via -r v1/requirements.in
 plotly==4.5.0
-    # via -r requirements.in
+    # via -r v1/requirements.in
 pluggy==0.13.1
     # via pytest
 prometheus-client==0.7.1
@@ -283,6 +285,7 @@ protobuf==3.11.3
     # via
     #   google-api-core
     #   google-cloud-bigquery
+    #   googleapis-common-protos
 ptyprocess==0.6.0
     # via
     #   pexpect
@@ -293,7 +296,7 @@ py==1.8.1
     #   retry
 pyarrow==3.0.0
     # via
-    #   -r requirements.in
+    #   -r v1/requirements.in
     #   opensafely-cohort-extractor
 pyasn1==0.4.8
     # via
@@ -321,7 +324,7 @@ pyrsistent==0.15.7
     # via jsonschema
 pytest==5.3.5
     # via
-    #   -r requirements.in
+    #   -r v1/requirements.in
     #   nbval
 python-dateutil==2.8.1
     # via
@@ -356,10 +359,10 @@ retrying==1.3.3
 rsa==4.0
     # via google-auth
 scikit-learn==0.24.1
-    # via -r requirements.in
+    # via -r v1/requirements.in
 scipy==1.4.1
     # via
-    #   -r requirements.in
+    #   -r v1/requirements.in
     #   autograd-gamma
     #   formulaic
     #   lifelines
@@ -384,11 +387,13 @@ six==1.14.0
     #   google-cloud-bigquery
     #   google-resumable-media
     #   jsonschema
+    #   munch
     #   nbval
     #   packaging
     #   patsy
     #   plotly
     #   protobuf
+    #   pyrsistent
     #   python-dateutil
     #   retrying
     #   traitlets
@@ -420,7 +425,7 @@ tornado==6.0.3
     #   notebook
     #   terminado
 tqdm==4.42.1
-    # via -r requirements.in
+    # via -r v1/requirements.in
 traitlets==4.3.3
     # via
     #   ipykernel
@@ -433,11 +438,11 @@ traitlets==4.3.3
     #   notebook
     #   qtconsole
 upsetplot==0.6.1
-    # via -r requirements.in
+    # via -r v1/requirements.in
 urllib3==1.25.8
     # via requests
 venn==0.1.3
-    # via -r requirements.in
+    # via -r v1/requirements.in
 wcwidth==0.1.8
     # via
     #   prompt-toolkit
diff --git a/v2/build-dependencies.txt b/v2/build-dependencies.txt
new file mode 100644
index 0000000..d5c2de8
--- /dev/null
+++ b/v2/build-dependencies.txt
@@ -0,0 +1,10 @@
+# build time dependencies
+build-essential
+gcc
+python3-dev
+python3-venv
+python3-wheel
+# for numba
+#llvm-10-dev
+# for cairosvg
+libffi-dev
diff --git a/v2/dependencies.txt b/v2/dependencies.txt
new file mode 100644
index 0000000..9fb4f06
--- /dev/null
+++ b/v2/dependencies.txt
@@ -0,0 +1,15 @@
+# run time dependencies
+# ensure fully working base python3 installation
+# see: https://gist.github.com/tiran/2dec9e03c6f901814f6d1e8dad09528e
+python3
+python3-venv
+python3-pip
+python3-distutils
+tzdata
+ca-certificates
+
+# for cairosvg
+libcairo2
+
+# Some jupyter notebooks rely on using git to get current version of repo
+git
diff --git a/v2/env b/v2/env
new file mode 100644
index 0000000..7cd7324
--- /dev/null
+++ b/v2/env
@@ -0,0 +1,2 @@
+MAJOR_VERSION=v2
+BASE=22.04
diff --git a/v2/packages.md b/v2/packages.md
new file mode 100644
index 0000000..b9d3e05
--- /dev/null
+++ b/v2/packages.md
@@ -0,0 +1,162 @@
+# Package Versions for v2
+
+This python:v2 OpenSAFELY image is based on Ubuntu 22.04 with Python 3.10.12.
+
+## Packages
+
+It comes pre-installed with a standard set of python packages.
+
+- [anyio: 4.1.0](https://pypi.org/project/anyio/4.1.0/)
+- [argon2-cffi: 23.1.0](https://pypi.org/project/argon2-cffi/23.1.0/)
+- [argon2-cffi-bindings: 21.2.0](https://pypi.org/project/argon2-cffi-bindings/21.2.0/)
+- [arrow: 1.3.0](https://pypi.org/project/arrow/1.3.0/)
+- [astor: 0.8.1](https://pypi.org/project/astor/0.8.1/)
+- [asttokens: 2.4.1](https://pypi.org/project/asttokens/2.4.1/)
+- [async-lru: 2.0.4](https://pypi.org/project/async-lru/2.0.4/)
+- [attrs: 23.1.0](https://pypi.org/project/attrs/23.1.0/)
+- [autograd: 1.6.2](https://pypi.org/project/autograd/1.6.2/)
+- [autograd-gamma: 0.5.0](https://pypi.org/project/autograd-gamma/0.5.0/)
+- [babel: 2.13.1](https://pypi.org/project/babel/2.13.1/)
+- [bash-kernel: 0.9.3](https://pypi.org/project/bash-kernel/0.9.3/)
+- [beautifulsoup4: 4.12.2](https://pypi.org/project/beautifulsoup4/4.12.2/)
+- [bleach: 6.1.0](https://pypi.org/project/bleach/6.1.0/)
+- [build: 1.0.3](https://pypi.org/project/build/1.0.3/)
+- [cairocffi: 1.6.1](https://pypi.org/project/cairocffi/1.6.1/)
+- [cairosvg: 2.7.1](https://pypi.org/project/cairosvg/2.7.1/)
+- [certifi: 2023.11.17](https://pypi.org/project/certifi/2023.11.17/)
+- [cffi: 1.16.0](https://pypi.org/project/cffi/1.16.0/)
+- [charset-normalizer: 3.3.2](https://pypi.org/project/charset-normalizer/3.3.2/)
+- [click: 8.1.7](https://pypi.org/project/click/8.1.7/)
+- [comm: 0.2.0](https://pypi.org/project/comm/0.2.0/)
+- [contourpy: 1.2.0](https://pypi.org/project/contourpy/1.2.0/)
+- [coverage: 7.3.2](https://pypi.org/project/coverage/7.3.2/)
+- [cssselect2: 0.7.0](https://pypi.org/project/cssselect2/0.7.0/)
+- [cycler: 0.12.1](https://pypi.org/project/cycler/0.12.1/)
+- [debugpy: 1.8.0](https://pypi.org/project/debugpy/1.8.0/)
+- [decorator: 5.1.1](https://pypi.org/project/decorator/5.1.1/)
+- [defusedxml: 0.7.1](https://pypi.org/project/defusedxml/0.7.1/)
+- [exceptiongroup: 1.2.0](https://pypi.org/project/exceptiongroup/1.2.0/)
+- [executing: 2.0.1](https://pypi.org/project/executing/2.0.1/)
+- [fastjsonschema: 2.19.0](https://pypi.org/project/fastjsonschema/2.19.0/)
+- [fonttools: 4.46.0](https://pypi.org/project/fonttools/4.46.0/)
+- [formulaic: 0.6.6](https://pypi.org/project/formulaic/0.6.6/)
+- [fqdn: 1.5.1](https://pypi.org/project/fqdn/1.5.1/)
+- [future: 0.18.3](https://pypi.org/project/future/0.18.3/)
+- [idna: 3.6](https://pypi.org/project/idna/3.6/)
+- [iniconfig: 2.0.0](https://pypi.org/project/iniconfig/2.0.0/)
+- [interface-meta: 1.3.0](https://pypi.org/project/interface-meta/1.3.0/)
+- [ipykernel: 6.27.1](https://pypi.org/project/ipykernel/6.27.1/)
+- [ipython: 8.18.1](https://pypi.org/project/ipython/8.18.1/)
+- [ipywidgets: 8.1.1](https://pypi.org/project/ipywidgets/8.1.1/)
+- [isoduration: 20.11.0](https://pypi.org/project/isoduration/20.11.0/)
+- [jedi: 0.19.1](https://pypi.org/project/jedi/0.19.1/)
+- [jinja2: 3.1.2](https://pypi.org/project/jinja2/3.1.2/)
+- [joblib: 1.3.2](https://pypi.org/project/joblib/1.3.2/)
+- [json5: 0.9.14](https://pypi.org/project/json5/0.9.14/)
+- [jsonpointer: 2.4](https://pypi.org/project/jsonpointer/2.4/)
+- [jsonschema[format-nongpl]: 4.20.0](https://pypi.org/project/jsonschema/4.20.0/)
+- [jsonschema-specifications: 2023.11.2](https://pypi.org/project/jsonschema-specifications/2023.11.2/)
+- [jupyter-client: 8.6.0](https://pypi.org/project/jupyter-client/8.6.0/)
+- [jupyter-console: 6.6.3](https://pypi.org/project/jupyter-console/6.6.3/)
+- [jupyter-core: 5.5.0](https://pypi.org/project/jupyter-core/5.5.0/)
+- [jupyter-events: 0.9.0](https://pypi.org/project/jupyter-events/0.9.0/)
+- [jupyter-lsp: 2.2.1](https://pypi.org/project/jupyter-lsp/2.2.1/)
+- [jupyter-server: 2.11.2](https://pypi.org/project/jupyter-server/2.11.2/)
+- [jupyter-server-terminals: 0.4.4](https://pypi.org/project/jupyter-server-terminals/0.4.4/)
+- [jupyterlab: 4.0.9](https://pypi.org/project/jupyterlab/4.0.9/)
+- [jupyterlab-pygments: 0.3.0](https://pypi.org/project/jupyterlab-pygments/0.3.0/)
+- [jupyterlab-server: 2.25.2](https://pypi.org/project/jupyterlab-server/2.25.2/)
+- [jupyterlab-widgets: 3.0.9](https://pypi.org/project/jupyterlab-widgets/3.0.9/)
+- [jupytext: 1.16.0](https://pypi.org/project/jupytext/1.16.0/)
+- [kaleido: 0.2.1](https://pypi.org/project/kaleido/0.2.1/)
+- [kiwisolver: 1.4.5](https://pypi.org/project/kiwisolver/1.4.5/)
+- [lifelines: 0.27.8](https://pypi.org/project/lifelines/0.27.8/)
+- [llvmlite: 0.41.1](https://pypi.org/project/llvmlite/0.41.1/)
+- [lz4: 4.3.2](https://pypi.org/project/lz4/4.3.2/)
+- [markdown-it-py: 3.0.0](https://pypi.org/project/markdown-it-py/3.0.0/)
+- [markupsafe: 2.1.3](https://pypi.org/project/markupsafe/2.1.3/)
+- [matplotlib: 3.8.2](https://pypi.org/project/matplotlib/3.8.2/)
+- [matplotlib-inline: 0.1.6](https://pypi.org/project/matplotlib-inline/0.1.6/)
+- [mdit-py-plugins: 0.4.0](https://pypi.org/project/mdit-py-plugins/0.4.0/)
+- [mdurl: 0.1.2](https://pypi.org/project/mdurl/0.1.2/)
+- [mistune: 3.0.2](https://pypi.org/project/mistune/3.0.2/)
+- [nbclient: 0.9.0](https://pypi.org/project/nbclient/0.9.0/)
+- [nbconvert: 7.12.0](https://pypi.org/project/nbconvert/7.12.0/)
+- [nbformat: 5.9.2](https://pypi.org/project/nbformat/5.9.2/)
+- [nbval: 0.10.0](https://pypi.org/project/nbval/0.10.0/)
+- [nest-asyncio: 1.5.8](https://pypi.org/project/nest-asyncio/1.5.8/)
+- [notebook: 7.0.6](https://pypi.org/project/notebook/7.0.6/)
+- [notebook-shim: 0.2.3](https://pypi.org/project/notebook-shim/0.2.3/)
+- [numba: 0.58.1](https://pypi.org/project/numba/0.58.1/)
+- [numpy: 1.26.2](https://pypi.org/project/numpy/1.26.2/)
+- [opensafely-cohort-extractor: 1.90.0](https://pypi.org/project/opensafely-cohort-extractor/1.90.0/)
+- [opensafely-matching: 0.2.0](https://pypi.org/project/opensafely-matching/0.2.0/)
+- [overrides: 7.4.0](https://pypi.org/project/overrides/7.4.0/)
+- [packaging: 23.2](https://pypi.org/project/packaging/23.2/)
+- [pandas: 2.1.3](https://pypi.org/project/pandas/2.1.3/)
+- [pandocfilters: 1.5.0](https://pypi.org/project/pandocfilters/1.5.0/)
+- [parso: 0.8.3](https://pypi.org/project/parso/0.8.3/)
+- [pexpect: 4.9.0](https://pypi.org/project/pexpect/4.9.0/)
+- [pillow: 10.1.0](https://pypi.org/project/pillow/10.1.0/)
+- [pip-tools: 7.3.0](https://pypi.org/project/pip-tools/7.3.0/)
+- [platformdirs: 4.1.0](https://pypi.org/project/platformdirs/4.1.0/)
+- [plotly: 5.18.0](https://pypi.org/project/plotly/5.18.0/)
+- [pluggy: 1.3.0](https://pypi.org/project/pluggy/1.3.0/)
+- [prometheus-client: 0.19.0](https://pypi.org/project/prometheus-client/0.19.0/)
+- [prompt-toolkit: 3.0.41](https://pypi.org/project/prompt-toolkit/3.0.41/)
+- [psutil: 5.9.6](https://pypi.org/project/psutil/5.9.6/)
+- [ptyprocess: 0.7.0](https://pypi.org/project/ptyprocess/0.7.0/)
+- [pure-eval: 0.2.2](https://pypi.org/project/pure-eval/0.2.2/)
+- [py: 1.11.0](https://pypi.org/project/py/1.11.0/)
+- [pyarrow: 14.0.1](https://pypi.org/project/pyarrow/14.0.1/)
+- [pycparser: 2.21](https://pypi.org/project/pycparser/2.21/)
+- [pygments: 2.17.2](https://pypi.org/project/pygments/2.17.2/)
+- [pyparsing: 3.1.1](https://pypi.org/project/pyparsing/3.1.1/)
+- [pyproject-hooks: 1.0.0](https://pypi.org/project/pyproject-hooks/1.0.0/)
+- [pytest: 7.4.3](https://pypi.org/project/pytest/7.4.3/)
+- [python-dateutil: 2.8.2](https://pypi.org/project/python-dateutil/2.8.2/)
+- [python-json-logger: 2.0.7](https://pypi.org/project/python-json-logger/2.0.7/)
+- [pytz: 2023.3.post1](https://pypi.org/project/pytz/2023.3.post1/)
+- [pyyaml: 6.0.1](https://pypi.org/project/pyyaml/6.0.1/)
+- [pyzmq: 25.1.2](https://pypi.org/project/pyzmq/25.1.2/)
+- [referencing: 0.31.1](https://pypi.org/project/referencing/0.31.1/)
+- [requests: 2.31.0](https://pypi.org/project/requests/2.31.0/)
+- [retry: 0.9.2](https://pypi.org/project/retry/0.9.2/)
+- [rfc3339-validator: 0.1.4](https://pypi.org/project/rfc3339-validator/0.1.4/)
+- [rfc3986-validator: 0.1.1](https://pypi.org/project/rfc3986-validator/0.1.1/)
+- [rpds-py: 0.13.2](https://pypi.org/project/rpds-py/0.13.2/)
+- [scikit-learn: 1.3.2](https://pypi.org/project/scikit-learn/1.3.2/)
+- [scipy: 1.11.4](https://pypi.org/project/scipy/1.11.4/)
+- [seaborn: 0.13.0](https://pypi.org/project/seaborn/0.13.0/)
+- [send2trash: 1.8.2](https://pypi.org/project/send2trash/1.8.2/)
+- [six: 1.16.0](https://pypi.org/project/six/1.16.0/)
+- [sniffio: 1.3.0](https://pypi.org/project/sniffio/1.3.0/)
+- [soupsieve: 2.5](https://pypi.org/project/soupsieve/2.5/)
+- [sqlparse: 0.4.4](https://pypi.org/project/sqlparse/0.4.4/)
+- [stack-data: 0.6.3](https://pypi.org/project/stack-data/0.6.3/)
+- [structlog: 23.2.0](https://pypi.org/project/structlog/23.2.0/)
+- [tabulate: 0.9.0](https://pypi.org/project/tabulate/0.9.0/)
+- [tenacity: 8.2.3](https://pypi.org/project/tenacity/8.2.3/)
+- [terminado: 0.18.0](https://pypi.org/project/terminado/0.18.0/)
+- [threadpoolctl: 3.2.0](https://pypi.org/project/threadpoolctl/3.2.0/)
+- [tinycss2: 1.2.1](https://pypi.org/project/tinycss2/1.2.1/)
+- [toml: 0.10.2](https://pypi.org/project/toml/0.10.2/)
+- [tomli: 2.0.1](https://pypi.org/project/tomli/2.0.1/)
+- [tornado: 6.4](https://pypi.org/project/tornado/6.4/)
+- [tqdm: 4.66.1](https://pypi.org/project/tqdm/4.66.1/)
+- [traitlets: 5.14.0](https://pypi.org/project/traitlets/5.14.0/)
+- [types-python-dateutil: 2.8.19.14](https://pypi.org/project/types-python-dateutil/2.8.19.14/)
+- [typing-extensions: 4.8.0](https://pypi.org/project/typing-extensions/4.8.0/)
+- [tzdata: 2023.3](https://pypi.org/project/tzdata/2023.3/)
+- [upsetplot: 0.8.0](https://pypi.org/project/upsetplot/0.8.0/)
+- [uri-template: 1.3.0](https://pypi.org/project/uri-template/1.3.0/)
+- [urllib3: 2.1.0](https://pypi.org/project/urllib3/2.1.0/)
+- [venn: 0.1.3](https://pypi.org/project/venn/0.1.3/)
+- [wcwidth: 0.2.12](https://pypi.org/project/wcwidth/0.2.12/)
+- [webcolors: 1.13](https://pypi.org/project/webcolors/1.13/)
+- [webencodings: 0.5.1](https://pypi.org/project/webencodings/0.5.1/)
+- [websocket-client: 1.7.0](https://pypi.org/project/websocket-client/1.7.0/)
+- [wheel: 0.42.0](https://pypi.org/project/wheel/0.42.0/)
+- [widgetsnbextension: 4.0.9](https://pypi.org/project/widgetsnbextension/4.0.9/)
+- [wrapt: 1.16.0](https://pypi.org/project/wrapt/1.16.0/)
+
diff --git a/v2/requirements.in b/v2/requirements.in
new file mode 100644
index 0000000..82a8776
--- /dev/null
+++ b/v2/requirements.in
@@ -0,0 +1,38 @@
+# Basic requirements for notebook infrastructure provided in base
+# docker image
+pip-tools
+notebook
+jupyter-console
+jupyterlab
+jupytext
+bash_kernel
+nbval
+opensafely-matching
+
+# Commonly-used packages provided in base docker image
+pandas
+numpy
+matplotlib
+scipy
+tqdm
+numba
+opensafely-cohort-extractor
+pyarrow
+venn
+kaleido
+scikit-learn
+lifelines
+
+# Both these required for plotly+notebooks
+plotly
+ipywidgets
+
+# Add extra per-notebook packages here
+pillow
+cairosvg
+
+# Allow for projects to run tests inside the container
+pytest
+
+# For visualisation of set overlaps
+upsetplot
diff --git a/v2/requirements.txt b/v2/requirements.txt
new file mode 100644
index 0000000..cf29f44
--- /dev/null
+++ b/v2/requirements.txt
@@ -0,0 +1,499 @@
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+#    pip-compile --output-file=v2/requirements.txt v2/requirements.in
+#
+anyio==4.1.0
+    # via jupyter-server
+argon2-cffi==23.1.0
+    # via jupyter-server
+argon2-cffi-bindings==21.2.0
+    # via argon2-cffi
+arrow==1.3.0
+    # via isoduration
+astor==0.8.1
+    # via formulaic
+asttokens==2.4.1
+    # via stack-data
+async-lru==2.0.4
+    # via jupyterlab
+attrs==23.1.0
+    # via
+    #   jsonschema
+    #   referencing
+autograd==1.6.2
+    # via
+    #   autograd-gamma
+    #   lifelines
+autograd-gamma==0.5.0
+    # via lifelines
+babel==2.13.1
+    # via jupyterlab-server
+bash-kernel==0.9.3
+    # via -r v2/requirements.in
+beautifulsoup4==4.12.2
+    # via nbconvert
+bleach==6.1.0
+    # via nbconvert
+build==1.0.3
+    # via pip-tools
+cairocffi==1.6.1
+    # via cairosvg
+cairosvg==2.7.1
+    # via -r v2/requirements.in
+certifi==2023.11.17
+    # via requests
+cffi==1.16.0
+    # via
+    #   argon2-cffi-bindings
+    #   cairocffi
+charset-normalizer==3.3.2
+    # via requests
+click==8.1.7
+    # via pip-tools
+comm==0.2.0
+    # via
+    #   ipykernel
+    #   ipywidgets
+contourpy==1.2.0
+    # via matplotlib
+coverage==7.3.2
+    # via nbval
+cssselect2==0.7.0
+    # via cairosvg
+cycler==0.12.1
+    # via matplotlib
+debugpy==1.8.0
+    # via ipykernel
+decorator==5.1.1
+    # via
+    #   ipython
+    #   retry
+defusedxml==0.7.1
+    # via
+    #   cairosvg
+    #   nbconvert
+exceptiongroup==1.2.0
+    # via
+    #   anyio
+    #   ipython
+    #   pytest
+executing==2.0.1
+    # via stack-data
+fastjsonschema==2.19.0
+    # via nbformat
+fonttools==4.46.0
+    # via matplotlib
+formulaic==0.6.6
+    # via lifelines
+fqdn==1.5.1
+    # via jsonschema
+future==0.18.3
+    # via autograd
+idna==3.6
+    # via
+    #   anyio
+    #   jsonschema
+    #   requests
+iniconfig==2.0.0
+    # via pytest
+interface-meta==1.3.0
+    # via formulaic
+ipykernel==6.27.1
+    # via
+    #   bash-kernel
+    #   jupyter-console
+    #   jupyterlab
+    #   nbval
+ipython==8.18.1
+    # via
+    #   ipykernel
+    #   ipywidgets
+    #   jupyter-console
+ipywidgets==8.1.1
+    # via -r v2/requirements.in
+isoduration==20.11.0
+    # via jsonschema
+jedi==0.19.1
+    # via ipython
+jinja2==3.1.2
+    # via
+    #   jupyter-server
+    #   jupyterlab
+    #   jupyterlab-server
+    #   nbconvert
+joblib==1.3.2
+    # via scikit-learn
+json5==0.9.14
+    # via jupyterlab-server
+jsonpointer==2.4
+    # via jsonschema
+jsonschema[format-nongpl]==4.20.0
+    # via
+    #   jupyter-events
+    #   jupyterlab-server
+    #   nbformat
+jsonschema-specifications==2023.11.2
+    # via jsonschema
+jupyter-client==8.6.0
+    # via
+    #   ipykernel
+    #   jupyter-console
+    #   jupyter-server
+    #   nbclient
+    #   nbval
+jupyter-console==6.6.3
+    # via -r v2/requirements.in
+jupyter-core==5.5.0
+    # via
+    #   ipykernel
+    #   jupyter-client
+    #   jupyter-console
+    #   jupyter-server
+    #   jupyterlab
+    #   nbclient
+    #   nbconvert
+    #   nbformat
+jupyter-events==0.9.0
+    # via jupyter-server
+jupyter-lsp==2.2.1
+    # via jupyterlab
+jupyter-server==2.11.2
+    # via
+    #   jupyter-lsp
+    #   jupyterlab
+    #   jupyterlab-server
+    #   notebook
+    #   notebook-shim
+jupyter-server-terminals==0.4.4
+    # via jupyter-server
+jupyterlab==4.0.9
+    # via
+    #   -r v2/requirements.in
+    #   notebook
+jupyterlab-pygments==0.3.0
+    # via nbconvert
+jupyterlab-server==2.25.2
+    # via
+    #   jupyterlab
+    #   notebook
+jupyterlab-widgets==3.0.9
+    # via ipywidgets
+jupytext==1.16.0
+    # via -r v2/requirements.in
+kaleido==0.2.1
+    # via -r v2/requirements.in
+kiwisolver==1.4.5
+    # via matplotlib
+lifelines==0.27.8
+    # via -r v2/requirements.in
+llvmlite==0.41.1
+    # via numba
+lz4==4.3.2
+    # via opensafely-cohort-extractor
+markdown-it-py==3.0.0
+    # via
+    #   jupytext
+    #   mdit-py-plugins
+markupsafe==2.1.3
+    # via
+    #   jinja2
+    #   nbconvert
+matplotlib==3.8.2
+    # via
+    #   -r v2/requirements.in
+    #   lifelines
+    #   seaborn
+    #   upsetplot
+    #   venn
+matplotlib-inline==0.1.6
+    # via
+    #   ipykernel
+    #   ipython
+mdit-py-plugins==0.4.0
+    # via jupytext
+mdurl==0.1.2
+    # via markdown-it-py
+mistune==3.0.2
+    # via nbconvert
+nbclient==0.9.0
+    # via nbconvert
+nbconvert==7.12.0
+    # via jupyter-server
+nbformat==5.9.2
+    # via
+    #   jupyter-server
+    #   jupytext
+    #   nbclient
+    #   nbconvert
+    #   nbval
+nbval==0.10.0
+    # via -r v2/requirements.in
+nest-asyncio==1.5.8
+    # via ipykernel
+notebook==7.0.6
+    # via -r v2/requirements.in
+notebook-shim==0.2.3
+    # via
+    #   jupyterlab
+    #   notebook
+numba==0.58.1
+    # via -r v2/requirements.in
+numpy==1.26.2
+    # via
+    #   -r v2/requirements.in
+    #   autograd
+    #   contourpy
+    #   formulaic
+    #   lifelines
+    #   matplotlib
+    #   numba
+    #   pandas
+    #   pyarrow
+    #   scikit-learn
+    #   scipy
+    #   seaborn
+opensafely-cohort-extractor==1.90.0
+    # via -r v2/requirements.in
+opensafely-matching==0.2.0
+    # via -r v2/requirements.in
+overrides==7.4.0
+    # via jupyter-server
+packaging==23.2
+    # via
+    #   build
+    #   ipykernel
+    #   jupyter-server
+    #   jupyterlab
+    #   jupyterlab-server
+    #   jupytext
+    #   matplotlib
+    #   nbconvert
+    #   plotly
+    #   pytest
+pandas==2.1.3
+    # via
+    #   -r v2/requirements.in
+    #   formulaic
+    #   lifelines
+    #   opensafely-cohort-extractor
+    #   opensafely-matching
+    #   seaborn
+    #   upsetplot
+pandocfilters==1.5.0
+    # via nbconvert
+parso==0.8.3
+    # via jedi
+pexpect==4.9.0
+    # via
+    #   bash-kernel
+    #   ipython
+pillow==10.1.0
+    # via
+    #   -r v2/requirements.in
+    #   cairosvg
+    #   matplotlib
+pip-tools==7.3.0
+    # via -r v2/requirements.in
+platformdirs==4.1.0
+    # via jupyter-core
+plotly==5.18.0
+    # via -r v2/requirements.in
+pluggy==1.3.0
+    # via pytest
+prometheus-client==0.19.0
+    # via jupyter-server
+prompt-toolkit==3.0.41
+    # via
+    #   ipython
+    #   jupyter-console
+psutil==5.9.6
+    # via ipykernel
+ptyprocess==0.7.0
+    # via
+    #   pexpect
+    #   terminado
+pure-eval==0.2.2
+    # via stack-data
+py==1.11.0
+    # via retry
+pyarrow==14.0.1
+    # via
+    #   -r v2/requirements.in
+    #   opensafely-cohort-extractor
+pycparser==2.21
+    # via cffi
+pygments==2.17.2
+    # via
+    #   ipython
+    #   jupyter-console
+    #   nbconvert
+pyparsing==3.1.1
+    # via matplotlib
+pyproject-hooks==1.0.0
+    # via build
+pytest==7.4.3
+    # via
+    #   -r v2/requirements.in
+    #   nbval
+python-dateutil==2.8.2
+    # via
+    #   arrow
+    #   jupyter-client
+    #   matplotlib
+    #   pandas
+python-json-logger==2.0.7
+    # via jupyter-events
+pytz==2023.3.post1
+    # via pandas
+pyyaml==6.0.1
+    # via
+    #   jupyter-events
+    #   jupytext
+    #   opensafely-cohort-extractor
+pyzmq==25.1.2
+    # via
+    #   ipykernel
+    #   jupyter-client
+    #   jupyter-console
+    #   jupyter-server
+referencing==0.31.1
+    # via
+    #   jsonschema
+    #   jsonschema-specifications
+    #   jupyter-events
+requests==2.31.0
+    # via
+    #   jupyterlab-server
+    #   opensafely-cohort-extractor
+retry==0.9.2
+    # via opensafely-cohort-extractor
+rfc3339-validator==0.1.4
+    # via
+    #   jsonschema
+    #   jupyter-events
+rfc3986-validator==0.1.1
+    # via
+    #   jsonschema
+    #   jupyter-events
+rpds-py==0.13.2
+    # via
+    #   jsonschema
+    #   referencing
+scikit-learn==1.3.2
+    # via -r v2/requirements.in
+scipy==1.11.4
+    # via
+    #   -r v2/requirements.in
+    #   autograd-gamma
+    #   formulaic
+    #   lifelines
+    #   scikit-learn
+seaborn==0.13.0
+    # via opensafely-cohort-extractor
+send2trash==1.8.2
+    # via jupyter-server
+six==1.16.0
+    # via
+    #   asttokens
+    #   bleach
+    #   python-dateutil
+    #   rfc3339-validator
+sniffio==1.3.0
+    # via anyio
+soupsieve==2.5
+    # via beautifulsoup4
+sqlparse==0.4.4
+    # via opensafely-cohort-extractor
+stack-data==0.6.3
+    # via ipython
+structlog==23.2.0
+    # via opensafely-cohort-extractor
+tabulate==0.9.0
+    # via opensafely-cohort-extractor
+tenacity==8.2.3
+    # via plotly
+terminado==0.18.0
+    # via
+    #   jupyter-server
+    #   jupyter-server-terminals
+threadpoolctl==3.2.0
+    # via scikit-learn
+tinycss2==1.2.1
+    # via
+    #   cairosvg
+    #   cssselect2
+    #   nbconvert
+toml==0.10.2
+    # via jupytext
+tomli==2.0.1
+    # via
+    #   build
+    #   jupyterlab
+    #   pip-tools
+    #   pyproject-hooks
+    #   pytest
+tornado==6.4
+    # via
+    #   ipykernel
+    #   jupyter-client
+    #   jupyter-server
+    #   jupyterlab
+    #   notebook
+    #   terminado
+tqdm==4.66.1
+    # via -r v2/requirements.in
+traitlets==5.14.0
+    # via
+    #   comm
+    #   ipykernel
+    #   ipython
+    #   ipywidgets
+    #   jupyter-client
+    #   jupyter-console
+    #   jupyter-core
+    #   jupyter-events
+    #   jupyter-server
+    #   jupyterlab
+    #   matplotlib-inline
+    #   nbclient
+    #   nbconvert
+    #   nbformat
+types-python-dateutil==2.8.19.14
+    # via arrow
+typing-extensions==4.8.0
+    # via
+    #   async-lru
+    #   formulaic
+tzdata==2023.3
+    # via pandas
+upsetplot==0.8.0
+    # via -r v2/requirements.in
+uri-template==1.3.0
+    # via jsonschema
+urllib3==2.1.0
+    # via requests
+venn==0.1.3
+    # via -r v2/requirements.in
+wcwidth==0.2.12
+    # via prompt-toolkit
+webcolors==1.13
+    # via jsonschema
+webencodings==0.5.1
+    # via
+    #   bleach
+    #   cssselect2
+    #   tinycss2
+websocket-client==1.7.0
+    # via jupyter-server
+wheel==0.42.0
+    # via pip-tools
+widgetsnbextension==4.0.9
+    # via ipywidgets
+wrapt==1.16.0
+    # via formulaic
+
+# The following packages are considered to be unsafe in a requirements file:
+# pip
+# setuptools