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>
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 | |