MIL-004: top 100 movies scraper #38

Merged
Tirsvad merged 7 commits from mil-004-top-100-movies into main 2026-10-28 07:33:37 +01:00
7 changed files with 242 additions and 0 deletions
Showing only changes of commit 05821ef069 - Show all commits
+105
View File
@@ -0,0 +1,105 @@
<!doctype html>
<html><head><meta charset="utf-8"><title>Empire best movies (headings excerpt)</title></head>
<body>
<!-- Only the 100 numbered headings of https://www.empireonline.com/movies/features/best-movies-2/ as fetched on 2026-10-28, markup unchanged. -->
<span class="content"><h2><strong>100) Avatar (2009)</strong></h2></span>
<span class="content"><h2><strong>99) Portrait Of A Lady On Fire (2020)</strong></h2></span>
<span class="content"><h2><strong>98) When Harry Met Sally... (1989)</strong></h2></span>
<span class="content"><h2><strong>97) Barbie (2023)</strong></h2></span>
<span class="content"><h2><strong>96) E.T. The Extra Terrestrial (1982)</strong></h2></span>
<span class="content"><h2><strong>95) Memento (2000)</strong></h2></span>
<span class="content"><h2><strong>94) Moonlight (2016)</strong></h2></span>
<span class="content"><h2><strong>93) Lost In Translation (2003)</strong></h2></span>
<span class="content"><h2><strong>92) One Flew Over The Cuckoo&#x27;s Nest (1975)</strong></h2></span>
<span class="content"><h2><strong>91) The Grand Budapest Hotel (2014)</strong></h2></span>
<span class="content"><h2><strong>90) Groundhog Day (1993)</strong></h2></span>
<span class="content"><h2><strong>89) American Psycho (2000)</strong></h2></span>
<span class="content"><h2><strong>88) The Piano (1993)</strong></h2></span>
<span class="content"><h2><strong>87) Raging Bull (1980)</strong></h2></span>
<span class="content"><h2><strong>86) Lady Bird (2017)</strong></h2></span>
<span class="content"><h2><strong>85) Rocky (1976)</strong></h2></span>
<span class="content"><h2><strong>84) Pan&#x27;s Labyrinth (2006)</strong></h2></span>
<span class="content"><h2><strong>83) Inglourious Basterds (2009)</strong></h2></span>
<span class="content"><h2><strong>82) Brokeback Mountain (2005)</strong></h2></span>
<span class="content"><h2><strong>81) Everything Everywhere All At Once (2022)</strong></h2></span>
<span class="content"><h2><strong>80) Spider-Man: Into The Spider-Verse (2018)</strong></h2></span>
<span class="content"><h2><strong>79) Paddington 2 (2017)</strong></h2></span>
<span class="content"><h2><strong>78) Ghostbusters (1984)</strong></h2></span>
<span class="content"><h2><strong>77) In The Mood For Love (2000)</strong></h2></span>
<span class="content"><h2><strong>76) Once Upon A Time In Hollywood (2019)</strong></h2></span>
<span class="content"><h2><strong>75) The Silence Of The Lambs (1991)</strong></h2></span>
<span class="content"><h2><strong>74) Trainspotting (1996)</strong></h2></span>
<span class="content"><h2><strong>73) The Lion King (1994)</strong></h2></span>
<span class="content"><h2><strong>72) Magnolia (1999)</strong></h2></span>
<span class="content"><h2><strong>71) Arrival (2016)</strong></h2></span>
<span class="content"><h2><strong>70) Children Of Men (2006)</strong></h2></span>
<span class="content"><h2><strong>69) The Babadook</strong> (2014)</h2></span>
<span class="content"><h2><strong>68) Avengers: Endgame (2019)</strong></h2></span>
<span class="content"><h2><strong>67) Do The Right Thing (1989)</strong></h2></span>
<span class="content"><h2><strong>66) Indiana Jones And The Last Crusade (1989)</strong></h2></span>
<span class="content"><h2><strong>65) Predator (1987)</strong></h2></span>
<span class="content"><h2><strong>64) Oppenheimer (2023)</strong></h2></span>
<span class="content"><h2><strong>63) The Big Lebowski (1998)</strong></h2></span>
<span class="content"><h2><strong>62) Eternal Sunshine Of The Spotless Mind (2004)</strong></h2></span>
<span class="content"><h2><strong>61) The Social Network (2010)</strong></h2></span>
<span class="content"><h2><strong>60) Fargo (1996)</strong></h2></span>
<span class="content"><h2><strong>59) The Exorcist (1973)</strong></h2></span>
<span class="content"><h2><strong>58) Titanic (1997)</strong></h2></span>
<span class="content"><h2><strong>57) Seven Samurai (1954)</strong></h2></span>
<span class="content"><h2><strong>56) Spirited Away (2001)</strong></h2></span>
<span class="content"><h2><strong>55) Hot Fuzz (2007)</strong></h2></span>
<span class="content"><h2><strong>54) Get Out (2017)</strong></h2></span>
<span class="content"><h2><strong>53) Vertigo (1958)</strong></h2></span>
<span class="content"><h2><strong>52) Citizen Kane (1941)</strong></h2></span>
<span class="content"><h2><strong>51) Rear Window (1954)</strong></h2></span>
<span class="content"><h2><strong>50) No Country For Old Men (2007)</strong></h2></span>
<span class="content"><h2><strong>49) Psycho (1960)</strong></h2></span>
<span class="content"><h2><strong>48) Avengers: Infinity War (2018)</strong></h2></span>
<span class="content"><h2><strong>47) It&#x27;s A Wonderful Life (1946)</strong></h2></span>
<span class="content"><h2><strong>46) Whiplash (2014)</strong></h2></span>
<span class="content"><h2><strong>45) Past Lives (2023)</strong></h2></span>
<span class="content"><h2><strong>44) Taxi Driver (1976)</strong></h2></span>
<span class="content"><h2><strong>43) La La Land (2016)</strong></h2></span>
<span class="content"><h2><strong>42) Saving Private Ryan (1998)</strong></h2></span>
<span class="content"><h2><strong>41) Schindler&#x27;s List (1993)</strong></h2></span>
<span class="content"><h2><strong>40) Mulholland Drive (2001)</strong></h2></span>
<span class="content"><h2><strong>39) Lawrence Of Arabia (1962)</strong></h2></span>
<span class="content"><h2><strong>38) The Good, The Bad And The Ugly (1966)</strong></h2></span>
<span class="content"><h2><strong>37) Fight Club (1999</strong>)</h2></span>
<span class="content"><h2><strong>36) Point Break (1991)</strong></h2></span>
<span class="content"><h2><strong>35) Mad Max: Fury Road (2015)</strong></h2></span>
<span class="content"><h2><strong>34) The Shining (1980)</strong></h2></span>
<span class="content"><h2><strong>33) The Lord Of The Rings: The Two Towers (2002)</strong></h2></span>
<span class="content"><h2><strong>32) Seven (1995)</strong></h2></span>
<span class="content"><h2><strong>31) The Thing (1982)</strong></h2></span>
<span class="content"><h2><strong>30) Casablanca (1942)</strong></h2></span>
<span class="content"><h2><strong>29) 12 Angry Men (1957)</strong></h2></span>
<span class="content"><h2><strong>28) Apocalypse Now (1979)</strong></h2></span>
<span class="content"><h2><strong>27) Parasite (2019)</strong></h2></span>
<span class="content"><h2><strong>26) Gladiator (2000)</strong></h2></span>
<span class="content"><h2><strong>25) 2001: A Space Odyssey (1968)</strong></h2></span>
<span class="content"><h2><strong>24) There Will Be Blood (2007)</strong></h2></span>
<span class="content"><h2><strong>23) Die Hard (1988)</strong></h2></span>
<span class="content"><h2><strong>22) The Matrix (1999)</strong></h2></span>
<span class="content"><h2><strong>21) The Lord Of The Rings: The Return Of The King (2003)</strong></h2></span>
<span class="content"><h2><strong>20) Heat (1995)</strong></h2></span>
<span class="content"><h2><strong>19) Interstellar (2014)</strong></h2></span>
<span class="content"><h2><strong>18) Terminator 2: Judgment Day (1991)</strong></h2></span>
<span class="content"><h2><strong>17) Alien (1979)</strong></h2></span>
<span class="content"><h2><strong>16) The Godfather Part II (1974)</strong></h2></span>
<span class="content"><h2><strong>15) Blade Runner (1982)</strong></h2></span>
<span class="content"><h2><strong>14) Raiders Of The Lost Ark (1981)</strong></h2></span>
<span class="content"><h2><strong>13) Jurassic Park (1993)</strong></h2></span>
<span class="content"><h2><strong>12) Back To The Future (1985)</strong></h2></span>
<span class="content"><h2><strong>11) Pulp Fiction (1994)</strong></h2></span>
<span class="content"><h2><strong>10) The Dark Knight (2008)</strong></h2></span>
<span class="content"><h2><strong>9) The Lord Of The Rings: The Fellowship Of The Ring (2001)</strong></h2></span>
<span class="content"><h2><strong>8) Star Wars: Episode IV – A New Hope (1977)</strong></h2></span>
<span class="content"><h2><strong>7) Goodfellas (1990)</strong></h2></span>
<span class="content"><h2><strong>6) Inception (2010)</strong></h2></span>
<span class="content"><h2><strong>5) Aliens (1986)</strong></h2></span>
<span class="content"><h2><strong>4) Star Wars: The Empire Strikes Back (1980)</strong></h2></span>
<span class="content"><h2><strong>3) The Shawshank Redemption (1994)</strong></h2></span>
<span class="content"><h2><strong>2) The Godfather (1972)</strong></h2></span>
<span class="content"><h2><strong>1) Jaws (1975)</strong></h2></span>
</body></html>
+27
View File
@@ -0,0 +1,27 @@
from pathlib import Path
import pytest
from scrape_movies.presentation import cli
def _run(target: Path, capsys: pytest.CaptureFixture[str]) -> str:
argv = ["movies", "write", "--source", "fixture", "--output", str(target)]
assert cli.main(argv) == 0
capsys.readouterr()
return target.read_text(encoding="utf-8")
def test_write_when_run_from_fixture_creates_100_ascending_lines(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
lines = _run(tmp_path / "movies.txt", capsys).splitlines()
assert len(lines) == 100
assert [int(line.split(")")[0]) for line in lines] == list(range(1, 101))
def test_write_when_run_twice_produces_identical_files(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
first = _run(tmp_path / "a.txt", capsys)
assert _run(tmp_path / "b.txt", capsys) == first
+7
View File
@@ -39,3 +39,10 @@ def test_main_when_every_hacker_news_step_runs_from_fixture_prints_output(
for step in hacker_news_steps.STEPS: for step in hacker_news_steps.STEPS:
assert cli.main(["hacker-news", step, "--source", "fixture"]) == 0 assert cli.main(["hacker-news", step, "--source", "fixture"]) == 0
assert capsys.readouterr().out.strip(), step assert capsys.readouterr().out.strip(), step
def test_main_when_movies_markup_from_fixture_prints_heading(
capsys: pytest.CaptureFixture[str],
) -> None:
assert cli.main(["movies", "markup", "--source", "fixture"]) == 0
assert "<h2>" in capsys.readouterr().out
+48
View File
@@ -0,0 +1,48 @@
from scrape_movies.constants import EMPIRE_FIXTURE_PATH
from scrape_movies.infrastructure import empire_movies
from scrape_movies.infrastructure.soup_basics import load_html
SNAPSHOT_LAYOUT = """
<h3 class="title">4) The Shawshank Redemption</h3>
<h3 class="title">3) The Dark Knight</h3>
<h3 class="other">not a title</h3>
<h3 class="title">2) The Empire Strikes Back</h3>
"""
def test_parse_titles_when_snapshot_layout_reads_h3_title_in_page_order() -> None:
assert empire_movies.parse_titles(SNAPSHOT_LAYOUT) == [
"4) The Shawshank Redemption",
"3) The Dark Knight",
"2) The Empire Strikes Back",
]
def test_parse_titles_when_live_layout_reads_ranked_h2_headings() -> None:
titles = empire_movies.parse_titles(load_html(EMPIRE_FIXTURE_PATH))
assert len(titles) == 100
assert titles[0].startswith("100) ")
assert titles[-1].startswith("1) ")
def test_parse_titles_when_h2_has_no_rank_ignores_it() -> None:
assert empire_movies.parse_titles("<h2>Jump to</h2><h2>2) Two</h2>") == ["2) Two"]
def test_parse_titles_when_page_has_no_titles_returns_empty() -> None:
assert empire_movies.parse_titles("<p>nothing</p>") == []
def test_first_title_markup_when_snapshot_layout_shows_h3() -> None:
assert empire_movies.first_title_markup(SNAPSHOT_LAYOUT).startswith(
'<h3 class="title">'
)
def test_first_title_markup_when_live_layout_shows_h2() -> None:
markup = empire_movies.first_title_markup(load_html(EMPIRE_FIXTURE_PATH))
assert markup.startswith("<h2>")
def test_first_title_markup_when_nothing_found_returns_empty() -> None:
assert empire_movies.first_title_markup("<p>x</p>") == ""
+18
View File
@@ -0,0 +1,18 @@
from scrape_movies.domain.movie import format_titles
def test_format_titles_when_titles_are_ranked_reverses_to_ascending() -> None:
page = ["3) Three", "2) Two", "1) One"]
assert format_titles(page) == ["1) One", "2) Two", "3) Three"]
def test_format_titles_when_titles_have_no_rank_numbers_them() -> None:
assert format_titles(["Third", "Second", "First"]) == [
"1) First",
"2) Second",
"3) Third",
]
def test_format_titles_when_list_is_empty_returns_empty() -> None:
assert format_titles([]) == []
+16
View File
@@ -0,0 +1,16 @@
from pathlib import Path
from scrape_movies.infrastructure.movie_file import write_movies
def test_write_movies_when_lines_given_writes_one_per_line(tmp_path: Path) -> None:
target = tmp_path / "movies.txt"
write_movies(["1) One", "2) Zoë"], target)
assert target.read_bytes() == "1) One\n2) Zoë\n".encode()
def test_write_movies_when_file_exists_replaces_it(tmp_path: Path) -> None:
target = tmp_path / "movies.txt"
target.write_text("old\n", encoding="utf-8")
write_movies(["1) New"], target)
assert target.read_text(encoding="utf-8") == "1) New\n"
+21
View File
@@ -1,3 +1,6 @@
from collections.abc import Sequence
from pathlib import Path
from scrape_movies.application import use_cases from scrape_movies.application import use_cases
from scrape_movies.domain.story import Story from scrape_movies.domain.story import Story
@@ -22,3 +25,21 @@ def test_find_top_story_when_stories_exist_returns_highest() -> None:
def test_find_top_story_when_page_is_empty_returns_none() -> None: def test_find_top_story_when_page_is_empty_returns_none() -> None:
assert use_cases.find_top_story(_fetch, lambda html: [], "http://x") is None assert use_cases.find_top_story(_fetch, lambda html: [], "http://x") is None
def test_save_movie_list_when_titles_are_descending_writes_ascending() -> None:
written: list[tuple[list[str], Path]] = []
def write(lines: Sequence[str], path: Path) -> None:
written.append((list(lines), path))
lines = use_cases.save_movie_list(
_fetch, lambda html: ["2) Two", "1) One"], write, "http://x", Path("m.txt")
)
assert list(lines) == ["1) One", "2) Two"]
assert written == [(["1) One", "2) Two"], Path("m.txt"))]
def test_scrape_movie_titles_when_called_keeps_page_order() -> None:
titles = use_cases.scrape_movie_titles(_fetch, lambda html: ["b", "a"], "http://x")
assert titles == ["b", "a"]