Refactoring
This commit is contained in:
1
tools/lib/__init__.py
Normal file
1
tools/lib/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Shared Python helpers for tools."""
|
||||||
@@ -3,7 +3,7 @@ const path = require("path");
|
|||||||
|
|
||||||
async function renderWithPython({ type, data, outputPath }) {
|
async function renderWithPython({ type, data, outputPath }) {
|
||||||
return new Promise((resolve, reject) => {
|
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], {
|
const child = spawn("python3", [scriptPath, "--type", type, "--output", outputPath], {
|
||||||
stdio: ["pipe", "inherit", "inherit"],
|
stdio: ["pipe", "inherit", "inherit"],
|
||||||
});
|
});
|
||||||
@@ -29,4 +29,3 @@ async function renderWithPython({ type, data, outputPath }) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
renderWithPython,
|
renderWithPython,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -57,7 +57,7 @@ def main():
|
|||||||
|
|
||||||
# Render via shared renderer
|
# Render via shared renderer
|
||||||
try:
|
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
|
except ImportError as exc: # noqa: BLE001
|
||||||
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -46,7 +46,7 @@ def main():
|
|||||||
values = [value for _, value in top]
|
values = [value for _, value in top]
|
||||||
|
|
||||||
try:
|
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
|
except ImportError as exc: # noqa: BLE001
|
||||||
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -48,7 +48,7 @@ def main():
|
|||||||
values = [counts[y] for y in sorted_years]
|
values = [counts[y] for y in sorted_years]
|
||||||
|
|
||||||
try:
|
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
|
except ImportError as exc: # noqa: BLE001
|
||||||
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def main():
|
|||||||
cum_words.append(total_w)
|
cum_words.append(total_w)
|
||||||
|
|
||||||
try:
|
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
|
except ImportError as exc: # noqa: BLE001
|
||||||
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ def main():
|
|||||||
values = [item[1] for item in top]
|
values = [item[1] for item in top]
|
||||||
|
|
||||||
try:
|
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
|
except ImportError as exc: # noqa: BLE001
|
||||||
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -54,7 +54,7 @@ def main():
|
|||||||
presses = [p for p in presses if p is not None]
|
presses = [p for p in presses if p is not None]
|
||||||
|
|
||||||
try:
|
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
|
except ImportError as exc: # noqa: BLE001
|
||||||
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def main():
|
|||||||
words[weekday] += article.get("wordCount") or 0
|
words[weekday] += article.get("wordCount") or 0
|
||||||
|
|
||||||
try:
|
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
|
except ImportError as exc: # noqa: BLE001
|
||||||
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def main():
|
|||||||
values = [a.get("wordCount") or 0 for a in articles if a.get("wordCount")]
|
values = [a.get("wordCount") or 0 for a in articles if a.get("wordCount")]
|
||||||
|
|
||||||
try:
|
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
|
except ImportError as exc: # noqa: BLE001
|
||||||
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -64,7 +64,7 @@ def main():
|
|||||||
average = round(total_words / total_articles) if total_articles > 0 else 0
|
average = round(total_words / total_articles) if total_articles > 0 else 0
|
||||||
|
|
||||||
try:
|
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
|
except ImportError as exc: # noqa: BLE001
|
||||||
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
print(f"Failed to import renderer: {exc}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user