SPB Git

spb/api-ka

Public

API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com

13.1 KB · 363 lines python
1# ============================================2# Projet   : API-KA3# Fichier  : src/api/ecopdf.py4# Node     : m3u96b5# Author   : Simon-Pierre Boucher6# Contact  : contact@spboucher.ai7# Date     : 2026-08-178# ============================================9"""Rapport écosystème Groupe KA — un seul PDF consolidant toutes les plateformes.1011Compose le moteur PDF commun (``src/api/kapdf.py``) sans le dupliquer :12``EcosystemReport`` étend ``GroupeKAReport`` et réutilise ses primitives13(cartes KPI, tableaux, records, page de fin). Il ajoute la couverture14« écosystème », la page de consolidation (totaux + classement) puis une15section par plateforme. Une plateforme injoignable devient une section16« données indisponibles » propre — jamais un échec global.17"""1819from __future__ import annotations2021from typing import Any2223from src.api.kapdf import (24    GREEN,25    INK,26    INK2,27    INK3,28    PAPER,29    SURFACE2,30    GroupeKAReport,31    _fr,32    _hex,33)3435# Identifiants de KPI « nouveautés de la période » connus des plateformes36# (chacune nomme la sienne : nouvelles annonces, nouveaux véhicules, ajouts…).37NEW_KPI_IDS = (38    "nouvelles",39    "nouveaux",40    "nouveautes",41    "new",42    "ajouts",43    "added",44    "indexees",45    "releves",46    "records",47)4849ECO_SITE = {50    "wordmark": "Écosystème",51    "accent": "#d9f26b",52    "domain": "www.groupe-ka.com",53    "tagline": "Le portail de l'écosystème ·Ka",54}555657def main_kpi(dash: dict | None) -> dict | None:58    """Premier KPI numérique = volume principal de la plateforme (convention59    du contrat SPEC : l'indicateur de volume est listé en premier)."""60    for k in (dash or {}).get("kpis") or []:61        if isinstance(k.get("value"), (int, float)) and not isinstance(k["value"], bool):62            return k63    return None646566def new_kpi(dash: dict | None) -> dict | None:67    """KPI « nouveautés de la période » si la plateforme le mesure."""68    kpis = (dash or {}).get("kpis") or []69    for kid in NEW_KPI_IDS:70        for k in kpis:71            if k.get("id") == kid and isinstance(k.get("value"), (int, float)):72                return k73    return None747576class EcosystemReport(GroupeKAReport):77    """PDF consolidé : couverture Groupe KA, consolidation, 1 section par78    plateforme (mode complet), page de fin. Mode ``synthese`` = couverture +79    consolidation + fin.8081    ``platforms`` : liste de ``{"site": …, "dashboard": dict | None,82    "error": str | None}`` (une entrée par plateforme de données)."""8384    def __init__(self, platforms: list[dict], period: dict, mode: str = "complet"):85        super().__init__(dict(ECO_SITE), {"period": period or {}}, mode)86        self.platforms = platforms87        available = [p for p in platforms if p.get("dashboard")]88        missing = [p for p in platforms if not p.get("dashboard")]8990        def _vol(p: dict) -> float:91            k = main_kpi(p.get("dashboard"))92            return float(k["value"]) if k else 0.09394        self.ranked = sorted(available, key=_vol, reverse=True) + missing9596    # ------------------------------------------------------------- couverture97    def _cover(self):98        p = self.pdf99        p.cover_mode = True100        p.set_auto_page_break(False)101        p.add_page()102        p.set_fill_color(*PAPER)103        p.rect(0, 0, 210, 297, style="F")104        p.set_draw_color(*INK)105        p.set_line_width(1.0)106        p.rect(10, 10, 190, 277)107        p.set_font("helvetica", "B", 10)108        p.set_text_color(*GREEN)109        p.set_xy(24, 34)110        p.cell(0, 6, "GROUPE KA · RAPPORT STATISTIQUE")111        # wordmark « Groupe [KA] » : boîte encre, lettres lime112        p.set_xy(24, 70)113        p.set_font("helvetica", "B", 40)114        p.set_text_color(*INK)115        p.cell(p.get_string_width("Groupe") + 2, 20, "Groupe")116        bw = p.get_string_width("KA") + 12117        x = p.get_x() + 2118        p.set_fill_color(*INK)119        p.rect(x, 68, bw, 22, style="F", round_corners=True, corner_radius=3)120        p.set_text_color(*self.accent)121        p.set_xy(x + 6, 70)122        p.cell(bw - 12, 18, "KA")123        p.set_xy(24, 100)124        p.set_font("helvetica", "B", 16)125        p.set_text_color(*INK)126        p.cell(0, 8, f"RAPPORT ÉCOSYSTÈME — {len(self.platforms)} plateformes")127        p.set_xy(24, 109)128        p.set_font("helvetica", "", 11)129        p.set_text_color(*INK2)130        p.cell(0, 6, "Consolidation des plateformes de données du Groupe KA")131        from datetime import datetime132        from zoneinfo import ZoneInfo133134        now = datetime.now(ZoneInfo("America/Toronto"))135        per = self.d.get("period", {}) or {}136        joined = sum(1 for pl in self.platforms if pl.get("dashboard"))137        rows = [138            (139                "Période couverte",140                self.period_label141                + (f" ({per.get('from')} → {per.get('to')})" if per.get("from") else ""),142            ),143            ("Généré le", now.strftime("%Y-%m-%d à %H:%M") + " (heure de l'Est)"),144            (145                "Plateformes",146                f"{len(self.platforms)} plateformes de données + le hub groupe-ka.com "147                f"({joined}/{len(self.platforms)} jointes)",148            ),149            ("Mode", "Rapport complet" if self.mode == "complet" else "Synthèse"),150        ]151        y = 128152        p.set_font("helvetica", "", 10.5)153        for k, v in rows:154            p.set_xy(24, y)155            p.set_text_color(*INK3)156            p.cell(40, 6, k)157            p.set_text_color(*INK)158            p.set_font("helvetica", "B", 10.5)159            p.cell(0, 6, str(v))160            p.set_font("helvetica", "", 10.5)161            y += 8162        # bande encre au pied163        p.set_fill_color(*INK)164        p.rect(10, 262, 190, 25, style="F")165        p.set_xy(24, 270)166        p.set_font("helvetica", "B", 12)167        p.set_text_color(255, 255, 255)168        p.cell(60, 8, "par Groupe ")169        p.set_text_color(*self.accent)170        p.set_xy(24 + p.get_string_width("par Groupe ") + 1, 270)171        p.cell(20, 8, "KA")172        p.set_font("helvetica", "B", 10)173        p.set_xy(24, 270)174        p.set_text_color(*self.accent)175        p.cell(162, 8, "groupe-ka.com", align="R")176        p.set_auto_page_break(True, margin=22)177        p.cover_mode = False178179    # ---------------------------------------------------------- consolidation180    def _consolidation(self):181        p = self.pdf182        available = [pl for pl in self.ranked if pl.get("dashboard")]183        missing = [pl for pl in self.ranked if not pl.get("dashboard")]184        total_vol = 0.0185        total_new = 0.0186        has_new = 0187        for pl in available:188            mk = main_kpi(pl["dashboard"])189            if mk:190                total_vol += float(mk["value"])191            nk = new_kpi(pl["dashboard"])192            if nk:193                total_new += float(nk["value"])194                has_new += 1195        kpis: list[dict[str, Any]] = [196            {197                "id": "vol",198                "label": "Volume principal agrégé (somme des indicateurs principaux)",199                "value": int(total_vol),200            },201            {202                "id": "joined",203                "label": "Plateformes de données jointes au moment de la génération",204                "value": len(available),205                "unit": f"/ {len(self.platforms)}",206            },207        ]208        if has_new:209            kpis.append(210                {211                    "id": "new",212                    "label": f"Nouveautés sur la période ({has_new} plateformes qui les mesurent)",213                    "value": int(total_new),214                }215            )216        saved = self.d217        self.d = {"kpis": kpis}218        self._kpis("Consolidation de l'écosystème")219        self.d = saved220221        rows: list[list[Any]] = []222        for i, pl in enumerate(self.ranked, 1):223            s = pl["site"]224            dash = pl.get("dashboard")225            if dash:226                mk = main_kpi(dash)227                nk = new_kpi(dash)228                rows.append(229                    [230                        f"{i}. {s.get('wordmark', s.get('id', ''))}",231                        str(mk.get("label", ""))[:34] if mk else "—",232                        mk["value"] if mk else "—",233                        nk["value"] if nk else "—",234                    ]235                )236            else:237                rows.append(238                    [239                        f"{i}. {s.get('wordmark', s.get('id', ''))}",240                        "données indisponibles",241                        "—",242                        "—",243                    ]244                )245        self._table(246            {247                "id": "classement",248                "title": "Classement des plateformes (volume principal)",249                "columns": [250                    "Plateforme",251                    "Indicateur principal",252                    "Volume",253                    "Nouveautés (période)",254                ],255                "rows": rows,256            }257        )258        p.set_font("helvetica", "", 7.6)259        p.set_text_color(*INK3)260        note = (261            "Le « volume principal » est le premier indicateur du tableau de bord de "262            "chaque plateforme (annonces actives, véhicules, produits, pages indexées…). "263            "Les volumes ne sont pas de même nature : la somme donne un ordre de "264            "grandeur de l'écosystème, pas une métrique homogène. Données lues en "265            "direct sur chaque plateforme (cache 10 minutes)."266        )267        if missing:268            names = ", ".join(pl["site"].get("wordmark", "") for pl in missing)269            note += f" Plateformes injoignables au moment de la génération : {names}."270        p.multi_cell(174, 4.2, note)271        p.ln(4)272273    # ------------------------------------------------- section par plateforme274    def _platform_header(self, site: dict, accent: tuple):275        """Titre de section : pastille accent + wordmark (boîte encre) + domaine."""276        p = self.pdf277        y = p.get_y() + 2278        p.set_fill_color(*accent)279        p.set_draw_color(*INK)280        p.set_line_width(0.45)281        p.rect(p.l_margin, y + 2, 6, 6, style="DF", round_corners=True, corner_radius=1.5)282        wm = site.get("wordmark", site.get("id", ""))283        left, boxed = (wm.split("·") + [None])[:2] if "·" in wm else (wm, None)284        p.set_xy(p.l_margin + 9, y - 1)285        p.set_font("helvetica", "B", 19)286        p.set_text_color(*INK)287        p.cell(p.get_string_width(left) + 1.5, 11, left)288        if boxed:289            bw = p.get_string_width(boxed) + 7290            bx = p.get_x() + 1291            p.set_fill_color(*INK)292            p.rect(bx, y - 0.6, bw, 10.4, style="F", round_corners=True, corner_radius=2)293            p.set_text_color(*accent)294            p.set_xy(bx + 3.5, y - 1)295            p.cell(bw - 7, 11, boxed)296        p.set_font("helvetica", "B", 9)297        p.set_text_color(*INK3)298        p.set_xy(p.l_margin, y + 2)299        p.cell(174, 6, "https://" + site.get("domain", ""), align="R")300        p.set_y(y + 12)301        if site.get("tagline"):302            p.set_x(p.l_margin)303            p.set_font("helvetica", "", 9)304            p.set_text_color(*INK2)305            p.cell(0, 5, str(site["tagline"]))306            p.ln(7)307308    def _platform_section(self, pl: dict):309        p = self.pdf310        site = pl["site"]311        dash = pl.get("dashboard")312        accent = _hex(site.get("accent", "#d9f26b"))313        p.add_page()314        self._platform_header(site, accent)315        if not dash:316            y = p.get_y() + 2317            self._card(p.l_margin, y, 174, 26, fill=SURFACE2)318            p.set_xy(p.l_margin + 6, y + 5)319            p.set_font("helvetica", "B", 11)320            p.set_text_color(*INK2)321            p.cell(0, 6, "Données indisponibles")322            p.set_xy(p.l_margin + 6, y + 12)323            p.set_font("helvetica", "", 8.6)324            p.set_text_color(*INK3)325            p.multi_cell(326                162,327                4.4,328                f"La plateforme n'a pas répondu au moment de la génération "329                f"({pl.get('error') or 'erreur inconnue'}). Ses statistiques restent "330                f"consultables sur https://{site.get('domain', '')}/stats.",331            )332            p.set_y(y + 30)333            return334        # fraîcheur de la plateforme335        upd = dash.get("updated")336        if upd:337            p.set_x(p.l_margin)338            p.set_font("helvetica", "", 7.6)339            p.set_text_color(*INK3)340            p.cell(0, 4, f"Mis à jour : {str(upd)[:19].replace('T', ' à ')}")341            p.ln(6)342        # réutilise le moteur commun avec l'identité de la plateforme343        saved = (self.d, self.site, self.accent)344        self.d, self.site, self.accent = dash, site, accent345        try:346            self._kpis("Indicateurs — " + site.get("wordmark", ""))347            self._records()348        finally:349            self.d, self.site, self.accent = saved350351    # ------------------------------------------------------------------ build352    def build(self) -> bytes:353        p = self.pdf354        p.alias_nb_pages()355        self._cover()356        p.add_page()357        self._consolidation()358        if self.mode == "complet":359            for pl in self.ranked:360                self._platform_section(pl)361        self._final_page()362        return bytes(p.output())363
Files · c047f3cdc8769817c51c4663b16200363c6b7876
Menu

Theme