Lou·Ka — tous les logements à louer du Québec, un seul endroit.
- 264commits
- 1branches
- 0releases
- 180.7 MBon disk
- maindefault branch
- 5 days agolast push
1# -----------------------------------------------------------------------------2# Lou-Ka — tests de la couche de normalisation commune (louka/normalize.py)3# -----------------------------------------------------------------------------4from datetime import date56from louka.normalize import (7 clean_address,8 extract_details,9 normalize_unit_type,10 parse_area_sqft,11 parse_availability_date,12 parse_price,13 price_is_from,14)15from louka.schema import Listing1617TODAY = date(2026, 8, 8)181920# -- prix ---------------------------------------------------------------------2122def test_parse_price_formats():23 assert parse_price("1 195 $/mois") == 119524 assert parse_price("À partir de 1 250,00$") == 125025 assert parse_price("$1,195") == 119526 assert parse_price("995$ - 1 200$") == 995 # le plus bas27 assert parse_price("1195,50 $") == 1195.528 assert parse_price("Contactez-nous") is None29 assert parse_price("") is None30 assert parse_price("50 $") is None # hors plage plausible31 assert parse_price("4 1/2 au 2e étage") is None # pas de $323334def test_price_is_from():35 assert price_is_from("À partir de 995$")36 assert price_is_from("From $1,200")37 assert not price_is_from("1 195 $/mois")383940# -- type d'unité ---------------------------------------------------------------4142def test_normalize_unit_type():43 assert normalize_unit_type("4 1/2") == "4½"44 assert normalize_unit_type("4½") == "4½"45 assert normalize_unit_type("4,5") == "4½"46 assert normalize_unit_type("5 1/5") == "5½" # coquille fréquente47 assert normalize_unit_type("2 chambres") == "4½"48 assert normalize_unit_type("3 bedrooms") == "5½"49 assert normalize_unit_type("6 1/2") == "6½+"50 assert normalize_unit_type("7½") == "6½+"51 assert normalize_unit_type("Studio") == "Studio"52 assert normalize_unit_type("bachelor") == "Studio"53 assert normalize_unit_type("MDV") == "Maison"54 assert normalize_unit_type("maison de ville") == "Maison"55 assert normalize_unit_type("Chambre") == "Chambre"56 assert normalize_unit_type("condo") == "Condo"57 assert normalize_unit_type("") == ""585960# -- disponibilité ---------------------------------------------------------------6162def test_parse_availability_now():63 for raw in ("Libre immédiatement", "Maintenant", "Disponible",64 "Available now", "Occupation immédiate"):65 assert parse_availability_date(raw, TODAY) == "now", raw666768def test_parse_availability_dates():69 assert parse_availability_date("1er juillet 2027", TODAY) == "2027-07-01"70 assert parse_availability_date("Disponibilité : 01 décembre 2026", TODAY) == "2026-12-01"71 assert parse_availability_date("octobre 2026", TODAY) == "2026-10-01"72 assert parse_availability_date("2026-09-15", TODAY) == "2026-09-15"73 assert parse_availability_date("01/11/2026", TODAY) == "2026-11-01"74 assert parse_availability_date("December 1, 2026", TODAY) == "2026-12-01"757677def test_parse_availability_year_inference():78 # sans année : prochaine occurrence, ou « now » si déjà passée79 assert parse_availability_date("1er septembre", TODAY) == "2026-09-01"80 assert parse_availability_date("1er juillet", TODAY) == "now"81 # date explicite passée -> le logement est libre82 assert parse_availability_date("1er juillet 2026", TODAY) == "now"838485def test_parse_availability_unknown():86 assert parse_availability_date("", TODAY) is None87 assert parse_availability_date("Sur demande", TODAY) is None888990# -- superficie ---------------------------------------------------------------9192def test_parse_area():93 assert parse_area_sqft("850 pi²") == 85094 assert parse_area_sqft("850 pi2") == 85095 assert parse_area_sqft("1 050 sq. ft.") == 105096 assert parse_area_sqft("78 m²") == round(78 * 10.7639)97 assert parse_area_sqft("aucune mention") is None98 assert parse_area_sqft("2 pi²") is None # hors plage99100101# -- adresse ---------------------------------------------------------------102103def test_clean_address():104 assert clean_address(" 555, rue Exemple ,Québec ") == "555, rue Exemple, Québec"105 assert clean_address("") == ""106107108# -- détails structurés ---------------------------------------------------------109110def test_extract_details_inclusions():111 d = extract_details(["Chauffage inclus", "Eau chaude", "Lave-vaisselle"],112 "Stationnement intérieur disponible. Animaux acceptés.")113 assert d["inclusions"]["heating"] is True114 assert d["inclusions"]["hot_water"] is True115 assert d["appliances"]["dishwasher"] is True116 assert d["parking"]["available"] is True117 assert d["parking"]["type"] == "intérieur"118 assert d["pets"] == "oui"119120121def test_extract_details_negations():122 d = extract_details([], "Pas d'animaux. Logement non-fumeur, non meublé. "123 "Entrées laveuse-sécheuse.")124 assert d["pets"] == "non"125 assert d["smoking"] is False126 assert d["furnished"] is False127 assert d["appliances"]["washer_dryer_hookup"] is True128 assert "washer_dryer" not in d["appliances"]129130131def test_extract_details_edge_cases_connecteurs():132 # « Non chauffé et éclairé » : chauffage NON inclus, électricité incluse133 d = extract_details(["Non chauffé et éclairé"], "")134 assert d["inclusions"]["heating"] is False135 assert d["inclusions"]["electricity"] is True136 # « Chats autorisés (*sous conditions) » -> conditions137 d = extract_details(["Chats autorisés (*sous conditions)"], "")138 assert d["pets"] == "conditions"139 # électroménagers en option payante : ne rien affirmer140 d = extract_details(["Électroménagers sur demande ($)"], "")141 assert "appliances" not in d142 # anglais : refrigerator / Cooling: Central Air reconnus,143 # « Heating: Forced Air » = type de chauffage, pas une inclusion144 d = extract_details(["Refrigerator", "Cooling: Central Air", "Heating: Forced Air"], "")145 assert d["appliances"]["fridge"] is True146 assert d["ac"] is True147 assert "inclusions" not in d148149150def test_extract_details_edge_cases_lot2():151 # « Wifi (optionnel) » = option payante, pas une inclusion152 d = extract_details(["Wifi (optionnel)"], "")153 assert "inclusions" not in d154 # « Les chiens ne sont pas permis » -> sous conditions (chats tolérés)155 d = extract_details([], "Les chiens ne sont pas permis dans nos propriétés.")156 assert d["pets"] == "conditions"157 # stationnement mixte158 d = extract_details(["Stationnement extérieur et intérieur"], "")159 assert d["parking"]["type"] == "intérieur et extérieur"160 # étage ignoré dans une description multi-unités, retenu en commodité161 d = extract_details([], "Grand 4½ au 3e étage d'un immeuble de 12 logements.")162 assert "floor" not in d163 d = extract_details(["3e étage"], "")164 assert d["floor"] == 3165166167def test_extract_details_pets_both_species():168 # « Chiens et chats acceptés » = oui (pas « conditions »)169 assert extract_details(["Chiens et chats acceptés"], "")["pets"] == "oui"170 assert extract_details(["Chats et chiens admis"], "")["pets"] == "oui"171 # « Chats acceptés » seul reste « conditions »172 assert extract_details(["Chats acceptés"], "")["pets"] == "conditions"173174175def test_parse_availability_english_month_first():176 assert parse_availability_date("Available September 17", TODAY) == "2026-09-17"177 assert parse_availability_date("April 1st, 2027", TODAY) == "2027-04-01"178 assert parse_availability_date("juillet 2027", TODAY) == "2027-07-01" # pas « jour 20 »179180181def test_parse_area_context_and_thousands():182 # la superficie du balcon n'est pas celle du logement183 assert parse_area_sqft("1134 pi², balcon 277 pi²") == 1134184 assert parse_area_sqft("1,607 sq. ft.") == 1607185186187def test_extract_details_edge_cases_lots_9_a_13():188 # « washer » ne doit pas matcher « dishwasher »189 d = extract_details(["Dishwasher"], "")190 assert d["appliances"] == {"dishwasher": True}191 # entrée lave-vaisselle = branchement, pas l'appareil192 d = extract_details(["Entrée lave-vaisselle"], "")193 assert d["appliances"].get("dishwasher") is not True194 assert d["appliances"]["dishwasher_hookup"] is True195 # coquille fréquente « ascenceur »196 assert extract_details(["Ascenceur"], "")["elevator"] is True197 # pets : formulations variées198 assert extract_details(["Chien interdit"], "")["pets"] == "conditions"199 assert extract_details(["Chiens permis"], "")["pets"] == "oui"200 assert extract_details(["Animaux de compagnie acceptés"], "")["pets"] == "oui"201 assert extract_details([], "Animaux permis : Non")["pets"] == "non"202 assert extract_details([], "Animaux acceptés (20lb et moins)")["pets"] == "conditions"203 # anglais : heat/hydro/terrace/laundry facilities ; heat pump ≠ inclusion204 d = extract_details(["Heat", "Hydro", "Rooftop terrace", "Laundry facilities"], "")205 assert d["inclusions"]["heating"] is True206 assert d["inclusions"]["electricity"] is True207 assert d["balcony"] is True and d["laundry"] is True208 # semi-meublé : nuance conservée, pas un booléen209 assert extract_details(["Semi-meublé"], "")["furnished"] == "semi"210 # vélo ≠ stationnement auto ; « ($) » = payant211 assert "parking" not in extract_details(["Stationnement à vélo intérieur"], "")212 assert "inclusions" not in extract_details(["Wi-Fi ($)"], "")213214215def test_extract_details_edge_cases_lots_4_et_7():216 # « chauffé » qualifiant autre chose que le logement217 d = extract_details(["Stationnement intérieur non chauffé"], "")218 assert "inclusions" not in d219 assert d["parking"]["available"] is True220 d = extract_details(["Piscine extérieure chauffée"], "")221 assert "inclusions" not in d and d["pool"] is True222 # aires communes ≠ unité223 d = extract_details(["Terrasse commune avec BBQ", "WI-FI dans les aires communes"], "")224 assert "balcony" not in d and "inclusions" not in d225 # étage d'une installation ≠ étage de l'unité226 assert "floor" not in extract_details(["Salon lounge au 25e étage"], "")227 # négations et « rien d'inclus »228 assert extract_details(["Pas de balcon"], "")["balcony"] is False229 d = extract_details([], "Rien d'inclus.")230 assert d["inclusions"]["heating"] is False231 assert extract_details([], "Les animaux sont interdits.")["pets"] == "non"232 # option payante « moyennant des frais mensuels »233 d = extract_details(["Électroménagers (moyennant des frais mensuels)"], "")234 assert "appliances" not in d235236237def test_normalize_unit_type_word_numbers():238 assert normalize_unit_type("Une chambre") == "3½"239 assert normalize_unit_type("Two bedrooms") == "4½"240241242def test_extract_details_edge_cases_lot6():243 # animal au singulier244 assert extract_details([], "Votre animal est le bienvenu !")["pets"] == "oui"245 # nom de promotion ≠ inclusion246 d = extract_details([], "Promotion : Crédit de 1 000 $ Éclaire à la signature.")247 assert "inclusions" not in d248 # prix de stationnement complet, formats FR et EN249 d = extract_details([], "Stationnement disponible : 2 espaces pour seulement 150 $.")250 assert d["parking"]["price"] == 150251 d = extract_details([], "Parking available for $85 per month.")252 assert d["parking"]["price"] == 85253254255def test_extract_details_edge_cases_lot5():256 # « à la charge des locataires » = NON inclus257 d = extract_details([], "Chauffage et électricité à la charge des locataires.")258 assert d["inclusions"]["heating"] is False259 assert d["inclusions"]["electricity"] is False260 # « Actuellement vacant » = libre maintenant261 assert parse_availability_date("Actuellement vacant", TODAY) == "now"262263264def test_extract_details_edge_cases_lot8():265 # « non-chauffé, non-éclairé » avec traits d'union266 d = extract_details(["Non-chauffé, non-éclairé"], "")267 assert d["inclusions"]["heating"] is False268 assert d["inclusions"]["electricity"] is False269 # branchement combiné : ni laveuse ni lave-vaisselle fournis270 d = extract_details(["Entrées lave-vaisselle et laveuse/sécheuse"], "")271 assert d["appliances"].get("washer_dryer") is not True272 assert d["appliances"].get("dishwasher") is not True273 assert d["appliances"]["washer_dryer_hookup"] is True274 # « refusés sauf un chat » = conditions275 assert extract_details([], "Animaux refusés, à l'exception d'un chat.")["pets"] == "conditions"276 # restriction entre parenthèses277 assert "ac" not in extract_details(["Appareil de climatisation (Sans utilisation)"], "")278279280def test_price_from_range():281 assert price_is_from("de 1 650 $ à 2 293 $")282283284def test_extract_details_unknown_stays_absent():285 d = extract_details(["Balcon"], "")286 assert d.get("balcony") is True287 assert "pets" not in d288 assert "smoking" not in d289 assert "inclusions" not in d290291292def test_extract_details_contact():293 d = extract_details([], "Pour info : 418 555-1234 ou louer@exemple.ca")294 assert d["contact"]["phone"] == "418-555-1234"295 assert d["contact"]["email"] == "louer@exemple.ca"296297298# -- finalize (enrichissement de bout en bout) -----------------------------------299300def test_listing_finalize():301 l = Listing(302 source="test", external_id="1", url="https://x/1",303 title="Le Test — 4 1/2",304 address="555,rue Exemple, Québec",305 unit_type="2 chambres",306 price_label="À partir de 1 195 $/mois",307 availability="1er décembre 2026",308 description="Superbe 4½ de 850 pi². Chauffage et eau chaude inclus. "309 "Animaux refusés. Info : (418) 555-9999.",310 amenities=["Balcon", "Ascenseur", "Piscine intérieure"],311 ).finalize()312 assert l.unit_type == "4½"313 assert l.price == 1195314 assert l.availability_date == "2026-12-01"315 assert l.area_sqft == 850316 assert l.pets == "non"317 assert l.address == "555, rue Exemple, Québec"318 assert l.details["price_from"] is True319 assert l.details["balcony"] is True320 assert l.details["elevator"] is True321 assert l.details["pool"] is True322 assert l.details["inclusions"]["heating"] is True323 assert l.details["contact"]["phone"] == "418-555-9999"324325326def test_listing_finalize_respects_connector_values():327 l = Listing(source="t", external_id="2", url="u",328 price=999.0, price_label="1 500 $",329 pets="oui", furnished=True,330 availability_date="2027-01-01", availability="maintenant",331 details={"parking": {"available": True, "price": 60.0}})332 l.finalize()333 assert l.price == 999.0 # valeur connecteur conservée334 assert l.availability_date == "2027-01-01"335 assert l.pets == "oui"336 assert l.furnished is True337 assert l.details["parking"]["price"] == 60.0338