|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | """ |
3 | | -Export system prompt JSON configs to a point-in-time Markdown snapshot. |
| 3 | +Export system prompt JSON configs to a point-in-time Markdown snapshot and archive it as a numbered release. |
4 | 4 |
|
5 | | -Reads all JSON files from system-prompts/json (JSON fork) and writes a |
6 | | -timestamped directory under exports/, generating one Markdown file per config |
7 | | -following the general markdown template used in the MD fork. |
| 5 | +Reads all JSON files from system-prompts/json and writes a timestamped |
| 6 | +directory under exports/, generating one Markdown file per config following the |
| 7 | +general markdown model-card template used in this repo. |
8 | 8 |
|
9 | 9 | Usage: |
10 | 10 | python3 scripts/export_point_in_time_md.py |
11 | 11 | python3 scripts/export_point_in_time_md.py --out-dir exports/20250101-120000 |
| 12 | + python3 scripts/export_point_in_time_md.py --archive-format zip |
12 | 13 |
|
13 | 14 | Notes: |
14 | 15 | - Handles both JSON schemas observed in this repo (lowercase-with-hyphens and |
15 | 16 | Title Case with spaces) by normalizing keys. |
16 | 17 | - Missing values are rendered as "Not provided". |
| 18 | + - Produces an archive (tar.gz by default) and assigns an auto-incremented |
| 19 | + release number stored in exports/releases.json. |
17 | 20 | """ |
18 | 21 |
|
19 | 22 | from __future__ import annotations |
|
22 | 25 | import json |
23 | 26 | import re |
24 | 27 | from datetime import datetime |
| 28 | +import tarfile |
| 29 | +import zipfile |
25 | 30 | from pathlib import Path |
26 | 31 | from typing import Any, Dict, Optional |
27 | 32 |
|
28 | 33 |
|
29 | 34 | REPO_ROOT = Path(__file__).resolve().parents[1] |
30 | 35 | JSON_DIR = REPO_ROOT / "system-prompts" / "json" |
31 | 36 | EXPORTS_DIR = REPO_ROOT / "exports" |
| 37 | +RELEASES_INDEX = EXPORTS_DIR / "releases.json" |
32 | 38 |
|
33 | 39 |
|
34 | 40 | def as_bool(value: Any) -> bool: |
@@ -249,8 +255,21 @@ def render_markdown(data: Dict[str, Any], json_filename: str) -> str: |
249 | 255 |
|
250 | 256 |
|
251 | 257 | def main() -> int: |
252 | | - parser = argparse.ArgumentParser(description="Export point-in-time Markdown snapshot from JSON configs") |
| 258 | + parser = argparse.ArgumentParser(description="Export point-in-time Markdown snapshot from JSON configs and package as a release") |
253 | 259 | parser.add_argument("--out-dir", type=str, help="Output directory under exports/ (optional)") |
| 260 | + parser.add_argument( |
| 261 | + "--archive-format", |
| 262 | + type=str, |
| 263 | + choices=["tar.gz", "zip"], |
| 264 | + default="tar.gz", |
| 265 | + help="Archive format for the release (default: tar.gz)", |
| 266 | + ) |
| 267 | + parser.add_argument( |
| 268 | + "--release-number", |
| 269 | + type=int, |
| 270 | + default=None, |
| 271 | + help="Override auto-incremented release number (advanced)", |
| 272 | + ) |
254 | 273 | args = parser.parse_args() |
255 | 274 |
|
256 | 275 | if not JSON_DIR.exists(): |
@@ -299,10 +318,78 @@ def main() -> int: |
299 | 318 | print(f"ERROR: Failed writing {md_path.name}: {e}") |
300 | 319 | continue |
301 | 320 |
|
302 | | - print(f"Exported {count} markdown files to {out_dir.relative_to(REPO_ROOT)}") |
| 321 | + # Determine release number |
| 322 | + releases = {"releases": []} |
| 323 | + if RELEASES_INDEX.exists(): |
| 324 | + try: |
| 325 | + with open(RELEASES_INDEX, "r", encoding="utf-8") as f: |
| 326 | + releases = json.load(f) or {"releases": []} |
| 327 | + except Exception: |
| 328 | + releases = {"releases": []} |
| 329 | + |
| 330 | + existing_numbers = [r.get("number", 0) for r in releases.get("releases", [])] |
| 331 | + next_number = (max(existing_numbers) if existing_numbers else 0) + 1 |
| 332 | + if args.release_number is not None: |
| 333 | + next_number = int(args.release_number) |
| 334 | + |
| 335 | + # Create archive |
| 336 | + timestamp = out_dir.name |
| 337 | + base_name = f"release-{next_number}_{timestamp}" |
| 338 | + archive_path: Path |
| 339 | + if args.archive_format == "tar.gz": |
| 340 | + archive_path = EXPORTS_DIR / f"{base_name}.tar.gz" |
| 341 | + # Avoid overwrite: append suffix if exists |
| 342 | + suffix = 1 |
| 343 | + while archive_path.exists(): |
| 344 | + archive_path = EXPORTS_DIR / f"{base_name}({suffix}).tar.gz" |
| 345 | + suffix += 1 |
| 346 | + with tarfile.open(archive_path, "w:gz") as tar: |
| 347 | + tar.add(out_dir, arcname=timestamp) |
| 348 | + else: |
| 349 | + archive_path = EXPORTS_DIR / f"{base_name}.zip" |
| 350 | + suffix = 1 |
| 351 | + while archive_path.exists(): |
| 352 | + archive_path = EXPORTS_DIR / f"{base_name}({suffix}).zip" |
| 353 | + suffix += 1 |
| 354 | + with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: |
| 355 | + for p in out_dir.rglob("*"): |
| 356 | + if p.is_file(): |
| 357 | + zf.write(p, arcname=str(Path(timestamp) / p.relative_to(out_dir))) |
| 358 | + |
| 359 | + # Write release marker inside the folder |
| 360 | + try: |
| 361 | + with open(out_dir / "RELEASE.txt", "w", encoding="utf-8") as f: |
| 362 | + f.write( |
| 363 | + f"Release: {next_number}\n" |
| 364 | + f"Timestamp: {timestamp}\n" |
| 365 | + f"Files: {count}\n" |
| 366 | + f"Archive: {archive_path.name}\n" |
| 367 | + f"Format: {args.archive_format}\n" |
| 368 | + ) |
| 369 | + except Exception: |
| 370 | + pass |
| 371 | + |
| 372 | + # Update releases index |
| 373 | + entry = { |
| 374 | + "number": next_number, |
| 375 | + "timestamp": timestamp, |
| 376 | + "dir": str(out_dir.relative_to(REPO_ROOT)), |
| 377 | + "count": count, |
| 378 | + "archive": str(archive_path.relative_to(REPO_ROOT)), |
| 379 | + "format": args.archive_format, |
| 380 | + "created_at": datetime.now().isoformat(), |
| 381 | + } |
| 382 | + releases.setdefault("releases", []).append(entry) |
| 383 | + RELEASES_INDEX.parent.mkdir(parents=True, exist_ok=True) |
| 384 | + with open(RELEASES_INDEX, "w", encoding="utf-8") as f: |
| 385 | + json.dump(releases, f, indent=2) |
| 386 | + |
| 387 | + print( |
| 388 | + f"Exported {count} markdown files to {out_dir.relative_to(REPO_ROOT)} | " |
| 389 | + f"Release #{next_number} -> {archive_path.relative_to(REPO_ROOT)}" |
| 390 | + ) |
303 | 391 | return 0 |
304 | 392 |
|
305 | 393 |
|
306 | 394 | if __name__ == "__main__": |
307 | 395 | raise SystemExit(main()) |
308 | | - |
|
0 commit comments