mirror of
https://github.com/samuk/awesome-meshcore.git
synced 2026-10-30 19:52:05 +01:00
Create sync_upstream.py
This commit is contained in:
1 parent
005d8fe2e9
commit
c95ad7be82
1 file changed
+166
@@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Copy entries whose primary link is missing from README.md out of the
|
||||||
|
upstream README, re-rendered in the format of the matching local section
|
||||||
|
(table columns or list). Entries that can't be placed are listed in the PR body."""
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import urllib.request
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
UPSTREAM = "https://raw.githubusercontent.com/meshcore-ita/awesome-meshcore/main/README.md"
|
||||||
|
README = pathlib.Path("README.md")
|
||||||
|
IGNORE = pathlib.Path(".upstream-ignore") # one URL per line
|
||||||
|
|
||||||
|
LINK = re.compile(r"(?<!!)\[([^\]]*)\]\((https?://[^)\s]+)\)")
|
||||||
|
IMG = re.compile(r"!\[[^\]]*\]\([^)]*\)")
|
||||||
|
GH = re.compile(r"^https?://github\.com/([^/\s]+/[^/\s#?]+?)/?$", re.I)
|
||||||
|
HEAD = re.compile(r"^#{2,4}\s+(.*?)\s*$")
|
||||||
|
|
||||||
|
|
||||||
|
def norm(url):
|
||||||
|
return url.strip().rstrip("/").lower()
|
||||||
|
|
||||||
|
|
||||||
|
def cells(line):
|
||||||
|
return [c.strip() for c in line.strip().strip("|").split("|")]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url):
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "readme-sync"})
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return r.read().decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def sections(lines):
|
||||||
|
"""heading text -> list of (start, end) ranges (end exclusive)."""
|
||||||
|
heads = [(i, HEAD.match(l).group(1)) for i, l in enumerate(lines) if HEAD.match(l)]
|
||||||
|
out = defaultdict(list)
|
||||||
|
for n, (i, text) in enumerate(heads):
|
||||||
|
end = heads[n + 1][0] if n + 1 < len(heads) else len(lines)
|
||||||
|
out[text].append((i, end))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def table_header(lines, start, end):
|
||||||
|
for i in range(start + 1, end):
|
||||||
|
if lines[i].startswith("|"):
|
||||||
|
return cells(lines[i])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse(line, header):
|
||||||
|
"""Return dict(name, url, desc, closed) or None."""
|
||||||
|
if line.startswith("|"):
|
||||||
|
cs = cells(line)
|
||||||
|
m = LINK.search(cs[0])
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
di = next((i for i, h in enumerate(header or []) if h.lower().startswith("desc")), 1)
|
||||||
|
desc = cs[di] if di < len(cs) else ""
|
||||||
|
closed = "🔒" in cs[0]
|
||||||
|
elif line.startswith("- "):
|
||||||
|
m = LINK.search(line)
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
desc = re.sub(r"^\s*[-–—:]\s*", "", IMG.sub("", line[m.end():]).replace("🔒", "").strip())
|
||||||
|
closed = "🔒" in line
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"name": m.group(1),
|
||||||
|
"url": m.group(2),
|
||||||
|
"desc": IMG.sub("", desc).strip(),
|
||||||
|
"closed": closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def local_format(lines, start, end):
|
||||||
|
"""Return (kind, header, last_entry_index) or None for a local section."""
|
||||||
|
tbl = [i for i in range(start + 1, end) if lines[i].startswith("|")]
|
||||||
|
if tbl:
|
||||||
|
return ("table", cells(lines[tbl[0]]), tbl[-1], len(tbl))
|
||||||
|
lst = [i for i in range(start + 1, end) if lines[i].startswith("- ") and LINK.search(lines[i])]
|
||||||
|
if lst:
|
||||||
|
return ("list", None, lst[-1], len(lst))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def render(e, kind, header):
|
||||||
|
lock = " 🔒" if e["closed"] else ""
|
||||||
|
if kind == "list":
|
||||||
|
return f"- [{e['name']}]({e['url']}){lock} - {e['desc']}"
|
||||||
|
|
||||||
|
gh = GH.match(e["url"])
|
||||||
|
repo = gh.group(1) if gh else None
|
||||||
|
out = []
|
||||||
|
for i, h in enumerate(header):
|
||||||
|
hl = h.lower()
|
||||||
|
if i == 0:
|
||||||
|
c = f"[{e['name']}]({e['url']})"
|
||||||
|
if repo and hl in ("project name", "project"):
|
||||||
|
c += f" "
|
||||||
|
c += lock
|
||||||
|
elif hl.startswith("desc"):
|
||||||
|
c = e["desc"]
|
||||||
|
elif hl.startswith("last"):
|
||||||
|
c = f"" if repo else ""
|
||||||
|
elif hl.startswith("conn"):
|
||||||
|
c = "—"
|
||||||
|
else:
|
||||||
|
c = ""
|
||||||
|
out.append(c)
|
||||||
|
return "|" + "".join(f" {c} |" if c else " |" for c in out)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
mine = README.read_text(encoding="utf-8").split("\n")
|
||||||
|
theirs = fetch(UPSTREAM).split("\n")
|
||||||
|
|
||||||
|
known = {norm(u) for l in mine for _, u in LINK.findall(l)}
|
||||||
|
if IGNORE.exists():
|
||||||
|
known |= {norm(l) for l in IGNORE.read_text().splitlines() if l.strip()}
|
||||||
|
|
||||||
|
my_secs = sections(mine)
|
||||||
|
their_secs = sections(theirs)
|
||||||
|
|
||||||
|
inserts = defaultdict(list)
|
||||||
|
unplaced = []
|
||||||
|
|
||||||
|
for heading, ranges in their_secs.items():
|
||||||
|
for start, end in ranges:
|
||||||
|
their_header = table_header(theirs, start, end)
|
||||||
|
for line in theirs[start + 1:end]:
|
||||||
|
e = parse(line, their_header)
|
||||||
|
if not e or norm(e["url"]) in known:
|
||||||
|
continue
|
||||||
|
known.add(norm(e["url"]))
|
||||||
|
|
||||||
|
best = None
|
||||||
|
for s, t in my_secs.get(heading, []):
|
||||||
|
f = local_format(mine, s, t)
|
||||||
|
if f and (best is None or f[3] > best[3]):
|
||||||
|
best = f
|
||||||
|
if best is None:
|
||||||
|
unplaced.append((heading, line, "no matching section with entries"))
|
||||||
|
continue
|
||||||
|
kind, header, last, _ = best
|
||||||
|
inserts[last].append(render(e, kind, header))
|
||||||
|
|
||||||
|
out = []
|
||||||
|
for i, l in enumerate(mine):
|
||||||
|
out.append(l)
|
||||||
|
out.extend(inserts.get(i, []))
|
||||||
|
README.write_text("\n".join(out), encoding="utf-8")
|
||||||
|
|
||||||
|
added = sum(len(v) for v in inserts.values())
|
||||||
|
body = [f"Added {added} entr{'y' if added == 1 else 'ies'} from {UPSTREAM}.", "",
|
||||||
|
"Appended to the end of each section; sort before merging."]
|
||||||
|
if unplaced:
|
||||||
|
body += ["", "### Not placed", ""]
|
||||||
|
body += [f"- **{h}** ({why}): `{l}`" for h, l, why in unplaced]
|
||||||
|
pathlib.Path("/tmp/pr_body.md").write_text("\n".join(body) + "\n", encoding="utf-8")
|
||||||
|
print(f"added={added} unplaced={len(unplaced)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user