course-setup
CLI tools for setting up and retiring GitHub-backed course repositories.
Installation
Install as a uv tool (recommended):
uv tool install course-setup
This makes setup-course, retire-course, unretire-course,
archive-course, list-courses, and setup-course-config available on your
PATH. All six commands support --version and --help, which display the
version number, PyPI URL, author name (Reuven Lerner), and email. To upgrade:
uv tool upgrade course-setup
You can also install with pip (pip install course-setup).
Configuration
Generate a starter config file:
setup-course-config
This creates a config.toml file in your platform's config directory
(e.g., ~/Library/Application Support/course-setup/ on macOS,
~/.config/course-setup/ on Linux, %APPDATA%\course-setup\ on Windows).
Open it and fill in your settings:
[github]
token = "ghp_YOUR_TOKEN_HERE"
[paths]
archive = "/path/to/your/archive"
# readme_source = "/path/to/custom/README.md" # or a URL
[defaults]
notebook_type = "jupyter" # or "marimo"
| Setting | Required | Description |
|---|---|---|
[github] token |
Yes | GitHub personal access token. Alternatively, set the GITHUB_TOKEN environment variable. |
[paths] archive |
Yes | Directory where retired courses are archived. |
[paths] readme_source |
No | Local path or URL to a custom README for new courses. Omit to use the bundled default. |
[paths] additional_files |
No | List of file/directory paths to copy into every new course (e.g. data files, exercise notebooks). |
[paths] course_dirs |
No | List of directories that list-courses scans for active courses (e.g. ["~/Courses/Current"]). ~ is expanded. Overridden by --dir on the list-courses command line. |
[defaults] notebook_type |
No | "jupyter" (default) or "marimo". |
[defaults] verbose |
No | true or false (default). Sets the default verbosity for setup-course. |
[defaults] private |
No | true or false (default). When true, setup-course creates private GitHub repos by default. |
[defaults] extras_group |
No | Default dependency group when --extras is not passed (e.g. "python"). |
[defaults] weekend |
No | "standard" (skip Sat/Sun) or "israeli" (skip Fri/Sat). Default for --skip-weekends/--skip-israeli-weekends. |
To regenerate the config file, use setup-course-config --force.
Usage
setup-course — Create a new course repo
setup-course -c Acme -t python-intro
| Flag | Description |
|---|---|
-c, --client |
Client name (required) |
-t, --topic |
Course topic (required) |
-d, --date |
YYYY-MM override (defaults to current month). Validated: must be a real month, not more than 2 years ahead. |
-n, --num-sessions |
Number of sessions (creates one notebook per session) |
--freq |
Session frequency: daily or weekly (requires -n, defaults to daily) |
--first-notebook-date |
Start date for notebook files (YYYY-MM-DD); defaults to today |
--skip-weekends |
Skip Saturdays and Sundays when scheduling notebooks |
--skip-israeli-weekends |
Skip Fridays and Saturdays when scheduling notebooks |
--notebook-type |
jupyter or marimo (overrides config default) |
--extras |
Dependency groups to add to the course pyproject.toml (see below) |
--add-imports |
Pre-populate notebooks with import statements from --extras groups |
-v, --verbose |
Show detailed output (paths, filenames, dependencies) |
--private |
Create the GitHub repo as private instead of public (overrides config default) |
--dry-run |
Preview what would be created without making any changes |
Dependency groups
| Group | Packages |
|---|---|
python |
ipython |
data |
numpy, pandas, xlrd, openpyxl, pyarrow |
viz |
matplotlib, seaborn, plotly |
geo |
geopandas, folium, shapely |
db |
duckdb, sqlalchemy |
ml |
scikit-learn |
You can also define custom groups in your config.toml under [extras].
Entries can be package names or references to other groups (built-in or custom):
[extras]
finance = ["yfinance", "pandas-datareader"]
reuven = ["python", "data", "plotly"] # expands python & data groups + plotly
Example — a Pandas course with Python extras and data/viz packages:
setup-course -c Acme -t pandas --extras python data
This will:
- Create a directory and GitHub repo named
{client}-{topic}-{YYYY-MM}(public by default; use--privatefor private) - Create a notebook per session, named
{client}-{topic}-{YYYY-MM-DD}(.ipynbfor Jupyter,.pyfor Marimo) - Generate a
pyproject.tomlwith the notebook dependency andgitautopush - Include a
.gitignorefor Python, virtual environments, and IDE files - Configure the local
.git/configwith the GitHub SSH remote - Make an initial commit and push to GitHub
- Run
uv syncto install all dependencies
By default, a single notebook is created for today's date. Use -n to
create multiple notebooks for multi-day or multi-week courses:
setup-course -c Acme -t python-intro -n 5 # 5 daily sessions
setup-course -c Acme -t python-intro -n 5 --freq weekly # 5 weekly sessions
retire-course — Archive a course repo
retire-course ./Acme-python-intro-2026-03
| Argument / Flag | Description |
|---|---|
DIRNAME... |
One or more course directories to retire |
--keep-public |
Archive without making the GitHub repo private |
--dry-run |
Preview the retirement without making any changes |
This will (for each directory):
- Make the GitHub repo private (unless
--keep-publicis passed) - Move the local directory to your configured archive path under the current year (prompts for confirmation if the year directory doesn't exist)
- Print a retirement summary showing: notebook count, date range, dependencies, archive location, and GitHub URL
With --dry-run, none of that happens: the GitHub repo is not touched, the
archive directory is not created, and the course directory is not moved.
Instead, retire-course prints a [DRY RUN] banner followed by the same
retirement summary the real run would show. It reads your config file but makes
no GitHub API calls, so a dry run touches nothing on the network. --dry-run
applies to every directory passed on the command line.
You can retire multiple courses at once:
retire-course ./Acme-2026-03 ./Beta-2026-03 ./Gamma-2026-02
If any directory fails, the rest are still processed and errors are reported at the end.
Tip: run
retire-coursefrom the parent of the course directory, not from inside it. If you're already inside, the command detects it and tells you tocd ..rather than failing with a confusing error.
archive-course — Create a zip archive of a course
archive-course ./Acme-python-intro-2026-03
| Flag | Description |
|---|---|
--output, -o |
Custom output zip path (defaults to {dirname}.zip) |
--no-html |
Skip HTML export of Jupyter notebooks |
--no-pdf |
Skip PDF export of Jupyter notebooks |
This archives all files in the course directory (.py, .csv, .toml,
notebooks, etc.), excluding .git, .venv, __pycache__, and
.ipynb_checkpoints directories. For Jupyter notebooks, each .ipynb is also
exported to HTML and to PDF, and both are included alongside the original;
use --no-html / --no-pdf to skip either export. PDF export is on by
default, via nbconvert --to webpdf (headless Chromium — no LaTeX
installation required). The first time you export to PDF, you may need to
run uv run playwright install chromium inside the course directory to
download the Chromium binary. If the PDF engine isn't available, archive-course
prints a warning and skips PDF for that notebook rather than failing — the
rest of the archive is still produced. After creating the archive, a summary
is printed listing the archive path, file count, size, notebooks (shown as
notebook.ipynb + notebook.html + notebook.pdf), export counts, and all
other included files.
list-courses — List active and archived courses
list-courses
A read-only command: nothing is modified, and no GitHub API calls are made (it reads your config file but does not use the network).
Breaking change (3.2.0): the positional argument used to be a scan-directory override (
list-courses ~/Other). It is now a name filter instead. To scan a different directory, use the repeatable--dir PATHflag:list-courses --dir ~/Other.
By default, list-courses prints the full list of active courses — a
directory qualifies as a course if it has a .git subdirectory and at least
one notebook (.ipynb, or a marimo .py) — followed by a one-line summary of
the archived courses found under your configured [paths] archive
directory (an archived course is any non-hidden, non-junk directory under a
4-digit-year folder that contains at least one notebook):
Active courses:
Acme-python-intro-2026-03 — 5 notebooks (2026-03-17 → 2026-03-21)
Beta-pandas-2026-02 — 3 notebooks (2026-02-02 → 2026-02-16)
Archived: 412 courses across 2018–2026 — use --archived to list them.
Each course line is shown as name — N notebooks (first-date → last-date),
with dates parsed from notebook filenames (n/a if none are found).
| Argument / Option | Description |
|---|---|
NAME... |
Filter courses by case-insensitive name substring. Multiple names match as OR. Narrows whatever is shown; does not by itself expand the archive. |
--dir PATH |
Directory to scan for active courses (repeatable; replaces course_dirs from config for this run) |
--active |
Show only the active-courses section |
--archived |
Show only the archived-courses section, expanded and grouped by year |
--year YYYY |
Restrict archived courses to a year (repeatable, 4-digit years only); implies --archived unless --active is also given, in which case --year is ignored |
--count |
Print counts instead of course lines, honoring all other filters |
--version |
Show the version number, PyPI URL, author name, and email |
Directories to scan for active courses are resolved in this order:
--dir PATHvalues passed on the command line (repeatable)- Otherwise,
[paths] course_dirsfrom your config file - Otherwise, the current directory
Examples
| Command | Result |
|---|---|
list-courses |
active courses in full + Archived: N courses across 2018–2026 — use --archived … |
list-courses --archived |
archive only, expanded and grouped by year |
list-courses --active |
active courses only |
list-courses cisco |
active courses matching "cisco" + a name-filtered archive summary line |
list-courses cisco --archived |
archived courses matching "cisco" (all years, expanded) |
list-courses --year 2024 |
archive for 2024 only (active section suppressed) |
list-courses --year 2024 --year 2025 |
archive for 2024 and 2025 only |
list-courses cisco --year 2024 |
archived courses matching "cisco" from 2024 only |
list-courses --active --year 2024 |
active only (--year is ignored for active) |
list-courses --count |
Active courses: 3 + Archived courses: 412 + per-year counts |
list-courses cisco --count |
counts of active + archived courses matching "cisco" |
list-courses --dir ~/Other |
scan ~/Other for active courses instead of config course_dirs |
If there are no matching active or archived courses, it prints an empty-state
line (e.g. No active courses found, or No active courses match: cisco when
a name filter excludes everything).
unretire-course — Restore a retired course
unretire-course /path/to/archive/2026/Acme-python-intro-2026-03
This will:
- Make the GitHub repo public again
- Move the directory from the archive back to your current working directory
Live teaching with gitautopush
In a separate terminal, run uv run gitautopush . from inside the course
directory. This watches for notebook changes and automatically pushes them to
GitHub, so students can follow along in real time by viewing the public repo.
Development
git clone https://github.com/reuven/course-setup.git
cd course-setup
uv sync --dev
Run tests, format, and lint:
uv run pytest
uv run ruff format src/ tests/
uv run ruff check src/ tests/
uv run mypy --strict src/
License
MIT — see LICENSE for details.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file course_setup-3.2.1.tar.gz.
File metadata
- Download URL: course_setup-3.2.1.tar.gz
- Upload date:
- Size: 22.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed60e9aa51f00b427e681551b848e049b1f3a588c2ff2e19d374679b31d303d7
|
|
| MD5 |
91ca00811c0052e087b070f5965c6f28
|
|
| BLAKE2b-256 |
c6b3fe9532e04e7b005a7b6989826a68a846427124b88c7591cf9e7775ffe081
|
File details
Details for the file course_setup-3.2.1-py3-none-any.whl.
File metadata
- Download URL: course_setup-3.2.1-py3-none-any.whl
- Upload date:
- Size: 29.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
580933c44637b59c606bb3d768b83e71206e5f79e03915dae0e2e8786316229d
|
|
| MD5 |
c1de565bb0ac8d2376d5674b41b33092
|
|
| BLAKE2b-256 |
3a8bbb1b055fbb2273a7b219af5eaf10c3391113206dc6bcd484c8bff45b9d28
|