diff --git a/migration.yml b/migration.yml index 61fcc0a..6b68dfb 100644 --- a/migration.yml +++ b/migration.yml @@ -82,8 +82,11 @@ datasets: # Planned waves that have not landed anything here yet. `datasets` names the # files as the audit sees them today, so the dashboard can join the two views. +# `title` is the reader-facing milestone name (the dashboard is read by people +# who are not doing the migration); `scope` carries the insider detail. pending: - pilot: P3 + title: Large data files, and the external data repository scope: the LFS case — heavy_tails Forbes/cities set + SCF extracts (fold in high_dim_data) datasets: - forbes-global2000.csv @@ -96,6 +99,7 @@ pending: - QuantEcon/data-lectures#2 - QuantEcon/meta#337 - pilot: P4 + title: Maintained snapshots for lectures that fetch live API data scope: the dynamic-snapshot case — a maintained UNRATE twin for the incidental FRED consumers datasets: [] # produces a NEW file here; no existing static file moves tracking: diff --git a/scripts/render_audit.py b/scripts/render_audit.py index c84a251..e24a6ea 100644 --- a/scripts/render_audit.py +++ b/scripts/render_audit.py @@ -205,6 +205,7 @@ .meter .seg + .seg { margin-left: 2px; } .meter .seg.a { background: var(--meter-a); } .meter .seg.b { background: var(--meter-b); } +.meter.mini { height: 8px; max-width: 340px; margin: 6px 0 2px; border-radius: 4px; } /* bar list — one measure, single hue, labeled rows */ .barlist { display: grid; grid-template-columns: max-content 1fr max-content; gap: 6px 12px; align-items: center; margin: 14px 0; } .barlist .lbl { font-size: 13px; } @@ -360,13 +361,13 @@ def migration_meter(audit: dict) -> str:

Migration progress

-repointed — consumers read data-lectures ({n_repointed}) -landed here, consumers not yet repointed ({n_landed}) -not migrated ({n_total - n_repointed - n_landed}, of which {len(queued)} queued in P3) +migrated — every consuming lecture reads the central copy ({n_repointed}) +copied here, lectures not yet switched ({n_landed}) +not migrated ({n_total - n_repointed - n_landed}, of which {len(queued)} queued for the next waves)
-

Denominator: the {n_total} distinct static files lectures read today. The interim→final -URL cutover ({issue_link("QuantEcon/data-lectures#15")}) advances datasets from repointed to -final once data.quantecon.org is live; no dataset is final yet.

+

Denominator: the {n_total} distinct static files lectures read today. The full +per-series manifest and the milestone plan are on the +migration tracker.

""" @@ -400,11 +401,12 @@ def what_changed() -> str:

What moved since the {PREV_SNAPSHOT} snapshot

The audit was first taken by hand on {PREV_SNAPSHOT}. Regenerating from live repo state two days later, the picture has already changed — which is why this dashboard is generated.

-
4 datasets migrated and repointed. -

P1 moved lingcod_msy_recovery.csv (was a Colab-breaking local path); -P2 moved the pandas_panel trio realwage.csv / countries.csv / -employ.csv (5 of their 6 reads pointed at the retired legacy repo). All consumers -now read data-lectures. Details on the migration tracker.

+
4 datasets migrated. +

The first two migration waves moved lingcod_msy_recovery.csv (was a +Colab-breaking local path) and the pandas_panel trio realwage.csv / +countries.csv / employ.csv (5 of their 6 reads pointed at the retired +legacy repo). Every consuming lecture now reads the central copy. Details on the +migration tracker.

Legacy-repo URLs: 8 → 0.

The pre-MyST QuantEcon/lecture-python repo was renamed to lecture-python.rst and archived; the P2 repoints plus the ols @@ -471,6 +473,88 @@ def render_index(audit: dict) -> str: # Page: migration tracker # --------------------------------------------------------------------------- +# Reader-facing status labels — the dashboard is read by people who are not +# doing the migration, so the manifest speaks plainly; the lifecycle terms +# (pending/landed/repointed/final) appear only in the explained stepper detail. +STATUS_META = { + # status → (pill css, label) + "final": ("p-local", "✓ migrated"), + "repointed": ("p-local", "✓ migrated"), + "landed": ("p-own", "● copied, not yet switched"), + "queued": ("p-legacy", "queued"), + "unscheduled": ("p-embed", "not scheduled"), +} + + +def dataset_status(fname: str, migration: dict) -> tuple[str, str]: + """→ (status, pilot) for any static dataset the audit sees.""" + rec = (migration.get("datasets") or {}).get(fname) + if rec: + return rec.get("status", "pending"), rec.get("pilot", "") + for wave in migration.get("pending") or []: + if fname in (wave.get("datasets") or []): + return "queued", wave.get("pilot", "") + return "unscheduled", "" + + +def series_manifest(audit: dict) -> str: + """Every static dataset, grouped by the lecture series that owns the bytes + today, with its migration status — the how-far-along view.""" + migration = audit["migration"] or {} + groups: dict[str, list] = {} + for d in audit["datasets"]: + groups.setdefault(repo_of_record(d), []).append(d) + order = ["lecture-python-intro", "lecture-python-programming", + "lecture-python.myst", "lecture-python-advanced.myst", + "data-lectures"] + rows = "" + for repo in [r for r in order if r in groups] + sorted(set(groups) - set(order)): + ds = sorted(groups[repo], key=lambda x: x["file"]) + statuses = [dataset_status(d["file"], migration)[0] for d in ds] + n = len(ds) + n_done = sum(1 for s in statuses if s in ("repointed", "final")) + n_landed = statuses.count("landed") + n_queued = statuses.count("queued") + counts = [f"{n_done} migrated" if n_done else "", + f"{n_landed} copied, not switched" if n_landed else "", + f"{n_queued} queued" if n_queued else "", + f"{n - n_done - n_landed - n_queued} not scheduled" + if n - n_done - n_landed - n_queued else ""] + pct = lambda k: max(0.8, 100 * k / n) if k else 0 + segs = "" + if n_done: + segs += f'

' + if n_landed: + segs += f'
' + rows += (f'{esc(repo)} — ' + f'{n} file{"s" if n != 1 else ""} · ' + f'{esc(" · ".join(c for c in counts if c))}' + f'' + f'') + for d, status_pilot in zip(ds, (dataset_status(d["file"], migration) for d in ds)): + status, pilot = status_pilot + css, label = STATUS_META.get(status, ("p-embed", status)) + if pilot and status == "queued": + label += f" · wave {pilot}" + pills = " ".join(pattern_pill(p) for p in d["patterns"]) + rows += (f'{esc(d["file"])}' + f'{esc(d["description"])}' + f'{pills}' + f'{esc(label)}') + return f""" +

All datasets by series

+

Every static file the lectures read today, grouped by the series that owns the +bytes, with its migration status — the how-far-along view. Not scheduled means no +milestone has claimed the file yet; the broad sweep (see the milestones below) eventually +covers them all. Data written by the lectures themselves and live-API reads are not migration +targets and are tracked on the audit page.

+
+ +{rows} +
FileContentsHosting todayStatus
+""" + + def stepper(fname: str, rec: dict, verified: bool) -> str: status = rec.get("status", "pending") if status not in STATUS_STEPS: @@ -499,6 +583,73 @@ def stepper(fname: str, rec: dict, verified: bool) -> str: return f'
{steps}

{check}

' +def milestones(audit: dict) -> str: + """The migration programme as reader-facing milestones — completed waves + (derived from migration.yml records), upcoming waves, the broad sweep, and + the final-URL switch. This section is what lets the rest of the dashboard + stay plan-agnostic: wave codes like P3 mean something only because they + are presented here.""" + mig = audit["migration"] or {} + recs = mig.get("datasets") or {} + manifests = audit["manifests"] + + by_pilot: dict[str, list] = {} + for fname, rec in recs.items(): + by_pilot.setdefault(rec.get("pilot", "?"), []).append((fname, rec)) + items = "" + for pilot in sorted(by_pilot): + entries = by_pilot[pilot] + files = sorted(f for f, _ in entries) + done = all(r.get("status") in ("repointed", "final") for _, r in entries) + dates = [str((r.get("repoints") or [{}])[-1].get("date", "")) for _, r in entries] + series = {c["repo"] for f, _ in entries + for c in (manifests.get(f, {}).get("consumers") or [])} + n_series = len(series) + mark, cls = ("✓", "ok") if done else ("○", "") + items += f""" +
+{mark} Wave {esc(pilot)} — {len(files)} dataset{"s" if len(files) != 1 else ""}, +consumed by {n_series} lecture series{" — completed " + esc(max(dates)) if done and max(dates) else ""} +

{" · ".join(f"{esc(f)}" for f in files)}

+
+""" + for w in mig.get("pending") or []: + files = w.get("datasets") or [] + flist = (" · ".join(f"{esc(f)}" for f in sorted(files)) + if files else "produces new snapshot files here; no existing file moves") + tracking = " · ".join(issue_link(t) for t in w.get("tracking") or []) + items += f""" +
+○ Wave {esc(w.get("pilot", ""))} — {esc(w.get("title", w.get("scope", "")))} +

{flist}

+

Tracking: {tracking}

+
+""" + n_unsched = sum(1 for d in audit["datasets"] + if dataset_status(d["file"], mig)[0] == "unscheduled") + items += f""" +
+○ Broad sweep — every remaining static file +

The {n_unsched} not-yet-scheduled files move once the waves above have proven the +process for each kind of data. Mechanical from there: one data PR here, one switch PR +per consuming lecture repo.

+
+
+○ Final URLs — serve everything from data.quantecon.org +

Migrated lectures currently read this repository's GitHub URL. Once the custom domain +is live, every migrated dataset switches to its permanent +data.quantecon.org/lectures/… address in a single sweep +({issue_link("QuantEcon/data-lectures#15")}). No dataset has made this step yet.

+
+""" + return f""" +

Milestones

+

The migration proceeds in small waves, each proving the process for a harder +kind of data before the broad sweep moves the rest.

+{items} +""" + + def render_migration(audit: dict) -> str: mig = audit["migration"] or {} recs = mig.get("datasets") or {} @@ -518,7 +669,7 @@ def render_migration(audit: dict) -> str:
{esc(fname)} -{esc(rec.get("pilot", ""))} +wave {esc(rec.get("pilot", ""))} {pattern_pill(prior)} {pattern_pill("data-lectures")} file · manifest @@ -526,59 +677,48 @@ def render_migration(audit: dict) -> str:

{esc(m.get("title", ""))} — consumed by {consumers or "—"}

{stepper(fname, rec, verified)}
-""" - - waves = "" - for w in mig.get("pending") or []: - files = "".join(f'
  • {esc(f)} — {esc((by_name.get(f) or {}).get("description", ""))}
  • ' - for f in w.get("datasets") or []) or \ - "
  • produces a new file here; no existing static file moves
  • " - tracking = " · ".join(issue_link(t) for t in w.get("tracking") or []) - waves += f""" -
    -{esc(w.get("pilot", ""))} — {esc(w.get("scope", ""))} -
      {files}
    -

    Tracking: {tracking}

    -
    """ problems = audit["problems"]["migration_inconsistencies"] consistency = ('
    Consistency check: clean.' - '

    migration.yml, the manifests and today\'s scan of every consumer repo agree.

    ' + '

    The tracker, the dataset manifests and today\'s scan of every lecture repo agree.

    ' if not problems else '
    Consistency check: FAILING.
    ") n_re = sum(1 for r in recs.values() if r.get("status") in ("repointed", "final")) queued = {f for w in (mig.get("pending") or []) for f in w.get("datasets") or []} + n_unsched = sum(1 for d in audit["datasets"] + if dataset_status(d["file"], mig)[0] == "unscheduled") body = f"""
    -

    Migration tracker · source of truth: migration.yml + lectures/*.yml

    -

    Datasets moving into data-lectures

    -

    Each dataset's position in the lifecycle -pending → landed → repointed → final, with the PRs that moved it. The build -verifies every status against a fresh scan of the consumer repos, so this page cannot +

    Migration tracker · generated and verified on every build

    +

    Moving lecture data into one central repository

    +

    The QuantEcon lectures read data from many places — their own repos, local +files, retired repos, live APIs. This tracker shows the move to a single canonical home +(data-lectures): what has moved, what's next, and how far along it is. +Every status is verified against a fresh scan of the lecture repos, so this page cannot claim a migration that didn't happen.

    -
    {n_re}repointed — consumers read data-lectures today
    -
    {len(queued)}queued in pending waves (P3)
    -
    0final — on data.quantecon.org (gated on DNS, {issue_link("QuantEcon/data-lectures#15")})
    +
    {n_re}datasets migrated — every consuming lecture reads the central copy
    +
    {len(queued)}queued for the next waves
    +
    {n_unsched}not yet scheduled (the broad sweep)
    +

    How a dataset moves

    +

    pending — identified for migration; nothing has moved yet. +landed — the file and its metadata are merged into the central repo; lectures unchanged. +repointed — every consuming lecture now reads the central copy (shown as +✓ migrated in the table below). +final — the lecture reads the permanent data.quantecon.org address +(the last milestone below).

    {consistency} -

    Migrated datasets

    +{series_manifest(audit)} +{milestones(audit)} +

    Migration record — dataset by dataset

    +

    The full provenance for each migrated dataset: which pull requests landed the +data and switched each consuming lecture, and where it sits in the lifecycle.

    {pipelines} -

    Pending waves

    -

    Scoped but not landed. Files listed here still appear under their current -hosting pattern in the audit.

    -{waves} -

    How a dataset moves

    -

    pending — identified for migration; nothing in data-lectures yet. -landed — file + manifest merged here; consumers unchanged. -repointed — every consumer reads this repo's interim raw URL -(github.com/QuantEcon/data-lectures/raw/main/lectures/…). -final — every consumer reads https://data.quantecon.org/lectures/…; -the cutover is one sweep tracked in {issue_link("QuantEcon/data-lectures#15")}, gated on Phase 4 DNS.

    """ return page("Migration tracker — data-lectures", "migration", body, audit)