SPB Git

spb/lou-ka

Public

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

retrait: connecteur Ubee supprimé et 466 inscriptions effacées (plainte de Ubee, 2026-10-08)

- louka/connectors/ubee.py + docs/connecteurs/ubee.md supprimés, ligne INDEX retirée
- data/sources.json : ubee → retiré (ne pas réactiver sans accord écrit)
- louka/web.py : les statuts inactif/retiré sont aussi déclarés disabled_sources à api-ka
- base : listings (466), price_log (505), fairvalue (375), listing_events (319),
  recycled_cache (77), managers (1), sync_log (372), image_checks (12 701 URL
  Cloudinary ubee-media) effacés ; dédup inter-sources recalculée
- sauvegarde à froid hors dépôt : ~/ubee-retrait-20261008/

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Oct 8, 2026) parent cf20c1b Browse files

5 changed files +2 −205

modified data/sources.json +1 −1
@@ -2606,7 +2606,7 @@
2606 2606 "listing_url": "https://ubee.com/a-louer",
2607 2607 "sectors": "Province entière — API SearchProperties listingType Renter (résidentiel)",
2608 2608 "connector": "ubee",
2609 − "status": "actif",
2609 + "status": "retiré — 2026-10-08, à la demande de Ubee (plainte) : connecteur supprimé, 466 inscriptions effacées. NE PAS RÉACTIVER sans accord écrit de Ubee.",
2610 2610 "region": "Province"
2611 2611 },
2612 2612 {
modified docs/connecteurs/INDEX.md +0 −1
@@ -365,7 +365,6 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` le 2026-09-04 14:
365 365 | [`trouve_ton_appart`](trouve_ton_appart.md) | Trouve Ton Appart (Construction Léandre… | Chaudière-Appalaches | pages HTML (rendu serveur) | direct | 5/5 | 80 % | 100 % | 100 % | actif | 2026-09-04 08:58 |
366 366 | [`trudel`](trudel.md) | Trudel (Fleur de Lys, LGC) | Québec | pages HTML (rendu serveur) | direct | 30/30 | 100 % | 100 % | 100 % | actif | 2026-09-04 14:08 |
367 367 | [`trylon`](trylon.md) | Trylon Apartments | Montréal | pages HTML (rendu serveur) | direct | 7/7 | 100 % | 100 % | 100 % | actif | 2026-09-04 08:58 |
368 −| [`ubee`](ubee.md) | Ubee (location) | Province | API JSON interne du site | direct | 247/258 | 100 % | 100 % | 100 % | actif | 2026-09-04 08:58 |
369 368 | [`uneo`](uneo.md) | Unéo Gestion Immobilière | Montérégie | pages HTML (rendu serveur) | direct | 4/4 | 25 % | 100 % | 100 % | actif | 2026-09-04 08:58 |
370 369 | [`uptimo`](uptimo.md) | Uptimo Gestion immobilière | Estrie | pages HTML (rendu serveur) | direct | 66/81 | 98 % | 98 % | 100 % | actif | 2026-09-04 08:58 |
371 370 | [`urban_services`](urban_services.md) | Urban Services (Urban Tenant Solutions) | Outaouais | pages HTML (rendu serveur) | direct | 48/61 | 100 % | 100 % | 100 % | actif | 2026-09-04 08:58 |
deleted docs/connecteurs/ubee.md +0 −79
@@ -1,79 +0,0 @@
1 −# Ubee (location) — connecteur `ubee`
2 −
3 −_Fiche générée automatiquement par `scripts/gen_connector_docs.py` le 2026-09-04 14:12 — ne pas éditer à la main, régénérer._
4 −
5 −**État : actif** · Région : Province · Backend : direct · Annonces actives : 247/258
6 −
7 −## Description de la source
8 −
9 −Ubee (ubee.com) — plateforme immobilière québécoise API publique anonyme : POST api.ubee.ca/api/anonymous/Search/SearchProperties (pageIndex=N, 24 résultats/page, JSON riche : adresse, GPS, loyer, pièces, superficies m², année, galerie Cloudinary complète). Volet LOCATION : listingType=Renter + isResidential (~250 annonces) ; `askPrice` est le loyer MENSUEL ($) pour ce volet (vérifié en live : 300–5 300 $/mois). Les fiches location vivent sous /a-louer/{ville}/{slug} (canonical du site). Les terrains et le volet commercial sont écartés — pas des logements. Adapté du connecteur « à vendre » d'Immo-Ka (agent-courtage/immoka).
10 −
11 −- **Site** : https://ubee.com
12 −- **Page des annonces** : https://ubee.com/a-louer
13 −- **Secteurs couverts** : Province entière — API SearchProperties listingType Renter (résidentiel)
14 −- **Module** : `louka/connectors/ubee.py` — classe `UbeeConnector`
15 −
16 −## Accès
17 −
18 −- **Type d'accès** : API JSON interne du site
19 −- **Endpoint de base** : https://api.ubee.ca/api/anonymous/Search/SearchProperties
20 −- **URLs de départ (constantes du module)** : https://api.ubee.ca/api/anonymous/Search/SearchProperties · https://ubee.com
21 −- **Authentification** : aucune — contenu public
22 −- **Pagination** : page de liste unique (pas de pagination)
23 −- **Backend anti-bot / rendu** : requests direct (session UA LouKaBot, throttling poli)
24 −- **Politesse** : 0.5 s entre requêtes, timeout 30 s, User-Agent identifiable `LouKaBot/1.0 (+https://www.lou-ka.com/bot)`
25 −
26 −## Champs récupérés → schéma cible
27 −
28 −Le connecteur (direct, API JSON interne du site) alimente les colonnes de la table `listings`. Complétude mesurée en SQL sur les 247 annonces actives ; exemple tiré d'une ligne réelle de la BD.
29 −
30 −| Colonne `listings` | Contenu | Renseignée (actives) | Exemple réel |
31 −|---|---|---|---|
32 −| `title` | Titre de l'annonce | 100 % | Condo à louer — Montréal (Le Plateau-Mont-Royal) |
33 −| `address` | Adresse civique | 100 % | 3456, Av. du Parc |
34 −| `city` | Ville (normalisée) | 100 % | Montréal (Le Plateau-Mont-Royal) |
35 −| `sector` | Secteur / quartier | 0 % | — |
36 −| `unit_type` | Type d'unité (3½, 4½…) | 100 % | 3½ |
37 −| `price` | Loyer mensuel ($) | 100 % | 1 800 $ |
38 −| `bedrooms` | Chambres | 100 % | 1 |
39 −| `bathrooms` | Salles de bain | 99 % | 1 |
40 −| `area_sqft` | Superficie (pi²) | 90 % | 592 |
41 −| `availability_date` | Date de disponibilité | 0 % | — |
42 −| `lat` | GPS (lat/lng) | 100 % | 45.50889466 |
43 −| `description` | Description | 0 % | — |
44 −| `images` | Photos (JSON) | 100 % | 10 photo(s) — https://res.cloudinary.com/ubee-media/image/private/t_default_size/v1786588… |
45 −| `amenities` | Commodités (JSON) | 6 % | [] |
46 −| `details` | Détails additionnels (JSON) | 100 % | {"Courtier": "Ubee", "Type": "Condo", "Salles de bain": "1", "Année de construction": "20… |
47 −| `url` | URL de l'annonce source | 100 % | https://ubee.com/a-louer/montreal/condo-3456-av-du-parc-49ac28 |
48 −
49 −## Fréquence & budget
50 −
51 −- **Cadence observée** (médiane des passages OK, sync_log) : ≈ 3.4 h
52 −- **Dernier passage OK** : 2026-09-04 08:58 — 243 trouvées, +7 / ~5 / -0
53 −- **Throttling** : 0.5 s entre requêtes (constante de classe `request_delay`)
54 −- **Cache des pages détail (BD `detail_cache`)** : activé — évite de re-visiter les fiches inchangées
55 −
56 −## Volumétrie & complétude
57 −
58 −- **Annonces en BD** : 258 au total, **247 actives**
59 −- **Première ingestion** : 2026-08-12 · **Dernière observation** : 2026-09-04
60 −- **Complétude clé (actives)** : GPS 100 % · prix 100 % · photos 100 % · description 0 %
61 −- **Runs journalisés (60 derniers)** : 60, dont 0 en erreur
62 −
63 −## Erreurs connues & dépannage
64 −
65 −Aucune erreur dans les 60 derniers runs journalisés.
66 −
67 −Rejouer la source seule : `python3 run.py sync ubee` · vérifier ensuite `sync_log` (`SELECT * FROM sync_log WHERE source='ubee' ORDER BY ts DESC LIMIT 5;`).
68 −
69 −## Licence, attribution & conditions
70 −
71 −- Scraping poli d'annonces **publiques** (données factuelles : prix, adresse, disponibilité) publiées par le gestionnaire sur son propre site.
72 −- User-Agent **identifiable** avec page d'information et contact : `LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)` ; throttling 0.5 s ; cache détail pour minimiser les requêtes.
73 −- Chaque annonce affichée sur Lou-Ka **pointe vers l'annonce d'origine** (colonne `url`) — la source garde le trafic de conversion.
74 −- Retrait sur demande : contact@spboucher.ai.
75 −
76 −## Historique
77 −
78 −- 2026-08-12 — premières annonces de la source ingérées dans la BD.
79 −- 2026-08-18 — vague d'enrichissement : standardisation de la documentation des connecteurs (fiche générée par `scripts/gen_connector_docs.py`).
deleted louka/connectors/ubee.py +0 −123
@@ -1,123 +0,0 @@
1 −# -----------------------------------------------------------------------------
2 −# Lou-Ka — Agrégateur de logements à louer (province de Québec)
3 −# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 −# connectors/ubee.py : Ubee (ubee.com) — plateforme immobilière québécoise
5 −# API publique anonyme : POST api.ubee.ca/api/anonymous/Search/SearchProperties
6 −# (pageIndex=N, 24 résultats/page, JSON riche : adresse, GPS, loyer, pièces,
7 −# superficies m², année, galerie Cloudinary complète). Volet LOCATION :
8 −# listingType=Renter + isResidential (~250 annonces) ; `askPrice` est le
9 −# loyer MENSUEL ($) pour ce volet (vérifié en live : 300–5 300 $/mois).
10 −# Les fiches location vivent sous /a-louer/{ville}/{slug} (canonical du
11 −# site). Les terrains et le volet commercial sont écartés — pas des
12 −# logements. Adapté du connecteur « à vendre » d'Immo-Ka
13 −# (agent-courtage/immoka).
14 −# -----------------------------------------------------------------------------
15 −from __future__ import annotations
16 −
17 −from ..schema import Listing
18 −from .base import BaseConnector
19 −
20 −API = "https://api.ubee.ca/api/anonymous/Search/SearchProperties"
21 −SITE = "https://ubee.com"
22 −M2_TO_SQFT = 10.7639
23 −
24 −# type d'inscription -> vocabulaire brut (finalize() normalise) ;
25 −# Terrain/Commercial ne sont pas des logements -> exclus au filtrage
26 −_TYPES = {
27 − "Unifamiliale": "Maison", "Condo": "Condo",
28 − "Plex": "Logement", "Chalet": "Chalet", "Fermette": "Fermette",
29 −}
30 −_EXCLUDE_TYPES = {"Terrain", "Commercial"}
31 −
32 −
33 −class UbeeConnector(BaseConnector):
34 − source_id = "ubee"
35 − request_delay = 0.5
36 −
37 − def _search(self, body: dict) -> list[dict]:
38 − out, page = [], 0
39 − while True:
40 − r = self.post(f"{API}?pageIndex={page}", json=body).json()
41 − results = r.get("results") or []
42 − out.extend(results)
43 − if len(out) >= (r.get("totalCount") or 0) or not results:
44 − break
45 − page += 1
46 − if page > 200: # garde-fou
47 − break
48 − return out
49 −
50 − def _to_listing(self, it: dict) -> Listing | None:
51 − lid = str(it.get("id") or "")
52 − slug = it.get("slugFr") or it.get("slugEn") or ""
53 − if not lid or not slug:
54 − return None
55 − itype = (it.get("inscriptionType") or "").strip()
56 − if itype in _EXCLUDE_TYPES: # terrain/local : pas un logement
57 − return None
58 − city = (it.get("city") or "").strip()
59 − url = f"{SITE}/a-louer/{it.get('citySlug') or ''}/{slug}"
60 − images = [im["publicUrls"]["default_size"]
61 − for im in it.get("images") or []
62 − if (im.get("publicUrls") or {}).get("default_size")]
63 − living = it.get("livingSurfaceInMeters")
64 − ptype = _TYPES.get(itype, itype or "Logement")
65 −
66 − # askPrice = loyer mensuel sur le volet Renter (vérifié en live)
67 − price = it.get("askPrice")
68 − if not isinstance(price, (int, float)) or price <= 0:
69 − price = None
70 −
71 − # type d'unité : nb de chambres (normalisé n+2 ½ par finalize())
72 − beds = it.get("nbBedrooms")
73 − unit_type = ""
74 − if isinstance(beds, int):
75 − unit_type = "Studio" if beds == 0 else f"{beds} chambres"
76 −
77 − details: dict = {"Courtier": "Ubee", "Type": ptype}
78 − if isinstance(it.get("nbBathrooms"), int) and it["nbBathrooms"] > 0:
79 − details["Salles de bain"] = str(it["nbBathrooms"])
80 − if isinstance(it.get("nbHalfBaths"), int) and it["nbHalfBaths"] > 0:
81 − details["Salles d'eau"] = str(it["nbHalfBaths"])
82 − for k, lab in (("propertyType", "Sous-type"),
83 − ("buildingType", "Type de bâtiment"),
84 − ("yearBuilt", "Année de construction")):
85 − if it.get(k):
86 − details[lab] = str(it[k])
87 −
88 − lst = Listing(
89 − source=self.source_id,
90 − external_id=lid,
91 − url=url,
92 − title=f"{ptype} à louer — {city}" if city else f"{ptype} à louer",
93 − address=it.get("address") or "",
94 − city=city,
95 − unit_type=unit_type,
96 − price=price,
97 − price_label=(f"{price:,.0f} $/mois".replace(",", " ")
98 − if price else ""),
99 − area_sqft=round(living * M2_TO_SQFT) if living else None,
100 − amenities=[f for f in (
101 − "Neuf / à construire" if it.get("toBuild") else "",
102 − ) if f],
103 − details=details,
104 − images=images,
105 − lat=it.get("latitude"),
106 − lng=it.get("longitude"),
107 − )
108 − return lst
109 −
110 − def fetch(self) -> list[Listing]:
111 − # une seule passe : le volet isCommercial=Renter ne renvoie que des
112 − # locaux commerciaux (vérifié en live) — pas des logements
113 − out: dict[str, Listing] = {}
114 − body = {"sortBy": "DateDescending", "listingType": "Renter",
115 − "isResidential": True}
116 − for it in self._search(body):
117 − # Québec seulement (l'API est QC par nature, on double-vérifie)
118 − if (it.get("province") or "QC") != "QC":
119 − continue
120 − lst = self._to_listing(it)
121 − if lst is not None:
122 − out.setdefault(lst.uid, lst)
123 − return list(out.values())
modified louka/web.py +1 −1
@@ -907,7 +907,7 @@ def stats(syncs_since_h: float | None = None):
907 907 disabled = [
908 908 s["id"]
909 909 for s in json.loads(SOURCES_PATH.read_text(encoding="utf-8"))["sources"]
910 − if (s.get("status") or "").startswith("pause")
910 + if (s.get("status") or "").startswith(("pause", "inactif", "retir"))
911 911 ]
912 912 return {**dict(row), "quality": dict(qual), "recent_syncs": log,
913 913 "disabled_sources": disabled}
914 914
Menu

Theme