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 "listing_url": "https://ubee.com/a-louer", 2606 "listing_url": "https://ubee.com/a-louer",
2607 "sectors": "Province entière — API SearchProperties listingType Renter (résidentiel)", 2607 "sectors": "Province entière — API SearchProperties listingType Renter (résidentiel)",
2608 "connector": "ubee", 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 "region": "Province" 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 | [`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 | 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 | [`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 | 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 | [`trylon`](trylon.md) | Trylon Apartments | Montréal | pages HTML (rendu serveur) | direct | 7/7 | 100 % | 100 % | 100 % | actif | 2026-09-04 08:58 | 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 | [`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 | 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 | [`uptimo`](uptimo.md) | Uptimo Gestion immobilière | Estrie | pages HTML (rendu serveur) | direct | 66/81 | 98 % | 98 % | 100 % | actif | 2026-09-04 08:58 | 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 | [`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 | 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 disabled = [ 907 disabled = [
908 s["id"] 908 s["id"]
909 for s in json.loads(SOURCES_PATH.read_text(encoding="utf-8"))["sources"] 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 return {**dict(row), "quality": dict(qual), "recent_syncs": log, 912 return {**dict(row), "quality": dict(qual), "recent_syncs": log,
913 "disabled_sources": disabled} 913 "disabled_sources": disabled}
914 914
Menu

Theme