course-setup 3.2.1__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.1 → course_setup-3.3.0}/PKG-INFO +17 -4
- {course_setup-3.2.1 → course_setup-3.3.0}/README.md +16 -3
- {course_setup-3.2.1 → course_setup-3.3.0}/pyproject.toml +1 -1
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/list_courses.py +81 -7
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/.DS_Store +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/.claude/settings.local.json +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/__init__.py +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/archive_course.py +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/config.py +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/generic/.gitignore +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/generic/.ipynb_checkpoints/Course notebook-checkpoint.ipynb +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/generic/Course notebook.ipynb +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/generic/README.md +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/init_config.py +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/notebooks.py +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/retire_course.py +0 -0
- {course_setup-3.2.1 → course_setup-3.3.0}/src/setup_course_github/setup_course.py +0 -0
- {course_setup-3.2.1 → 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
|
|
|
@@ -139,12 +169,46 @@ def find_archived_courses(archive_path: Path) -> dict[str, list[Path]]:
|
|
|
139
169
|
|
|
140
170
|
|
|
141
171
|
def _year_arg(value: str) -> str:
|
|
142
|
-
"""argparse type: accept
|
|
143
|
-
if not
|
|
144
|
-
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
|
+
)
|
|
145
177
|
return value
|
|
146
178
|
|
|
147
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
|
+
|
|
148
212
|
def main(argv: list[str] | None = None) -> None:
|
|
149
213
|
pypi_url = "https://pypi.org/project/course-setup/"
|
|
150
214
|
author_line = f"{__author__} <{__email__}>"
|
|
@@ -188,15 +252,24 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
188
252
|
dest="years",
|
|
189
253
|
type=_year_arg,
|
|
190
254
|
metavar="YYYY",
|
|
191
|
-
help="Restrict archived courses to a year (repeatable
|
|
255
|
+
help="Restrict archived courses to a year or YYYY-YYYY range (repeatable)",
|
|
192
256
|
)
|
|
193
257
|
parser.add_argument(
|
|
194
258
|
"--count",
|
|
195
259
|
action="store_true",
|
|
196
260
|
help="Show counts instead of individual course lines",
|
|
197
261
|
)
|
|
262
|
+
parser.add_argument(
|
|
263
|
+
"--no-urls",
|
|
264
|
+
action="store_true",
|
|
265
|
+
help="Do not show each course's GitHub URL",
|
|
266
|
+
)
|
|
198
267
|
args = parser.parse_args(argv)
|
|
199
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
|
+
|
|
200
273
|
config = load_config()
|
|
201
274
|
scan_dirs = resolve_scan_dirs(args.dir, config)
|
|
202
275
|
|
|
@@ -209,6 +282,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
209
282
|
show_active = not (only_archived or year_focus)
|
|
210
283
|
show_archived = not only_active
|
|
211
284
|
archive_expanded = args.archived or has_years
|
|
285
|
+
show_urls = not args.no_urls
|
|
212
286
|
|
|
213
287
|
if show_active:
|
|
214
288
|
active = filter_active(find_active_courses(scan_dirs), patterns)
|
|
@@ -218,7 +292,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
218
292
|
print("Active courses:")
|
|
219
293
|
if active:
|
|
220
294
|
for course in active:
|
|
221
|
-
|
|
295
|
+
_print_course(course, 2, show_urls)
|
|
222
296
|
elif has_names:
|
|
223
297
|
print(f" No active courses match: {', '.join(patterns)}")
|
|
224
298
|
else:
|
|
@@ -226,7 +300,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
226
300
|
|
|
227
301
|
if show_archived:
|
|
228
302
|
archived = filter_archived(
|
|
229
|
-
find_archived_courses(config.archive_path),
|
|
303
|
+
find_archived_courses(config.archive_path), years, patterns
|
|
230
304
|
)
|
|
231
305
|
if show_active:
|
|
232
306
|
print()
|
|
@@ -241,7 +315,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
241
315
|
for year in sorted(archived):
|
|
242
316
|
print(f" {year}:")
|
|
243
317
|
for course in archived[year]:
|
|
244
|
-
|
|
318
|
+
_print_course(course, 4, show_urls)
|
|
245
319
|
elif has_names or has_years:
|
|
246
320
|
print(" No archived courses match your filters")
|
|
247
321
|
else:
|
|
File without changes
|
{course_setup-3.2.1 → 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.1 → 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
|