course-setup 3.2.0__tar.gz → 3.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {course_setup-3.2.0 → course_setup-3.3.0}/PKG-INFO +17 -4
- {course_setup-3.2.0 → course_setup-3.3.0}/README.md +16 -3
- {course_setup-3.2.0 → course_setup-3.3.0}/pyproject.toml +1 -1
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/list_courses.py +83 -8
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/.DS_Store +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/.claude/settings.local.json +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/__init__.py +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/archive_course.py +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/config.py +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/generic/.gitignore +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/generic/.ipynb_checkpoints/Course notebook-checkpoint.ipynb +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/generic/Course notebook.ipynb +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/generic/README.md +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/init_config.py +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/notebooks.py +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/retire_course.py +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/setup_course.py +0 -0
- {course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/unretire_course.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: course-setup
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: CLI tools for setting up and retiring GitHub-backed course repositories
|
|
5
5
|
Author: Reuven Lerner
|
|
6
6
|
Author-email: Reuven Lerner <reuven@lernerpython.com>
|
|
@@ -226,7 +226,8 @@ list-courses
|
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
A read-only command: nothing is modified, and no GitHub API calls are made (it
|
|
229
|
-
reads your config file but does not use
|
|
229
|
+
reads your config file, and each course's local `git` remote, but does not use
|
|
230
|
+
the network).
|
|
230
231
|
|
|
231
232
|
> **Breaking change (3.2.0):** the positional argument used to be a
|
|
232
233
|
> scan-directory override (`list-courses ~/Other`). It is now a **name
|
|
@@ -243,13 +244,21 @@ directory (an archived course is any non-hidden, non-junk directory under a
|
|
|
243
244
|
```
|
|
244
245
|
Active courses:
|
|
245
246
|
Acme-python-intro-2026-03 — 5 notebooks (2026-03-17 → 2026-03-21)
|
|
247
|
+
https://github.com/acme/python-intro-2026-03
|
|
246
248
|
Beta-pandas-2026-02 — 3 notebooks (2026-02-02 → 2026-02-16)
|
|
249
|
+
https://github.com/beta/pandas-2026-02
|
|
247
250
|
|
|
248
251
|
Archived: 412 courses across 2018–2026 — use --archived to list them.
|
|
249
252
|
```
|
|
250
253
|
|
|
251
254
|
Each course line is shown as `name — N notebooks (first-date → last-date)`,
|
|
252
|
-
with dates parsed from notebook filenames (`n/a` if none are found).
|
|
255
|
+
with dates parsed from notebook filenames (`n/a` if none are found). By
|
|
256
|
+
default, an indented line below each *listed* course (active courses, and
|
|
257
|
+
archived courses in the expanded `--archived`/`--year` views) shows its GitHub
|
|
258
|
+
URL, read from the course's local `git` `origin` remote — no network call. A
|
|
259
|
+
course with no usable GitHub remote shows `(no GitHub remote)` instead. Pass
|
|
260
|
+
`--no-urls` to hide these lines. URLs are not shown in the one-line archive
|
|
261
|
+
summary or with `--count`.
|
|
253
262
|
|
|
254
263
|
| Argument / Option | Description |
|
|
255
264
|
|----------|-------------|
|
|
@@ -257,8 +266,9 @@ with dates parsed from notebook filenames (`n/a` if none are found).
|
|
|
257
266
|
| `--dir PATH` | Directory to scan for active courses (repeatable; replaces `course_dirs` from config for this run) |
|
|
258
267
|
| `--active` | Show only the active-courses section |
|
|
259
268
|
| `--archived` | Show only the archived-courses section, expanded and grouped by year |
|
|
260
|
-
| `--year YYYY` | Restrict archived courses to a year (repeatable
|
|
269
|
+
| `--year YYYY[-YYYY]` | Restrict archived courses to a year or an inclusive `YYYY-YYYY` range (repeatable; singles and ranges mix freely); implies `--archived` unless `--active` is also given, in which case `--year` is ignored. A reversed range (e.g. `2022-2020`) is auto-swapped, printing `Note: swapped reversed year range 2022-2020 → 2020-2022` to stderr. |
|
|
261
270
|
| `--count` | Print counts instead of course lines, honoring all other filters |
|
|
271
|
+
| `--no-urls` | Hide the per-course GitHub URL line shown by default under each listed course |
|
|
262
272
|
| `--version` | Show the version number, PyPI URL, author name, and email |
|
|
263
273
|
|
|
264
274
|
Directories to scan for active courses are resolved in this order:
|
|
@@ -278,11 +288,14 @@ Directories to scan for active courses are resolved in this order:
|
|
|
278
288
|
| `list-courses cisco --archived` | archived courses matching "cisco" (all years, expanded) |
|
|
279
289
|
| `list-courses --year 2024` | archive for 2024 only (active section suppressed) |
|
|
280
290
|
| `list-courses --year 2024 --year 2025` | archive for 2024 and 2025 only |
|
|
291
|
+
| `list-courses --year 2020-2022` | archive for 2020 through 2022 only (inclusive range) |
|
|
292
|
+
| `list-courses --year 2019 --year 2021-2023` | archive for 2019, 2021, 2022, 2023 (singles and ranges mix) |
|
|
281
293
|
| `list-courses cisco --year 2024` | archived courses matching "cisco" from 2024 only |
|
|
282
294
|
| `list-courses --active --year 2024` | active only (`--year` is ignored for active) |
|
|
283
295
|
| `list-courses --count` | `Active courses: 3` + `Archived courses: 412` + per-year counts |
|
|
284
296
|
| `list-courses cisco --count` | counts of active + archived courses matching "cisco" |
|
|
285
297
|
| `list-courses --dir ~/Other` | scan `~/Other` for active courses instead of config `course_dirs` |
|
|
298
|
+
| `list-courses --no-urls` | active courses in full, without the per-course GitHub URL lines |
|
|
286
299
|
|
|
287
300
|
If there are no matching active or archived courses, it prints an empty-state
|
|
288
301
|
line (e.g. `No active courses found`, or `No active courses match: cisco` when
|
|
@@ -206,7 +206,8 @@ list-courses
|
|
|
206
206
|
```
|
|
207
207
|
|
|
208
208
|
A read-only command: nothing is modified, and no GitHub API calls are made (it
|
|
209
|
-
reads your config file but does not use
|
|
209
|
+
reads your config file, and each course's local `git` remote, but does not use
|
|
210
|
+
the network).
|
|
210
211
|
|
|
211
212
|
> **Breaking change (3.2.0):** the positional argument used to be a
|
|
212
213
|
> scan-directory override (`list-courses ~/Other`). It is now a **name
|
|
@@ -223,13 +224,21 @@ directory (an archived course is any non-hidden, non-junk directory under a
|
|
|
223
224
|
```
|
|
224
225
|
Active courses:
|
|
225
226
|
Acme-python-intro-2026-03 — 5 notebooks (2026-03-17 → 2026-03-21)
|
|
227
|
+
https://github.com/acme/python-intro-2026-03
|
|
226
228
|
Beta-pandas-2026-02 — 3 notebooks (2026-02-02 → 2026-02-16)
|
|
229
|
+
https://github.com/beta/pandas-2026-02
|
|
227
230
|
|
|
228
231
|
Archived: 412 courses across 2018–2026 — use --archived to list them.
|
|
229
232
|
```
|
|
230
233
|
|
|
231
234
|
Each course line is shown as `name — N notebooks (first-date → last-date)`,
|
|
232
|
-
with dates parsed from notebook filenames (`n/a` if none are found).
|
|
235
|
+
with dates parsed from notebook filenames (`n/a` if none are found). By
|
|
236
|
+
default, an indented line below each *listed* course (active courses, and
|
|
237
|
+
archived courses in the expanded `--archived`/`--year` views) shows its GitHub
|
|
238
|
+
URL, read from the course's local `git` `origin` remote — no network call. A
|
|
239
|
+
course with no usable GitHub remote shows `(no GitHub remote)` instead. Pass
|
|
240
|
+
`--no-urls` to hide these lines. URLs are not shown in the one-line archive
|
|
241
|
+
summary or with `--count`.
|
|
233
242
|
|
|
234
243
|
| Argument / Option | Description |
|
|
235
244
|
|----------|-------------|
|
|
@@ -237,8 +246,9 @@ with dates parsed from notebook filenames (`n/a` if none are found).
|
|
|
237
246
|
| `--dir PATH` | Directory to scan for active courses (repeatable; replaces `course_dirs` from config for this run) |
|
|
238
247
|
| `--active` | Show only the active-courses section |
|
|
239
248
|
| `--archived` | Show only the archived-courses section, expanded and grouped by year |
|
|
240
|
-
| `--year YYYY` | Restrict archived courses to a year (repeatable
|
|
249
|
+
| `--year YYYY[-YYYY]` | Restrict archived courses to a year or an inclusive `YYYY-YYYY` range (repeatable; singles and ranges mix freely); implies `--archived` unless `--active` is also given, in which case `--year` is ignored. A reversed range (e.g. `2022-2020`) is auto-swapped, printing `Note: swapped reversed year range 2022-2020 → 2020-2022` to stderr. |
|
|
241
250
|
| `--count` | Print counts instead of course lines, honoring all other filters |
|
|
251
|
+
| `--no-urls` | Hide the per-course GitHub URL line shown by default under each listed course |
|
|
242
252
|
| `--version` | Show the version number, PyPI URL, author name, and email |
|
|
243
253
|
|
|
244
254
|
Directories to scan for active courses are resolved in this order:
|
|
@@ -258,11 +268,14 @@ Directories to scan for active courses are resolved in this order:
|
|
|
258
268
|
| `list-courses cisco --archived` | archived courses matching "cisco" (all years, expanded) |
|
|
259
269
|
| `list-courses --year 2024` | archive for 2024 only (active section suppressed) |
|
|
260
270
|
| `list-courses --year 2024 --year 2025` | archive for 2024 and 2025 only |
|
|
271
|
+
| `list-courses --year 2020-2022` | archive for 2020 through 2022 only (inclusive range) |
|
|
272
|
+
| `list-courses --year 2019 --year 2021-2023` | archive for 2019, 2021, 2022, 2023 (singles and ranges mix) |
|
|
261
273
|
| `list-courses cisco --year 2024` | archived courses matching "cisco" from 2024 only |
|
|
262
274
|
| `list-courses --active --year 2024` | active only (`--year` is ignored for active) |
|
|
263
275
|
| `list-courses --count` | `Active courses: 3` + `Archived courses: 412` + per-year counts |
|
|
264
276
|
| `list-courses cisco --count` | counts of active + archived courses matching "cisco" |
|
|
265
277
|
| `list-courses --dir ~/Other` | scan `~/Other` for active courses instead of config `course_dirs` |
|
|
278
|
+
| `list-courses --no-urls` | active courses in full, without the per-course GitHub URL lines |
|
|
266
279
|
|
|
267
280
|
If there are no matching active or archived courses, it prints an empty-state
|
|
268
281
|
line (e.g. `No active courses found`, or `No active courses match: cisco` when
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
|
|
5
7
|
from setup_course_github import __author__, __email__, __version__
|
|
@@ -10,6 +12,8 @@ JUNK_NAMES = frozenset(
|
|
|
10
12
|
{"__pycache__", "build", "dist", "bin", "include", "lib", "node_modules"}
|
|
11
13
|
)
|
|
12
14
|
_YEAR_RE = re.compile(r"^\d{4}$")
|
|
15
|
+
_YEAR_TOKEN_RE = re.compile(r"^\d{4}(-\d{4})?$")
|
|
16
|
+
_GITHUB_RE = re.compile(r"github\.com[:/](?P<path>[^/]+/[^/]+?)(?:\.git)?/?$")
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
def _is_junk_name(name: str) -> bool:
|
|
@@ -45,6 +49,32 @@ def course_summary_line(path: Path) -> str:
|
|
|
45
49
|
return f"{path.name} — {count} notebooks ({span})"
|
|
46
50
|
|
|
47
51
|
|
|
52
|
+
def _github_url(remote: str) -> str | None:
|
|
53
|
+
"""Normalize a GitHub SSH/HTTPS remote to https://github.com/<owner>/<repo>.
|
|
54
|
+
|
|
55
|
+
Returns None for an empty string or a non-GitHub remote.
|
|
56
|
+
"""
|
|
57
|
+
match = _GITHUB_RE.search(remote)
|
|
58
|
+
if not match:
|
|
59
|
+
return None
|
|
60
|
+
return f"https://github.com/{match.group('path')}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def github_url(course_path: Path) -> str | None:
|
|
64
|
+
"""Return the course repo's GitHub URL from its local origin remote, or None.
|
|
65
|
+
|
|
66
|
+
Reads ``git config --get remote.origin.url`` in *course_path*; no network.
|
|
67
|
+
"""
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
["git", "config", "--get", "remote.origin.url"],
|
|
70
|
+
cwd=str(course_path),
|
|
71
|
+
capture_output=True,
|
|
72
|
+
)
|
|
73
|
+
if result.returncode != 0:
|
|
74
|
+
return None
|
|
75
|
+
return _github_url(result.stdout.decode().strip())
|
|
76
|
+
|
|
77
|
+
|
|
48
78
|
def _matches_names(name: str, patterns: list[str]) -> bool:
|
|
49
79
|
"""True if *name* (case-insensitively) contains any of *patterns*.
|
|
50
80
|
|
|
@@ -90,8 +120,9 @@ def archive_summary_line(archived: dict[str, list[Path]], patterns: list[str]) -
|
|
|
90
120
|
return f"Archived: none{match_clause}"
|
|
91
121
|
years = sorted(archived.keys())
|
|
92
122
|
span = years[0] if len(years) == 1 else f"{years[0]}–{years[-1]}"
|
|
123
|
+
noun = "course" if total == 1 else "courses"
|
|
93
124
|
return (
|
|
94
|
-
f"Archived: {total}
|
|
125
|
+
f"Archived: {total} {noun}{match_clause} across {span}"
|
|
95
126
|
" — use --archived to list them."
|
|
96
127
|
)
|
|
97
128
|
|
|
@@ -138,12 +169,46 @@ def find_archived_courses(archive_path: Path) -> dict[str, list[Path]]:
|
|
|
138
169
|
|
|
139
170
|
|
|
140
171
|
def _year_arg(value: str) -> str:
|
|
141
|
-
"""argparse type: accept
|
|
142
|
-
if not
|
|
143
|
-
raise argparse.ArgumentTypeError(
|
|
172
|
+
"""argparse type: accept a 4-digit year or an inclusive YYYY-YYYY range."""
|
|
173
|
+
if not _YEAR_TOKEN_RE.match(value):
|
|
174
|
+
raise argparse.ArgumentTypeError(
|
|
175
|
+
f"invalid year '{value}' (expected YYYY or YYYY-YYYY)"
|
|
176
|
+
)
|
|
144
177
|
return value
|
|
145
178
|
|
|
146
179
|
|
|
180
|
+
def _expand_year_tokens(tokens: list[str]) -> tuple[list[str], list[str]]:
|
|
181
|
+
"""Flatten single-year and range tokens into individual 4-digit years.
|
|
182
|
+
|
|
183
|
+
Returns ``(years, notes)`` where *notes* holds a message for each reversed
|
|
184
|
+
range that was auto-swapped. Ranges are inclusive; a reversed range like
|
|
185
|
+
``2022-2020`` is swapped to ``2020-2022``.
|
|
186
|
+
"""
|
|
187
|
+
years: list[str] = []
|
|
188
|
+
notes: list[str] = []
|
|
189
|
+
for token in tokens:
|
|
190
|
+
if "-" in token:
|
|
191
|
+
start_s, end_s = token.split("-")
|
|
192
|
+
start, end = int(start_s), int(end_s)
|
|
193
|
+
if start > end:
|
|
194
|
+
notes.append(
|
|
195
|
+
f"swapped reversed year range {start_s}-{end_s} → {end_s}-{start_s}"
|
|
196
|
+
)
|
|
197
|
+
start, end = end, start
|
|
198
|
+
years.extend(f"{year:04d}" for year in range(start, end + 1))
|
|
199
|
+
else:
|
|
200
|
+
years.append(token)
|
|
201
|
+
return years, notes
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _print_course(course: Path, indent: int, show_urls: bool) -> None:
|
|
205
|
+
"""Print a course summary line and, when *show_urls*, its indented URL line."""
|
|
206
|
+
print(f"{' ' * indent}{course_summary_line(course)}")
|
|
207
|
+
if show_urls:
|
|
208
|
+
url = github_url(course)
|
|
209
|
+
print(f"{' ' * (indent + 2)}{url or '(no GitHub remote)'}")
|
|
210
|
+
|
|
211
|
+
|
|
147
212
|
def main(argv: list[str] | None = None) -> None:
|
|
148
213
|
pypi_url = "https://pypi.org/project/course-setup/"
|
|
149
214
|
author_line = f"{__author__} <{__email__}>"
|
|
@@ -187,15 +252,24 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
187
252
|
dest="years",
|
|
188
253
|
type=_year_arg,
|
|
189
254
|
metavar="YYYY",
|
|
190
|
-
help="Restrict archived courses to a year (repeatable
|
|
255
|
+
help="Restrict archived courses to a year or YYYY-YYYY range (repeatable)",
|
|
191
256
|
)
|
|
192
257
|
parser.add_argument(
|
|
193
258
|
"--count",
|
|
194
259
|
action="store_true",
|
|
195
260
|
help="Show counts instead of individual course lines",
|
|
196
261
|
)
|
|
262
|
+
parser.add_argument(
|
|
263
|
+
"--no-urls",
|
|
264
|
+
action="store_true",
|
|
265
|
+
help="Do not show each course's GitHub URL",
|
|
266
|
+
)
|
|
197
267
|
args = parser.parse_args(argv)
|
|
198
268
|
|
|
269
|
+
years, year_notes = _expand_year_tokens(args.years)
|
|
270
|
+
for note in year_notes:
|
|
271
|
+
print(f"Note: {note}", file=sys.stderr)
|
|
272
|
+
|
|
199
273
|
config = load_config()
|
|
200
274
|
scan_dirs = resolve_scan_dirs(args.dir, config)
|
|
201
275
|
|
|
@@ -208,6 +282,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
208
282
|
show_active = not (only_archived or year_focus)
|
|
209
283
|
show_archived = not only_active
|
|
210
284
|
archive_expanded = args.archived or has_years
|
|
285
|
+
show_urls = not args.no_urls
|
|
211
286
|
|
|
212
287
|
if show_active:
|
|
213
288
|
active = filter_active(find_active_courses(scan_dirs), patterns)
|
|
@@ -217,7 +292,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
217
292
|
print("Active courses:")
|
|
218
293
|
if active:
|
|
219
294
|
for course in active:
|
|
220
|
-
|
|
295
|
+
_print_course(course, 2, show_urls)
|
|
221
296
|
elif has_names:
|
|
222
297
|
print(f" No active courses match: {', '.join(patterns)}")
|
|
223
298
|
else:
|
|
@@ -225,7 +300,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
225
300
|
|
|
226
301
|
if show_archived:
|
|
227
302
|
archived = filter_archived(
|
|
228
|
-
find_archived_courses(config.archive_path),
|
|
303
|
+
find_archived_courses(config.archive_path), years, patterns
|
|
229
304
|
)
|
|
230
305
|
if show_active:
|
|
231
306
|
print()
|
|
@@ -240,7 +315,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
240
315
|
for year in sorted(archived):
|
|
241
316
|
print(f" {year}:")
|
|
242
317
|
for course in archived[year]:
|
|
243
|
-
|
|
318
|
+
_print_course(course, 4, show_urls)
|
|
244
319
|
elif has_names or has_years:
|
|
245
320
|
print(" No archived courses match your filters")
|
|
246
321
|
else:
|
|
File without changes
|
{course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/.claude/settings.local.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{course_setup-3.2.0 → course_setup-3.3.0}/src/setup_course_github/generic/Course notebook.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|