Skip to content

Commit 04ed9e3

Browse files
committed
shortener WIP
1 parent 11a05f1 commit 04ed9e3

File tree

2 files changed

+65
-37
lines changed

2 files changed

+65
-37
lines changed

links/shortener.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,25 @@
5757

5858
import itertools
5959
from collections.abc import Iterable, Iterator
60-
from typing import NamedTuple
60+
from typing import NamedTuple, TextIO
61+
from datetime import datetime
6162

6263

63-
class ShortenResult(NamedTuple):
64-
url: str
64+
class PathURL(NamedTuple):
6565
path: str
66-
new: bool
66+
url: str
67+
new: bool = False
6768

6869

69-
def parse_htaccess(text: str) -> Iterator[tuple[str, str]]:
70+
def parse_htaccess(text: str) -> Iterator[PathURL]:
7071
for line in text.splitlines():
7172
fields = line.split()
7273
if len(fields) >= 3 and fields[0] == 'RedirectTemp':
7374
path = fields[1]
7475
assert path[0] == '/', f'Missing /: {path!r}'
7576
path = path[1:] # Remove leading slash
7677
assert len(path) > 0, f'Root path in line {line!r}'
77-
yield (path, fields[2])
78+
yield PathURL(path, fields[2])
7879

7980

8081
def choose(a: str, b: str) -> str:
@@ -87,38 +88,43 @@ def key(k: str) -> tuple[int, bool, list[str]]:
8788
return min(a, b, key=key)
8889

8990

90-
def load_redirects(pairs: Iterable[tuple[str, str]]) -> tuple[dict, dict]:
91+
def load_redirects(pairs: Iterable[PathURL]) -> tuple[dict, dict]:
9192
redirects = {}
9293
targets = {}
93-
for short_url, url in pairs:
94-
url = redirects.setdefault(short_url, url)
95-
existing_short_url = targets.get(url)
96-
if existing_short_url is None:
97-
targets[url] = short_url
94+
for path, url, _new in pairs:
95+
url = redirects.setdefault(path, url)
96+
existing_path = targets.get(url)
97+
if existing_path is None:
98+
targets[url] = path
9899
else:
99-
targets[url] = choose(short_url, existing_short_url)
100+
targets[url] = choose(path, existing_path)
100101

101102
return redirects, targets
102103

103104

104105
NO_PATH = ''
105106

106-
def shorten_one(target: str, path_gen: Iterator[str], redirects: dict, targets: dict) -> ShortenResult:
107+
108+
def shorten_one(target: str, path_gen: Iterator[str], redirects: dict, targets: dict) -> PathURL:
107109
if path := targets.get(target, NO_PATH):
108-
return ShortenResult(target, path, False)
110+
return PathURL(path, target, False)
109111
path = next(path_gen)
110112
redirects[path] = target
111113
targets[target] = path
112-
return ShortenResult(target, path, True)
114+
return PathURL(path, target, True)
115+
116+
117+
def timestamp():
118+
return datetime.now().isoformat(sep=' ', timespec='seconds')
113119

114120

115-
def update_htaccess(f: file, srs: list[ShortenResult]) -> int:
121+
def update_htaccess(f: TextIO, directives: list[PathURL]) -> int:
116122
"""append new redirects, returns count of new redirects"""
117-
directives = [t for t in srs if t.new]
123+
directives = [d for d in directives if d.new]
118124
if directives:
119-
# xxx write timestamp, then...
120-
for url, path, _new in directives:
121-
f.write(f'RedirectTemp /{path} {url}')
125+
f.write(f'\n# appended {timestamp()}\n')
126+
for path, url, _new in directives:
127+
f.write(f'RedirectTemp /{path} {url}\n')
122128
return len(directives)
123129

124130

links/test_shortener.py

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import io
2+
from unittest import mock
3+
14
from pytest import mark
25

6+
import shortener # for mocking timestamp()
37
from shortener import parse_htaccess, choose, load_redirects
4-
from shortener import gen_short, gen_unused_short, shorten_one, ShortenResult
8+
from shortener import gen_short, gen_unused_short, shorten_one, PathURL
9+
from shortener import update_htaccess
510

611

712
SAMPLE_HTACCESS = """
@@ -23,21 +28,24 @@
2328

2429
FROZEN_TIME = '2025-06-07 01:02:03'
2530

26-
UPDATED_SAMPLE_HTACCESS = SAMPLE_HTACCESS + f"""
27-
# appended: {FROZEN_TIME}
31+
UPDATED_SAMPLE_HTACCESS = (
32+
SAMPLE_HTACCESS
33+
+ f"""
34+
# appended {FROZEN_TIME}
2835
RedirectTemp /23 https://new.site/
2936
RedirectTemp /24 https://other.new.site/
3037
"""
38+
)
3139

3240

3341
PARSED_SAMPLE_HTACCESS = [
34-
('book', 'https://www.oreilly.com/.../9781492056348/'),
35-
('home', 'https://www.fluentpython.com/'),
36-
('1-20', 'https://www.fluentpython.com/'),
37-
('ora', 'https://www.oreilly.com/.../9781492056348/'),
38-
('2-10', 'http://example.com/'),
39-
('10-2', 'http://example.com/'),
40-
('22', 'http://firstshortened.co')
42+
PathURL('book', 'https://www.oreilly.com/.../9781492056348/'),
43+
PathURL('home', 'https://www.fluentpython.com/'),
44+
PathURL('1-20', 'https://www.fluentpython.com/'),
45+
PathURL('ora', 'https://www.oreilly.com/.../9781492056348/'),
46+
PathURL('2-10', 'http://example.com/'),
47+
PathURL('10-2', 'http://example.com/'),
48+
PathURL('22', 'http://firstshortened.co'),
4149
]
4250

4351
# straightforward mapping of .htaccess; some targets may be duplicated.
@@ -60,7 +68,6 @@
6068
}
6169

6270

63-
6471
def test_parse_htaccess():
6572
res = list(parse_htaccess(SAMPLE_HTACCESS))
6673
assert res == PARSED_SAMPLE_HTACCESS
@@ -83,8 +90,8 @@ def test_choose(a, b, expected):
8390

8491

8592
def test_load_redirects():
86-
redirects, _ = load_redirects(PARSED_SAMPLE_HTACCESS)
87-
assert redirects == SAMPLE_REDIRECTS
93+
redirects, _ = load_redirects(PARSED_SAMPLE_HTACCESS)
94+
assert redirects == SAMPLE_REDIRECTS
8895

8996

9097
def test_load_redirect_targets():
@@ -99,8 +106,8 @@ def test_load_redirect_targets():
99106
('https://new.site/', '23', True),
100107
],
101108
)
102-
def test_shorten(target, path, new):
103-
expected = ShortenResult(target, path, new)
109+
def test_shorten_one(target, path, new):
110+
expected = PathURL(path, target, new)
104111
redirects = dict(SAMPLE_REDIRECTS)
105112
targets = dict(SAMPLE_TARGETS)
106113
result = shorten_one(target, gen_unused_short(redirects), redirects, targets)
@@ -118,8 +125,23 @@ def test_shorten(target, path, new):
118125
assert targets == SAMPLE_TARGETS
119126

120127

128+
def test_timestamp():
129+
with mock.patch('shortener.timestamp', return_value=FROZEN_TIME):
130+
assert shortener.timestamp() == FROZEN_TIME
131+
132+
121133
def test_update_htaccess():
122-
pass
134+
directives = [
135+
PathURL('home', 'https://www.fluentpython.com/', False),
136+
PathURL('23', 'https://new.site/', True),
137+
PathURL('24', 'https://other.new.site/', True)
138+
]
139+
given = io.StringIO(SAMPLE_HTACCESS)
140+
given.seek(0, io.SEEK_END) # emulate append mode
141+
with mock.patch('shortener.timestamp', return_value=FROZEN_TIME):
142+
res = update_htaccess(given, directives)
143+
assert res == 2
144+
assert given.getvalue() == UPDATED_SAMPLE_HTACCESS
123145

124146

125147
def test_gen_short():

0 commit comments

Comments
 (0)