lyte-codes commited on
Commit
3e73a0f
·
verified ·
1 Parent(s): 2e1a304

Add version.py: eval.py imports it and 263702 shipped without it

Browse files
Files changed (1) hide show
  1. version.py +142 -0
version.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Version stamps: clockface YYWWNN, and the provenance that goes with them.
3
+
4
+ python3 version.py # the next version for this week
5
+ python3 version.py --parse 263601 # what does that number mean
6
+ python3 version.py --tag # create the git tag
7
+ python3 version.py --stamp # the full one-line provenance stamp
8
+
9
+ A version is six digits: two-digit ISO year, two-digit ISO week, two-digit
10
+ release number within that week. 263601 is the first release of ISO week 36 of
11
+ 2026, which began 2026-08-31.
12
+
13
+ The year MUST be the ISO week-numbering year (%G), not the calendar year (%y).
14
+ They disagree either side of new year: 2024-12-30 is in ISO week 1 of 2025, so
15
+ naive %y%%V yields 2401 -- a year wrong, and it sorts before everything else
16
+ from 2024. Six digits like this sort lexically into chronological order, which
17
+ is the whole point of the scheme.
18
+
19
+ The number alone means nothing without what produced it, so a released number
20
+ is always written with its provenance:
21
+
22
+ clockface 263601 - code 5c4983b - synth 50000 - eval 137
23
+
24
+ code the git commit the model and evaluator came from
25
+ synth how many synthetic images it was trained on
26
+ eval how many REAL photographs the number was measured on
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import datetime as dt
33
+ import re
34
+ import subprocess
35
+
36
+ VERSION_RE = re.compile(r"^v?(\d{2})(\d{2})(\d{2})$")
37
+
38
+
39
+ def git(*args, cwd=None):
40
+ try:
41
+ out = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, timeout=10)
42
+ return out.stdout.strip() if out.returncode == 0 else None
43
+ except Exception:
44
+ return None
45
+
46
+
47
+ def week_stamp(date=None):
48
+ """Four digits: ISO year (2) + ISO week (2). Never the calendar year."""
49
+ d = date or dt.date.today()
50
+ return f"{d.strftime('%G')[2:]}{d.strftime('%V')}"
51
+
52
+
53
+ def existing_versions():
54
+ tags = git("tag", "--list") or ""
55
+ out = []
56
+ for t in tags.splitlines():
57
+ m = VERSION_RE.match(t.strip())
58
+ if m:
59
+ out.append("".join(m.groups()))
60
+ return sorted(out)
61
+
62
+
63
+ def next_version(date=None):
64
+ wk = week_stamp(date)
65
+ used = [int(v[4:]) for v in existing_versions() if v[:4] == wk]
66
+ return f"{wk}{max(used, default=0) + 1:02d}"
67
+
68
+
69
+ def parse(version):
70
+ m = VERSION_RE.match(str(version).strip())
71
+ if not m:
72
+ raise ValueError(f"not a clockface version: {version!r} (want six digits, e.g. 263601)")
73
+ yy, ww, nn = (int(g) for g in m.groups())
74
+ year = 2000 + yy
75
+ try:
76
+ monday = dt.date.fromisocalendar(year, ww, 1)
77
+ except ValueError as exc:
78
+ raise ValueError(f"{version}: no ISO week {ww} in {year} ({exc})")
79
+ return {"version": f"{yy:02d}{ww:02d}{nn:02d}", "iso_year": year, "iso_week": ww,
80
+ "release": nn, "week_starts": monday.isoformat(),
81
+ "week_ends": (monday + dt.timedelta(days=6)).isoformat()}
82
+
83
+
84
+ def code_hash():
85
+ """Short commit, flagged dirty when the tree does not match it."""
86
+ h = git("rev-parse", "--short", "HEAD")
87
+ if not h:
88
+ return "nogit"
89
+ dirty = git("status", "--porcelain")
90
+ return f"{h}-dirty" if dirty else h
91
+
92
+
93
+ def stamp(version=None, synth=None, n_eval=None):
94
+ v = version or next_version()
95
+ if not version and not existing_versions():
96
+ # next_version() derives NN by scanning tags. With no tags it returns 01
97
+ # forever, so two different releases can both be stamped ...01 -- which
98
+ # happened: two models were published under 263701. Say so rather than
99
+ # emit a number that looks unique and is not.
100
+ v += " (UNTAGGED: no release tags exist, so this number is not unique)"
101
+ parts = [f"clockface {v}", f"code {code_hash()}"]
102
+ parts.append(f"synth {synth}" if synth is not None else "synth none")
103
+ parts.append(f"eval {n_eval}" if n_eval is not None else "eval none")
104
+ return " - ".join(parts)
105
+
106
+
107
+ def main():
108
+ ap = argparse.ArgumentParser(description=__doc__,
109
+ formatter_class=argparse.RawDescriptionHelpFormatter)
110
+ ap.add_argument("--parse", metavar="VERSION")
111
+ ap.add_argument("--tag", action="store_true", help="git tag the next version")
112
+ ap.add_argument("--stamp", action="store_true")
113
+ ap.add_argument("--synth", type=int)
114
+ ap.add_argument("--eval", dest="n_eval", type=int)
115
+ args = ap.parse_args()
116
+
117
+ if args.parse:
118
+ for k, v in parse(args.parse).items():
119
+ print(f" {k:<12} {v}")
120
+ return
121
+ if args.stamp:
122
+ print(stamp(synth=args.synth, n_eval=args.n_eval))
123
+ return
124
+ if args.tag:
125
+ v = next_version()
126
+ if git("rev-parse", "--short", "HEAD") is None:
127
+ raise SystemExit("not a git repository")
128
+ if git("status", "--porcelain"):
129
+ raise SystemExit("working tree is dirty; commit before tagging, or the "
130
+ "tag will not describe what was measured")
131
+ res = subprocess.run(["git", "tag", "-a", v, "-m", f"clockface {v}"],
132
+ capture_output=True, text=True)
133
+ if res.returncode:
134
+ raise SystemExit(res.stderr.strip())
135
+ print(f"tagged {v} (push with: git push origin {v})")
136
+ return
137
+
138
+ print(next_version())
139
+
140
+
141
+ if __name__ == "__main__":
142
+ main()