1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00
pip/tools/update-rtd-redirects.py
Pradyun Gedam fea8ae9f9f
Enable managing RTD redirects in-tree
This is designed as a script and a data file (in YAML format), and meant
to manage the RTD redirects with a version controlled file.

This makes it possible for pull requests to this repository to update
the redirects for this project's documentation (eg: for better error
urls) and for this evolution to be tracked as a part of version control
history.
2022-12-30 02:57:24 +00:00

156 lines
4.4 KiB
Python

"""Update the 'exact' redirects on Read the Docs to match an in-tree file's contents.
Relevant API reference: https://docs.readthedocs.io/en/stable/api/v3.html#redirects
"""
import operator
import os
import sys
from pathlib import Path
import httpx
import rich
import yaml
try:
_TOKEN = os.environ["RTD_API_TOKEN"]
except KeyError:
rich.print(
"[bold]error[/]: [red]No API token provided. Please set `RTD_API_TOKEN`.[/]",
file=sys.stderr,
)
sys.exit(1)
RTD_API_HEADERS = {"Authorization": f"token {_TOKEN}"}
RTD_API_BASE_URL = "https://readthedocs.org/api/v3/projects/pip/"
REPO_ROOT = Path(__file__).resolve().parent.parent
# --------------------------------------------------------------------------------------
# Helpers
# --------------------------------------------------------------------------------------
def next_step(msg: str) -> None:
rich.print(f"> [blue]{msg}[/]")
def log_response(response: httpx.Response) -> None:
request = response.request
rich.print(f"[bold magenta]{request.method}[/] {request.url} -> {response}")
def get_rtd_api() -> httpx.Client:
return httpx.Client(
headers=RTD_API_HEADERS,
base_url=RTD_API_BASE_URL,
event_hooks={"response": [log_response]},
)
# --------------------------------------------------------------------------------------
# Actual logic
# --------------------------------------------------------------------------------------
next_step("Loading local redirects from the yaml file.")
with open(REPO_ROOT / ".readthedocs-custom-redirects.yml") as f:
local_redirects = yaml.safe_load(f)
rich.print("Loaded local redirects!")
for src, dst in sorted(local_redirects.items()):
rich.print(f" [yellow]{src}[/] --> {dst}")
rich.print(f"{len(local_redirects)} entries.")
next_step("Fetch redirects configured on RTD.")
with get_rtd_api() as rtd_api:
response = rtd_api.get("redirects/")
response.raise_for_status()
rtd_redirects = response.json()
for redirect in sorted(
rtd_redirects["results"], key=operator.itemgetter("type", "from_url", "to_url")
):
if redirect["type"] != "exact":
rich.print(f" [magenta]{redirect['type']}[/]")
continue
pk = redirect["pk"]
src = redirect["from_url"]
dst = redirect["to_url"]
rich.print(f" [yellow]{src}[/] -({pk:^5})-> {dst}")
rich.print(f"{rtd_redirects['count']} entries.")
next_step("Compare and determine modifications.")
redirects_to_remove: list[int] = []
redirects_to_add: dict[str, str] = {}
for redirect in rtd_redirects["results"]:
if redirect["type"] != "exact":
continue
rtd_src = redirect["from_url"]
rtd_dst = redirect["to_url"]
redirect_id = redirect["pk"]
if rtd_src not in local_redirects:
redirects_to_remove.append(redirect_id)
continue
local_dst = local_redirects[rtd_src]
if local_dst != rtd_dst:
redirects_to_remove.append(redirect_id)
redirects_to_add[rtd_src] = local_dst
del local_redirects[rtd_src]
for src, dst in sorted(local_redirects.items()):
redirects_to_add[src] = dst
del local_redirects[src]
assert not local_redirects
if not redirects_to_remove:
rich.print("Nothing to remove.")
else:
rich.print(f"To remove: ({len(redirects_to_remove)} entries)")
for redirect_id in redirects_to_remove:
rich.print(" ", redirect_id)
if not redirects_to_add:
rich.print("Nothing to add.")
else:
rich.print(f"To add: ({len(redirects_to_add)} entries)")
for src, dst in redirects_to_add.items():
rich.print(f" {src} --> {dst}")
next_step("Update the RTD redirects.")
if not (redirects_to_add or redirects_to_remove):
rich.print("[green]Nothing to do![/]")
sys.exit(0)
exit_code = 0
with get_rtd_api() as rtd_api:
for redirect_id in redirects_to_remove:
response = rtd_api.delete(f"redirects/{redirect_id}/")
response.raise_for_status()
if response.status_code != 204:
rich.print("[red]This might not have been removed correctly.[/]")
exit_code = 1
for src, dst in redirects_to_add.items():
response = rtd_api.post(
"redirects/",
json={"from_url": src, "to_url": dst, "type": "exact"},
)
response.raise_for_status()
if response.status_code != 201:
rich.print("[red]This might not have been added correctly.[/]")
exit_code = 1
sys.exit(exit_code)