From bcf7c71b966dde71c6b77593f0c86fcb4849b1e8 Mon Sep 17 00:00:00 2001 From: Thibault Ducret Date: Tue, 29 Sep 2026 14:04:05 +0200 Subject: [PATCH] Add location-based theater search --- allocine/__init__.py | 2 + allocine/api.py | 18 +- allocine/client.py | 266 ++++++- allocine/models.py | 11 +- ..._get_geocodes_lists_department_cities.json | 198 +++++ .../test_get_geocodes_lists_departments.json | 702 ++++++++++++++++++ .../test_search_theaters_by_department.json | 79 ++ .../test_search_theaters_by_zipcode.json | 30 + .../get_geocodes_lists_department_cities.json | 263 +++++++ .../get_geocodes_lists_departments.json | 91 +++ .../search_theaters_by_department.json | 177 +++++ .../cassettes/search_theaters_by_zipcode.json | 232 ++++++ ...arch_theaters_rejects_unknown_zipcode.json | 88 +++ tests/test_allocine.py | 35 + 14 files changed, 2186 insertions(+), 6 deletions(-) create mode 100644 tests/__snapshots__/test_allocine/test_get_geocodes_lists_department_cities.json create mode 100644 tests/__snapshots__/test_allocine/test_get_geocodes_lists_departments.json create mode 100644 tests/__snapshots__/test_allocine/test_search_theaters_by_department.json create mode 100644 tests/__snapshots__/test_allocine/test_search_theaters_by_zipcode.json create mode 100644 tests/cassettes/get_geocodes_lists_department_cities.json create mode 100644 tests/cassettes/get_geocodes_lists_departments.json create mode 100644 tests/cassettes/search_theaters_by_department.json create mode 100644 tests/cassettes/search_theaters_by_zipcode.json create mode 100644 tests/cassettes/search_theaters_rejects_unknown_zipcode.json diff --git a/allocine/__init__.py b/allocine/__init__.py index b30db02..5d81885 100644 --- a/allocine/__init__.py +++ b/allocine/__init__.py @@ -5,6 +5,7 @@ from allocine.api import AllocineApi from allocine.client import DEFAULT_DATE_FORMAT, Allocine from allocine.models import ( + Location, Movie, MovieVersion, Schedule, @@ -35,6 +36,7 @@ "Allocine", "AllocineApi", "DEFAULT_DATE_FORMAT", + "Location", "Movie", "MovieVersion", "Schedule", diff --git a/allocine/api.py b/allocine/api.py index 6301b12..403165e 100644 --- a/allocine/api.py +++ b/allocine/api.py @@ -60,8 +60,22 @@ def get_theater_page(self, theater_id: str) -> str: url = f"https://www.allocine.fr/seance/salle_gen_csalle={theater_id}.html" return self._get_text(url) - def get_theaterlist_by_geocode(self, geocode: int | str, page: int = 1) -> str: - url = f"{THEATERS_BASE_URL}/ville-{geocode}/" + def get_locationlist(self, department_geocode: int | None = None, page: int = 1) -> str: + if department_geocode is None: + return self._get_text("https://www.allocine.fr/salle/") + params = {"page": page} if page > 1 else None + return self._get_text(f"{THEATERS_BASE_URL}/departement-{department_geocode}/", params=params) + + def get_city_locations(self, search: str) -> dict: + return self._get(f"https://www.allocine.fr/_/localization_city/{search}") + + def get_theaterlist_by_geocode( + self, + geocode: int | str, + page: int = 1, + location_type: str = "ville", + ) -> str: + url = f"{THEATERS_BASE_URL}/{location_type}-{geocode}/" params = {"page": page} if page > 1 else None return self._get_text(url, params=params) diff --git a/allocine/client.py b/allocine/client.py index 90ad7ca..ba07c4a 100644 --- a/allocine/client.py +++ b/allocine/client.py @@ -1,6 +1,8 @@ +import base64 import json import re import unicodedata +from collections import Counter from datetime import date as Date from datetime import datetime, timedelta from os import PathLike @@ -10,10 +12,113 @@ from allocine.api import AllocineApi from allocine.cache import CacheOption -from allocine.models import MovieVersion, Showtime, Theater +from allocine.models import Location, MovieVersion, Showtime, Theater DEFAULT_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" +DEPARTMENT_NAMES = { + "01": "Ain", + "02": "Aisne", + "03": "Allier", + "04": "Alpes-de-Haute-Provence", + "05": "Hautes-Alpes", + "06": "Alpes-Maritimes", + "07": "Ardèche", + "08": "Ardennes", + "09": "Ariège", + "10": "Aube", + "11": "Aude", + "12": "Aveyron", + "13": "Bouches-du-Rhône", + "14": "Calvados", + "15": "Cantal", + "16": "Charente", + "17": "Charente-Maritime", + "18": "Cher", + "19": "Corrèze", + "2A": "Corse-du-Sud", + "2B": "Haute-Corse", + "21": "Côte-d'Or", + "22": "Côtes-d'Armor", + "23": "Creuse", + "24": "Dordogne", + "25": "Doubs", + "26": "Drôme", + "27": "Eure", + "28": "Eure-et-Loir", + "29": "Finistère", + "30": "Gard", + "31": "Haute-Garonne", + "32": "Gers", + "33": "Gironde", + "34": "Hérault", + "35": "Ille-et-Vilaine", + "36": "Indre", + "37": "Indre-et-Loire", + "38": "Isère", + "39": "Jura", + "40": "Landes", + "41": "Loir-et-Cher", + "42": "Loire", + "43": "Haute-Loire", + "44": "Loire-Atlantique", + "45": "Loiret", + "46": "Lot", + "47": "Lot-et-Garonne", + "48": "Lozère", + "49": "Maine-et-Loire", + "50": "Manche", + "51": "Marne", + "52": "Haute-Marne", + "53": "Mayenne", + "54": "Meurthe-et-Moselle", + "55": "Meuse", + "56": "Morbihan", + "57": "Moselle", + "58": "Nièvre", + "59": "Nord", + "60": "Oise", + "61": "Orne", + "62": "Pas-de-Calais", + "63": "Puy-de-Dôme", + "64": "Pyrénées-Atlantiques", + "65": "Hautes-Pyrénées", + "66": "Pyrénées-Orientales", + "67": "Bas-Rhin", + "68": "Haut-Rhin", + "69": "Rhône", + "70": "Haute-Saône", + "71": "Saône-et-Loire", + "72": "Sarthe", + "73": "Savoie", + "74": "Haute-Savoie", + "75": "Paris", + "76": "Seine-Maritime", + "77": "Seine-et-Marne", + "78": "Yvelines", + "79": "Deux-Sèvres", + "80": "Somme", + "81": "Tarn", + "82": "Tarn-et-Garonne", + "83": "Var", + "84": "Vaucluse", + "85": "Vendée", + "86": "Vienne", + "87": "Haute-Vienne", + "88": "Vosges", + "89": "Yonne", + "90": "Territoire de Belfort", + "91": "Essonne", + "92": "Hauts-de-Seine", + "93": "Seine-Saint-Denis", + "94": "Val-de-Marne", + "95": "Val-d'Oise", + "971": "Guadeloupe", + "972": "Martinique", + "973": "Guyane", + "974": "La Réunion", +} + class Allocine: def __init__( @@ -67,16 +172,81 @@ def get_showtimes( return showtimes - def search_theaters(self, geocode: int | str) -> list[Theater]: + def get_geocodes(self, dept: str | int | None = None) -> list[Location]: + if dept is None: + selector = Selector(text=self._client.get_locationlist()) + return _parse_departments(selector) + + department = self._find_department(dept) + if department.dept == "75": + return [ + Location( + geocode=department.geocode, + name=department.name, + location_type="city", + dept=department.dept, + zipcode="75000", + ) + ] + locations = [] + page = 1 + while True: + selector = Selector(text=self._client.get_locationlist(department.geocode, page=page)) + locations.extend(_parse_cities(selector, department.dept)) + if not selector.css(".button-right:not(.button-disabled)"): + return list({location.geocode: location for location in locations}.values()) + page += 1 + + def search_theaters( + self, + geocode: int | str | None = None, + dept: str | int | None = None, + zipcode: str | int | None = None, + ) -> list[Theater]: + if sum(value is not None for value in (geocode, dept, zipcode)) != 1: + raise ValueError("Exactly one of geocode, dept, or zipcode must be provided") + + location_type = "ville" + if dept is not None: + department = self._find_department(dept) + geocode = department.geocode + if department.dept != "75": + location_type = "departement" + elif zipcode is not None: + normalized_zipcode = str(zipcode).zfill(5) + response = self._client.get_city_locations(normalized_zipcode) + raw_cities = response.get("values", {}).get("cities", []) + location = next( + (item["node"] for item in raw_cities if item.get("node", {}).get("zip") == normalized_zipcode), + None, + ) + if location is None: + raise ValueError(f"No location found for zipcode {normalized_zipcode!r}") + geocode = location["id"] + + assert geocode is not None theaters = [] page = 1 while True: - selector = Selector(text=self._client.get_theaterlist_by_geocode(geocode=geocode, page=page)) + selector = Selector( + text=self._client.get_theaterlist_by_geocode( + geocode=geocode, + page=page, + location_type=location_type, + ) + ) theaters.extend(_parse_theaters(selector)) if not selector.css(".button-right:not(.button-disabled)"): return theaters page += 1 + def _find_department(self, dept: str | int) -> Location: + department_code = str(dept).upper().zfill(2) + department = next((item for item in self.get_geocodes() if item.dept == department_code), None) + if department is None: + raise ValueError(f"Department not found: {department_code!r}") + return department + def clear_cache(self) -> int: return self._client.clear_cache() @@ -218,6 +388,96 @@ def _person_name(person): return " ".join(part for part in (person.get("firstName"), person.get("lastName")) if part) +def _normalize_location_name(name: str) -> str: + name = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode() + return re.sub(r"[^a-z0-9]+", " ", name.lower()).strip() + + +DEPARTMENT_CODES = {_normalize_location_name(name): code for code, name in DEPARTMENT_NAMES.items()} +DEPARTMENT_CODES["reunion"] = "974" +DEPARTMENT_CODES["alpes de haute provence"] = "04" +DEPARTMENT_CODES["cotes d armor"] = "22" +DEPARTMENT_CODES["cote d or"] = "21" +DEPARTMENT_CODES["pyrenees atlantiques"] = "64" +DEPARTMENT_CODES["l eure et loir"] = "28" + + +def _location_path(selector: Selector) -> str | None: + if path := selector.attrib.get("href"): + return path + for class_name in (selector.attrib.get("class") or "").split(): + if not class_name.startswith("ACr"): + continue + try: + return base64.b64decode(class_name.replace("ACr", "")).decode() + except (ValueError, UnicodeDecodeError): + continue + return None + + +def _parse_departments(selector: Selector) -> list[Location]: + departments = [] + for item in selector.css(".mdl-more-item"): + path = _location_path(item) + match = re.search(r"/departement-(\d+)/", path or "") + if not match: + continue + name = " ".join((item.xpath("string()").get() or "").split()) + department_code = DEPARTMENT_CODES.get(_normalize_location_name(name)) + if department_code is None: + continue + departments.append( + Location( + geocode=int(match.group(1)), + name=name, + location_type="department", + dept=department_code, + zipcode=None, + ) + ) + if not any(department.dept == "75" for department in departments): + for item in selector.css(".mdl-more-item"): + path = _location_path(item) + name = " ".join((item.xpath("string()").get() or "").split()) + match = re.search(r"/ville-(\d+)/", path or "") + if match and name == "Paris": + departments.append( + Location( + geocode=int(match.group(1)), + name=name, + location_type="department", + dept="75", + zipcode=None, + ) + ) + break + return departments + + +def _parse_cities(selector: Selector, department_code: str) -> list[Location]: + cities = [] + for group in selector.css(".gd-col-left > .hred"): + item = group.css(".titlebar-link")[0] if group.css(".titlebar-link") else None + path = _location_path(item) if item is not None else None + match = re.search(r"/ville-(\d+)/", path or "") + if item is None or not match: + continue + zipcodes = [] + for raw_address in group.css("address.address").xpath("string()").getall(): + if zipcode_match := re.search(r"\b\d{5}\b", raw_address): + zipcodes.append(zipcode_match.group()) + cities.append( + Location( + geocode=int(match.group(1)), + name=" ".join((item.xpath("string()").get() or "").split()), + location_type="city", + dept=department_code, + zipcode=Counter(zipcodes).most_common(1)[0][0] if zipcodes else None, + ) + ) + return cities + + def _parse_theaters(selector: Selector) -> list[Theater]: theaters = [] for card in selector.css("div.theater-card"): diff --git a/allocine/models.py b/allocine/models.py index 6c488e8..005f8e5 100644 --- a/allocine/models.py +++ b/allocine/models.py @@ -4,7 +4,7 @@ from datetime import date as Date from datetime import datetime, timedelta from datetime import time as Time -from typing import List, Optional +from typing import List, Literal, Optional from allocine import nationalities @@ -197,6 +197,15 @@ def short_day_str(date: Date) -> str: return day_str(date)[:3] +@dataclass +class Location: + geocode: int + name: str + location_type: Literal["department", "city"] + dept: str + zipcode: str | None + + @dataclass class Theater: theater_id: str diff --git a/tests/__snapshots__/test_allocine/test_get_geocodes_lists_department_cities.json b/tests/__snapshots__/test_allocine/test_get_geocodes_lists_department_cities.json new file mode 100644 index 0000000..2ba3a97 --- /dev/null +++ b/tests/__snapshots__/test_allocine/test_get_geocodes_lists_department_cities.json @@ -0,0 +1,198 @@ +[ + { + "dept": "29", + "geocode": 95171, + "location_type": "city", + "name": "Brest", + "zipcode": "29200" + }, + { + "dept": "29", + "geocode": 95372, + "location_type": "city", + "name": "Quimper", + "zipcode": "29000" + }, + { + "dept": "29", + "geocode": 95198, + "location_type": "city", + "name": "Douarnenez", + "zipcode": "29100" + }, + { + "dept": "29", + "geocode": 95252, + "location_type": "city", + "name": "Landerneau", + "zipcode": "29800" + }, + { + "dept": "29", + "geocode": 95156, + "location_type": "city", + "name": "Audierne", + "zipcode": "29770" + }, + { + "dept": "29", + "geocode": 95174, + "location_type": "city", + "name": "Camaret-sur-Mer", + "zipcode": "29570" + }, + { + "dept": "29", + "geocode": 95175, + "location_type": "city", + "name": "Carantec", + "zipcode": "29660" + }, + { + "dept": "29", + "geocode": 95176, + "location_type": "city", + "name": "Carhaix-Plouguer", + "zipcode": "29270" + }, + { + "dept": "29", + "geocode": 95194, + "location_type": "city", + "name": "Crozon", + "zipcode": "29160" + }, + { + "dept": "29", + "geocode": 95232, + "location_type": "city", + "name": "Huelgoat", + "zipcode": "29690" + }, + { + "dept": "29", + "geocode": 95236, + "location_type": "city", + "name": "Île-Tudy", + "zipcode": "29980" + }, + { + "dept": "29", + "geocode": 95272, + "location_type": "city", + "name": "Lesneven", + "zipcode": "29260" + }, + { + "dept": "29", + "geocode": 95297, + "location_type": "city", + "name": "Moëlan-sur-Mer", + "zipcode": "29350" + }, + { + "dept": "29", + "geocode": 95303, + "location_type": "city", + "name": "Penmarch", + "zipcode": "29760" + }, + { + "dept": "29", + "geocode": 95329, + "location_type": "city", + "name": "Plouescat", + "zipcode": "29430" + }, + { + "dept": "29", + "geocode": 95334, + "location_type": "city", + "name": "Plougonvelin", + "zipcode": "29217" + }, + { + "dept": "29", + "geocode": 95373, + "location_type": "city", + "name": "Quimperlé", + "zipcode": "29300" + }, + { + "dept": "29", + "geocode": 95379, + "location_type": "city", + "name": "Roscoff", + "zipcode": "29680" + }, + { + "dept": "29", + "geocode": 95396, + "location_type": "city", + "name": "Saint-Pol-de-Léon", + "zipcode": "29250" + }, + { + "dept": "29", + "geocode": 95397, + "location_type": "city", + "name": "Saint-Renan", + "zipcode": "29290" + }, + { + "dept": "29", + "geocode": 95178, + "location_type": "city", + "name": "Châteaulin", + "zipcode": "29150" + }, + { + "dept": "29", + "geocode": 95254, + "location_type": "city", + "name": "Landivisiau", + "zipcode": "29400" + }, + { + "dept": "29", + "geocode": 95333, + "location_type": "city", + "name": "Plougastel-Daoulas", + "zipcode": "29470" + }, + { + "dept": "29", + "geocode": 95298, + "location_type": "city", + "name": "Morlaix", + "zipcode": "29600" + }, + { + "dept": "29", + "geocode": 95159, + "location_type": "city", + "name": "Bénodet", + "zipcode": "29950" + }, + { + "dept": "29", + "geocode": 95362, + "location_type": "city", + "name": "Pont-l'Abbé", + "zipcode": "29120" + }, + { + "dept": "29", + "geocode": 95191, + "location_type": "city", + "name": "Concarneau", + "zipcode": "29900" + }, + { + "dept": "29", + "geocode": 95349, + "location_type": "city", + "name": "Plourin-lès-Morlaix", + "zipcode": "29600" + } +] diff --git a/tests/__snapshots__/test_allocine/test_get_geocodes_lists_departments.json b/tests/__snapshots__/test_allocine/test_get_geocodes_lists_departments.json new file mode 100644 index 0000000..3475f93 --- /dev/null +++ b/tests/__snapshots__/test_allocine/test_get_geocodes_lists_departments.json @@ -0,0 +1,702 @@ +[ + { + "dept": "01", + "geocode": 83191, + "location_type": "department", + "name": "Ain", + "zipcode": null + }, + { + "dept": "02", + "geocode": 83178, + "location_type": "department", + "name": "Aisne", + "zipcode": null + }, + { + "dept": "03", + "geocode": 83111, + "location_type": "department", + "name": "Allier", + "zipcode": null + }, + { + "dept": "04", + "geocode": 83185, + "location_type": "department", + "name": "Alpes de Haute-Provence", + "zipcode": null + }, + { + "dept": "06", + "geocode": 83187, + "location_type": "department", + "name": "Alpes-Maritimes", + "zipcode": null + }, + { + "dept": "07", + "geocode": 83192, + "location_type": "department", + "name": "Ardèche", + "zipcode": null + }, + { + "dept": "08", + "geocode": 83129, + "location_type": "department", + "name": "Ardennes", + "zipcode": null + }, + { + "dept": "09", + "geocode": 83150, + "location_type": "department", + "name": "Ariège", + "zipcode": null + }, + { + "dept": "10", + "geocode": 83130, + "location_type": "department", + "name": "Aube", + "zipcode": null + }, + { + "dept": "11", + "geocode": 83138, + "location_type": "department", + "name": "Aude", + "zipcode": null + }, + { + "dept": "12", + "geocode": 83151, + "location_type": "department", + "name": "Aveyron", + "zipcode": null + }, + { + "dept": "67", + "geocode": 83104, + "location_type": "department", + "name": "Bas-Rhin", + "zipcode": null + }, + { + "dept": "13", + "geocode": 83188, + "location_type": "department", + "name": "Bouches-du-Rhône", + "zipcode": null + }, + { + "dept": "14", + "geocode": 83160, + "location_type": "department", + "name": "Calvados", + "zipcode": null + }, + { + "dept": "15", + "geocode": 83112, + "location_type": "department", + "name": "Cantal", + "zipcode": null + }, + { + "dept": "16", + "geocode": 83181, + "location_type": "department", + "name": "Charente", + "zipcode": null + }, + { + "dept": "17", + "geocode": 83182, + "location_type": "department", + "name": "Charente-Maritime", + "zipcode": null + }, + { + "dept": "18", + "geocode": 83123, + "location_type": "department", + "name": "Cher", + "zipcode": null + }, + { + "dept": "19", + "geocode": 83143, + "location_type": "department", + "name": "Corrèze", + "zipcode": null + }, + { + "dept": "2A", + "geocode": 265496, + "location_type": "department", + "name": "Corse-du-Sud", + "zipcode": null + }, + { + "dept": "21", + "geocode": 83115, + "location_type": "department", + "name": "Côte-d'or", + "zipcode": null + }, + { + "dept": "22", + "geocode": 83119, + "location_type": "department", + "name": "Côtes d'Armor", + "zipcode": null + }, + { + "dept": "23", + "geocode": 83144, + "location_type": "department", + "name": "Creuse", + "zipcode": null + }, + { + "dept": "79", + "geocode": 83183, + "location_type": "department", + "name": "Deux-Sèvres", + "zipcode": null + }, + { + "dept": "24", + "geocode": 83106, + "location_type": "department", + "name": "Dordogne", + "zipcode": null + }, + { + "dept": "25", + "geocode": 83134, + "location_type": "department", + "name": "Doubs", + "zipcode": null + }, + { + "dept": "26", + "geocode": 83193, + "location_type": "department", + "name": "Drôme", + "zipcode": null + }, + { + "dept": "91", + "geocode": 83168, + "location_type": "department", + "name": "Essonne", + "zipcode": null + }, + { + "dept": "27", + "geocode": 83163, + "location_type": "department", + "name": "Eure", + "zipcode": null + }, + { + "dept": "29", + "geocode": 83120, + "location_type": "department", + "name": "Finistère", + "zipcode": null + }, + { + "dept": "30", + "geocode": 83139, + "location_type": "department", + "name": "Gard", + "zipcode": null + }, + { + "dept": "32", + "geocode": 83153, + "location_type": "department", + "name": "Gers", + "zipcode": null + }, + { + "dept": "33", + "geocode": 83107, + "location_type": "department", + "name": "Gironde", + "zipcode": null + }, + { + "dept": "971", + "geocode": 83199, + "location_type": "department", + "name": "Guadeloupe", + "zipcode": null + }, + { + "dept": "973", + "geocode": 83201, + "location_type": "department", + "name": "Guyane", + "zipcode": null + }, + { + "dept": "68", + "geocode": 83105, + "location_type": "department", + "name": "Haut-Rhin", + "zipcode": null + }, + { + "dept": "2B", + "geocode": 83133, + "location_type": "department", + "name": "Haute-Corse", + "zipcode": null + }, + { + "dept": "31", + "geocode": 83152, + "location_type": "department", + "name": "Haute-Garonne", + "zipcode": null + }, + { + "dept": "43", + "geocode": 83113, + "location_type": "department", + "name": "Haute-Loire", + "zipcode": null + }, + { + "dept": "52", + "geocode": 83132, + "location_type": "department", + "name": "Haute-Marne", + "zipcode": null + }, + { + "dept": "70", + "geocode": 83136, + "location_type": "department", + "name": "Haute-Saône", + "zipcode": null + }, + { + "dept": "74", + "geocode": 83198, + "location_type": "department", + "name": "Haute-Savoie", + "zipcode": null + }, + { + "dept": "87", + "geocode": 83145, + "location_type": "department", + "name": "Haute-Vienne", + "zipcode": null + }, + { + "dept": "05", + "geocode": 83186, + "location_type": "department", + "name": "Hautes-Alpes", + "zipcode": null + }, + { + "dept": "65", + "geocode": 83155, + "location_type": "department", + "name": "Hautes-Pyrénées", + "zipcode": null + }, + { + "dept": "92", + "geocode": 83169, + "location_type": "department", + "name": "Hauts-de-Seine", + "zipcode": null + }, + { + "dept": "34", + "geocode": 83140, + "location_type": "department", + "name": "Hérault", + "zipcode": null + }, + { + "dept": "35", + "geocode": 83121, + "location_type": "department", + "name": "Ille-et-Vilaine", + "zipcode": null + }, + { + "dept": "36", + "geocode": 83125, + "location_type": "department", + "name": "Indre", + "zipcode": null + }, + { + "dept": "37", + "geocode": 83126, + "location_type": "department", + "name": "Indre-et-Loire", + "zipcode": null + }, + { + "dept": "38", + "geocode": 83194, + "location_type": "department", + "name": "Isère", + "zipcode": null + }, + { + "dept": "39", + "geocode": 83135, + "location_type": "department", + "name": "Jura", + "zipcode": null + }, + { + "dept": "28", + "geocode": 83124, + "location_type": "department", + "name": "L'Eure-et-Loir", + "zipcode": null + }, + { + "dept": "40", + "geocode": 83108, + "location_type": "department", + "name": "Landes", + "zipcode": null + }, + { + "dept": "41", + "geocode": 83127, + "location_type": "department", + "name": "Loir-et-Cher", + "zipcode": null + }, + { + "dept": "42", + "geocode": 83195, + "location_type": "department", + "name": "Loire", + "zipcode": null + }, + { + "dept": "44", + "geocode": 83173, + "location_type": "department", + "name": "Loire-Atlantique", + "zipcode": null + }, + { + "dept": "45", + "geocode": 83128, + "location_type": "department", + "name": "Loiret", + "zipcode": null + }, + { + "dept": "46", + "geocode": 83154, + "location_type": "department", + "name": "Lot", + "zipcode": null + }, + { + "dept": "47", + "geocode": 83109, + "location_type": "department", + "name": "Lot-et-Garonne", + "zipcode": null + }, + { + "dept": "48", + "geocode": 83141, + "location_type": "department", + "name": "Lozère", + "zipcode": null + }, + { + "dept": "49", + "geocode": 83174, + "location_type": "department", + "name": "Maine-et-Loire", + "zipcode": null + }, + { + "dept": "50", + "geocode": 83161, + "location_type": "department", + "name": "Manche", + "zipcode": null + }, + { + "dept": "51", + "geocode": 83131, + "location_type": "department", + "name": "Marne", + "zipcode": null + }, + { + "dept": "972", + "geocode": 83200, + "location_type": "department", + "name": "Martinique", + "zipcode": null + }, + { + "dept": "53", + "geocode": 83175, + "location_type": "department", + "name": "Mayenne", + "zipcode": null + }, + { + "dept": "54", + "geocode": 83146, + "location_type": "department", + "name": "Meurthe-et-Moselle", + "zipcode": null + }, + { + "dept": "55", + "geocode": 83147, + "location_type": "department", + "name": "Meuse", + "zipcode": null + }, + { + "dept": "56", + "geocode": 83122, + "location_type": "department", + "name": "Morbihan", + "zipcode": null + }, + { + "dept": "57", + "geocode": 83148, + "location_type": "department", + "name": "Moselle", + "zipcode": null + }, + { + "dept": "58", + "geocode": 83116, + "location_type": "department", + "name": "Nièvre", + "zipcode": null + }, + { + "dept": "59", + "geocode": 83158, + "location_type": "department", + "name": "Nord", + "zipcode": null + }, + { + "dept": "60", + "geocode": 83179, + "location_type": "department", + "name": "Oise", + "zipcode": null + }, + { + "dept": "61", + "geocode": 83162, + "location_type": "department", + "name": "Orne", + "zipcode": null + }, + { + "dept": "62", + "geocode": 83159, + "location_type": "department", + "name": "Pas-de-Calais", + "zipcode": null + }, + { + "dept": "63", + "geocode": 83114, + "location_type": "department", + "name": "Puy-de-Dôme", + "zipcode": null + }, + { + "dept": "64", + "geocode": 83110, + "location_type": "department", + "name": "Pyrénées Atlantiques", + "zipcode": null + }, + { + "dept": "66", + "geocode": 83142, + "location_type": "department", + "name": "Pyrénées-Orientales", + "zipcode": null + }, + { + "dept": "974", + "geocode": 83202, + "location_type": "department", + "name": "Réunion", + "zipcode": null + }, + { + "dept": "69", + "geocode": 83196, + "location_type": "department", + "name": "Rhône", + "zipcode": null + }, + { + "dept": "71", + "geocode": 83117, + "location_type": "department", + "name": "Saône-et-Loire", + "zipcode": null + }, + { + "dept": "72", + "geocode": 83176, + "location_type": "department", + "name": "Sarthe", + "zipcode": null + }, + { + "dept": "73", + "geocode": 83197, + "location_type": "department", + "name": "Savoie", + "zipcode": null + }, + { + "dept": "77", + "geocode": 83166, + "location_type": "department", + "name": "Seine-et-Marne", + "zipcode": null + }, + { + "dept": "76", + "geocode": 83164, + "location_type": "department", + "name": "Seine-Maritime", + "zipcode": null + }, + { + "dept": "93", + "geocode": 83170, + "location_type": "department", + "name": "Seine-Saint-Denis", + "zipcode": null + }, + { + "dept": "80", + "geocode": 83180, + "location_type": "department", + "name": "Somme", + "zipcode": null + }, + { + "dept": "81", + "geocode": 83156, + "location_type": "department", + "name": "Tarn", + "zipcode": null + }, + { + "dept": "82", + "geocode": 83157, + "location_type": "department", + "name": "Tarn-et-Garonne", + "zipcode": null + }, + { + "dept": "90", + "geocode": 83137, + "location_type": "department", + "name": "Territoire de Belfort", + "zipcode": null + }, + { + "dept": "95", + "geocode": 83172, + "location_type": "department", + "name": "Val-d'Oise", + "zipcode": null + }, + { + "dept": "94", + "geocode": 83171, + "location_type": "department", + "name": "Val-de-Marne", + "zipcode": null + }, + { + "dept": "83", + "geocode": 83189, + "location_type": "department", + "name": "Var", + "zipcode": null + }, + { + "dept": "84", + "geocode": 83190, + "location_type": "department", + "name": "Vaucluse", + "zipcode": null + }, + { + "dept": "85", + "geocode": 83177, + "location_type": "department", + "name": "Vendée", + "zipcode": null + }, + { + "dept": "86", + "geocode": 83184, + "location_type": "department", + "name": "Vienne", + "zipcode": null + }, + { + "dept": "88", + "geocode": 83149, + "location_type": "department", + "name": "Vosges", + "zipcode": null + }, + { + "dept": "89", + "geocode": 83118, + "location_type": "department", + "name": "Yonne", + "zipcode": null + }, + { + "dept": "78", + "geocode": 83167, + "location_type": "department", + "name": "Yvelines", + "zipcode": null + }, + { + "dept": "75", + "geocode": 115755, + "location_type": "department", + "name": "Paris", + "zipcode": null + } +] diff --git a/tests/__snapshots__/test_allocine/test_search_theaters_by_department.json b/tests/__snapshots__/test_allocine/test_search_theaters_by_department.json new file mode 100644 index 0000000..3eea641 --- /dev/null +++ b/tests/__snapshots__/test_allocine/test_search_theaters_by_department.json @@ -0,0 +1,79 @@ +[ + { + "address": "25 quai Gambetta", + "city": "Laval", + "name": "Cinéville Laval", + "theater_id": "P1140", + "zipcode": "53000" + }, + { + "address": "29 All. du Vieux Saint-Louis", + "city": "Laval", + "name": "Cinéma l'Avant-scène", + "theater_id": "W5300", + "zipcode": "53000" + }, + { + "address": "40, rue du Trianon", + "city": "Bourgneuf-la-Forêt", + "name": "Le Trianon", + "theater_id": "P0303", + "zipcode": "53410" + }, + { + "address": "6, rue Lelièvre", + "city": "Ernée", + "name": "Le Majestic", + "theater_id": "W0105", + "zipcode": "53500" + }, + { + "address": "Boulevard Faverie", + "city": "Gorron", + "name": "Gorron Cinéma", + "theater_id": "W0616", + "zipcode": "53120" + }, + { + "address": "Rue de Gesnes", + "city": "Montsûrs", + "name": "Le Majestic", + "theater_id": "W0617", + "zipcode": "53150" + }, + { + "address": "Place de l'Avant-Garde", + "city": "Renazé", + "name": "Vox", + "theater_id": "P0173", + "zipcode": "53800" + }, + { + "address": "12, rue du Pré de la Ville", + "city": "Saint-Pierre-des-Nids", + "name": "L'Aiglon", + "theater_id": "P9185", + "zipcode": "53370" + }, + { + "address": "3, place du Pilori", + "city": "Château-Gontier", + "name": "Le Palace", + "theater_id": "W0613", + "zipcode": "53200" + }, + { + "address": "34 rue de la fontaine", + "city": "Évron", + "name": "Cinéma Yves Robert", + "theater_id": "W0161", + "zipcode": "53600" + }, + { + "address": "16 place Juhel", + "city": "Mayenne", + "name": "Le Vox", + "theater_id": "P0295", + "zipcode": "53100" + } +] diff --git a/tests/__snapshots__/test_allocine/test_search_theaters_by_zipcode.json b/tests/__snapshots__/test_allocine/test_search_theaters_by_zipcode.json new file mode 100644 index 0000000..97c34cb --- /dev/null +++ b/tests/__snapshots__/test_allocine/test_search_theaters_by_zipcode.json @@ -0,0 +1,30 @@ +[ + { + "address": "10 Avenue Georges Clemenceau", + "city": "Brest", + "name": "Multiplexe Liberté", + "theater_id": "P0417", + "zipcode": "29200" + }, + { + "address": "136, rue Jean-Jaurès", + "city": "Brest", + "name": "Les Studios", + "theater_id": "P0153", + "zipcode": "29200" + }, + { + "address": "187, rue Jean-Jaures", + "city": "Brest", + "name": "CGR Brest Le Celtic", + "theater_id": "P0151", + "zipcode": "29200" + }, + { + "address": "25 Rue de Pontaniou", + "city": "Brest", + "name": "Pathé Capucins", + "theater_id": "W2920", + "zipcode": "29200" + } +] diff --git a/tests/cassettes/get_geocodes_lists_department_cities.json b/tests/cassettes/get_geocodes_lists_department_cities.json new file mode 100644 index 0000000..3a767d1 --- /dev/null +++ b/tests/cassettes/get_geocodes_lists_department_cities.json @@ -0,0 +1,263 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://www.allocine.fr/salle/", + "body": "", + "headers": { + "host": [ + "www.allocine.fr" + ], + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate, zstd" + ], + "connection": [ + "keep-alive" + ], + "user-agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Tue, 29 Sep 2026 11:37:28 GMT" + ], + "content-type": [ + "text/html; charset=UTF-8" + ], + "transfer-encoding": [ + "chunked" + ], + "connection": [ + "keep-alive" + ], + "cache-control": [ + "max-age=3600, public, stale-if-error=172800, stale-while-revalidate=86400" + ], + "link": [ + "; rel=\"preload\"; as=\"font\"; type=\"font/woff2\",; rel=\"preload\"; as=\"image\"; type=\"image/svg+xml\",; rel=\"preload\"; as=\"script\",; rel=\"preload\"; as=\"script\"" + ], + "vary": [ + "Accept-Encoding,Origin" + ], + "server": [ + "cloudflare" + ], + "via": [ + "1.1 google" + ], + "alt-svc": [ + "h3=\":443\"; ma=86400" + ], + "age": [ + "2814" + ], + "last-modified": [ + "Tue, 29 Sep 2026 10:50:34 GMT" + ], + "cf-cache-status": [ + "HIT" + ], + "nel": [ + "{\"report_to\":\"cf-nel\",\"success_fraction\":0.01,\"max_age\":604800}" + ], + "report-to": [ + "{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=55usAbsJfnVy1i%2Bw%2Bipp8nU6sV%2FsVNWICSmueV65%2FQFWTvbhQsURBfrpdQvHwgQobqmdS1ZXFl8pWJU8mf46boH52%2F96Akqz7jUMRYthnG1Kynii9I33z%2BKRFhsroaeoFA%3D%3D\"}]}" + ], + "cf-ray": [ + "a42ab2342f3fd626-CDG" + ], + "content-length": [ + "229476" + ] + }, + "body": { + "string": "\n\n\n \n \n \n \n Salles de cinéma en France - AlloCiné\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n\n \n \n \n\n \n\n \n \n \n \n \n \n \n\n\n\n\n\n\n
\n
\n\n \n\n \n \n \n \n\n\n\n\n\n
\n
\n
\n menu\n close2\n
\n\n \n \n \n \n
\n
\n \n
\n \n profil\n
\n
\n\n
\n
    \n
\n
\n
\n \n search\n
\n
\n\n \n\n\n \n
\n
Annuler
\n\n \n
\n
\n \n
\n
\n \n
\n
\n\n ok \n
\n
\n\n
\n Ex:\n\n Pinocchio, Mort sur le Nil, Meilleurs films émouvants\n\n
\n
\n \n \n\n \n \n \n
\n
\n
\n \n
\n\n
\n AccueilCinéma

\n Séances cinéma

\n \n
Séances cinéma
\n\n \n \n\n\n\n\n
\n
\n \n\n
\n
Rechercher un cinéma
\n\n \n\n
\n
\n
\n \n \n \n
\n
\n
\n
\n\n
\n\n\n\n
\n

Top villes

\n\n \n\n\n
\n \n
\n
Voir plus de villes
\n
\n \n \n
\n
\n
\n
\n\n
\n

Départements

\n \n\n\n
\n \n
\n
Voir plus de départements
\n
\n \n \n
\n
\n
\n
\n\n
\n

Les cinémas par circuit

\n
\n \n \n \n
\n \n
\n Pathé Cinémas\n
\n \n \n \n
\n \n
\n CGR\n
\n \n \n \n
\n \n
\n Cinéligue Champagne-Ardennes\n
\n \n \n \n
\n \n
\n Cinéligue Hauts-de-France\n
\n \n \n \n
\n \n
\n UGC\n
\n \n \n \n
\n \n
\n Cravlor Cinéligue\n
\n \n \n \n
\n \n
\n Cinémobile\n
\n \n \n \n
\n \n
\n Cinéfol31\n
\n \n \n \n
\n \n
\n Megarama\n
\n \n \n \n
\n \n
\n Balad Images 49 (Itinérant)\n
\n \n \n \n
\n \n
\n Cineco Itinérant\n
\n \n \n \n
\n \n
\n Ciné Passion\n
\n \n \n \n
\n \n
\n Cinémaginaire (Itinérant)\n
\n \n \n \n
\n \n
\n Ecran Village (Itinérant)\n
\n \n \n \n
\n \n
\n Noe Cinemas\n
\n \n \n \n
\n \n
\n Ciné-Off\n
\n \n \n \n
\n \n
\n Ciné Parc (Itinérant)\n
\n \n \n \n
\n \n
\n Réseau Génériques\n
\n \n \n \n
\n \n
\n Cinéville\n
\n \n \n \n
\n \n
\n La Strada Cinewest Itinérant\n
\n \n \n \n
\n \n
\n CIP (Cinémas Indépendants Parisiens)\n
\n \n \n \n
\n \n
\n Cinévasion 43\n
\n \n \n \n
\n \n
\n Kinepolis - FRA\n
\n \n \n \n
\n \n
\n Véo Cinémas\n
\n \n \n \n
\n \n
\n Ciné 32\n
\n \n \n \n
\n \n
\n Ciné Monts du Lyonnais - Itinérant\n
\n \n \n \n
\n \n
\n Cinémas Grand Ecran\n
\n \n \n \n
\n \n
\n Itinérant Mondes et multitudes\n
\n \n \n \n
\n \n
\n MK2 Cinémas\n
\n \n \n \n
\n \n
\n Artec cinémas\n
\n \n \n \n
\n \n
\n Cinewest\n
\n \n \n \n
\n \n
\n Ecran mobile FOL 74\n
\n \n \n \n
\n \n
\n Est Ensemble Bobigny\n
\n \n \n \n
\n \n
\n Ciné Méditerranée (Itinérant)\n
\n \n \n \n
\n \n
\n Cinémas Confluences\n
\n \n \n \n
\n \n
\n Majestic Cinémas\n
\n \n \n \n
\n \n
\n Cinecaraibes\n
\n \n \n \n
\n \n
\n Bauciné\n
\n \n \n \n
\n \n
\n Grand Club\n
\n \n \n \n
\n \n
\n Itinérant Alain Resnais\n
\n
\n
\n\n\n
\n \n
\n
\n\n \n \n \n \n \n \n\n\n\n \n \n \n \n \n
\n
\n \n \n
\n \n \n\n\n
\n \n\n \n
\n \n \n \n \n \n
\n\n
\n \n \n
\n
\n \n \n Back to Top\n\n \n
 
\n\n
\n \n
\n \n \n\n\n\n\n\n \n\n\n \n \n\n \n\n\n \n\n \n \n \n \n \n\n \n\n \n
\n\n \n\n\n \n \n \n \n\n\n
\n\n\n\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://www.allocine.fr/salle/cinema/departement-83120/", + "body": "", + "headers": { + "host": [ + "www.allocine.fr" + ], + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate, zstd" + ], + "connection": [ + "keep-alive" + ], + "user-agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Tue, 29 Sep 2026 11:37:29 GMT" + ], + "content-type": [ + "text/html; charset=UTF-8" + ], + "transfer-encoding": [ + "chunked" + ], + "connection": [ + "keep-alive" + ], + "cache-control": [ + "max-age=3600, public, stale-if-error=172800, stale-while-revalidate=86400" + ], + "link": [ + "; rel=\"preload\"; as=\"font\"; type=\"font/woff2\",; rel=\"preload\"; as=\"image\"; type=\"image/svg+xml\",; rel=\"preload\"; as=\"script\",; rel=\"preload\"; as=\"script\"" + ], + "vary": [ + "Accept-Encoding,Origin" + ], + "server": [ + "cloudflare" + ], + "via": [ + "1.1 google" + ], + "alt-svc": [ + "h3=\":443\"; ma=86400" + ], + "age": [ + "7548" + ], + "last-modified": [ + "Tue, 29 Sep 2026 09:31:40 GMT" + ], + "cf-cache-status": [ + "UPDATING" + ], + "nel": [ + "{\"report_to\":\"cf-nel\",\"success_fraction\":0.01,\"max_age\":604800}" + ], + "report-to": [ + "{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=XtZhCn6Sc4dVfNbDKhwQnJvW7R4zxwNvGe4yCcBynp%2BcEW8ky4N6rBqA%2BBhHFuhf7CLiqdQo2nkYtcjw9s4g0SJeaTMS9y9MaBUUuEkRs0D7H%2FufutYJTBIdTzK7h95xWw%3D%3D\"}]}" + ], + "cf-ray": [ + "a42ab2345f7ad626-CDG" + ], + "content-length": [ + "289191" + ] + }, + "body": { + "string": "\n\n\n \n \n \n \n Cinéma Finistère (29) - AlloCiné\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n\n \n \n \n\n \n\n \n \n \n \n \n \n \n\n\n\n\n\n\n
\n
\n\n \n\n \n \n \n \n\n\n\n\n\n
\n
\n
\n menu\n close2\n
\n\n \n \n \n \n
\n
\n \n
\n \n profil\n
\n
\n\n
\n
    \n
\n
\n
\n \n search\n
\n
\n\n \n\n\n \n
\n
Annuler
\n\n \n
\n
\n \n
\n
\n \n
\n
\n\n ok \n
\n
\n\n
\n Ex:\n\n Meilleurs thrillers psychologiques, Meilleurs buddy movies\n\n
\n
\n \n \n\n \n \n \n
\n
\n
\n \n
\n\n
\n AccueilCinémaSéances Cinéma

\n Cinéma Finistère : Séances cinéma et horaires

\n \n \n
\n
Salles de cinéma en Finistère
\n
\n\n \n \n\n\n\n\n
\n
\n \n \n \n
\n Trier par\n \n
\n nombre de cinéma\n \n
    \n
  • \n \n \n nombre de cinéma\n \n
  • \n
  • \n \n \n alphabétique\n \n
  • \n
\n
\n
\n \n
\n \n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Multiplexe\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Multiplexe Liberté \n

    \n\n \n \n
    \n\n \n
    10 Avenue Georges Clemenceau 29200 Brest
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 15 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"CGR\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n CGR Brest Le Celtic \n

    \n\n \n \n
    \n\n \n
    187, rue Jean-Jaures 29200 Brest
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n \n \n
    \n \n
    \n \n
    \n
    \n 8 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Pathé\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Pathé Capucins \n

    \n\n \n \n
    \n\n \n
    25 Rue de Pontaniou 29200 Brest
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 5 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Les\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Les Studios \n

    \n\n \n \n
    \n\n \n
    136, rue Jean-Jaurès 29200 Brest
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n \n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinéville\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Cinéville Quimper \n

    \n\n \n \n
    \n\n \n
    Parc de la Providence 29000 Quimper
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 10 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Katorza\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Katorza Quimper \n

    \n\n \n \n
    \n\n \n
    38, bd Dupleix 29000 Quimper
    \n \n \n
    \n \n
    \n\n \n
    \n
    \n 6 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Quai\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Quai Dupleix \n

    \n\n \n \n
    \n\n \n
    38 boulevard Dupleix 29000 Quimper
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 2 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n \n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"La\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n La Balise \n

    \n\n \n \n
    \n\n \n
    39, rue Louis-Pasteur 29100 Douarnenez
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 2 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Club \n

    \n\n \n \n
    \n\n \n
    39, rue Berthelot 29100 Douarnenez
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Landerneau

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Ciné\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Ciné Galaxy \n

    \n\n \n \n
    \n\n \n
    4 All. du Bois Noir 29800 Landerneau
    \n \n \n
    \n \n
    \n\n \n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Audierne

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Goyen \n

    \n\n \n \n
    \n\n \n
    13, rue Louis Pasteur 29770 Audierne
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Camaret-sur-Mer

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinéma\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Cinéma Rocamadour \n

    \n\n \n \n
    \n\n \n
    4 bis, rue du Roz 29570 Camaret-sur-Mer
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Carantec

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinéma\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Cinéma Etoile \n

    \n\n \n \n
    \n\n \n
    Place de l'Etoile 29660 Carantec
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Carhaix-Plouguer

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Grand Bleu \n

    \n\n \n \n
    \n\n \n
    Cinéma Le Grand Bleu rue Jean Monnet 29270 Carhaix
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Crozon

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Rex \n

    \n\n \n \n
    \n\n \n
    41, rue de la Chalotais 29160 Crozon
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Huelgoat

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Arthus\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Arthus Ciné \n

    \n\n \n \n
    \n\n \n
    Venelle de Carn Ar Guillou - accès place Aristide Briand 29690 Huelgoat
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Île-Tudy

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinéma\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Cinéma du port \n

    \n\n \n \n
    \n\n \n
    7 place Cale 29980 Île-Tudy
    \n \n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Lesneven

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinéma\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Cinéma Even \n

    \n\n \n \n
    \n\n \n
    18 rue Alsace Lorraine 29260 Lesneven
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Moëlan-sur-Mer

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Kerfany \n

    \n\n \n \n
    \n\n \n
    13 rue des Moulins 29350 Moëlan-sur-Mer
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Penmarch

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Eckmühl\"\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Eckmühl \n

    \n\n \n \n
    \n\n \n
    105 Av. de Skibbereen 29760 Penmarch
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Plouescat

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Dauphin \n

    \n\n \n \n
    \n\n \n
    10 rue Foch 29430 Plouescat
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n \n \n\n \n \n\n\n\n
\n \n
\n
\n\n \n \n \n \n \n \n\n\n\n \n \n \n \n \n
\n
\n \n \n
\n \n \n\n\n
\n \n\n \n
\n \n \n \n \n \n
\n\n
\n \n \n
\n
\n \n \n Back to Top\n\n \n
 
\n\n
\n \n
\n \n \n\n\n\n\n\n \n\n\n \n \n\n \n\n\n \n\n \n \n \n \n \n\n \n\n \n
\n\n \n\n\n \n \n \n \n\n\n
\n\n\n\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://www.allocine.fr/salle/cinema/departement-83120/?page=2", + "body": "", + "headers": { + "host": [ + "www.allocine.fr" + ], + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate, zstd" + ], + "connection": [ + "keep-alive" + ], + "user-agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Tue, 29 Sep 2026 11:37:29 GMT" + ], + "content-type": [ + "text/html; charset=UTF-8" + ], + "transfer-encoding": [ + "chunked" + ], + "connection": [ + "keep-alive" + ], + "cache-control": [ + "max-age=3600, public, stale-if-error=172800, stale-while-revalidate=86400" + ], + "link": [ + "; rel=\"preload\"; as=\"font\"; type=\"font/woff2\",; rel=\"preload\"; as=\"image\"; type=\"image/svg+xml\",; rel=\"preload\"; as=\"script\",; rel=\"preload\"; as=\"script\"" + ], + "vary": [ + "Accept-Encoding,Origin" + ], + "server": [ + "cloudflare" + ], + "via": [ + "1.1 google" + ], + "alt-svc": [ + "h3=\":443\"; ma=86400" + ], + "age": [ + "20125" + ], + "last-modified": [ + "Tue, 29 Sep 2026 06:02:03 GMT" + ], + "cf-cache-status": [ + "UPDATING" + ], + "nel": [ + "{\"report_to\":\"cf-nel\",\"success_fraction\":0.01,\"max_age\":604800}" + ], + "report-to": [ + "{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=STA7AkYV1h2%2BQpp0CWQM%2BI042X7vKo0Qs8qnPl9NAj9TDugHwhr0TgMSDJgsgQ81O3gg1EU2CcA%2FOb0pksy7vnGxDJIX3iVcr3kLxaTqM4FjAdNvoKp9Rx8Qz7iBf6l73g%3D%3D\"}]}" + ], + "cf-ray": [ + "a42ab234afb7d626-CDG" + ], + "content-length": [ + "268582" + ] + }, + "body": { + "string": "\n\n\n \n \n \n \n Salles de cinéma Finistère - Page 2 - AlloCiné\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n\n \n \n \n\n \n\n \n \n \n \n \n \n \n\n\n\n\n\n\n
\n
\n\n \n\n \n \n \n \n\n\n\n\n\n
\n
\n
\n menu\n close2\n
\n\n \n \n \n \n
\n
\n \n
\n \n profil\n
\n
\n\n
\n
    \n
\n
\n
\n \n search\n
\n
\n\n \n\n\n \n
\n
Annuler
\n\n \n
\n
\n \n
\n
\n \n
\n
\n\n ok \n
\n
\n\n
\n Ex:\n\n Meilleurs thrillers psychologiques, Meilleurs buddy movies\n\n
\n
\n \n \n\n \n \n \n
\n
\n
\n \n
\n\n
\n AccueilCinémaSéances CinémaCinéma Finistère

\n Cinéma Finistère : Séances cinéma et horaires - Page 2

\n \n \n
\n
Salles de cinéma en Finistère
\n
\n\n \n \n\n\n\n\n
\n
\n \n \n \n
\n Trier par\n \n
\n nombre de cinéma\n \n
    \n
  • \n \n \n nombre de cinéma\n \n
  • \n
  • \n \n \n alphabétique\n \n
  • \n
\n
\n
\n \n
\n

Plougonvelin

\n\n \n
\n
\n

Quimperlé

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"La\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n La Bobine \n

    \n\n \n \n
    \n\n \n
    18, rue Bremond d'Ars 29300 Quimperlé
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n \n \n
    \n \n
    \n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Roscoff

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Sainte\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Sainte Barbe \n

    \n\n \n \n
    \n\n \n
    14 bis rue Yan d'Argent 29680 Roscoff
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Saint-Pol-de-Léon

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinéma\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Cinéma Majestic \n

    \n\n \n \n
    \n\n \n
    Place Michel Colombe 29250 Saint-Pol-de-Léon
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Saint-Renan

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Bretagne \n

    \n\n \n \n
    \n\n \n
    18 rue Saint-Mathieu 29290 Saint-Renan
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Châteaulin

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Agora\"\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Agora \n

    \n\n \n \n
    \n\n \n
    5 Place de la Résistance 29150 Châteaulin
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 2 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Landivisiau

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Les\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Les Studios \n

    \n\n \n \n
    \n\n \n
    3, av. Foch 29400 Landivisiau
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 2 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Plougastel-Daoulas

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"L'Image\"\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n L'Image \n

    \n\n \n \n
    \n\n \n
    rue de saltash 29470 Plougastel-Daoulas
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 2 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Morlaix

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"La\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n La Salamandre \n

    \n\n \n \n
    \n\n \n
    39 Ter Quai du Léon 29600 Morlaix
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 3 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Bénodet

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinemarine\"\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Cinemarine \n

    \n\n \n \n
    \n\n \n
    PLAGE 29950 Bénodet
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 4 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Pont-l'Abbé

\n\n \n
\n
\n

Concarneau

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinéville\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Cinéville Concarneau \n

    \n\n \n \n
    \n\n \n
    Le Poteau Vert - Rue de Colguen 29900 Concarneau
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 5 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Plourin-lès-Morlaix

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinéville\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n \n\n \n
    ZAC Saint Fiacre 29600 Plourin-lès-Morlaix
    \n \n \n
    \n \n
    \n\n \n
    \n
    \n 6 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n \n \n\n \n \n\n\n\n
\n \n
\n
\n\n \n \n \n \n \n \n\n\n\n \n \n \n \n \n
\n
\n \n \n
\n \n \n\n\n
\n \n\n \n
\n \n \n \n \n \n
\n\n
\n \n \n
\n
\n \n \n Back to Top\n\n \n
 
\n\n
\n \n
\n \n \n\n\n\n\n\n \n\n\n \n \n\n \n\n\n \n\n \n \n \n \n \n\n \n\n \n
\n\n \n\n\n \n \n \n \n\n\n
\n\n\n\n" + } + } + } + ] +} diff --git a/tests/cassettes/get_geocodes_lists_departments.json b/tests/cassettes/get_geocodes_lists_departments.json new file mode 100644 index 0000000..a1de71f --- /dev/null +++ b/tests/cassettes/get_geocodes_lists_departments.json @@ -0,0 +1,91 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://www.allocine.fr/salle/", + "body": "", + "headers": { + "host": [ + "www.allocine.fr" + ], + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate, zstd" + ], + "connection": [ + "keep-alive" + ], + "user-agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Tue, 29 Sep 2026 11:37:28 GMT" + ], + "content-type": [ + "text/html; charset=UTF-8" + ], + "transfer-encoding": [ + "chunked" + ], + "connection": [ + "keep-alive" + ], + "cache-control": [ + "max-age=3600, public, stale-if-error=172800, stale-while-revalidate=86400" + ], + "link": [ + "; rel=\"preload\"; as=\"font\"; type=\"font/woff2\",; rel=\"preload\"; as=\"image\"; type=\"image/svg+xml\",; rel=\"preload\"; as=\"script\",; rel=\"preload\"; as=\"script\"" + ], + "vary": [ + "Accept-Encoding,Origin" + ], + "server": [ + "cloudflare" + ], + "via": [ + "1.1 google" + ], + "alt-svc": [ + "h3=\":443\"; ma=86400" + ], + "age": [ + "2814" + ], + "last-modified": [ + "Tue, 29 Sep 2026 10:50:34 GMT" + ], + "cf-cache-status": [ + "HIT" + ], + "nel": [ + "{\"report_to\":\"cf-nel\",\"success_fraction\":0.01,\"max_age\":604800}" + ], + "report-to": [ + "{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=fLnfOBBqO1poU0mYL0257a6pojXZi3aQY5iLoVyeCGc0zvPqyUbs70rSwQYM778gV0QHJFYsZKsP5i0auxKwNjfqGZMpR5C7%2FDjUQMk%2BqKJ7f2dqwBpJdhgZpL43u5JctQ%3D%3D\"}]}" + ], + "cf-ray": [ + "a42ab233bebcd626-CDG" + ], + "content-length": [ + "229476" + ] + }, + "body": { + "string": "\n\n\n \n \n \n \n Salles de cinéma en France - AlloCiné\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n\n \n \n \n\n \n\n \n \n \n \n \n \n \n\n\n\n\n\n\n
\n
\n\n \n\n \n \n \n \n\n\n\n\n\n
\n
\n
\n menu\n close2\n
\n\n \n \n \n \n
\n
\n \n
\n \n profil\n
\n
\n\n
\n
    \n
\n
\n
\n \n search\n
\n
\n\n \n\n\n \n
\n
Annuler
\n\n \n
\n
\n \n
\n
\n \n
\n
\n\n ok \n
\n
\n\n
\n Ex:\n\n Pinocchio, Mort sur le Nil, Meilleurs films émouvants\n\n
\n
\n \n \n\n \n \n \n
\n
\n
\n \n
\n\n
\n AccueilCinéma

\n Séances cinéma

\n \n
Séances cinéma
\n\n \n \n\n\n\n\n
\n
\n \n\n
\n
Rechercher un cinéma
\n\n \n\n
\n
\n
\n \n \n \n
\n
\n
\n
\n\n
\n\n\n\n
\n

Top villes

\n\n \n\n\n
\n \n
\n
Voir plus de villes
\n
\n \n \n
\n
\n
\n
\n\n
\n

Départements

\n \n\n\n
\n \n
\n
Voir plus de départements
\n
\n \n \n
\n
\n
\n
\n\n
\n

Les cinémas par circuit

\n
\n \n \n \n
\n \n
\n Pathé Cinémas\n
\n \n \n \n
\n \n
\n CGR\n
\n \n \n \n
\n \n
\n Cinéligue Champagne-Ardennes\n
\n \n \n \n
\n \n
\n Cinéligue Hauts-de-France\n
\n \n \n \n
\n \n
\n UGC\n
\n \n \n \n
\n \n
\n Cravlor Cinéligue\n
\n \n \n \n
\n \n
\n Cinémobile\n
\n \n \n \n
\n \n
\n Cinéfol31\n
\n \n \n \n
\n \n
\n Megarama\n
\n \n \n \n
\n \n
\n Balad Images 49 (Itinérant)\n
\n \n \n \n
\n \n
\n Cineco Itinérant\n
\n \n \n \n
\n \n
\n Ciné Passion\n
\n \n \n \n
\n \n
\n Cinémaginaire (Itinérant)\n
\n \n \n \n
\n \n
\n Ecran Village (Itinérant)\n
\n \n \n \n
\n \n
\n Noe Cinemas\n
\n \n \n \n
\n \n
\n Ciné-Off\n
\n \n \n \n
\n \n
\n Ciné Parc (Itinérant)\n
\n \n \n \n
\n \n
\n Réseau Génériques\n
\n \n \n \n
\n \n
\n Cinéville\n
\n \n \n \n
\n \n
\n La Strada Cinewest Itinérant\n
\n \n \n \n
\n \n
\n CIP (Cinémas Indépendants Parisiens)\n
\n \n \n \n
\n \n
\n Cinévasion 43\n
\n \n \n \n
\n \n
\n Kinepolis - FRA\n
\n \n \n \n
\n \n
\n Véo Cinémas\n
\n \n \n \n
\n \n
\n Ciné 32\n
\n \n \n \n
\n \n
\n Ciné Monts du Lyonnais - Itinérant\n
\n \n \n \n
\n \n
\n Cinémas Grand Ecran\n
\n \n \n \n
\n \n
\n Itinérant Mondes et multitudes\n
\n \n \n \n
\n \n
\n MK2 Cinémas\n
\n \n \n \n
\n \n
\n Artec cinémas\n
\n \n \n \n
\n \n
\n Cinewest\n
\n \n \n \n
\n \n
\n Ecran mobile FOL 74\n
\n \n \n \n
\n \n
\n Est Ensemble Bobigny\n
\n \n \n \n
\n \n
\n Ciné Méditerranée (Itinérant)\n
\n \n \n \n
\n \n
\n Cinémas Confluences\n
\n \n \n \n
\n \n
\n Majestic Cinémas\n
\n \n \n \n
\n \n
\n Cinecaraibes\n
\n \n \n \n
\n \n
\n Bauciné\n
\n \n \n \n
\n \n
\n Grand Club\n
\n \n \n \n
\n \n
\n Itinérant Alain Resnais\n
\n
\n
\n\n\n
\n \n
\n
\n\n \n \n \n \n \n \n\n\n\n \n \n \n \n \n
\n
\n \n \n
\n \n \n\n\n
\n \n\n \n
\n \n \n \n \n \n
\n\n
\n \n \n
\n
\n \n \n Back to Top\n\n \n
 
\n\n
\n \n
\n \n \n\n\n\n\n\n \n\n\n \n \n\n \n\n\n \n\n \n \n \n \n \n\n \n\n \n
\n\n \n\n\n \n \n \n \n\n\n
\n\n\n\n" + } + } + } + ] +} diff --git a/tests/cassettes/search_theaters_by_department.json b/tests/cassettes/search_theaters_by_department.json new file mode 100644 index 0000000..0bef771 --- /dev/null +++ b/tests/cassettes/search_theaters_by_department.json @@ -0,0 +1,177 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://www.allocine.fr/salle/", + "body": "", + "headers": { + "host": [ + "www.allocine.fr" + ], + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate, zstd" + ], + "connection": [ + "keep-alive" + ], + "user-agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Tue, 29 Sep 2026 11:37:29 GMT" + ], + "content-type": [ + "text/html; charset=UTF-8" + ], + "transfer-encoding": [ + "chunked" + ], + "connection": [ + "keep-alive" + ], + "cache-control": [ + "max-age=3600, public, stale-if-error=172800, stale-while-revalidate=86400" + ], + "link": [ + "; rel=\"preload\"; as=\"font\"; type=\"font/woff2\",; rel=\"preload\"; as=\"image\"; type=\"image/svg+xml\",; rel=\"preload\"; as=\"script\",; rel=\"preload\"; as=\"script\"" + ], + "vary": [ + "Accept-Encoding,Origin" + ], + "server": [ + "cloudflare" + ], + "via": [ + "1.1 google" + ], + "alt-svc": [ + "h3=\":443\"; ma=86400" + ], + "age": [ + "2814" + ], + "last-modified": [ + "Tue, 29 Sep 2026 10:50:34 GMT" + ], + "cf-cache-status": [ + "HIT" + ], + "nel": [ + "{\"report_to\":\"cf-nel\",\"success_fraction\":0.01,\"max_age\":604800}" + ], + "report-to": [ + "{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=EBO8e5VwFzO8DzIn1VolnBOArPTeDA3akTosVsx6HN%2B%2FIIQJ2mCug4l%2FHGqzJEC7gfCTNIpWBBrwiPf1H%2FmweHathPCbg0PBoVb3UZW5b6usPYxb9udvYB0m%2BkZxn7FzSA%3D%3D\"}]}" + ], + "cf-ray": [ + "a42ab235484ad626-CDG" + ], + "content-length": [ + "229476" + ] + }, + "body": { + "string": "\n\n\n \n \n \n \n Salles de cinéma en France - AlloCiné\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n\n \n \n \n\n \n\n \n \n \n \n \n \n \n\n\n\n\n\n\n
\n
\n\n \n\n \n \n \n \n\n\n\n\n\n
\n
\n
\n menu\n close2\n
\n\n \n \n \n \n
\n
\n \n
\n \n profil\n
\n
\n\n
\n
    \n
\n
\n
\n \n search\n
\n
\n\n \n\n\n \n
\n
Annuler
\n\n \n
\n
\n \n
\n
\n \n
\n
\n\n ok \n
\n
\n\n
\n Ex:\n\n Pinocchio, Mort sur le Nil, Meilleurs films émouvants\n\n
\n
\n \n \n\n \n \n \n
\n
\n
\n \n
\n\n
\n AccueilCinéma

\n Séances cinéma

\n \n
Séances cinéma
\n\n \n \n\n\n\n\n
\n
\n \n\n
\n
Rechercher un cinéma
\n\n \n\n
\n
\n
\n \n \n \n
\n
\n
\n
\n\n
\n\n\n\n
\n

Top villes

\n\n \n\n\n
\n \n
\n
Voir plus de villes
\n
\n \n \n
\n
\n
\n
\n\n
\n

Départements

\n \n\n\n
\n \n
\n
Voir plus de départements
\n
\n \n \n
\n
\n
\n
\n\n
\n

Les cinémas par circuit

\n
\n \n \n \n
\n \n
\n Pathé Cinémas\n
\n \n \n \n
\n \n
\n CGR\n
\n \n \n \n
\n \n
\n Cinéligue Champagne-Ardennes\n
\n \n \n \n
\n \n
\n Cinéligue Hauts-de-France\n
\n \n \n \n
\n \n
\n UGC\n
\n \n \n \n
\n \n
\n Cravlor Cinéligue\n
\n \n \n \n
\n \n
\n Cinémobile\n
\n \n \n \n
\n \n
\n Cinéfol31\n
\n \n \n \n
\n \n
\n Megarama\n
\n \n \n \n
\n \n
\n Balad Images 49 (Itinérant)\n
\n \n \n \n
\n \n
\n Cineco Itinérant\n
\n \n \n \n
\n \n
\n Ciné Passion\n
\n \n \n \n
\n \n
\n Cinémaginaire (Itinérant)\n
\n \n \n \n
\n \n
\n Ecran Village (Itinérant)\n
\n \n \n \n
\n \n
\n Noe Cinemas\n
\n \n \n \n
\n \n
\n Ciné-Off\n
\n \n \n \n
\n \n
\n Ciné Parc (Itinérant)\n
\n \n \n \n
\n \n
\n Réseau Génériques\n
\n \n \n \n
\n \n
\n Cinéville\n
\n \n \n \n
\n \n
\n La Strada Cinewest Itinérant\n
\n \n \n \n
\n \n
\n CIP (Cinémas Indépendants Parisiens)\n
\n \n \n \n
\n \n
\n Cinévasion 43\n
\n \n \n \n
\n \n
\n Kinepolis - FRA\n
\n \n \n \n
\n \n
\n Véo Cinémas\n
\n \n \n \n
\n \n
\n Ciné 32\n
\n \n \n \n
\n \n
\n Ciné Monts du Lyonnais - Itinérant\n
\n \n \n \n
\n \n
\n Cinémas Grand Ecran\n
\n \n \n \n
\n \n
\n Itinérant Mondes et multitudes\n
\n \n \n \n
\n \n
\n MK2 Cinémas\n
\n \n \n \n
\n \n
\n Artec cinémas\n
\n \n \n \n
\n \n
\n Cinewest\n
\n \n \n \n
\n \n
\n Ecran mobile FOL 74\n
\n \n \n \n
\n \n
\n Est Ensemble Bobigny\n
\n \n \n \n
\n \n
\n Ciné Méditerranée (Itinérant)\n
\n \n \n \n
\n \n
\n Cinémas Confluences\n
\n \n \n \n
\n \n
\n Majestic Cinémas\n
\n \n \n \n
\n \n
\n Cinecaraibes\n
\n \n \n \n
\n \n
\n Bauciné\n
\n \n \n \n
\n \n
\n Grand Club\n
\n \n \n \n
\n \n
\n Itinérant Alain Resnais\n
\n
\n
\n\n\n
\n \n
\n
\n\n \n \n \n \n \n \n\n\n\n \n \n \n \n \n
\n
\n \n \n
\n \n \n\n\n
\n \n\n \n
\n \n \n \n \n \n
\n\n
\n \n \n
\n
\n \n \n Back to Top\n\n \n
 
\n\n
\n \n
\n \n \n\n\n\n\n\n \n\n\n \n \n\n \n\n\n \n\n \n \n \n \n \n\n \n\n \n
\n\n \n\n\n \n \n \n \n\n\n
\n\n\n\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://www.allocine.fr/salle/cinema/departement-83175/", + "body": "", + "headers": { + "host": [ + "www.allocine.fr" + ], + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate, zstd" + ], + "connection": [ + "keep-alive" + ], + "user-agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Tue, 29 Sep 2026 11:37:29 GMT" + ], + "content-type": [ + "text/html; charset=UTF-8" + ], + "transfer-encoding": [ + "chunked" + ], + "connection": [ + "keep-alive" + ], + "cache-control": [ + "max-age=3600, public, stale-if-error=172800, stale-while-revalidate=86400" + ], + "link": [ + "; rel=\"preload\"; as=\"font\"; type=\"font/woff2\",; rel=\"preload\"; as=\"image\"; type=\"image/svg+xml\",; rel=\"preload\"; as=\"script\",; rel=\"preload\"; as=\"script\"" + ], + "vary": [ + "Accept-Encoding,Origin" + ], + "server": [ + "cloudflare" + ], + "via": [ + "1.1 google" + ], + "alt-svc": [ + "h3=\":443\"; ma=86400" + ], + "age": [ + "1555" + ], + "last-modified": [ + "Tue, 29 Sep 2026 11:11:33 GMT" + ], + "cf-cache-status": [ + "HIT" + ], + "nel": [ + "{\"report_to\":\"cf-nel\",\"success_fraction\":0.01,\"max_age\":604800}" + ], + "report-to": [ + "{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=TaXSGzlhpN%2FX1zv9CnqxylieIBEgHZxccGzzv5FqB%2BbjqF0nEfq4yHkgdeLsxi6wpPgUBQIxVcpOqY8O%2B29V%2BXQaxSlweUVFykyT0xOydKMs8WRtmR%2FvDFEB5paV%2FfxoPg%3D%3D\"}]}" + ], + "cf-ray": [ + "a42ab2357878d626-CDG" + ], + "content-length": [ + "258803" + ] + }, + "body": { + "string": "\n\n\n \n \n \n \n Cinéma Mayenne (53) - AlloCiné\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n\n \n \n \n\n \n\n \n \n \n \n \n \n \n\n\n\n\n\n\n
\n
\n\n \n\n \n \n \n \n\n\n\n\n\n
\n
\n
\n menu\n close2\n
\n\n \n \n \n \n
\n
\n \n
\n \n profil\n
\n
\n\n
\n
    \n
\n
\n
\n \n search\n
\n
\n\n \n\n\n \n
\n
Annuler
\n\n \n
\n
\n \n
\n
\n \n
\n
\n\n ok \n
\n
\n\n
\n Ex:\n\n Meilleurs thrillers psychologiques, Meilleurs buddy movies\n\n
\n
\n \n \n\n \n \n \n
\n
\n
\n \n
\n\n
\n AccueilCinémaSéances Cinéma

\n Cinéma Mayenne : Séances cinéma et horaires

\n \n \n
\n
Salles de cinéma en Mayenne
\n
\n\n \n \n\n\n\n\n
\n
\n \n \n \n
\n Trier par\n \n
\n nombre de cinéma\n \n
    \n
  • \n \n \n nombre de cinéma\n \n
  • \n
  • \n \n \n alphabétique\n \n
  • \n
\n
\n
\n \n
\n \n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinéville\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Cinéville Laval \n

    \n\n \n \n
    \n\n \n
    25 quai Gambetta 53000 Laval
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 9 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinéma\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Cinéma l'Avant-scène \n

    \n\n \n \n
    \n\n \n
    29 All. du Vieux Saint-Louis 53000 Laval
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Bourgneuf-la-Forêt

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Trianon \n

    \n\n \n \n
    \n\n \n
    40, rue du Trianon 53410 Bourgneuf-la-Forêt
    \n \n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Ernée

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Majestic \n

    \n\n \n \n
    \n\n \n
    6, rue Lelièvre 53500 Ernée
    \n \n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Gorron

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Gorron\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Gorron Cinéma \n

    \n\n \n \n
    \n\n \n
    Boulevard Faverie 53120 Gorron
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Montsûrs

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Majestic \n

    \n\n \n \n
    \n\n \n
    Rue de Gesnes 53150 Montsûrs
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Renazé

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Vox\"\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Vox \n

    \n\n \n \n
    \n\n \n
    Place de l'Avant-Garde 53800 Renazé
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Saint-Pierre-des-Nids

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"L'Aiglon\"\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n L'Aiglon \n

    \n\n \n \n
    \n\n \n
    12, rue du Pré de la Ville 53370 Saint-Pierre-des-Nids
    \n \n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Château-Gontier

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Palace \n

    \n\n \n \n
    \n\n \n
    3, place du Pilori 53200 Château-Gontier
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 3 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Évron

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Cinéma\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Cinéma Yves Robert \n

    \n\n \n \n
    \n\n \n
    34 rue de la fontaine 53600 Évron
    \n \n \n
    \n \n
    \n\n \n
    \n
    \n 3 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n
\n

Mayenne

\n\n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Le\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Le Vox \n

    \n\n \n \n
    \n\n \n
    16 place Juhel 53100 Mayenne
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 3 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
\n
\n \n \n\n\n \n\n\n\n
\n \n
\n
\n\n \n \n \n \n \n \n\n\n\n \n \n \n \n \n
\n
\n \n \n
\n \n \n\n\n
\n \n\n \n
\n \n \n \n \n \n
\n\n
\n \n \n
\n
\n \n \n Back to Top\n\n \n
 
\n\n
\n \n
\n \n \n\n\n\n\n\n \n\n\n \n \n\n \n\n\n \n\n \n \n \n \n \n\n \n\n \n
\n\n \n\n\n \n \n \n \n\n\n
\n\n\n\n" + } + } + } + ] +} diff --git a/tests/cassettes/search_theaters_by_zipcode.json b/tests/cassettes/search_theaters_by_zipcode.json new file mode 100644 index 0000000..2c022dd --- /dev/null +++ b/tests/cassettes/search_theaters_by_zipcode.json @@ -0,0 +1,232 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://www.allocine.fr/_/localization_city/29200", + "body": "", + "headers": { + "host": [ + "www.allocine.fr" + ], + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate, zstd" + ], + "connection": [ + "keep-alive" + ], + "user-agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Tue, 29 Sep 2026 11:37:29 GMT" + ], + "content-type": [ + "application/json" + ], + "transfer-encoding": [ + "chunked" + ], + "connection": [ + "keep-alive" + ], + "cache-control": [ + "max-age=86400, public, stale-if-error=172800, stale-while-revalidate=86400" + ], + "vary": [ + "Origin, accept-encoding" + ], + "server": [ + "cloudflare" + ], + "via": [ + "1.1 google" + ], + "alt-svc": [ + "h3=\":443\"; ma=86400" + ], + "last-modified": [ + "Tue, 29 Sep 2026 11:37:29 GMT" + ], + "cf-cache-status": [ + "MISS" + ], + "nel": [ + "{\"report_to\":\"cf-nel\",\"success_fraction\":0.01,\"max_age\":604800}" + ], + "report-to": [ + "{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=FcR%2Bv9S%2BHPfZBmcp6exrPKW0gryfcWm39ha8fNcnMuM3FOmkG7kJE%2BCFodpH9%2F%2Bew%2BDUuOdzCSS4rG7%2F80yaYl8QPaIUxdPIOO32cGp%2FVdgKQOeZKjyMcCLCg7LYzHe8xw%3D%3D\"}]}" + ], + "cf-ray": [ + "a42ab2365952d626-CDG" + ], + "content-length": [ + "719" + ] + }, + "body": { + "values": { + "cities": [ + { + "node": { + "__typename": "City", + "id": 95171, + "name": "Brest", + "zip": "29200" + } + } + ], + "theaters": [ + { + "node": { + "__typename": "Theater", + "id": "VGhlYXRlcjpQMDE1MQ==", + "internalId": "P0151", + "name": "CGR Brest Le Celtic", + "location": { + "zip": "29200", + "city": "Brest" + } + } + }, + { + "node": { + "__typename": "Theater", + "id": "VGhlYXRlcjpQMDE1Mw==", + "internalId": "P0153", + "name": "Les Studios", + "location": { + "zip": "29200", + "city": "Brest" + } + } + }, + { + "node": { + "__typename": "Theater", + "id": "VGhlYXRlcjpQMDQxNw==", + "internalId": "P0417", + "name": "Multiplexe Liberté", + "location": { + "zip": "29200", + "city": "Brest" + } + } + }, + { + "node": { + "__typename": "Theater", + "id": "VGhlYXRlcjpXMjkyMA==", + "internalId": "W2920", + "name": "Pathé Capucins", + "location": { + "zip": "29200", + "city": "Brest" + } + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://www.allocine.fr/salle/cinema/ville-95171/", + "body": "", + "headers": { + "host": [ + "www.allocine.fr" + ], + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate, zstd" + ], + "connection": [ + "keep-alive" + ], + "user-agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Tue, 29 Sep 2026 11:37:29 GMT" + ], + "content-type": [ + "text/html; charset=UTF-8" + ], + "transfer-encoding": [ + "chunked" + ], + "connection": [ + "keep-alive" + ], + "cache-control": [ + "max-age=3600, public, stale-if-error=172800, stale-while-revalidate=86400" + ], + "link": [ + "; rel=\"preload\"; as=\"font\"; type=\"font/woff2\",; rel=\"preload\"; as=\"image\"; type=\"image/svg+xml\",; rel=\"preload\"; as=\"script\",; rel=\"preload\"; as=\"script\"" + ], + "vary": [ + "Accept-Encoding,Origin" + ], + "server": [ + "cloudflare" + ], + "via": [ + "1.1 google" + ], + "alt-svc": [ + "h3=\":443\"; ma=86400" + ], + "age": [ + "2144" + ], + "last-modified": [ + "Tue, 29 Sep 2026 11:01:45 GMT" + ], + "cf-cache-status": [ + "HIT" + ], + "nel": [ + "{\"report_to\":\"cf-nel\",\"success_fraction\":0.01,\"max_age\":604800}" + ], + "report-to": [ + "{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=Km3u0F1rEwwja00nlLPTEuRAc%2FkSGQ3AesAWU%2FLtoJ9n4E48FrrZyGoGB8jXywY6CgKj9%2FSK2yBXRhoCuoYYGMEbItE7ghkNrN8femG7T2knumrHg0M8Bf73lsNF1yfthQ%3D%3D\"}]}" + ], + "cf-ray": [ + "a42ab2373a7bd626-CDG" + ], + "content-length": [ + "210982" + ] + }, + "body": { + "string": "\n\n\n \n \n \n \n Cinéma à Brest (29200) - AlloCiné\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n\n \n \n \n\n \n\n \n \n \n \n \n \n \n\n\n\n\n\n\n
\n
\n\n \n\n \n \n \n \n\n\n\n\n\n
\n
\n
\n menu\n close2\n
\n\n \n \n \n \n
\n
\n \n
\n \n profil\n
\n
\n\n
\n
    \n
\n
\n
\n \n search\n
\n
\n\n \n\n\n \n
\n
Annuler
\n\n \n
\n
\n \n
\n
\n \n
\n
\n\n ok \n
\n
\n\n
\n Ex:\n\n Meilleurs thrillers psychologiques, Meilleurs buddy movies\n\n
\n
\n \n \n\n \n \n \n
\n
\n
\n \n
\n\n
\n AccueilCinémaSéances CinémaCinéma Brest

\n Cinéma Brest : Séances cinéma et horaires

\n \n \n
\n
Salles de cinéma à Brest
\n
\n\n \n \n\n\n\n\n
\n
\n
\n \n \n \n \n
\n Trier par\n \n
\n popularité\n \n
    \n
  • \n \n \n popularité\n \n
  • \n
  • \n \n \n alphabétique\n \n
  • \n
  • \n \n \n nombre de salles\n \n
  • \n
\n
\n
\n \n
    \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Multiplexe\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Multiplexe Liberté \n

    \n\n \n \n
    \n\n \n
    10 Avenue Georges Clemenceau 29200 Brest
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 15 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Les\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Les Studios \n

    \n\n \n \n
    \n\n \n
    136, rue Jean-Jaurès 29200 Brest
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 1 Salle\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"CGR\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n CGR Brest Le Celtic \n

    \n\n \n \n
    \n\n \n
    187, rue Jean-Jaures 29200 Brest
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n \n \n
    \n \n
    \n \n
    \n
    \n 8 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n
  • \n \n\n
    \n
    \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n \n \n \"Pathé\n \n \n\n \n \n \n\n
    \n
    \n\n \n
    \n
    \n

    \n Pathé Capucins \n

    \n\n \n \n
    \n\n \n
    25 Rue de Pontaniou 29200 Brest
    \n \n \n
    \nVoir les informations d'accessibilité\n\n \n
    \n \n
    \n\n \n
    \n
    \n 5 Salles\n
    \n
    \n
    \n
    \n\n
    \n\n
  • \n \n \n\n\n \n\n\n
\n\n
\n \n
\n
\n\n \n \n \n \n \n \n\n\n\n \n \n \n \n \n
\n
\n \n \n
\n \n \n\n\n
\n \n\n \n
\n \n \n \n \n \n
\n\n
\n \n \n
\n
\n \n \n Back to Top\n\n \n
 
\n\n
\n \n
\n \n \n\n\n\n\n\n \n\n\n \n \n\n \n\n\n \n\n \n \n \n \n \n\n \n\n \n
\n\n \n\n\n \n \n \n \n\n\n
\n\n\n\n" + } + } + } + ] +} diff --git a/tests/cassettes/search_theaters_rejects_unknown_zipcode.json b/tests/cassettes/search_theaters_rejects_unknown_zipcode.json new file mode 100644 index 0000000..876d311 --- /dev/null +++ b/tests/cassettes/search_theaters_rejects_unknown_zipcode.json @@ -0,0 +1,88 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://www.allocine.fr/_/localization_city/99999", + "body": "", + "headers": { + "host": [ + "www.allocine.fr" + ], + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate, zstd" + ], + "connection": [ + "keep-alive" + ], + "user-agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Tue, 29 Sep 2026 11:37:29 GMT" + ], + "content-type": [ + "application/json" + ], + "transfer-encoding": [ + "chunked" + ], + "connection": [ + "keep-alive" + ], + "cache-control": [ + "max-age=86400, public, stale-if-error=172800, stale-while-revalidate=86400" + ], + "vary": [ + "Origin, accept-encoding" + ], + "server": [ + "cloudflare" + ], + "via": [ + "1.1 google" + ], + "alt-svc": [ + "h3=\":443\"; ma=86400" + ], + "last-modified": [ + "Tue, 29 Sep 2026 11:37:29 GMT" + ], + "cf-cache-status": [ + "MISS" + ], + "nel": [ + "{\"report_to\":\"cf-nel\",\"success_fraction\":0.01,\"max_age\":604800}" + ], + "report-to": [ + "{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=4Gvz3nDwG%2F35oCekdYhq4XlTD4yYgLR%2FJj9bRbsBWiqjH5qyUNQcuZsI%2F3Im2uQP5GsG0JNj1otWUCcMnWF%2BtqNzlbMASAydWj2oIfQuo4KqGicAe9fM0I2p8jQR%2BOrwxg%3D%3D\"}]}" + ], + "cf-ray": [ + "a42ab2377abcd626-CDG" + ], + "content-length": [ + "38" + ] + }, + "body": { + "values": { + "cities": [], + "theaters": [] + } + } + } + } + ] +} diff --git a/tests/test_allocine.py b/tests/test_allocine.py index 8c76aec..1e26735 100644 --- a/tests/test_allocine.py +++ b/tests/test_allocine.py @@ -26,6 +26,41 @@ def test_search_theaters(allocine: Allocine, json_snapshot, allocine_vcr): assert theaters == json_snapshot +def test_get_geocodes_lists_departments(allocine: Allocine, json_snapshot, allocine_vcr): + locations = allocine.get_geocodes() + + assert [asdict(location) for location in locations] == json_snapshot + + +def test_get_geocodes_lists_department_cities(allocine: Allocine, json_snapshot, allocine_vcr): + locations = allocine.get_geocodes(dept=29) + + assert [asdict(location) for location in locations] == json_snapshot + + +def test_search_theaters_by_department(allocine: Allocine, json_snapshot, allocine_vcr): + theaters = allocine.search_theaters(dept=53) + + assert [asdict(theater) for theater in theaters] == json_snapshot + + +def test_search_theaters_by_zipcode(allocine: Allocine, json_snapshot, allocine_vcr): + theaters = allocine.search_theaters(zipcode=29200) + + assert [asdict(theater) for theater in theaters] == json_snapshot + + +@pytest.mark.parametrize("kwargs", [{}, {"geocode": 95171, "dept": 29}]) +def test_search_theaters_requires_one_location_filter(allocine: Allocine, kwargs): + with pytest.raises(ValueError, match="Exactly one"): + allocine.search_theaters(**kwargs) + + +def test_search_theaters_rejects_unknown_zipcode(allocine: Allocine, allocine_vcr): + with pytest.raises(ValueError, match="No location found for zipcode '99999'"): + allocine.search_theaters(zipcode=99999) + + @freeze_time("2026-09-27") def test_get_showtimes_defaults_to_today(allocine: Allocine, json_snapshot, allocine_vcr): showtimes = allocine.get_showtimes("G0FOX")