SPB Git

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

12.6 KB · 296 lines python
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_unknown_stays_absent():243    d = extract_details(["Balcon"], "")244    assert d.get("balcony") is True245    assert "pets" not in d246    assert "smoking" not in d247    assert "inclusions" not in d248249250def test_extract_details_contact():251    d = extract_details([], "Pour info : 418 555-1234 ou louer@exemple.ca")252    assert d["contact"]["phone"] == "418-555-1234"253    assert d["contact"]["email"] == "louer@exemple.ca"254255256# -- finalize (enrichissement de bout en bout) -----------------------------------257258def test_listing_finalize():259    l = Listing(260        source="test", external_id="1", url="https://x/1",261        title="Le Test — 4 1/2",262        address="555,rue Exemple,  Québec",263        unit_type="2 chambres",264        price_label="À partir de 1 195 $/mois",265        availability="1er décembre 2026",266        description="Superbe 4½ de 850 pi². Chauffage et eau chaude inclus. "267                    "Animaux refusés. Info : (418) 555-9999.",268        amenities=["Balcon", "Ascenseur", "Piscine intérieure"],269    ).finalize()270    assert l.unit_type == "4½"271    assert l.price == 1195272    assert l.availability_date == "2026-12-01"273    assert l.area_sqft == 850274    assert l.pets == "non"275    assert l.address == "555, rue Exemple, Québec"276    assert l.details["price_from"] is True277    assert l.details["balcony"] is True278    assert l.details["elevator"] is True279    assert l.details["pool"] is True280    assert l.details["inclusions"]["heating"] is True281    assert l.details["contact"]["phone"] == "418-555-9999"282283284def test_listing_finalize_respects_connector_values():285    l = Listing(source="t", external_id="2", url="u",286                price=999.0, price_label="1 500 $",287                pets="oui", furnished=True,288                availability_date="2027-01-01", availability="maintenant",289                details={"parking": {"available": True, "price": 60.0}})290    l.finalize()291    assert l.price == 999.0                       # valeur connecteur conservée292    assert l.availability_date == "2027-01-01"293    assert l.pets == "oui"294    assert l.furnished is True295    assert l.details["parking"]["price"] == 60.0296
Files · 957fbe70cc1f4d660c72acc0d9e9cf677a3d7185
Menu

Theme