Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

e2x Course Hub

CI PyPI Python versions Documentation Status License: MIT

Documentation: https://e2x-course-hub.readthedocs.io/en/latest/index.html · PyPI: https://pypi.org/project/e2x-course-hub/

A JupyterHub service for running multi-course, multi-term teaching deployments. It manages courses, terms, and course membership; enforces who may do what via role-based access control; and tells a separate infrastructure spawner which courses/terms a user may launch and with which environment — without ever deciding hardware itself.

It ships as a FastAPI backend (REST API under /v1/...) plus a React single-page app (course-service-ui) for course and membership management, both served from the same process.

What it does

  • Course & term management — courses and their terms are stored in a database (SQLAlchemy) and managed through a REST API: create/remove courses and terms, edit metadata, and configure the environment (image, resource tier, profile) per course, overridable per term.
  • Role-based access control — powered by e2x-hub-rbac. Roles (LMS admin, course creator, course owner, instructor, teaching assistant, student, observer) are scoped to the LMS, a course, or a term, and membership is backed by JupyterHub groups.
  • A typed contract with the spawner — e2x_course_hub.contract defines the boundary between this package and the JupyterHub Spawner that actually launches containers. The course hub owns authorization and course configuration; the spawner owns the runtime. They meet only through two Protocols:
    • SpawnOfferingProvider (implemented here): tells the spawner, for a given user, which course/terms they may launch, in which role, with which selection.
    • InfrastructureCatalogProvider (implemented by the spawner): tells the course hub what images, resource tiers, and profiles actually exist, so course configuration can only ever offer valid choices.
  • A management UI — course-service-ui, a React SPA served by the same FastAPI app, for browsing courses and managing membership in the browser.

See e2x_course_hub/contract/README.md for the full contract reference, including the catalog/selection data model and how a spawn plays out end to end.

Architecture

e2x_course_hub/
├── contract/           # Protocol boundary shared with the infrastructure spawner
│   └── providers.py       # InfrastructureCatalogProvider, SpawnOfferingProvider
│       # (re-exports the catalog/selection models from schema/ below)
├── schema/              # Pydantic models backing the contract and the API
│   ├── catalog.py         # InfrastructureCatalogOptions and friends
│   ├── selection.py        # SpawnSelection, SpawnOffering, CourseReference
│   ├── course.py           # CourseConfig / TermConfig
│   └── types.py            # SpawnRole, UserLike
├── db/                  # SQLAlchemy models + CourseRepository
├── api/                 # Business logic, framework-agnostic
│   ├── api.py              # API — aggregates the sub-APIs below
│   ├── course_api.py        # Course/term CRUD and environment configuration
│   ├── membership_api.py    # Course membership (extends e2x-hub-rbac's MembershipAPI)
│   ├── infrastructure_api.py # Read-only access to the infrastructure catalog
│   ├── spawn_offering_api.py # Implements SpawnOfferingProvider for the spawner
│   └── *_permissions.py      # Role → permission mappings per sub-API
├── course_service/      # The FastAPI service
│   ├── fastapi_app.py       # App wiring: routers, auth, static files, SPA route
│   ├── settings.py          # ServiceSettings (extends settings.CoreSettings)
│   ├── whoami.py            # Minimal standalone service: returns the caller's identity
│   └── {courses,terms,infrastructure,membership,me}/  # One router + schemas per resource
├── loader.py            # Composition root: settings → concrete API objects
└── settings.py          # CoreSettings: hub API, DB, membership, infrastructure provider

course-service-ui/       # React 19 + TypeScript SPA, built with Vite

The two sides

Course hub (this repo) Infrastructure spawner
Knows courses, terms, users, roles (via e2x-hub-rbac), permissions what can actually be run: image families/tags, resource tiers, profiles
Implements SpawnOfferingProvider InfrastructureCatalogProvider
Consumes InfrastructureCatalogProvider SpawnOfferingProvider
Never does translates a selection to hardware decides who is allowed to do what

The spawner is a separate, pluggable package. The course hub discovers it at startup through the e2x_course_hub.infrastructure_catalog_providers entry-point group, selected by name via the E2X_INFRASTRUCTURE_PROVIDER setting — the hub never imports it directly. See e2x-course-hub-kubespawner for an example implementation based on KubeSpawner.

Installation

pip install e2x-course-hub

To install the latest development version from GitHub instead:

pip install git+https://github.com/Digiklausur/e2x-course-hub.git

For development:

git clone https://github.com/Digiklausur/e2x-course-hub
cd e2x-course-hub
pip install -e ".[dev]"

Building the package (pip install . or python -m build) also builds course-service-ui via hatch-jupyter-builder and bundles the result into the wheel. For frontend-only iteration:

cd course-service-ui
npm install
npm run dev

Running it

The service is a standard JupyterHub managed service, run with uvicorn:

# jupyterhub_config.py
c.JupyterHub.services = [
    {
        "name": "course-service",
        "url": "http://127.0.0.1:10105",
        "command": ["uvicorn", "e2x_course_hub.course_service.fastapi_app:app"],
        "environment": {
            "UVICORN_HOST": "0.0.0.0",
            "UVICORN_PORT": "10105",
            "E2X_INFRASTRUCTURE_PROVIDER": "k8s_catalog",
            "E2X_ADD_USERS_TO_HUB": "true",
            "E2X_DELETE_EMPTY_GROUPS": "true",
            "E2X_COURSE_DB_URL": "sqlite:////srv/jupyterhub/courses.db",
        },
    }
]

c.JupyterHub.load_roles = [
    {
        "name": "course-service",
        "services": ["course-service"],
        "scopes": [
            "access:services!service=course-service",
            "admin:users",
            "list:users",
            "delete:users",
            "admin:groups",
        ],
    }
]

JUPYTERHUB_API_URL, JUPYTERHUB_API_TOKEN, and JUPYTERHUB_SERVICE_PREFIX are injected automatically by JupyterHub for a managed service and don't need to be set explicitly.

Configuration

All settings are read from the environment (via pydantic-settings):

Variable Default Purpose
JUPYTERHUB_API_URL — JupyterHub Hub API URL, used to manage users and groups
JUPYTERHUB_API_TOKEN — API token for the above
JUPYTERHUB_SERVICE_PREFIX / URL prefix the service is mounted under
E2X_COURSE_DB_URL sqlite:///courses.db SQLAlchemy URL for the course/term database
E2X_INFRASTRUCTURE_PROVIDER default Entry-point name of the InfrastructureCatalogProvider to load
E2X_ADD_USERS_TO_HUB false Create JupyterHub users automatically on membership changes
E2X_DELETE_EMPTY_GROUPS false Delete a course/term's JupyterHub group once it has no members

Optional: whoami service

e2x_course_hub.course_service.whoami:app is a minimal standalone FastAPI service that returns the authenticated caller's identity — handy for verifying JupyterHub OAuth is wired up correctly before debugging the full course service.

REST API

All routes are served under {service_prefix}/v1. Interactive docs (Swagger UI) are available at {service_prefix}/docs once the service is running, and the schema can be exported standalone with python scripts/export_openapi.py.

Resource Routes
Courses GET/POST /courses, GET/DELETE /courses/{course_id}, GET/PATCH /courses/{course_id}/metadata, GET/PATCH /courses/{course_id}/environment, GET /courses/{course_id}/terms
Terms POST/GET/DELETE /courses/{course_id}/terms/{term_id}, GET/PATCH /courses/{course_id}/terms/{term_id}/environment
Infrastructure GET /catalogs/image-catalog, GET /catalogs/resource-tiers, GET /catalogs/profile-catalog
Membership GET/PATCH /lms/admins, GET/PATCH /lms/course-creators, GET/PATCH /courses/{course_id}/owners, GET/PATCH /courses/{course_id}/terms/{term_id}/{instructors,teaching-assistants,students,observers}
Roles GET /roles/{role}/actions
Current user GET /me

course-service-ui consumes this API with types generated from the OpenAPI schema (npm run generate:api-types in course-service-ui/); CI fails if the committed types drift from the schema.

Roles & permissions

Roles are defined by e2x-hub-rbac and scoped to the LMS, a course, or a term. Default permissions for course/term management (e2x_course_hub/api/course_permissions.py):

Role Scope Default permissions
LMS admin LMS Everything
Course creator LMS Create courses, view courses
Course owner Course Manage the course (metadata, environment, remove) and its terms (add/remove, environment)
Instructor Term View the course, manage the term (environment, remove)
Teaching assistant Term View the course and term
Observer Term View the course and term
Student Term (none — spawns only, no management permissions)

Membership itself (who holds which role, per course/term) is managed separately through the membership API, backed by JupyterHub groups. The membership, infrastructure catalog and spawn permission tables, and the group names behind each role, are in docs/source/reference/roles.md.

Development

ruff check .
ruff format --check .
cd course-service-ui
npm run lint
npm run format

CI (.github/workflows/ci.yml) runs Python linting, frontend linting/formatting, and checks that the generated OpenAPI schema and TypeScript types are committed and in sync with the routers.

License

MIT License — see LICENSE for details.

Metadata

Release files for e2x-course-hub 0.3.0rc0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for e2x-course-hub 0.3.0rc0
File Size Uploaded Provenance Actions
e2x_course_hub-0.3.0rc0.tar.gz 1.6 MB With provenance Details for e2x_course_hub-0.3.0rc0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for e2x-course-hub 0.3.0rc0
File Interpreter ABI Platform Actions
e2x_course_hub-0.3.0rc0-py3-none-any.whl Python 3 none any Details for e2x_course_hub-0.3.0rc0-py3-none-any.whl

Total release size: 2.2 MB

Release files / e2x_course_hub-0.3.0rc0.tar.gz

Download URL e2x_course_hub-0.3.0rc0.tar.gz
Size 1.6 MB
Tags Source
SHA-256 checksum
How to use checksums
09197590c007b6f383bd374990e2bd9d72cca7154c9a69495e097149d8f4cd00
BLAKE2b-256 checksum
How to use checksums
52b0cdec57c4f104b9af78c0ca774f000a09b68c292255b5e80c44028abdd980
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.

Transparency log

Release files / e2x_course_hub-0.3.0rc0-py3-none-any.whl

Download URL e2x_course_hub-0.3.0rc0-py3-none-any.whl
Size 584.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bbd4efe2e11bc8e080dcdc885a3aafeebc1e848dc41bddf0cf4626cf50834a1c
BLAKE2b-256 checksum
How to use checksums
9bb552ff46ceccd87dc723de45773b16ce0e23aa9ffdfe6c36e2a4684c421215
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.

Transparency log

Security for e2x-course-hub 0.3.0rc0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for e2x_course_hub-0.3.0rc0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page