Skip to content

Commit c987c68

Browse files
authored
Merge pull request #11652 from pradyunsg/rtd-redirects
Enable managing RTD redirects in-tree
2 parents 90db7b6 + 8328135 commit c987c68

File tree

5 files changed

+199
-0
lines changed

5 files changed

+199
-0
lines changed
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Update documentation redirects
2+
3+
on:
4+
push:
5+
branches: [main]
6+
schedule:
7+
- cron: 0 0 * * MON # Run every Monday at 00:00 UTC
8+
9+
env:
10+
FORCE_COLOR: "1"
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
update-rtd-redirects:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v3
21+
- uses: actions/setup-python@v4
22+
with:
23+
python-version: "3.11"
24+
- run: pip install httpx requests pyyaml
25+
- run: python tools/update-rtd-redirects.py
26+
env:
27+
RTD_API_TOKEN: ${{ secrets.RTD_API_TOKEN }}

.pre-commit-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ repos:
5252
'types-setuptools==57.4.14',
5353
'types-freezegun==1.1.9',
5454
'types-six==1.16.15',
55+
'types-pyyaml==6.0.12.2',
5556
]
5657

5758
- repo: https://github.com/pre-commit/pygrep-hooks

.readthedocs-custom-redirects.yml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# This file is read by tools/update-rtd-redirects.py.
2+
# It is related to Read the Docs, but is not a file processed by the platform.
3+
4+
/dev/news-entry-failure: >-
5+
https://pip.pypa.io/en/stable/development/contributing/#news-entries
6+
/errors/resolution-impossible: >-
7+
https://pip.pypa.io/en/latest/topics/dependency-resolution/#dealing-with-dependency-conflicts
8+
/surveys/backtracking: >-
9+
https://forms.gle/LkZP95S4CfqBAU1N6
10+
/warnings/backtracking: >-
11+
https://pip.pypa.io/en/stable/topics/dependency-resolution/#possible-ways-to-reduce-backtracking
12+
/warnings/enable-long-paths: >-
13+
https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later
14+
/warnings/venv: >-
15+
https://docs.python.org/3/tutorial/venv.html

MANIFEST.in

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ exclude .mailmap
1818
exclude .appveyor.yml
1919
exclude .readthedocs.yml
2020
exclude .pre-commit-config.yaml
21+
exclude .readthedocs-custom-redirects.yml
2122
exclude tox.ini
2223
exclude noxfile.py
2324

tools/update-rtd-redirects.py

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""Update the 'exact' redirects on Read the Docs to match an in-tree file's contents.
2+
3+
Relevant API reference: https://docs.readthedocs.io/en/stable/api/v3.html#redirects
4+
"""
5+
import operator
6+
import os
7+
import sys
8+
from pathlib import Path
9+
10+
import httpx
11+
import rich
12+
import yaml
13+
14+
try:
15+
_TOKEN = os.environ["RTD_API_TOKEN"]
16+
except KeyError:
17+
rich.print(
18+
"[bold]error[/]: [red]No API token provided. Please set `RTD_API_TOKEN`.[/]",
19+
file=sys.stderr,
20+
)
21+
sys.exit(1)
22+
23+
RTD_API_HEADERS = {"Authorization": f"token {_TOKEN}"}
24+
RTD_API_BASE_URL = "https://readthedocs.org/api/v3/projects/pip/"
25+
REPO_ROOT = Path(__file__).resolve().parent.parent
26+
27+
28+
# --------------------------------------------------------------------------------------
29+
# Helpers
30+
# --------------------------------------------------------------------------------------
31+
def next_step(msg: str) -> None:
32+
rich.print(f"> [blue]{msg}[/]")
33+
34+
35+
def log_response(response: httpx.Response) -> None:
36+
request = response.request
37+
rich.print(f"[bold magenta]{request.method}[/] {request.url} -> {response}")
38+
39+
40+
def get_rtd_api() -> httpx.Client:
41+
return httpx.Client(
42+
headers=RTD_API_HEADERS,
43+
base_url=RTD_API_BASE_URL,
44+
event_hooks={"response": [log_response]},
45+
)
46+
47+
48+
# --------------------------------------------------------------------------------------
49+
# Actual logic
50+
# --------------------------------------------------------------------------------------
51+
next_step("Loading local redirects from the yaml file.")
52+
53+
with open(REPO_ROOT / ".readthedocs-custom-redirects.yml") as f:
54+
local_redirects = yaml.safe_load(f)
55+
56+
rich.print("Loaded local redirects!")
57+
for src, dst in sorted(local_redirects.items()):
58+
rich.print(f" [yellow]{src}[/] --> {dst}")
59+
rich.print(f"{len(local_redirects)} entries.")
60+
61+
62+
next_step("Fetch redirects configured on RTD.")
63+
64+
with get_rtd_api() as rtd_api:
65+
response = rtd_api.get("redirects/")
66+
response.raise_for_status()
67+
68+
rtd_redirects = response.json()
69+
70+
for redirect in sorted(
71+
rtd_redirects["results"], key=operator.itemgetter("type", "from_url", "to_url")
72+
):
73+
if redirect["type"] != "exact":
74+
rich.print(f" [magenta]{redirect['type']}[/]")
75+
continue
76+
77+
pk = redirect["pk"]
78+
src = redirect["from_url"]
79+
dst = redirect["to_url"]
80+
rich.print(f" [yellow]{src}[/] -({pk:^5})-> {dst}")
81+
82+
rich.print(f"{rtd_redirects['count']} entries.")
83+
84+
85+
next_step("Compare and determine modifications.")
86+
87+
redirects_to_remove: list[int] = []
88+
redirects_to_add: dict[str, str] = {}
89+
90+
for redirect in rtd_redirects["results"]:
91+
if redirect["type"] != "exact":
92+
continue
93+
94+
rtd_src = redirect["from_url"]
95+
rtd_dst = redirect["to_url"]
96+
redirect_id = redirect["pk"]
97+
98+
if rtd_src not in local_redirects:
99+
redirects_to_remove.append(redirect_id)
100+
continue
101+
102+
local_dst = local_redirects[rtd_src]
103+
if local_dst != rtd_dst:
104+
redirects_to_remove.append(redirect_id)
105+
redirects_to_add[rtd_src] = local_dst
106+
107+
del local_redirects[rtd_src]
108+
109+
for src, dst in sorted(local_redirects.items()):
110+
redirects_to_add[src] = dst
111+
del local_redirects[src]
112+
113+
assert not local_redirects
114+
115+
if not redirects_to_remove:
116+
rich.print("Nothing to remove.")
117+
else:
118+
rich.print(f"To remove: ({len(redirects_to_remove)} entries)")
119+
for redirect_id in redirects_to_remove:
120+
rich.print(" ", redirect_id)
121+
122+
if not redirects_to_add:
123+
rich.print("Nothing to add.")
124+
else:
125+
rich.print(f"To add: ({len(redirects_to_add)} entries)")
126+
for src, dst in redirects_to_add.items():
127+
rich.print(f" {src} --> {dst}")
128+
129+
130+
next_step("Update the RTD redirects.")
131+
132+
if not (redirects_to_add or redirects_to_remove):
133+
rich.print("[green]Nothing to do![/]")
134+
sys.exit(0)
135+
136+
exit_code = 0
137+
with get_rtd_api() as rtd_api:
138+
for redirect_id in redirects_to_remove:
139+
response = rtd_api.delete(f"redirects/{redirect_id}/")
140+
response.raise_for_status()
141+
if response.status_code != 204:
142+
rich.print("[red]This might not have been removed correctly.[/]")
143+
exit_code = 1
144+
145+
for src, dst in redirects_to_add.items():
146+
response = rtd_api.post(
147+
"redirects/",
148+
json={"from_url": src, "to_url": dst, "type": "exact"},
149+
)
150+
response.raise_for_status()
151+
if response.status_code != 201:
152+
rich.print("[red]This might not have been added correctly.[/]")
153+
exit_code = 1
154+
155+
sys.exit(exit_code)

0 commit comments

Comments
 (0)