From 3477ed04bed6acd427a2946769f714f4645ff4f7 Mon Sep 17 00:00:00 2001 From: Richard Dern Date: Fri, 28 Nov 2025 16:05:31 +0100 Subject: [PATCH] Refactoring --- tools/lib/__init__.py | 1 + tools/{ => lib}/render_stats_charts.py | 0 tools/lib/stats/python.js | 3 +- tools/stats/articles_per_month.js | 67 -------------------- tools/stats/articles_per_month.py | 2 +- tools/stats/articles_per_section.js | 55 ----------------- tools/stats/articles_per_section.py | 2 +- tools/stats/articles_per_year.js | 54 ---------------- tools/stats/articles_per_year.py | 2 +- tools/stats/cumulative_articles.py | 2 +- tools/stats/top_requests.py | 2 +- tools/stats/unique_visitors_per_month.js | 46 -------------- tools/stats/weather_hexbin.py | 2 +- tools/stats/weekday_activity.py | 2 +- tools/stats/words_histogram.py | 2 +- tools/stats/words_per_article.js | 79 ------------------------ tools/stats/words_per_article.py | 2 +- 17 files changed, 11 insertions(+), 312 deletions(-) create mode 100644 tools/lib/__init__.py rename tools/{ => lib}/render_stats_charts.py (100%) delete mode 100644 tools/stats/articles_per_month.js delete mode 100644 tools/stats/articles_per_section.js delete mode 100644 tools/stats/articles_per_year.js delete mode 100644 tools/stats/unique_visitors_per_month.js delete mode 100644 tools/stats/words_per_article.js diff --git a/tools/lib/__init__.py b/tools/lib/__init__.py new file mode 100644 index 00000000..76061fd1 --- /dev/null +++ b/tools/lib/__init__.py @@ -0,0 +1 @@ +"""Shared Python helpers for tools.""" diff --git a/tools/render_stats_charts.py b/tools/lib/render_stats_charts.py similarity index 100% rename from tools/render_stats_charts.py rename to tools/lib/render_stats_charts.py diff --git a/tools/lib/stats/python.js b/tools/lib/stats/python.js index ff9b5eb0..acd408c1 100644 --- a/tools/lib/stats/python.js +++ b/tools/lib/stats/python.js @@ -3,7 +3,7 @@ const path = require("path"); async function renderWithPython({ type, data, outputPath }) { return new Promise((resolve, reject) => { - const scriptPath = path.resolve(__dirname, "../../render_stats_charts.py"); + const scriptPath = path.resolve(__dirname, "../render_stats_charts.py"); const child = spawn("python3", [scriptPath, "--type", type, "--output", outputPath], { stdio: ["pipe", "inherit", "inherit"], }); @@ -29,4 +29,3 @@ async function renderWithPython({ type, data, outputPath }) { module.exports = { renderWithPython, }; - diff --git a/tools/stats/articles_per_month.js b/tools/stats/articles_per_month.js deleted file mode 100644 index 0388242f..00000000 --- a/tools/stats/articles_per_month.js +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env node - -const { loadArticles } = require("../lib/stats/articles"); -const { renderWithPython } = require("../lib/stats/python"); - -const MONTH_LABELS = ["Jan", "Fev", "Mar", "Avr", "Mai", "Jun", "Jul", "Aou", "Sep", "Oct", "Nov", "Dec"]; - -function groupByMonthAndYear(articles) { - const counts = new Map(); - const years = new Set(); - let first = null; - let last = null; - - for (const article of articles) { - if (!article.date) continue; - const year = article.date.year; - const month = article.date.month; // 1..12 - years.add(year); - const key = `${year}-${month}`; - counts.set(key, (counts.get(key) || 0) + 1); - if (!first || article.date < first) first = article.date; - if (!last || article.date > last) last = article.date; - } - - const monthNumbers = Array.from({ length: 12 }, (_, index) => index + 1); - const labels = monthNumbers.map((month) => MONTH_LABELS[month - 1]); - const sortedYears = Array.from(years).sort((a, b) => a - b); - - const series = sortedYears.map((year) => { - return { - label: String(year), - values: monthNumbers.map((month) => counts.get(`${year}-${month}`) || 0), - }; - }); - - return { labels, series, first, last }; -} - -async function run({ contentDir, outputPath, publicPath }) { - if (!outputPath) { - throw new Error("outputPath manquant pour articles_per_month"); - } - - const articles = await loadArticles(contentDir || "content"); - const { labels, series, first, last } = groupByMonthAndYear(articles); - - await renderWithPython({ - type: "articles_per_month", - outputPath, - data: { - labels, - series, - title: "Articles par mois", - }, - }); - - return { - image: publicPath, - meta: { - from: first ? first.toISODate() : null, - to: last ? last.toISODate() : null, - months: labels.length, - }, - }; -} - -module.exports = { run }; diff --git a/tools/stats/articles_per_month.py b/tools/stats/articles_per_month.py index 7a25d617..e63a8d27 100644 --- a/tools/stats/articles_per_month.py +++ b/tools/stats/articles_per_month.py @@ -57,7 +57,7 @@ def main(): # Render via shared renderer try: - from render_stats_charts import render_articles_per_month, setup_rcparams + from lib.render_stats_charts import render_articles_per_month, setup_rcparams except ImportError as exc: # noqa: BLE001 print(f"Failed to import renderer: {exc}", file=sys.stderr) sys.exit(1) diff --git a/tools/stats/articles_per_section.js b/tools/stats/articles_per_section.js deleted file mode 100644 index 1937099b..00000000 --- a/tools/stats/articles_per_section.js +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env node - -const { loadArticles } = require("../lib/stats/articles"); -const { renderWithPython } = require("../lib/stats/python"); - -function groupBySection(articles) { - const counts = new Map(); - - for (const article of articles) { - const key = article.section || "root"; - counts.set(key, (counts.get(key) || 0) + 1); - } - - const entries = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]); - return entries; -} - -async function run({ contentDir, outputPath, publicPath }) { - if (!outputPath) { - throw new Error("outputPath manquant pour articles_per_section"); - } - - const articles = await loadArticles(contentDir || "content"); - const entries = groupBySection(articles); - - const maxSlices = 21; - const top = entries.slice(0, maxSlices); - const rest = entries.slice(maxSlices); - if (rest.length > 0) { - const restSum = rest.reduce((sum, [, value]) => sum + value, 0); - top.push(["Others", restSum]); - } - - const labels = top.map(([key]) => key); - const values = top.map(([, value]) => value); - - await renderWithPython({ - type: "articles_per_section", - outputPath, - data: { - labels, - values, - title: "Articles par section", - }, - }); - - return { - image: publicPath, - meta: { - sections: entries.length, - }, - }; -} - -module.exports = { run }; diff --git a/tools/stats/articles_per_section.py b/tools/stats/articles_per_section.py index 00d1a596..222545b1 100644 --- a/tools/stats/articles_per_section.py +++ b/tools/stats/articles_per_section.py @@ -46,7 +46,7 @@ def main(): values = [value for _, value in top] try: - from render_stats_charts import render_articles_per_section, setup_rcparams + from lib.render_stats_charts import render_articles_per_section, setup_rcparams except ImportError as exc: # noqa: BLE001 print(f"Failed to import renderer: {exc}", file=sys.stderr) sys.exit(1) diff --git a/tools/stats/articles_per_year.js b/tools/stats/articles_per_year.js deleted file mode 100644 index b55e3a98..00000000 --- a/tools/stats/articles_per_year.js +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env node - -const { loadArticles } = require("../lib/stats/articles"); -const { renderWithPython } = require("../lib/stats/python"); - -function groupByYear(articles) { - const counts = new Map(); - let first = null; - let last = null; - - for (const article of articles) { - if (!article.date) continue; - const year = article.date.year; - counts.set(year, (counts.get(year) || 0) + 1); - if (!first || article.date < first) first = article.date; - if (!last || article.date > last) last = article.date; - } - - const entries = Array.from(counts.entries()).sort((a, b) => a[0] - b[0]); - const labels = entries.map(([year]) => `${year}`); - const values = entries.map(([, value]) => value); - - return { labels, values, first, last }; -} - -async function run({ contentDir, outputPath, publicPath }) { - if (!outputPath) { - throw new Error("outputPath manquant pour articles_per_year"); - } - - const articles = await loadArticles(contentDir || "content"); - const { labels, values, first, last } = groupByYear(articles); - - await renderWithPython({ - type: "articles_per_year", - outputPath, - data: { - labels, - values, - title: "Articles par an", - }, - }); - - return { - image: publicPath, - meta: { - from: first ? first.toISODate() : null, - to: last ? last.toISODate() : null, - years: labels.length, - }, - }; -} - -module.exports = { run }; diff --git a/tools/stats/articles_per_year.py b/tools/stats/articles_per_year.py index 60fcb2c1..009f1cb4 100644 --- a/tools/stats/articles_per_year.py +++ b/tools/stats/articles_per_year.py @@ -48,7 +48,7 @@ def main(): values = [counts[y] for y in sorted_years] try: - from render_stats_charts import render_articles_per_year, setup_rcparams + from lib.render_stats_charts import render_articles_per_year, setup_rcparams except ImportError as exc: # noqa: BLE001 print(f"Failed to import renderer: {exc}", file=sys.stderr) sys.exit(1) diff --git a/tools/stats/cumulative_articles.py b/tools/stats/cumulative_articles.py index a4089e5c..a9ae5485 100644 --- a/tools/stats/cumulative_articles.py +++ b/tools/stats/cumulative_articles.py @@ -62,7 +62,7 @@ def main(): cum_words.append(total_w) try: - from render_stats_charts import render_cumulative, setup_rcparams + from lib.render_stats_charts import render_cumulative, setup_rcparams except ImportError as exc: # noqa: BLE001 print(f"Failed to import renderer: {exc}", file=sys.stderr) sys.exit(1) diff --git a/tools/stats/top_requests.py b/tools/stats/top_requests.py index 0c686876..eced5d04 100644 --- a/tools/stats/top_requests.py +++ b/tools/stats/top_requests.py @@ -115,7 +115,7 @@ def main(): values = [item[1] for item in top] try: - from render_stats_charts import render_top_requests, setup_rcparams + from lib.render_stats_charts import render_top_requests, setup_rcparams except ImportError as exc: # noqa: BLE001 print(f"Failed to import renderer: {exc}", file=sys.stderr) sys.exit(1) diff --git a/tools/stats/unique_visitors_per_month.js b/tools/stats/unique_visitors_per_month.js deleted file mode 100644 index a0984a6b..00000000 --- a/tools/stats/unique_visitors_per_month.js +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env node - -const fs = require("fs/promises"); -const { renderChart, makeBarConfig } = require("../lib/stats/charts"); -const { fetchGoAccessJson, groupVisitsByMonth } = require("../lib/stats/goaccess"); -const { loadToolsConfig } = require("../lib/config"); - -async function run({ stat, outputPath, publicPath }) { - if (!outputPath) { - throw new Error("outputPath manquant pour unique_visitors_per_month"); - } - - const toolsConfig = await loadToolsConfig(); - const url = stat.url || toolsConfig.goaccess?.url; - if (!url) { - throw new Error("URL GoAccess manquante (GOACCESS_URL ou goaccess.url dans tools/config.json)"); - } - const data = await fetchGoAccessJson(url); - const months = groupVisitsByMonth(data, { adjustCrawlers: true }); - - const labels = months.map((entry) => entry.month); - const values = months.map((entry) => entry.visitors); - - const buffer = await renderChart( - makeBarConfig({ - labels, - data: values, - title: "Visiteurs uniques par mois (hors crawlers)", - color: "rgba(239, 68, 68, 0.8)", - }), - ); - - await fs.writeFile(outputPath, buffer); - - const latest = months[months.length - 1]; - - return { - image: publicPath, - meta: { - month: latest?.month || null, - visitors: latest?.visitors || 0, - }, - }; -} - -module.exports = { run }; diff --git a/tools/stats/weather_hexbin.py b/tools/stats/weather_hexbin.py index 541df482..39ad6372 100644 --- a/tools/stats/weather_hexbin.py +++ b/tools/stats/weather_hexbin.py @@ -54,7 +54,7 @@ def main(): presses = [p for p in presses if p is not None] try: - from render_stats_charts import render_weather_hexbin, setup_rcparams + from lib.render_stats_charts import render_weather_hexbin, setup_rcparams except ImportError as exc: # noqa: BLE001 print(f"Failed to import renderer: {exc}", file=sys.stderr) sys.exit(1) diff --git a/tools/stats/weekday_activity.py b/tools/stats/weekday_activity.py index e3bbee64..a8600bc6 100644 --- a/tools/stats/weekday_activity.py +++ b/tools/stats/weekday_activity.py @@ -41,7 +41,7 @@ def main(): words[weekday] += article.get("wordCount") or 0 try: - from render_stats_charts import render_weekday_activity, setup_rcparams + from lib.render_stats_charts import render_weekday_activity, setup_rcparams except ImportError as exc: # noqa: BLE001 print(f"Failed to import renderer: {exc}", file=sys.stderr) sys.exit(1) diff --git a/tools/stats/words_histogram.py b/tools/stats/words_histogram.py index 98ed8c40..810d71c8 100644 --- a/tools/stats/words_histogram.py +++ b/tools/stats/words_histogram.py @@ -29,7 +29,7 @@ def main(): values = [a.get("wordCount") or 0 for a in articles if a.get("wordCount")] try: - from render_stats_charts import render_words_histogram, setup_rcparams + from lib.render_stats_charts import render_words_histogram, setup_rcparams except ImportError as exc: # noqa: BLE001 print(f"Failed to import renderer: {exc}", file=sys.stderr) sys.exit(1) diff --git a/tools/stats/words_per_article.js b/tools/stats/words_per_article.js deleted file mode 100644 index 76f1865b..00000000 --- a/tools/stats/words_per_article.js +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env node - -const { loadArticles } = require("../lib/stats/articles"); -const { renderWithPython } = require("../lib/stats/python"); - -const MONTH_LABELS = ["Jan", "Fev", "Mar", "Avr", "Mai", "Jun", "Jul", "Aou", "Sep", "Oct", "Nov", "Dec"]; - -function groupAverageWordsByMonth(articles) { - const buckets = new Map(); - const years = new Set(); - let totalWords = 0; - let totalArticles = 0; - - for (const article of articles) { - if (!article.date) continue; - const year = article.date.year; - const month = article.date.month; - const key = `${year}-${month}`; - - years.add(year); - const current = buckets.get(key) || { words: 0, count: 0 }; - current.words += article.wordCount || 0; - current.count += 1; - buckets.set(key, current); - - totalWords += article.wordCount || 0; - totalArticles += 1; - } - - const monthNumbers = Array.from({ length: 12 }, (_, index) => index + 1); - const labels = monthNumbers.map((month) => MONTH_LABELS[month - 1]); - const sortedYears = Array.from(years).sort((a, b) => a - b); - - const series = sortedYears.map((year) => { - const values = monthNumbers.map((month) => { - const entry = buckets.get(`${year}-${month}`); - if (!entry || entry.count === 0) return 0; - return Math.round(entry.words / entry.count); - }); - - return { - label: String(year), - values, - }; - }); - - const average = totalArticles > 0 ? Math.round(totalWords / totalArticles) : 0; - - return { labels, series, average, articles: totalArticles }; -} - -async function run({ contentDir, outputPath, publicPath }) { - if (!outputPath) { - throw new Error("outputPath manquant pour le graphique words_per_article"); - } - - const articles = await loadArticles(contentDir || "content"); - const { labels, series, average, articles: totalArticles } = groupAverageWordsByMonth(articles); - - await renderWithPython({ - type: "words_per_article", - outputPath, - data: { - labels, - series, - title: "Moyenne de mots par article (par mois)", - }, - }); - - return { - image: publicPath, - meta: { - average, - articles: totalArticles, - }, - }; -} - -module.exports = { run }; diff --git a/tools/stats/words_per_article.py b/tools/stats/words_per_article.py index e4d29587..a366e8cb 100644 --- a/tools/stats/words_per_article.py +++ b/tools/stats/words_per_article.py @@ -64,7 +64,7 @@ def main(): average = round(total_words / total_articles) if total_articles > 0 else 0 try: - from render_stats_charts import render_words_per_article, setup_rcparams + from lib.render_stats_charts import render_words_per_article, setup_rcparams except ImportError as exc: # noqa: BLE001 print(f"Failed to import renderer: {exc}", file=sys.stderr) sys.exit(1)