mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
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.
156 lines
4.4 KiB
Python
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)
|