Skip to content

Commit b6cd421

Browse files
authored
Merge pull request #11 from GetTechAPI/data/harvest-public-es
data(cpu): harvest public ES tables
2 parents 207b366 + 2aba690 commit b6cd421

88 files changed

Lines changed: 3607 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ site/catalog.json
3333

3434
# Local GitHub setup helpers (not part of the catalog)
3535
.github-setup/
36+
tests/.tmp-from-table/

app/ingest/from_table.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
"""Write ES JSON from a local table (JSON/JSONL). No network.
2+
3+
Rows that classify as qs/retail/unknown are skipped. Existing slugs are skipped.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import argparse
9+
import json
10+
import re
11+
import sys
12+
from pathlib import Path
13+
from typing import Any
14+
15+
from app.ingest.classify import CLASS_ES, classify
16+
17+
ROOT = Path(__file__).resolve().parents[2]
18+
DATA = ROOT / "data" / "cpu"
19+
SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
20+
21+
RECORD_KEYS = (
22+
"slug",
23+
"name",
24+
"manufacturer",
25+
"sample_class",
26+
"qspec",
27+
"opn",
28+
"stepping",
29+
"cpuid",
30+
"sample_revision",
31+
"retail_equivalent",
32+
"first_seen_date",
33+
"release_date",
34+
"segment",
35+
"architecture",
36+
"socket",
37+
"process_node",
38+
"cores",
39+
"threads",
40+
"p_cores",
41+
"e_cores",
42+
"base_clock_ghz",
43+
"boost_clock_ghz",
44+
"l3_cache_mb",
45+
"tdp_w",
46+
"max_tdp_w",
47+
"integrated_graphics",
48+
"memory_support",
49+
"msrp_usd",
50+
"verified",
51+
"markings",
52+
"notes",
53+
"source_urls",
54+
)
55+
56+
57+
def _kebab(value: str) -> str:
58+
text = value.strip().lower().replace("_", "-")
59+
text = re.sub(r"[^a-z0-9-]+", "-", text)
60+
text = re.sub(r"-{2,}", "-", text).strip("-")
61+
return text
62+
63+
64+
def slug_for(row: dict[str, Any]) -> str:
65+
if isinstance(row.get("slug"), str) and row["slug"].strip():
66+
return _kebab(row["slug"])
67+
qspec = row.get("qspec")
68+
if isinstance(qspec, str) and qspec.strip():
69+
return "intel-" + _kebab(qspec)
70+
opn = row.get("opn")
71+
if isinstance(opn, str) and opn.strip():
72+
return "amd-" + _kebab(opn)
73+
raise ValueError("row needs slug, qspec, or opn")
74+
75+
76+
def existing_slugs() -> set[str]:
77+
found: set[str] = set()
78+
if not DATA.exists():
79+
return found
80+
for path in DATA.rglob("*.json"):
81+
found.add(path.stem)
82+
return found
83+
84+
85+
def load_rows(path: Path) -> list[dict[str, Any]]:
86+
text = path.read_text(encoding="utf-8-sig").strip()
87+
if not text:
88+
return []
89+
if path.suffix == ".jsonl" or text[:1] != "[":
90+
rows: list[dict[str, Any]] = []
91+
for line_no, line in enumerate(text.splitlines(), 1):
92+
line = line.strip()
93+
if not line:
94+
continue
95+
item = json.loads(line)
96+
if not isinstance(item, dict):
97+
raise ValueError(f"{path}:{line_no} is not an object")
98+
rows.append(item)
99+
return rows
100+
payload = json.loads(text)
101+
if not isinstance(payload, list):
102+
raise ValueError(f"{path} must be a JSON array or JSONL")
103+
return [item for item in payload if isinstance(item, dict)]
104+
105+
106+
def _record(row: dict[str, Any], slug: str) -> dict[str, Any]:
107+
manufacturer = str(row.get("manufacturer") or "").strip().lower()
108+
name = str(row.get("name") or "").strip()
109+
if not name:
110+
ident = row.get("qspec") or row.get("opn") or slug
111+
brand = "Intel" if manufacturer == "intel" else "AMD"
112+
name = f"{brand} {ident} Engineering Sample"
113+
rec: dict[str, Any] = {key: row.get(key) for key in RECORD_KEYS}
114+
rec["slug"] = slug
115+
rec["name"] = name
116+
rec["manufacturer"] = manufacturer
117+
rec["sample_class"] = CLASS_ES
118+
rec["segment"] = str(row.get("segment") or "desktop").strip().lower()
119+
rec["verified"] = bool(row.get("verified", False))
120+
rec["source_urls"] = list(row.get("source_urls") or [])
121+
rec["markings"] = list(row.get("markings") or [])
122+
if manufacturer == "intel" and "Intel Confidential" not in rec["markings"]:
123+
rec["markings"].append("Intel Confidential")
124+
if manufacturer == "amd" and "AMD Eng Sample" not in rec["markings"]:
125+
rec["markings"].append("AMD Eng Sample")
126+
if rec.get("qspec") == "":
127+
rec["qspec"] = None
128+
if rec.get("opn") == "":
129+
rec["opn"] = None
130+
return rec
131+
132+
133+
def output_path(rec: dict[str, Any], year: int) -> Path:
134+
return DATA / rec["manufacturer"] / str(year) / rec["segment"] / f"{rec['slug']}.json"
135+
136+
137+
def ingest_rows(
138+
rows: list[dict[str, Any]],
139+
*,
140+
dry_run: bool = False,
141+
) -> dict[str, list[str]]:
142+
tallies: dict[str, list[str]] = {
143+
"written": [],
144+
"duplicate": [],
145+
"skipped-qs": [],
146+
"skipped-retail": [],
147+
"unknown": [],
148+
"invalid": [],
149+
}
150+
seen = existing_slugs()
151+
for row in rows:
152+
try:
153+
slug = slug_for(row)
154+
except ValueError as exc:
155+
tallies["invalid"].append(str(exc))
156+
continue
157+
if not SLUG_RE.match(slug):
158+
tallies["invalid"].append(f"{slug}: bad slug")
159+
continue
160+
if slug in seen:
161+
tallies["duplicate"].append(slug)
162+
continue
163+
manufacturer = str(row.get("manufacturer") or "").strip().lower()
164+
if manufacturer == "intel" and not row.get("qspec"):
165+
row = {**row, "qspec": slug.removeprefix("intel-").upper()}
166+
candidate = {
167+
**row,
168+
"slug": slug,
169+
"name": row.get("name") or f"{slug} engineering sample",
170+
"sample_class": "es",
171+
}
172+
result = classify(candidate)
173+
if result.sample_class != CLASS_ES:
174+
key = {
175+
"qs": "skipped-qs",
176+
"retail": "skipped-retail",
177+
}.get(result.sample_class, "unknown")
178+
tallies[key].append(f"{slug}:{result.sample_class}")
179+
continue
180+
year = int(row.get("year") or str(row.get("first_seen_date") or "1970")[:4])
181+
rec = _record(row, slug)
182+
if result.sample_revision and not rec.get("sample_revision"):
183+
rec["sample_revision"] = result.sample_revision
184+
path = output_path(rec, year)
185+
if not dry_run:
186+
path.parent.mkdir(parents=True, exist_ok=True)
187+
path.write_text(json.dumps(rec, indent=2) + "\n", encoding="utf-8")
188+
seen.add(slug)
189+
tallies["written"].append(str(path.relative_to(ROOT)).replace("\\", "/"))
190+
return tallies
191+
192+
193+
def print_summary(tallies: dict[str, list[str]]) -> None:
194+
for key in (
195+
"written",
196+
"duplicate",
197+
"skipped-qs",
198+
"skipped-retail",
199+
"unknown",
200+
"invalid",
201+
):
202+
items = tallies[key]
203+
print(f"{key}: {len(items)}")
204+
for item in items[:30]:
205+
print(f" {item}")
206+
if len(items) > 30:
207+
print(f" … {len(items) - 30} more")
208+
209+
210+
def main(argv: list[str] | None = None) -> int:
211+
parser = argparse.ArgumentParser(description="Ingest ES rows from a local JSON/JSONL table.")
212+
parser.add_argument("table", type=Path)
213+
parser.add_argument("--dry-run", action="store_true")
214+
args = parser.parse_args(argv)
215+
rows = load_rows(args.table)
216+
tallies = ingest_rows(rows, dry_run=args.dry_run)
217+
print_summary(tallies)
218+
return 0 if not tallies["invalid"] else 1
219+
220+
221+
if __name__ == "__main__":
222+
sys.exit(main())
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"slug": "amd-1d2801a2m88e4",
3+
"name": "AMD Eng Sample 1D2801A2M88E4",
4+
"manufacturer": "amd",
5+
"sample_class": "es",
6+
"qspec": null,
7+
"opn": "1D2801A2M88E4",
8+
"stepping": null,
9+
"cpuid": null,
10+
"sample_revision": "es1",
11+
"retail_equivalent": "ryzen-7-1800x",
12+
"first_seen_date": null,
13+
"release_date": null,
14+
"segment": "desktop",
15+
"architecture": "Zen",
16+
"socket": "AM4",
17+
"process_node": "14 nm",
18+
"cores": 8,
19+
"threads": 16,
20+
"p_cores": null,
21+
"e_cores": null,
22+
"base_clock_ghz": 2.8,
23+
"boost_clock_ghz": 3.2,
24+
"l3_cache_mb": null,
25+
"tdp_w": null,
26+
"max_tdp_w": null,
27+
"integrated_graphics": null,
28+
"memory_support": "DDR4",
29+
"msrp_usd": null,
30+
"verified": false,
31+
"markings": [
32+
"AMD Eng Sample"
33+
],
34+
"notes": "Summit Ridge ES1. Prefix 1. 2.8/3.2 GHz in Ashes of the Singularity listings.",
35+
"source_urls": [
36+
"https://www.guru3d.com/story/amd-zen-engineering-sample-shows-promising-perf/",
37+
"https://en.wikipedia.org/wiki/Zen_(microarchitecture)"
38+
]
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"slug": "amd-2d2801a2m88e4",
3+
"name": "AMD Eng Sample 2D2801A2M88E4",
4+
"manufacturer": "amd",
5+
"sample_class": "es",
6+
"qspec": null,
7+
"opn": "2D2801A2M88E4",
8+
"stepping": null,
9+
"cpuid": null,
10+
"sample_revision": "es2",
11+
"retail_equivalent": "ryzen-7-1800x",
12+
"first_seen_date": null,
13+
"release_date": null,
14+
"segment": "desktop",
15+
"architecture": "Zen",
16+
"socket": "AM4",
17+
"process_node": "14 nm",
18+
"cores": 8,
19+
"threads": 16,
20+
"p_cores": null,
21+
"e_cores": null,
22+
"base_clock_ghz": 2.8,
23+
"boost_clock_ghz": 3.2,
24+
"l3_cache_mb": null,
25+
"tdp_w": null,
26+
"max_tdp_w": null,
27+
"integrated_graphics": null,
28+
"memory_support": "DDR4",
29+
"msrp_usd": null,
30+
"verified": false,
31+
"markings": [
32+
"AMD Eng Sample"
33+
],
34+
"notes": "Summit Ridge ES2. Prefix 2. Same 2.8/3.2 GHz sample clocks as the ES1 sibling.",
35+
"source_urls": [
36+
"https://www.guru3d.com/story/amd-zen-engineering-sample-shows-promising-perf/",
37+
"https://en.wikipedia.org/wiki/Zen_(microarchitecture)"
38+
]
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"slug": "amd-2d3212bgmcwh2",
3+
"name": "AMD Eng Sample 2D3212BGMCWH2",
4+
"manufacturer": "amd",
5+
"sample_class": "es",
6+
"qspec": null,
7+
"opn": "2D3212BGMCWH2",
8+
"stepping": null,
9+
"cpuid": null,
10+
"sample_revision": "es2",
11+
"retail_equivalent": "ryzen-9-3900x",
12+
"first_seen_date": null,
13+
"release_date": null,
14+
"segment": "desktop",
15+
"architecture": "Zen 2",
16+
"socket": "AM4",
17+
"process_node": "TSMC 7 nm",
18+
"cores": 12,
19+
"threads": 24,
20+
"p_cores": null,
21+
"e_cores": null,
22+
"base_clock_ghz": 3.4,
23+
"boost_clock_ghz": 3.7,
24+
"l3_cache_mb": null,
25+
"tdp_w": null,
26+
"max_tdp_w": null,
27+
"integrated_graphics": null,
28+
"memory_support": "DDR4",
29+
"msrp_usd": null,
30+
"verified": false,
31+
"markings": [
32+
"AMD Eng Sample"
33+
],
34+
"notes": "Matisse 12C ES2. UserBenchmark OPN 2D3212BGMCWH2_37/34_N.",
35+
"source_urls": [
36+
"https://www.techspot.com/news/78452-amd-next-12-core-cpu-appears-benchmark-database.html",
37+
"https://en.wikipedia.org/wiki/Zen_2"
38+
]
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"slug": "amd-100-000000059-15",
3+
"name": "AMD Eng Sample 100-000000059-15",
4+
"manufacturer": "amd",
5+
"sample_class": "es",
6+
"qspec": null,
7+
"opn": "100-000000059-15",
8+
"stepping": null,
9+
"cpuid": null,
10+
"sample_revision": "es1",
11+
"retail_equivalent": "ryzen-9-5950x",
12+
"first_seen_date": null,
13+
"release_date": null,
14+
"segment": "desktop",
15+
"architecture": "Zen 3",
16+
"socket": "AM4",
17+
"process_node": "TSMC 7 nm",
18+
"cores": 16,
19+
"threads": 32,
20+
"p_cores": null,
21+
"e_cores": null,
22+
"base_clock_ghz": 3.7,
23+
"boost_clock_ghz": 4.6,
24+
"l3_cache_mb": null,
25+
"tdp_w": null,
26+
"max_tdp_w": null,
27+
"integrated_graphics": null,
28+
"memory_support": "DDR4",
29+
"msrp_usd": null,
30+
"verified": false,
31+
"markings": [
32+
"AMD Eng Sample"
33+
],
34+
"notes": "Vermeer 16C ES. OPN clock field 46/37, N suffix.",
35+
"source_urls": [
36+
"https://videocardz.com/newz/amd-16-core-zen3-ryzen-9-4950x-engineering-sample-boosts-up-to-4-9-ghz",
37+
"https://en.wikipedia.org/wiki/Zen_3"
38+
]
39+
}

0 commit comments

Comments
 (0)