chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python3
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ruff: noqa: E501, RUF005
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Any
|
||||
from urllib import error, request
|
||||
|
||||
DRY_RUN = object()
|
||||
|
||||
|
||||
def compress_query(query: str) -> str:
|
||||
query = query.replace("\n", "")
|
||||
query = re.sub("\s+", " ", query)
|
||||
return query
|
||||
|
||||
|
||||
def post(url: str, body: Any | None = None, auth: tuple[str, str] | None = None):
|
||||
logging.info(f"Requesting POST to {url} with {body}")
|
||||
headers = {}
|
||||
req = request.Request(url, headers=headers, method="POST")
|
||||
if auth is not None:
|
||||
auth_str = base64.b64encode(f"{auth[0]}:{auth[1]}".encode())
|
||||
req.add_header("Authorization", f"Basic {auth_str.decode()}")
|
||||
|
||||
if body is None:
|
||||
body = ""
|
||||
|
||||
req.add_header("Content-Type", "application/json; charset=utf-8")
|
||||
data = json.dumps(body)
|
||||
data = data.encode("utf-8")
|
||||
req.add_header("Content-Length", len(data))
|
||||
|
||||
with request.urlopen(req, data) as response:
|
||||
return response.read()
|
||||
|
||||
|
||||
def dry_run_token(is_dry_run: bool) -> Any:
|
||||
if is_dry_run:
|
||||
return DRY_RUN
|
||||
return os.environ["GITHUB_TOKEN"]
|
||||
|
||||
|
||||
class GitHubRepo:
|
||||
GRAPHQL_URL = "https://api.github.com/graphql"
|
||||
|
||||
def __init__(self, user, repo, token, test_data=None):
|
||||
self.token = token
|
||||
self.user = user
|
||||
self.repo = repo
|
||||
self.test_data = test_data
|
||||
self.num_calls = 0
|
||||
self.base = f"https://api.github.com/repos/{user}/{repo}/"
|
||||
|
||||
def headers(self):
|
||||
return {
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
}
|
||||
|
||||
def dry_run(self) -> bool:
|
||||
return self.token == DRY_RUN
|
||||
|
||||
def graphql(self, query: str, variables: dict[str, str] | None = None) -> dict[str, Any]:
|
||||
query = compress_query(query)
|
||||
if variables is None:
|
||||
variables = {}
|
||||
|
||||
response = self._request(
|
||||
self.GRAPHQL_URL,
|
||||
{"query": query, "variables": variables},
|
||||
method="POST",
|
||||
)
|
||||
if self.dry_run():
|
||||
return self.testing_response("POST", self.GRAPHQL_URL)
|
||||
|
||||
if "data" not in response:
|
||||
msg = f"Error fetching data with query:\n{query}\n\nvariables:\n{variables}\n\nerror:\n{json.dumps(response, indent=2)}"
|
||||
raise RuntimeError(msg)
|
||||
return response
|
||||
|
||||
def testing_response(self, method: str, url: str) -> Any:
|
||||
self.num_calls += 1
|
||||
key = f"[{self.num_calls}] {method} - {url}"
|
||||
if self.test_data is not None and key in self.test_data:
|
||||
return self.test_data[key]
|
||||
logging.info(f"Unknown URL in dry run: {key}")
|
||||
return {}
|
||||
|
||||
def _request(self, full_url: str, body: dict[str, Any], method: str) -> dict[str, Any]:
|
||||
if self.dry_run():
|
||||
logging.info(f"Dry run, would have requested a {method} to {full_url} with {body}")
|
||||
return self.testing_response(method, full_url)
|
||||
|
||||
logging.info(f"Requesting {method} to {full_url} with {body}")
|
||||
req = request.Request(full_url, headers=self.headers(), method=method.upper())
|
||||
req.add_header("Content-Type", "application/json; charset=utf-8")
|
||||
data = json.dumps(body)
|
||||
data = data.encode("utf-8")
|
||||
req.add_header("Content-Length", len(data))
|
||||
|
||||
try:
|
||||
with request.urlopen(req, data) as response:
|
||||
content = response.read()
|
||||
except error.HTTPError as e:
|
||||
msg = str(e)
|
||||
error_data = e.read().decode()
|
||||
raise RuntimeError(f"Error response: {msg}\n{error_data}")
|
||||
|
||||
logging.info(f"Got response from {full_url}: {content}")
|
||||
try:
|
||||
response = json.loads(content)
|
||||
except json.decoder.JSONDecodeError:
|
||||
return content
|
||||
|
||||
return response
|
||||
|
||||
def put(self, url: str, data: dict[str, Any]) -> dict[str, Any]:
|
||||
return self._request(self.base + url, data, method="PUT")
|
||||
|
||||
def patch(self, url: str, data: dict[str, Any]) -> dict[str, Any]:
|
||||
return self._request(self.base + url, data, method="PATCH")
|
||||
|
||||
def post(self, url: str, data: dict[str, Any]) -> dict[str, Any]:
|
||||
return self._request(self.base + url, data, method="POST")
|
||||
|
||||
def get(self, url: str) -> dict[str, Any]:
|
||||
if self.dry_run():
|
||||
logging.info(f"Dry run, would have requested a GET to {url}")
|
||||
return self.testing_response("GET", url)
|
||||
url = self.base + url
|
||||
logging.info(f"Requesting GET to {url}")
|
||||
req = request.Request(url, headers=self.headers())
|
||||
with request.urlopen(req) as response:
|
||||
response = json.loads(response.read())
|
||||
return response
|
||||
|
||||
def delete(self, url: str) -> dict[str, Any]:
|
||||
if self.dry_run():
|
||||
logging.info(f"Dry run, would have requested a DELETE to {url}")
|
||||
return self.testing_response("DELETE", url)
|
||||
url = self.base + url
|
||||
logging.info(f"Requesting DELETE to {url}")
|
||||
req = request.Request(url, headers=self.headers(), method="DELETE")
|
||||
with request.urlopen(req) as response:
|
||||
response = json.loads(response.read())
|
||||
return response
|
||||
|
||||
|
||||
def parse_remote(remote: str) -> tuple[str, str]:
|
||||
"""
|
||||
Get a GitHub (user, repo) pair out of a git remote
|
||||
"""
|
||||
if remote.startswith("https://"):
|
||||
# Parse HTTP remote
|
||||
parts = remote.split("/")
|
||||
if len(parts) < 2:
|
||||
raise RuntimeError(f"Unable to parse remote '{remote}'")
|
||||
user, repo = parts[-2], parts[-1].replace(".git", "")
|
||||
else:
|
||||
# Parse SSH remote
|
||||
m = re.search(r":(.*)/(.*)\.git", remote)
|
||||
if m is None or len(m.groups()) != 2:
|
||||
raise RuntimeError(f"Unable to parse remote '{remote}'")
|
||||
user, repo = m.groups()
|
||||
|
||||
user = os.getenv("DEBUG_USER", user)
|
||||
repo = os.getenv("DEBUG_REPO", repo)
|
||||
return user, repo
|
||||
|
||||
|
||||
def git(command, **kwargs):
|
||||
command = ["git"] + command
|
||||
logging.info(f"Running {command}")
|
||||
proc = subprocess.run(command, stdout=subprocess.PIPE, encoding="utf-8", **kwargs)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"Command failed {command}:\nstdout:\n{proc.stdout}")
|
||||
return proc.stdout.strip()
|
||||
|
||||
|
||||
def find_ccs(body: str) -> list[str]:
|
||||
matches = re.findall(r"(cc( @[-A-Za-z0-9]+)+)", body, flags=re.MULTILINE)
|
||||
matches = [full for full, last in matches]
|
||||
|
||||
reviewers = []
|
||||
for match in matches:
|
||||
if match.startswith("cc "):
|
||||
match = match.replace("cc ", "")
|
||||
users = [x.strip() for x in match.split("@")]
|
||||
reviewers += users
|
||||
|
||||
reviewers = set(x for x in reviewers if x != "")
|
||||
return list(reviewers)
|
||||
Reference in New Issue
Block a user