44This script reads a `.htaccess` file and a plain text file with
55URLs (the target URLs).
66
7- It outputs a list of target URLs and their corresponding short URLs,
8- made from paths in the FPI.LI domain like `/2d`, `/2e`, etc.
7+ It outputs a list of target URLs and corresponding paths
8+ for short URLs in the FPI.LI domain like `/2d`, `/2e`, etc.
99This list is used to replace the target URLs with short URLs
1010in the `.adoc` files where the target URLs are used.
1111
1212If a target URL is not in the `.htaccess` file,
1313the script generates a new short URL
14- and appends a new `RedirectTemp` directive to the `.htaccess` file.
15-
16-
17- ## `.httaccess` file
18-
19- A file named `.htaccess` in this format is deployed to the web server
20- at FPY.LI to redirect short URLs to target URLs (the longer ones).
21-
22- ```
23- # added: 2025-05-26 16:01:24
24- RedirectTemp /2d https://mitpress.mit.edu/9780262111584/the-art-of-the-metaobject-protocol/
25- RedirectTemp /2e https://dabeaz.com/per.html
26- RedirectTemp /2f https://pythonfluente.com/2/#iter_closer_look
27-
28- ```
29-
30- When a user agent requests a URL like `https://fpy.li/2d`,
31- the web server responds with a 302 redirect to the longer URL
32- `https://mitpress.mit.edu/9780262111584/the-art-of-the-metaobject-protocol/`.
33-
34- A temporary redirect (code 302)
35- tells user agents to come back to the same URL at FPY.LI later,
36- and not update their bookmark.
37- This allows me update the target URL, if needed.
14+ and adds a new `RedirectTemp` directive to the `.htaccess` file,
15+ appending it in place with a timestamp.
3816
3917## Redirects in memory
4018
4826but the algorithm is more complicated.
4927
5028The same target URL can be mapped to multiple short paths
51- due to past mistakes when updating the `.htaccess` file.
29+ in `.htaccess` when the same target URL was added more
30+ than once with different short paths by mistake.
31+ We cannot fix these mistakes because the redundant
32+ short paths are printed in Fluent Python Second Edition.
5233
5334When loading the `.htaccess` file,
5435if a target URL is already in the `targets` dict,
5536we compare the existing short path with the new one
5637and save the shorter one in the `targets` dict.
5738
5839That way, we ensure that the shortest path is used for each target URL
59- in the list of replacements we output to apply to the `.adoc` files.
40+ in the list of replacements to apply to the `.adoc` files.
6041
6142
6243## Shortening URLs
6546
6647To shorten a target URL, find it in the `targets` dict.
6748If the target URL is found:
68- use the existing path.
49+ use the existing short path.
6950If the target URL is not found:
7051 generate a new short path;
7152 store target and path in both `targets` and `redirects` dicts;
7253 collect new short path and target URL in a `new_redirects` list
73- to be appended to the `.htaccess` file later.
74- Targets in memory
75-
76- To avoid generating a new short URL for a target URL,
77-
78-
79-
80- the `shortener` module provides a way to generate new short URLs
81-
82-
83- Procedure:
84-
85- 0. create empty dicts named targets and redirects
86- 1. given a target_url, find it in targets;
87- 1.1. if found, use the short_url stored there
88- 1.2. if not, generate new short_url and store it in targets and redirects
54+ to be appended to the `.htaccess` file at the end of the process.
8955
9056"""
9157
92-
58+ import itertools
9359from collections .abc import Iterable , Iterator
9460
9561
@@ -111,6 +77,7 @@ def key(k: str) -> tuple[int, bool, list[str]]:
11177 if len (parts ) > 1 :
11278 parts = [(f'z{ p :>08} ' if p .isnumeric () else p ) for p in parts ]
11379 return len (k ), '-' in k , parts
80+
11481 return min (a , b , key = key )
11582
11683
@@ -125,4 +92,23 @@ def load_redirects(pairs: Iterable[tuple[str, str]]) -> tuple[dict, dict]:
12592 else :
12693 targets [url ] = choose (short_url , existing_short_url )
12794
128- return redirects , targets
95+ return redirects , targets
96+
97+
98+ SDIGITS = '23456789abcdefghjkmnpqrstvwxyz'
99+
100+
101+ def gen_short (start_len = 1 ) -> Iterator [str ]:
102+ """Generate every possible sequence of SDIGITS, starting with start_len"""
103+ length = start_len
104+ while True :
105+ for digits in itertools .product (SDIGITS , repeat = length ):
106+ yield '' .join (digits )
107+ length += 1
108+
109+
110+ def gen_unused_short (redirects : dict ) -> Iterator [str ]:
111+ """Generate next available short URL of len >= 2."""
112+ for short in gen_short (2 ):
113+ if short not in redirects :
114+ yield short
0 commit comments