4242 run : |
4343 python - <<'PY'
4444 import os, re, csv, sys, requests
45- from requests.adapters import HTTPAdapter, Retry
45+ from requests.adapters import HTTPAdapter
46+ try:
47+ # urllib3 v1/v2 compatibility
48+ from urllib3.util.retry import Retry
49+ try:
50+ retry = Retry(
51+ total=8,
52+ backoff_factor=0.7,
53+ status_forcelist=[429,500,502,503,504],
54+ allowed_methods=frozenset(["GET","POST","HEAD","OPTIONS"])
55+ )
56+ except TypeError:
57+ # Older urllib3 uses method_whitelist
58+ retry = Retry(
59+ total=8,
60+ backoff_factor=0.7,
61+ status_forcelist=[429,500,502,503,504],
62+ method_whitelist=frozenset(["GET","POST","HEAD","OPTIONS"])
63+ )
64+ except Exception as e:
65+ print("Retry configuration error:", e, file=sys.stderr)
66+ retry = None
4667
4768 token = os.getenv("GITHUB_TOKEN")
4869 repo_full = os.getenv("REPO_FULL")
5980 "Accept": "application/vnd.github+json",
6081 "X-GitHub-Api-Version": "2022-11-28"
6182 })
62- s.mount("https://", HTTPAdapter(max_retries=Retry(
63- total=8, backoff_factor=0.7,
64- status_forcelist=[429,500,502,503,504],
65- allowed_methods=["GET","POST","HEAD","OPTIONS"]
66- )))
83+ if retry:
84+ s.mount("https://", HTTPAdapter(max_retries=retry))
6785
6886 MESSAGE_TEMPLATE = """Hello {first_name}
6987
@@ -90,29 +108,42 @@ Best regards,
90108The PyTorch PMO
91109" " "
92110
93- # ---------- Helpers ----------
111+ def api_get(url, **kwargs):
112+ r = s.get(url, **kwargs)
113+ if r.status_code != 200:
114+ print(f" GET {url} failed: {r.status_code} {r.text}", file=sys.stderr)
115+ sys.exit(1)
116+ return r
117+
118+ def api_post(url, json) :
119+ r = s.post(url, json=json)
120+ if r.status_code not in (200,201) :
121+ print(f"POST {url} failed : {r.status_code} {r.text}", file=sys.stderr)
122+ return r
123+
94124 def get_accepted_open_issues() :
125+ # labels param supports comma-separated labels; pass as-is to allow spaces
95126 url = f"https://api.github.com/repos/{owner}/{repo}/issues"
96127 params = {"state":"open", "labels":label_name, "per_page":100}
128+ first = True
97129 while url :
98- r = s.get(url, params=params if 'params' in locals() else None)
130+ r = s.get(url, params=params if first else None)
131+ first = False
99132 if r.status_code != 200 :
100- print(f"Failed to list issues : {r.status_code} {r.text}", file=sys.stderr); sys.exit(1)
101- for it in r.json() :
133+ print(f"List issues failed : {r.status_code} {r.text}", file=sys.stderr)
134+ sys.exit(1)
135+ data = r.json()
136+ for it in data :
102137 if "pull_request" not in it :
103138 yield it
104139 url = r.links.get("next",{}).get("url")
105- params = None
106140
107141 def parse_after_label(body:str, label:str) :
108- " " "
109- Finds the line immediately following a label header block:
110- Label
111- value
112- Returns '' if not found.
113- " " "
142+ # Matches:
143+ # Label
144+ # value
114145 pat = rf"{re.escape(label)}\s*\r?\n([^\n\r]+)"
115- m = re.search(pat, body, re.IGNORECASE)
146+ m = re.search(pat, body or "" , re.IGNORECASE)
116147 return m.group(1).strip() if m else ""
117148
118149 def extract_fields(issue) :
@@ -122,23 +153,17 @@ The PyTorch PMO
122153 location = parse_after_label(body, "City, State/Province, Country")
123154 gh_handle = parse_after_label(body, "Nominee's GitHub or GitLab Handle")
124155
125- # Normalize handle: if it's a URL like https://github.com/user -> take last path segment
126156 if gh_handle.startswith("http") :
127157 m = re.search(r"github\.com/([^/\s]+)", gh_handle, re.IGNORECASE)
128- if m :
129- gh_handle = m.group(1)
158+ if m : gh_handle = m.group(1)
130159
131- # First name: from nominee_name first token; fallback to profile name; then login
132- first_name = ""
160+ # First name from nominee name; fallback to login
133161 if nominee_name :
134162 first_name = nominee_name.split()[0].strip()
135- if not first_name :
163+ else :
136164 user = issue.get("user") or {}
137- prof_name = (user.get("name") or "").strip()
138- if prof_name :
139- first_name = prof_name.split()[0]
140- else :
141- first_name = (user.get("login") or "there")
165+ prof_name = (user.get("name") or "").strip() # usually empty on Issues API
166+ first_name = prof_name.split()[0] if prof_name else (user.get("login") or "there")
142167
143168 return {
144169 " issue_number " : issue["number"],
@@ -153,17 +178,14 @@ The PyTorch PMO
153178 if dry_run :
154179 print(f"DRY-RUN : would post to #{issue_number}:\n{body[:300]}...\n---")
155180 return
156- r = s.post(
157- f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}/comments",
158- json={"body" : body}
159- )
160- if r.status_code not in (200,201) :
161- print(f"Failed to comment on #{issue_number}: {r.status_code} {r.text}", file=sys.stderr)
181+ api_post(f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}/comments",
182+ json={"body" : body})
162183
163- # ---------- Process ----------
164184 rows = []
165185 count = 0
186+ any_issues = False
166187 for issue in get_accepted_open_issues() :
188+ any_issues = True
167189 data = extract_fields(issue)
168190 msg = MESSAGE_TEMPLATE.format(first_name=data["first_name"])
169191 post_comment(data["issue_number"], msg)
@@ -176,14 +198,16 @@ The PyTorch PMO
176198 data["github_handle"]
177199 ])
178200
179- # Export CSV
201+ if not any_issues :
202+ print(f"No open issues found with label '{label_name}'.")
203+ else :
204+ print(f"Posted to {count} accepted issue(s).")
205+
180206 out = "accepted_export.csv"
181207 with open(out, "w", newline="", encoding="utf-8") as f :
182208 w = csv.writer(f)
183209 w.writerow(["Issue", "Nominee Name", "Nominee Email", "Location", "GitHub Handle"])
184210 w.writerows(rows)
185-
186- print(f"Posted to {count} accepted issue(s).")
187211 print(f"Exported {len(rows)} row(s) -> {out}")
188212 PY
189213
0 commit comments