Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)
Python 73.6%
HTML 13.2%
TypeScript 6%
JavaScript 4.5%
CSS 1.7%
Dockerfile 0.6%
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File: creaka/connectors/wikidata_qc.py4# Desc: Connecteur DÉCOUVERTE — créateurs québécois recensés dans Wikidata5# (personnes nées ou résidant au Québec, occupation créateur, avec6# handles sociaux curés P2003/P7085/P2397/P5797/P2002). Mode d'accès :7# API SPARQL publique de Wikidata (gratuite, conforme). Palier 3 (§9).8# ==============================================================================9"""Découverte via Wikidata : au-delà des listes médias.1011Wikidata référence les créateurs notables (humoristes, youtubeurs, vidéastes12web, streamers, podcasteurs, influenceurs) avec leurs comptes sociaux CURÉS13par la communauté → signal `base_publique` (0.85). Deux requêtes (lieu de14naissance / lieu de résidence au Québec) fusionnées.1516Prudence mineurs (§15) : l'année de naissance publique permet de lever17`is_minor` automatiquement → régime restreint appliqué par ethics.py.18"""19from __future__ import annotations2021import sys22from datetime import datetime, timezone2324from ..identity import account25from ..normalize import slugify26from ..schema import Creator27from .base import BaseConnector2829SPARQL_URL = "https://query.wikidata.org/sparql"3031# occupations « créateur de contenu » (ids Wikidata)32# Q17125263 vidéaste web · Q94791573 tiktokeur · Q50279140 streamer33# Q15077007 podcasteur · Q2906862 influenceur · Q245068 humoriste34_OCCS = "wd:Q17125263 wd:Q94791573 wd:Q50279140 wd:Q15077007 wd:Q2906862 wd:Q245068"3536_OCC_NICHE = {37 "Q245068": "humour", "Q50279140": "gaming", "Q15077007": "actualite-opinion",38 "Q17125263": "lifestyle", "Q94791573": "lifestyle", "Q2906862": "lifestyle",39}4041_QUERY = """42SELECT DISTINCT ?person ?personLabel ?occ ?ig ?tk ?yt ?tw ?x (YEAR(?dob) AS ?birthYear) WHERE {43 VALUES ?occ { %(occs)s }44 ?person wdt:P106 ?occ .45 ?person wdt:%(place_prop)s ?place .46 ?place wdt:P131* wd:Q176 . hint:Prior hint:gearing "forward" .47 OPTIONAL { ?person wdt:P2003 ?ig }48 OPTIONAL { ?person wdt:P7085 ?tk }49 OPTIONAL { ?person wdt:P2397 ?yt }50 OPTIONAL { ?person wdt:P5797 ?tw }51 OPTIONAL { ?person wdt:P2002 ?x }52 OPTIONAL { ?person wdt:P569 ?dob }53 FILTER(BOUND(?ig) || BOUND(?tk) || BOUND(?yt) || BOUND(?tw) || BOUND(?x))54 SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,en". }55}56LIMIT 300057"""585960class WikidataQcConnector(BaseConnector):61 source_id = "wikidata-qc"62 kind = "discovery"63 request_delay = 2.064 timeout = 906566 def _query(self, place_prop: str) -> list[dict]:67 resp = self.get(SPARQL_URL,68 params={"query": _QUERY % {"occs": _OCCS,69 "place_prop": place_prop}},70 headers={"Accept": "application/sparql-results+json"})71 return resp.json().get("results", {}).get("bindings", [])7273 def fetch(self) -> list[Creator]:74 rows: list[dict] = []75 self.errors = 0 # comptabilisé dans sync_log.errors par le pipeline (§16)76 for prop in ("P19", "P551"): # naissance, résidence77 try:78 rows.extend(self._query(prop))79 except Exception as exc:80 # une branche en échec ne bloque pas l'autre — mais JAMAIS en81 # silence : la couverture chute si une requête SPARQL casse82 self.errors += 183 print(f"[crea-ka] ⚠ wikidata-qc : requête SPARQL {prop} en "84 f"échec : {exc}", file=sys.stderr)85 by_person: dict[str, dict] = {}86 for r in rows:87 def val(key):88 return (r.get(key) or {}).get("value")89 pid = val("person")90 if not pid:91 continue92 entry = by_person.setdefault(93 pid, {"name": val("personLabel"), "occs": set(), "birth": None,94 "handles": {}})95 entry["occs"].add((val("occ") or "").rsplit("/", 1)[-1])96 if val("birthYear"):97 entry["birth"] = int(val("birthYear"))98 for key, platform in (("ig", "instagram"), ("tk", "tiktok"),99 ("tw", "twitch"), ("x", "x")):100 if val(key):101 entry["handles"][platform] = val(key)102 # P2397 = ID de chaîne YouTube (UC…) → URL directe, pas un handle @103 if val("yt"):104 entry["handles"]["youtube_channel"] = val("yt")105 year_now = datetime.now(timezone.utc).year106 creators: list[Creator] = []107 for pid, e in by_person.items():108 name = (e["name"] or "").strip()109 qid = pid.rsplit("/", 1)[-1]110 if not name or name == qid:111 continue # entité sans libellé humain112 accounts = []113 for platform, handle in e["handles"].items():114 if platform == "youtube_channel":115 accounts.append(account(116 "youtube", handle, "base_publique",117 url=f"https://www.youtube.com/channel/{handle}"))118 else:119 accounts.append(account(platform, handle, "base_publique"))120 if not accounts:121 continue122 is_minor = bool(e["birth"] and (year_now - e["birth"]) < 18)123 niches = sorted({_OCC_NICHE.get(o, "autre") for o in e["occs"]}) or ["autre"]124 creators.append(Creator(125 source=self.source_id,126 external_id=qid,127 display_name=name,128 niches=niches,129 is_minor=is_minor,130 platforms=accounts,131 source_ids=[f"{self.source_id}:{qid}"],132 ))133 # tri stable pour des passages reproductibles134 return sorted(creators, key=lambda c: slugify(c.display_name))135