<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dev du Sud</title>
    <description>The latest articles on DEV Community by Dev du Sud (@devdusud).</description>
    <link>https://dev.to/devdusud</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4144889%2Fead5322a-de85-4109-8a26-f9854b867d52.jpg</url>
      <title>DEV Community: Dev du Sud</title>
      <link>https://dev.to/devdusud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devdusud"/>
    <language>en</language>
    <item>
      <title>127 vues et 0 clic — le tableau de bord le plus utile que j'aie construit</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Sat, 14 Nov 2026 13:40:04 +0000</pubDate>
      <link>https://dev.to/devdusud/127-vues-et-0-clic-le-tableau-de-bord-le-plus-utile-que-jaie-construit-3a37</link>
      <guid>https://dev.to/devdusud/127-vues-et-0-clic-le-tableau-de-bord-le-plus-utile-que-jaie-construit-3a37</guid>
      <description>&lt;p&gt;J'ai écrit mon propre tracker de clics pour une raison simple : je voulais savoir si les&lt;br&gt;
visiteurs d'une page cliquaient sur le lien qu'elle contenait. Pas combien de temps ils&lt;br&gt;
restaient, pas d'où ils venaient, pas leur empreinte de navigateur. Une question, deux&lt;br&gt;
chiffres.&lt;/p&gt;

&lt;p&gt;Le premier rapport a répondu : &lt;strong&gt;127 vues, 0 clic&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ma première réaction a été de chercher le bug. Ma seconde, plus utile, a été de vérifier la&lt;br&gt;
mesure avant d'accuser les visiteurs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Le tracker, en quarante lignes
&lt;/h2&gt;

&lt;p&gt;Un fichier PHP, une table SQLite, deux types d'événements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;
&lt;span class="c1"&gt;// tracker.php — appelez-le en GET avec ?p=slug&amp;amp;e=vue ou ?p=slug&amp;amp;e=clic&lt;/span&gt;
&lt;span class="nv"&gt;$db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sqlite:'&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;__DIR__&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/stats.db'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_ERRMODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ERRMODE_EXCEPTION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"CREATE TABLE IF NOT EXISTS evenements (
  id       INTEGER PRIMARY KEY AUTOINCREMENT,
  ts       TEXT NOT NULL,
  page     TEXT NOT NULL,
  type     TEXT NOT NULL CHECK (type IN ('vue','clic')),
  cible    TEXT,
  ref      TEXT,
  ua_hash  TEXT
)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$page&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[^a-z0-9-]/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'p'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$type&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'e'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$cible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'c'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'vue'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'clic'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$ua&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_USER_AGENT'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// on garde une empreinte tronquée, pas l'adresse IP en clair&lt;/span&gt;
&lt;span class="nv"&gt;$empreinte&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ua&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"INSERT INTO evenements (ts, page, type, cible, ref, ua_hash)
     VALUES (datetime('now'), ?, ?, ?, ?, ?)"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$cible&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_REFERER'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$empreinte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// pas de corps : rien à afficher, rien à charger&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Côté page, deux appels : un &lt;code&gt;fetch&lt;/code&gt; au chargement, un sur le clic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/destination"&lt;/span&gt; &lt;span class="na"&gt;data-track&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lien-principal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Voir la fiche&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"tracker.php?p=ma-page&amp;amp;e=vue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;keepalive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"a[data-track]"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"click"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`tracker.php?p=ma-page&amp;amp;e=clic&amp;amp;c=${a.dataset.track}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;keepalive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;keepalive: true&lt;/code&gt; est le seul détail technique qui compte : sans lui, le navigateur annule la&lt;br&gt;
requête au moment où la page de destination commence à se charger, et le clic n'est jamais&lt;br&gt;
enregistré. C'est le bug classique de tous les trackers maison, et il produit exactement le&lt;br&gt;
symptôme que j'avais — sauf que ce n'était pas lui cette fois, ce qui m'a fait perdre une heure&lt;br&gt;
de plus.&lt;/p&gt;
&lt;h2&gt;
  
  
  127 et 0 : vérifier avant de conclure
&lt;/h2&gt;

&lt;p&gt;Trois vérifications, dans l'ordre.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. La base reçoit-elle quelque chose ?&lt;/strong&gt; Oui, 127 lignes de type &lt;code&gt;vue&lt;/code&gt;. Le tracker écrivait.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Le type &lt;code&gt;clic&lt;/code&gt; arrive-t-il quand on le provoque ?&lt;/strong&gt; Test manuel :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -s -o /dev/null -w &lt;span class="s2"&gt;"%{http_code}\n"&lt;/span&gt; \
  &lt;span class="s2"&gt;"https://exemple.fr/tracker.php?p=ma-page&amp;amp;e=clic&amp;amp;c=test"&lt;/span&gt;
sqlite3 stats.db &lt;span class="s2"&gt;"SELECT type, COUNT(*) FROM evenements GROUP BY type;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La ligne apparaît. Le code d'écriture est correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. La page envoie-t-elle vraiment un clic ?&lt;/strong&gt; C'est là qu'était la réponse : le bouton&lt;br&gt;
« Voir la fiche » n'était pas un lien. C'était une image avec un &lt;code&gt;onclick&lt;/code&gt; qui appelait&lt;br&gt;
&lt;code&gt;window.location&lt;/code&gt;, dans une balise sans &lt;code&gt;href&lt;/code&gt;. Le script de tracking écoutait les &lt;code&gt;a&lt;/code&gt;, et il&lt;br&gt;
n'y avait aucun &lt;code&gt;a&lt;/code&gt; à écouter.&lt;/p&gt;

&lt;p&gt;Le tracker mesurait correctement une page qui n'avait rien à cliquer.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pourquoi le zéro vaut mieux que la moyenne
&lt;/h2&gt;

&lt;p&gt;Un outil d'analytics classique m'aurait affiché autre chose. Il aurait compté une visite, une&lt;br&gt;
page vue, un taux de rebond, une durée de session, et j'en aurais déduit que la page&lt;br&gt;
« intéressait ». Aucun de ces chiffres ne répond à la question que je m'étais posée.&lt;/p&gt;

&lt;p&gt;Le zéro, lui, répond. Il dit : personne n'a cliqué. Et comme il est inconfortable, il oblige à&lt;br&gt;
descendre dans le détail — jusqu'à la balise &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; manquante.&lt;/p&gt;

&lt;p&gt;C'est la différence entre une métrique de vanité et une mesure honnête :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;une métrique de vanité monte quand on travaille plus, et ne descend jamais toute seule ;&lt;/li&gt;
&lt;li&gt;une mesure honnête peut valoir zéro, et c'est précisément pour ça qu'on la croit quand elle
vaut autre chose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Une page vue n'est pas un intérêt. Un temps de lecture estimé n'est pas une lecture. Un taux de&lt;br&gt;
clic de 0 % est un fait : il y a un lien à réparer, ou une audience qui n'est pas là.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ce que j'ai changé : la page, pas le tracker
&lt;/h2&gt;

&lt;p&gt;La tentation, quand un chiffre déplaît, c'est de changer le thermomètre. Ajouter une mesure de&lt;br&gt;
défilement, compter les « intentions de clic », élargir la définition d'un clic jusqu'à ce&lt;br&gt;
qu'elle inclue les survols.&lt;/p&gt;

&lt;p&gt;J'ai fait l'inverse. J'ai corrigé le HTML, remis un vrai &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt;, et laissé le tracker&lt;br&gt;
identique. La question « est-ce que les gens cliquent » n'a de valeur que si la réponse peut&lt;br&gt;
être non.&lt;/p&gt;

&lt;p&gt;Et j'ai gardé le zéro dans l'historique au lieu de le masquer. Un rapport qui commence par&lt;br&gt;
« 127 vues, 0 clic » raconte une histoire vraie : une page a été vue, et elle n'offrait rien à&lt;br&gt;
cliquer. Effacer cette ligne aurait rendu les chiffres suivants illisibles.&lt;/p&gt;
&lt;h2&gt;
  
  
  Les limites d'un tracker maison
&lt;/h2&gt;

&lt;p&gt;Elles existent, et il faut les connaître avant de prendre une décision sur ses propres&lt;br&gt;
chiffres.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Les robots.&lt;/strong&gt; Une partie des 127 vues n'est peut-être pas humaine. Un filtre sur le
&lt;code&gt;User-Agent&lt;/code&gt; en élimine une partie, jamais la totalité.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Les bloqueurs.&lt;/strong&gt; Un visiteur avec un bloqueur de publicité peut bloquer &lt;code&gt;tracker.php&lt;/code&gt; s'il
est sur une liste. La mesure est donc un plancher, pas un total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pas de déduplication.&lt;/strong&gt; Un rechargement compte une vue de plus. C'est acceptable pour
comparer deux périodes, faux pour compter des personnes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L'attribution est approximative.&lt;/strong&gt; Le &lt;code&gt;Referer&lt;/code&gt; est absent dans beaucoup de cas, et
falsifiable dans tous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le stockage.&lt;/strong&gt; Une empreinte de &lt;code&gt;User-Agent&lt;/code&gt; n'identifie personne, mais ce n'est pas
anonyme pour autant. Je la tronque, je ne stocke pas l'IP, et je purge la table au bout de
90 jours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ces limites ne rendent pas la mesure inutile. Elles rendent le chiffre interprétable : 127&lt;br&gt;
vues est un ordre de grandeur, pas une audience. 0 clic est un fait, pas une estimation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ce qu'un tableau de bord honnête doit accepter
&lt;/h2&gt;

&lt;p&gt;Un tableau de bord n'est pas un miroir de son travail. C'est un miroir de ce qu'on a décidé de&lt;br&gt;
mesurer — et cette décision est plus importante que l'outil.&lt;/p&gt;

&lt;p&gt;J'ai construit celui-ci pour répondre à une question précise, avec deux colonnes et une&lt;br&gt;
requête :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="nb"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'vue'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;vues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="nb"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'clic'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;clics&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;evenements&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="kt"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-30 day'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;vues&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deux colonnes, une période, pas de score composite. C'est tout ce que j'ai besoin de savoir, et&lt;br&gt;
c'est suffisant pour constater qu'une page est vue et que son lien ne fonctionne pas.&lt;/p&gt;

&lt;p&gt;Le reste — le temps passé, la profondeur de défilement, le parcours de conversion — ne&lt;br&gt;
répondrait pas mieux. Il donnerait juste plus de chiffres à interpréter, et une pente plus&lt;br&gt;
facile à trouver flatteuse.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>17 modèles LLM en local sur un iGPU et 14 Go de RAM — ce que ça vaut vraiment</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Fri, 13 Nov 2026 08:46:17 +0000</pubDate>
      <link>https://dev.to/devdusud/17-modeles-llm-en-local-sur-un-igpu-et-14-go-de-ram-ce-que-ca-vaut-vraiment-572h</link>
      <guid>https://dev.to/devdusud/17-modeles-llm-en-local-sur-un-igpu-et-14-go-de-ram-ce-que-ca-vaut-vraiment-572h</guid>
      <description>&lt;p&gt;Dix-sept modèles téléchargés, un processeur graphique intégré, 14 Go de mémoire partagée avec&lt;br&gt;
le système. Pas de GPU dédié, pas de serveur, pas de facture à la requête.&lt;/p&gt;

&lt;p&gt;Beaucoup la décriraient comme insuffisante, et elle l'est en effet pour certaines choses. Mais&lt;br&gt;
elle tourne tous les jours, et sur une partie de mes tâches elle est plus utile qu'une API.&lt;/p&gt;

&lt;p&gt;La question n'est pas « est-ce que ça marche », mais « à quoi ça sert, et jusqu'à quand ».&lt;/p&gt;
&lt;h2&gt;
  
  
  Ce que 14 Go veulent dire pour un modèle
&lt;/h2&gt;

&lt;p&gt;Un modèle occupe de la mémoire pour trois raisons, et il faut les trois.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Les poids.&lt;/strong&gt; Sept milliards de paramètres en 4 bits, environ 4,5 Go. En 8 bits, le double.
En 16 bits, quatre fois plus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le cache de contexte.&lt;/strong&gt; Chaque token occupe de la mémoire, pour chaque couche. Il croît
avec la longueur du contexte, et c'est souvent lui qui fait déborder, pas les poids.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le runtime.&lt;/strong&gt; Quelques centaines de mégaoctets, plus ce que le système utilise déjà.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sur un iGPU, il n'y a pas de VRAM séparée : le graphique intégré prend sa mémoire dans les&lt;br&gt;
14 Go systèmes. Un modèle de 7B en 4 bits laisse de quoi respirer. Un 14B en 4 bits frôle la&lt;br&gt;
limite et ne supporte pas un contexte long. Au-delà de 30B, ce n'est pas une question de&lt;br&gt;
patience : ça ne rentre pas.&lt;/p&gt;

&lt;p&gt;La taille du modèle est une contrainte dure, pas un réglage : aucun paramètre d'Ollama ne fera&lt;br&gt;
tenir 40 Go dans 14.&lt;/p&gt;
&lt;h2&gt;
  
  
  Dix-sept modèles sur le disque, un seul en mémoire
&lt;/h2&gt;

&lt;p&gt;Avoir dix-sept modèles ne veut pas dire en faire tourner dix-sept. Ollama charge un modèle quand&lt;br&gt;
une requête arrive, le garde quelques minutes, puis le décharge quand un autre arrive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama list      &lt;span class="c"&gt;# ce qui est sur le disque&lt;/span&gt;
ollama ps        &lt;span class="c"&gt;# ce qui est réellement chargé, et pour combien de temps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ollama ps&lt;/code&gt; est la commande que je regarde le plus : elle explique pourquoi une requête&lt;br&gt;
« rapide » prend parfois quinze secondes. Le modèle était froid.&lt;/p&gt;

&lt;p&gt;Deux conséquences. Un modèle à la fois : deux 7B chargés ensemble, et la machine échange sur le&lt;br&gt;
disque, avec un ralentissement d'un facteur dix, pas deux. Et le premier token coûte cher : sur&lt;br&gt;
un usage interactif, la latence de chargement se sent ; sur un lot de deux cents documents, elle&lt;br&gt;
est amortie.&lt;/p&gt;
&lt;h2&gt;
  
  
  Là où le local gagne, sans discussion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Les données qui ne sortent pas.&lt;/strong&gt; Un document client, un export de base, un journal&lt;br&gt;
d'incident : tout ça part chez un fournisseur dès qu'on appelle une API. En local, ça reste sur&lt;br&gt;
le disque. Ce n'est pas une opinion sur la confidentialité, c'est une différence de nature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le volume régulier.&lt;/strong&gt; Classer deux mille lignes, extraire des champs de trois cents factures,&lt;br&gt;
reformater du texte : le coût par appel, multiplié par le volume, devient un budget. En local,&lt;br&gt;
le coût marginal est de l'électricité.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;L'absence de limite de débit.&lt;/strong&gt; Pas de quota, pas de 429, pas de fenêtre de tokens à&lt;br&gt;
surveiller. Un script qui traite toute la nuit ne s'arrête pas au bout de cent requêtes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La reproductibilité.&lt;/strong&gt; Le même modèle, la même version, le même fichier de poids dans six&lt;br&gt;
mois. Une API change de modèle derrière son nom sans prévenir, et une sortie qui marchait cesse&lt;br&gt;
de marcher sans qu'une ligne de code ait bougé.&lt;/p&gt;
&lt;h2&gt;
  
  
  Là où l'API gagne, et il faut le dire
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Le contexte long.&lt;/strong&gt; Un document de cent pages, une base de code entière : sur 14 Go, le cache&lt;br&gt;
de contexte devient le facteur limitant bien avant la qualité du modèle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le raisonnement difficile.&lt;/strong&gt; Un petit modèle produit des réponses plausibles et fausses sur&lt;br&gt;
les problèmes à plusieurs étapes. Il n'échoue pas bruyamment : il échoue avec assurance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La vision et l'audio.&lt;/strong&gt; Ceux qui tiennent dans 14 Go existent, mais leur qualité n'est pas&lt;br&gt;
comparable : pour lire un graphique ou transcrire une réunion, l'API reste devant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La latence sur les gros modèles.&lt;/strong&gt; Un 70B sur une API répond en quelques secondes ; le même en&lt;br&gt;
local ne rentre pas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La maintenance.&lt;/strong&gt; Un modèle local, c'est plusieurs gigaoctets à télécharger et une version à&lt;br&gt;
figer. Une API, c'est une clé.&lt;/p&gt;
&lt;h2&gt;
  
  
  Router selon la tâche
&lt;/h2&gt;

&lt;p&gt;Le code ne fait pas de magie : il choisit un modèle selon la tâche et appelle Ollama en HTTP&lt;br&gt;
local.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;local&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modele&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contexte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"http://127.0.0.1:11434/api/generate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;modele&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"keep_alive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"10m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# le modèle chaud reste 10 min en mémoire&lt;/span&gt;
            &lt;span class="s2"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"num_ctx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contexte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="no"&gt;TACHES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"classer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"petit-modele-3b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"extraire"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"petit-modele-3b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"resumer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"moyen-modele-7b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deux réglages comptent plus que le choix du modèle. &lt;code&gt;num_ctx&lt;/code&gt; explicite : la valeur par défaut&lt;br&gt;
peut être plus grande que nécessaire, et le cache de contexte est ce qui fait planter une&lt;br&gt;
machine à 14 Go. &lt;code&gt;keep_alive&lt;/code&gt; à dix minutes : sur un lot de deux cents documents, ça évite de&lt;br&gt;
recharger le modèle entre chaque appel ; sur un usage sporadique, ça libère la mémoire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que la mesure tranche
&lt;/h2&gt;

&lt;p&gt;Je ne publie pas de tokens par seconde, parce que le chiffre ne veut rien dire sans le modèle,&lt;br&gt;
la quantification, la longueur de contexte et la machine. Je mesure trois choses.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Est-ce que ça tient dans une conversation interactive ?&lt;/strong&gt; Une réponse après vingt secondes
sur une question courte fait du local un outil batch, pas un assistant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Est-ce que la mémoire reste sous le plafond ?&lt;/strong&gt; Une machine qui échange sur le disque ment
sur ses performances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Est-ce que la qualité passe sur vingt cas réels ?&lt;/strong&gt; Mes documents, mes erreurs, comparés à
la main. Pas un benchmark public.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sur ces trois critères, la répartition est simple : le local prend le volume et la&lt;br&gt;
confidentialité, l'API prend le raisonnement et le multimédia. Le choix se fait par une ligne&lt;br&gt;
dans un dictionnaire.&lt;/p&gt;

&lt;p&gt;Dix-sept modèles sur un disque et 14 Go de RAM, ce n'est pas une performance : c'est un poste de&lt;br&gt;
travail qui fait ce qu'il peut faire. Le local ne remplace pas une API, il la complète là où&lt;br&gt;
payer n'a pas de sens et où envoyer les données n'en a pas non plus. La seule erreur serait de&lt;br&gt;
trancher par principe — ça se tranche par tâche, avec une mesure.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>tools</category>
      <category>performance</category>
    </item>
    <item>
      <title>317 tâches et 36 projets dans SQLite — le statut « abandonné » m'a plus servi que les autres</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Thu, 12 Nov 2026 07:40:16 +0000</pubDate>
      <link>https://dev.to/devdusud/317-taches-et-36-projets-dans-sqlite-le-statut-abandonne-ma-plus-servi-que-les-autres-4lp8</link>
      <guid>https://dev.to/devdusud/317-taches-et-36-projets-dans-sqlite-le-statut-abandonne-ma-plus-servi-que-les-autres-4lp8</guid>
      <description>&lt;p&gt;Trois cent douze tâches, trente-six projets, un fichier. Pas de serveur, pas d'abonnement, pas&lt;br&gt;
d'application mobile.&lt;/p&gt;

&lt;p&gt;Le chiffre qui m'intéresse n'est pas 317. C'est 33 : le nombre de tâches que j'ai&lt;br&gt;
&lt;strong&gt;abandonnées&lt;/strong&gt; — ni faites, ni supprimées, marquées comme telles. C'est le statut qui a le&lt;br&gt;
plus changé ma façon de travailler, et celui qu'aucun outil de gestion de tâches ne met en&lt;br&gt;
avant.&lt;/p&gt;
&lt;h2&gt;
  
  
  La répartition, brute
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;COUNT&lt;/span&gt;&lt;span class="cm"&gt;(*) AS n
  FROM missions
 GROUP BY statut
 ORDER BY n DESC;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;done        231
todo         38
abandonne    33
encours      10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Trois cent douze lignes dans ces quatre statuts. La table en contient 317 : les cinq autres sont&lt;br&gt;
des tâches bloquées — quatre qui attendent une décision de ma part, une qui attend un client.&lt;br&gt;
À l'échelle d'un projet, c'est un écart qu'on ne voit pas ; à l'échelle de trois cents lignes,&lt;br&gt;
c'est le genre de trou qu'un tableau de bord propre vous cache.&lt;/p&gt;

&lt;p&gt;Ce qui manque encore, c'est la contrainte. Le schéma réel ne l'a pas :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;missions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;       &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;projet&lt;/span&gt;   &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;titre&lt;/span&gt;    &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;statut&lt;/span&gt;   &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'todo'&lt;/span&gt;
             &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'todo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'encours'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'done'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'abandonne'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'bloque_ludo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'bloque_externe'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;type&lt;/span&gt;     &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'agent'&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'agent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'humain'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;preuve&lt;/span&gt;   &lt;span class="kt"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cree_le&lt;/span&gt;  &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;maj_le&lt;/span&gt;   &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Une contrainte &lt;code&gt;CHECK&lt;/code&gt; ne répare pas les lignes existantes, mais elle empêche la suivante. Sur&lt;br&gt;
SQLite, c'est trois lignes de SQL qui remplacent une convention documentée que personne ne lit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Dix tâches en cours, et pas une de plus
&lt;/h2&gt;

&lt;p&gt;Dix, sur 317. Ce n'est pas une politique, c'est un constat, et il est plutôt sain : au-delà&lt;br&gt;
d'une douzaine de chantiers ouverts en parallèle, je passe mon temps à changer de contexte au&lt;br&gt;
lieu d'avancer.&lt;/p&gt;

&lt;p&gt;La requête qui surveille ça est banale, mais je la regarde chaque semaine :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;projet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;COUNT&lt;/span&gt;&lt;span class="cm"&gt;(*) AS en_cours
  FROM missions
 WHERE statut = 'encours'
 GROUP BY projet
 ORDER BY en_cours DESC;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trente-huit tâches à faire, pour trente-six projets : la plupart des projets ont une tâche&lt;br&gt;
ouverte, quelques-uns en ont trois. Cette forme de distribution est une information en soi —&lt;br&gt;
elle dit que je travaille en largeur, pas en profondeur.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pourquoi « abandonné » est un statut
&lt;/h2&gt;

&lt;p&gt;Pendant des années, j'ai eu deux façons de traiter une tâche que je ne ferais pas : la&lt;br&gt;
supprimer, ou la laisser en &lt;code&gt;todo&lt;/code&gt; pour toujours.&lt;/p&gt;

&lt;p&gt;Les deux sont mauvaises.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supprimer&lt;/strong&gt; efface la décision. Six mois plus tard, je retrouve la même idée, je la recrée,
et je repasse une heure à me demander si je l'ai déjà tentée. La réponse était dans une ligne
que j'avais effacée.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laisser en &lt;code&gt;todo&lt;/code&gt;&lt;/strong&gt; pollue la liste. Une liste de tâches qui contient vingt lignes qu'on ne
fera jamais cesse d'être une liste de tâches : elle devient un cimetière qu'on évite
d'ouvrir.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;abandonne&lt;/code&gt; tranche : la ligne reste, avec sa raison dans le champ &lt;code&gt;note&lt;/code&gt;, et elle sort de&lt;br&gt;
toutes les vues de travail. Trente-trois fois, j'ai écrit pourquoi. Trente-trois fois, ça m'a&lt;br&gt;
évité de recommencer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;missions&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'abandonne'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' | abandonné: '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;raison&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;maj_le&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Une tâche abandonnée peut revenir : elle repasse en &lt;code&gt;todo&lt;/code&gt;, avec son historique. C'est une&lt;br&gt;
décision réversible, ce qui est précisément la raison pour laquelle je la prends plus&lt;br&gt;
facilement.&lt;/p&gt;
&lt;h2&gt;
  
  
  La colonne qui décide : &lt;code&gt;preuve&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Une ligne ne passe pas en &lt;code&gt;done&lt;/code&gt; sans un artefact vérifiable. Trois formes acceptées, pas&lt;br&gt;
quatre :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;un chemin de fichier qui existe, produit après la création de la tâche ;&lt;/li&gt;
&lt;li&gt;une URL qui répond &lt;code&gt;200&lt;/code&gt; au moment de la clôture ;&lt;/li&gt;
&lt;li&gt;un hash de commit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Le contrôle est mécanique, et il tient en une requête.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- tâches marquées faites dont la preuve ne pointe plus sur rien&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;projet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;titre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preuve&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;missions&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'done'&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preuve&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;preuve&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sur 231 tâches terminées, cette requête doit renvoyer zéro ligne. Elle en renvoie zéro, mais&lt;br&gt;
seulement parce que j'ai passé une soirée à remplir les preuves manquantes quand je l'ai&lt;br&gt;
écrite pour la première fois. Avant ça, il y en avait une trentaine — dont un « site livré »&lt;br&gt;
dont personne ne retrouvait l'URL.&lt;/p&gt;

&lt;p&gt;Le contrôle côté fichier, pour les preuves de type chemin :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sqlite3 missions.db &lt;span class="s2"&gt;"SELECT preuve FROM missions WHERE statut='done' AND preuve LIKE '/%';"&lt;/span&gt; \
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; -r p&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; -f &lt;span class="s2"&gt;"$p"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"preuve absente: $p"&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et la partie que je n'automatise pas : vérifier que le fichier a été produit &lt;strong&gt;après&lt;/strong&gt; la&lt;br&gt;
création de la tâche. Un rapport de septembre ne prouve pas un travail de novembre. C'est un&lt;br&gt;
&lt;code&gt;stat&lt;/code&gt; comparé à &lt;code&gt;cree_le&lt;/code&gt;, et ça se fait à la main quand un doute existe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que SQLite encaisse, et ce qu'il n'encaisse pas
&lt;/h2&gt;

&lt;p&gt;À 317 lignes et 36 projets, la question de la base ne se pose même pas : le fichier fait moins&lt;br&gt;
d'un mégaoctet, les requêtes sont instantanées, et la sauvegarde est un &lt;code&gt;.backup&lt;/code&gt; dans un&lt;br&gt;
cron.&lt;/p&gt;

&lt;p&gt;Les limites réelles sont ailleurs, et elles n'ont rien à voir avec le volume :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;un seul écrivain.&lt;/strong&gt; Si deux scripts écrivent en même temps, le second attend. Avec
&lt;code&gt;PRAGMA busy_timeout = 5000&lt;/code&gt;, il attend cinq secondes au lieu de planter. Sur un poste de
travail, c'est suffisant ; sur un serveur partagé, ce ne l'est pas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pas de vue partagée.&lt;/strong&gt; Dès que quelqu'un d'autre doit voir l'état, il faut exporter. Donner
accès au fichier, c'est donner accès à tout, y compris aux notes internes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;les suppressions de colonnes sont pénibles.&lt;/strong&gt; Ajouter une colonne est trivial, en retirer
une demande de recréer la table. Je garde donc les migrations numérotées dans un dossier, avec
&lt;code&gt;PRAGMA user_version&lt;/code&gt; comme compteur.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ce que 317 lignes m'ont appris
&lt;/h2&gt;

&lt;p&gt;Trois choses, et aucune n'est technique.&lt;/p&gt;

&lt;p&gt;Un total ne dit rien. 317 tâches, c'est un chiffre de vanité ; 10 en cours, c'est une charge de&lt;br&gt;
travail. La répartition raconte l'état réel du système, pas le total.&lt;/p&gt;

&lt;p&gt;Un statut « abandonné » est un outil de clarté, pas un échec. Trente-trois décisions explicites&lt;br&gt;
valent mieux que trente-trois lignes fantômes.&lt;/p&gt;

&lt;p&gt;Et une tâche sans preuve est une opinion. La colonne &lt;code&gt;preuve&lt;/code&gt; est la seule partie du schéma qui&lt;br&gt;
m'a obligé à changer ma façon de travailler : quand on sait qu'on devra citer un fichier, on&lt;br&gt;
produit le fichier. Pas parce qu'on est discipliné — parce que la case ne se coche pas sinon.&lt;/p&gt;

</description>
      <category>data</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>26 automatisations surveillées par un tableau de bord qui ne consomme aucun token</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Wed, 11 Nov 2026 11:44:35 +0000</pubDate>
      <link>https://dev.to/devdusud/26-automatisations-surveillees-par-un-tableau-de-bord-qui-ne-consomme-aucun-token-339a</link>
      <guid>https://dev.to/devdusud/26-automatisations-surveillees-par-un-tableau-de-bord-qui-ne-consomme-aucun-token-339a</guid>
      <description>&lt;p&gt;À un moment, j'ai eu vingt-six automatisations en fonctionnement : des publications, des&lt;br&gt;
sauvegardes, des synchronisations, des vérifications de certificats, des collectes.&lt;/p&gt;

&lt;p&gt;Vingt-six choses qui peuvent tomber en silence, et aucune raison de croire qu'elles vont bien.&lt;br&gt;
Ma première idée a été de demander à un modèle de langage de résumer l'état de chaque job.&lt;br&gt;
C'était une très mauvaise idée : vingt-six résumés toutes les deux minutes, c'est de l'argent&lt;br&gt;
brûlé pour reformuler des codes de sortie.&lt;/p&gt;

&lt;p&gt;Aujourd'hui, le tableau de bord tourne en local, ne fait aucun appel payant, et rafraîchit les&lt;br&gt;
vingt-six jobs en 9 à 11 secondes. Voici comment il est fait, et surtout ce qu'il ne sait pas&lt;br&gt;
faire.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ce qu'un job laisse derrière lui
&lt;/h2&gt;

&lt;p&gt;Un job ne se surveille pas en l'interrogeant : on ne demande pas à une tâche « est-ce que tu&lt;br&gt;
vas bien ? ». On lit ce qu'elle a laissé.&lt;/p&gt;

&lt;p&gt;Quatre traces suffisent, et elles existent presque toujours déjà :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;l'âge d'un heartbeat&lt;/strong&gt; — un fichier écrit à la fin d'une exécution réussie ;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;le code de sortie&lt;/strong&gt; de la dernière exécution, quand le scheduler le conserve ;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;le nombre d'objets traités&lt;/strong&gt; — trois lignes ajoutées en base, deux fichiers déplacés ;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;le journal&lt;/strong&gt;, pour la dernière ligne d'erreur.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le tableau de bord n'invente rien. Il lit ces traces, les compare à une règle, et affiche une&lt;br&gt;
couleur. Un job vert n'est pas un job qui fonctionne : c'est un job dont les traces ressemblent&lt;br&gt;
à celles d'un job qui fonctionne. La nuance compte, j'y reviens plus bas.&lt;/p&gt;
&lt;h2&gt;
  
  
  Le registre : une ligne par job
&lt;/h2&gt;

&lt;p&gt;Tout tient dans un dictionnaire. Pas de table, pas de configuration YAML à parser, pas de&lt;br&gt;
fichier par job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="no"&gt;JOBS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"publication-devto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"heartbeat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"~/.local/state/devto-last-success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"seuil_h"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"sauvegarde-quotidienne"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"heartbeat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"~/.local/state/backup-last-success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"seuil_h"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"veille-certificats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"tache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"veille-ssl.timer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"seuil_h"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"compteur-missions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"sqlite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"~/pilotage/missions.db"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="s2"&gt;"SELECT COUNT(*) FROM missions WHERE date(maj_le) = date('now')"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"seuil"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trois types de sonde, et c'est tout : un fichier horodaté, un timer systemd, une requête SQL.&lt;br&gt;
Chaque job déclare son type et son seuil. Le collecteur ne connaît aucun job en particulier —&lt;br&gt;
ajouter une automatisation, c'est ajouter une entrée.&lt;/p&gt;

&lt;p&gt;Cette propriété est la plus importante du système : le tableau de bord ne grossit pas quand le&lt;br&gt;
nombre de jobs grossit. Il reste le même code, avec un dictionnaire plus long.&lt;/p&gt;
&lt;h2&gt;
  
  
  Le cycle, en 9 à 11 secondes
&lt;/h2&gt;

&lt;p&gt;Chaque sonde est une fonction courte qui renvoie un état et un message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sonde_heartbeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maintenant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expanduser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"heartbeat"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"rouge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"aucun heartbeat"&lt;/span&gt;
    &lt;span class="n"&gt;age_h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maintenant&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;age_h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"seuil_h"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"rouge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"muet depuis {age_h:.0f} h"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"vert"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"il y a {age_h:.1f} h"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sonde_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maintenant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"systemctl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"is-active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tache"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;etat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"vert"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;etat&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"active"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;"rouge"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etat&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sonde_sqlite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maintenant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;chemin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sqlite"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expanduser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chemin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"vert"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"seuil"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;"rouge"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{n} lignes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le collecteur les enchaîne, mesure le temps, et écrit un JSON.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;debut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resultats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;nom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;JOBS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s2"&gt;"heartbeat"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;etat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sonde_heartbeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="s2"&gt;"tache"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;etat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sonde_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;etat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sonde_sqlite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                     &lt;span class="c1"&gt;# une sonde cassée ne casse pas l'affichage&lt;/span&gt;
        &lt;span class="n"&gt;etat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gris"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"sonde en erreur: {type(e).__name__}"&lt;/span&gt;
    &lt;span class="n"&gt;resultats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nom&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"etat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;etat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resultats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"_cycle"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"secondes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;debut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deux détails qui font la différence entre 10 secondes et 10 minutes :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;un &lt;code&gt;timeout&lt;/code&gt; sur chaque appel externe&lt;/strong&gt; (3 secondes pour &lt;code&gt;systemctl&lt;/code&gt;, 2 pour SQLite). Sans
lui, un job bloqué bloque tout le cycle, et le tableau de bord affiche l'état d'il y a une
heure en croyant afficher le présent ;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;des sondes séquentielles, pas parallèles&lt;/strong&gt;. Vingt-six processus simultanés coûtent plus en
mémoire et en contexte que les dix secondes qu'on gagne. Sur un poste de travail, la
séquentialité est le choix paresseux et correct.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le JSON est écrit de façon atomique, sinon le navigateur lit un fichier à moitié rempli une&lt;br&gt;
fois sur cent :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/etat-jobs.json.tmp"&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/home/moi/etat-jobs.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Le rendu : du HTML, du &lt;code&gt;fetch&lt;/code&gt;, et rien d'autre
&lt;/h2&gt;

&lt;p&gt;Pas de framework, pas d'étape de build, pas de serveur. Une page HTML de cent lignes avec une&lt;br&gt;
boucle de rafraîchissement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"t"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;thead&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;Job&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;État&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;Détail&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;tbody&amp;gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rafraichir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"etat-jobs.json?_="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;tbody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"#t tbody"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"_"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"tr"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;etat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`&amp;lt;td&amp;gt;${nom}&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;${v.etat}&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;${v.message}&amp;lt;/td&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"#cycle"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cycle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secondes&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;" s"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;rafraichir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rafraichir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// toutes les 2 minutes&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le collecteur tourne lui aussi toutes les deux minutes, dans un timer systemd. La page ne&lt;br&gt;
calcule rien : elle affiche un fichier. C'est ce qui rend le coût nul — ni requête à une API&lt;br&gt;
distante, ni modèle, ni quota.&lt;/p&gt;

&lt;h2&gt;
  
  
  0 token, et ce que ça implique
&lt;/h2&gt;

&lt;p&gt;Le compte est simple : vingt-six jobs, un cycle toutes les deux minutes, donc 720 cycles par&lt;br&gt;
jour. Aucun de ces cycles n'appelle un service payant. Le coût marginal est une lecture de&lt;br&gt;
fichier et un &lt;code&gt;stat&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Mais l'absence de modèle a un prix, et il faut le dire clairement :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pas de détection d'anomalie&lt;/strong&gt;. Le tableau de bord compare à un seuil que j'ai écrit. Il ne
remarque pas qu'un job tourne deux fois plus longtemps que d'habitude, ni que son volume de
sortie a chuté de moitié. Pour ça, il faudrait un historique, pas un instantané ;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pas de diagnostic&lt;/strong&gt;. Il dit « muet depuis 31 h ». Il ne dit pas pourquoi. La cause est dans
le journal, et c'est moi qui vais la lire ;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pas de priorisation&lt;/strong&gt;. Vingt-six lignes rouges se valent à l'écran. Un modèle pourrait
trier ; un humain qui connaît ses jobs le fait aussi bien en trois secondes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un tableau de bord à 0 token n'est pas un tableau de bord intelligent. C'est un miroir. Et un&lt;br&gt;
miroir honnête vaut mieux qu'un analyste qui se trompe.&lt;/p&gt;

&lt;h2&gt;
  
  
  La leçon que m'a donnée le premier job vert
&lt;/h2&gt;

&lt;p&gt;Un job est resté vert pendant onze jours alors qu'il ne publiait plus rien. Son heartbeat était&lt;br&gt;
écrit au bon endroit, à la bonne heure, par un script qui sortait en &lt;code&gt;0&lt;/code&gt;… après avoir trouvé&lt;br&gt;
zéro fichier à traiter.&lt;/p&gt;

&lt;p&gt;« Code de sortie 0 » et « travail effectué » ne sont pas la même chose. Depuis, la sonde de ce&lt;br&gt;
job ne regarde plus son heartbeat mais le nombre de lignes ajoutées dans son journal de&lt;br&gt;
publication. Un job qui tourne pour rien est rouge, même s'il est content de lui.&lt;/p&gt;

&lt;p&gt;C'est la seule sonde que j'ai réécrite, et c'est celle qui a le plus servi. Le reste du&lt;br&gt;
système n'a pas changé : un dictionnaire, trois types de sondes, un JSON, une page qui&lt;br&gt;
rafraîchit toutes les deux minutes.&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Mon cron de 3 h ne tournait que les nuits où j'avais laissé le PC allumé</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Sun, 08 Nov 2026 07:40:11 +0000</pubDate>
      <link>https://dev.to/devdusud/mon-cron-de-3-h-ne-tournait-que-les-nuits-ou-javais-laisse-le-pc-allume-3nij</link>
      <guid>https://dev.to/devdusud/mon-cron-de-3-h-ne-tournait-que-les-nuits-ou-javais-laisse-le-pc-allume-3nij</guid>
      <description>&lt;p&gt;La ligne était propre. Trois heures du matin, tous les jours, un script de maintenance qui&lt;br&gt;
compacte une base, archive des journaux et pousse une sauvegarde.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 3 * * * /home/moi/bin/maintenance.sh &amp;gt;&amp;gt; /home/moi/logs/maintenance.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Elle a tourné quatre fois en trois semaines.&lt;/p&gt;

&lt;p&gt;Pas parce qu'elle était mal écrite. Parce qu'à 3 h du matin, mon poste de travail est éteint.&lt;br&gt;
Et cron, dans ce cas, ne fait rien du tout : ni exécution, ni erreur, ni ligne de log, ni trace&lt;br&gt;
nulle part. L'exécution n'a simplement jamais existé.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ce que cron ne promet pas
&lt;/h2&gt;

&lt;p&gt;cron est un déclencheur, pas un ordonnanceur. La différence est minuscule dans la&lt;br&gt;
documentation et énorme en pratique.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un déclencheur dit : « à cette minute précise, si la machine est allumée et que le démon
tourne, lance cette commande ».&lt;/li&gt;
&lt;li&gt;Un ordonnanceur dit : « cette tâche doit s'être exécutée une fois par jour ; si elle a été
manquée, rattrape-la ».&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;cron appartient à la première catégorie. Il n'a aucune mémoire des exécutions passées, aucun&lt;br&gt;
état « en retard », aucun mécanisme de rattrapage. C'est un choix de conception assumé depuis&lt;br&gt;
les années 1970, pas un bug.&lt;/p&gt;

&lt;p&gt;Le corollaire est ce qui m'a eu : &lt;strong&gt;l'absence d'exécution ne produit aucun signal&lt;/strong&gt;. Un job qui&lt;br&gt;
échoue écrit au moins une trace. Un job qui n'a pas été déclenché n'écrit rien, et rien, dans&lt;br&gt;
un fichier de log, ressemble exactement à « tout va bien ».&lt;/p&gt;
&lt;h2&gt;
  
  
  Pourquoi je ne l'ai vu que trois semaines plus tard
&lt;/h2&gt;

&lt;p&gt;Mon garde-fou habituel est un fichier d'horodatage écrit après chaque succès. Je regarde son&lt;br&gt;
âge, et s'il dépasse le seuil, je crie.&lt;/p&gt;

&lt;p&gt;Sauf que ce garde-fou tournait dans le même cron, sur la même machine, à une heure où elle est&lt;br&gt;
allumée. Il mesurait donc l'âge d'un heartbeat écrit par un job qui, lui, ne tournait jamais.&lt;br&gt;
Le compteur montait, mais rien ne l'interrogeait : le contrôle était dans le même fichier que&lt;br&gt;
le problème, et le cron ne l'a jamais exécuté non plus.&lt;/p&gt;

&lt;p&gt;C'est le piège classique du chien de garde hébergé sur la machine gardée. Quand le poste est&lt;br&gt;
éteint, les deux dorment ensemble.&lt;/p&gt;
&lt;h2&gt;
  
  
  La réponse historique : anacron
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;anacron&lt;/code&gt; existe précisément pour ça. Il ne raisonne pas en minutes mais en jours : chaque&lt;br&gt;
tâche déclare un intervalle (« tous les jours »), et anacron vérifie au démarrage et une fois&lt;br&gt;
par heure si l'intervalle est écoulé. Si oui, il lance, quelle que soit l'heure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# /etc/anacrontab
1  10  maintenance  /home/moi/bin/maintenance.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le &lt;code&gt;1&lt;/code&gt; est la périodicité en jours, le &lt;code&gt;10&lt;/code&gt; le délai en minutes après le démarrage, et le&lt;br&gt;
troisième champ sert au nom du fichier d'état qui empêche de relancer deux fois.&lt;/p&gt;

&lt;p&gt;C'est solide, ça existe partout, et ça règle le cas « PC éteint ». Trois limites m'ont fait&lt;br&gt;
choisir autre chose :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;la granularité est la journée ; une tâche qui doit tourner toutes les six heures n'est pas
son fort ;&lt;/li&gt;
&lt;li&gt;anacron n'est pas toujours un service supervisé par systemd, et son journal ne se lit pas
avec &lt;code&gt;journalctl&lt;/code&gt; comme le reste du système ;&lt;/li&gt;
&lt;li&gt;sur plusieurs distributions, il n'est même pas installé par défaut. Sur un poste de bureau
avec une session utilisateur systemd, le timer est déjà là.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Ce que j'utilise maintenant : un timer systemd persistant
&lt;/h2&gt;

&lt;p&gt;Un &lt;code&gt;.service&lt;/code&gt; et un &lt;code&gt;.timer&lt;/code&gt;, tous les deux dans &lt;code&gt;~/.config/systemd/user/&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# maintenance.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Maintenance nocturne&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/moi/bin/maintenance.sh&lt;/span&gt;
&lt;span class="py"&gt;Nice&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;IOSchedulingClass&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;idle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# maintenance.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Maintenance quotidienne à 3 h, rattrapage inclus&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nn"&gt;[Timer]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;*-*-* 03:00:00&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;600&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nn"&gt;[Install]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La ligne qui compte tient en un mot : &lt;code&gt;Persistent=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Avec elle, systemd enregistre la dernière exécution dans&lt;br&gt;
&lt;code&gt;~/.local/share/systemd/timers/&lt;/code&gt;. Au démarrage, il compare l'horodatage à la planification : si&lt;br&gt;
une échéance a été manquée, il lance le service immédiatement, une fois. Pas cinq fois pour&lt;br&gt;
cinq jours manqués — une fois. C'est le comportement qu'on veut : un état à rattraper, pas une&lt;br&gt;
file de travail à vider.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RandomizedDelaySec=600&lt;/code&gt; étale le déclenchement sur dix minutes. Sur un parc de machines, ça&lt;br&gt;
évite que tout le monde frappe la même API à 3 h 00 pile. Sur un seul poste, ça évite aussi de&lt;br&gt;
tomber pile sur une fenêtre de mise à jour.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; maintenance.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Vérifier que ça a réellement rattrapé
&lt;/h2&gt;

&lt;p&gt;Activer un timer ne prouve rien. Trois commandes, et deux d'entre elles doivent être lues&lt;br&gt;
régulièrement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-timers maintenance.timer
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status maintenance.service
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; maintenance.service &lt;span class="nt"&gt;-n&lt;/span&gt; 20 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;list-timers&lt;/code&gt; affiche la prochaine échéance &lt;strong&gt;et&lt;/strong&gt; la dernière exécution. C'est la colonne&lt;br&gt;
« LAST » qui m'intéresse : si elle date de trois jours, le rattrapage n'a pas eu lieu.&lt;/p&gt;

&lt;p&gt;Pour tester le rattrapage sans attendre 3 h du matin : un timer de test avec un &lt;code&gt;OnCalendar&lt;/code&gt;&lt;br&gt;
dans deux minutes, on éteint la machine avant l'échéance, on la rallume après, et on regarde si&lt;br&gt;
le service a tourné. C'est fastidieux une fois, et ça évite de découvrir le problème en&lt;br&gt;
production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que le rattrapage ne répare pas
&lt;/h2&gt;

&lt;p&gt;Il faut être honnête sur les limites, parce que « persistant » fait croire à une garantie plus&lt;br&gt;
large qu'elle ne l'est.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Un seul rattrapage, pas un historique.&lt;/strong&gt; Cinq jours d'absence produisent une exécution, pas
cinq. Si le script traite « les données du jour », celles des jours manqués ne seront jamais
traitées : il lui faut une plage de dates, pas « hier ».&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le timer ne vérifie pas le résultat.&lt;/strong&gt; Un service qui sort en 0 sans rien faire reste un
succès. Le timer garantit le déclenchement, pas le travail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Et aucun outil local ne peut rattraper quoi que ce soit si la machine ne redémarre jamais : la&lt;br&gt;
surveillance des tâches critiques doit vivre ailleurs, dans un service qui attend un ping et qui&lt;br&gt;
crie quand il manque.&lt;/p&gt;

&lt;p&gt;La différence tient en une phrase. cron répond à « est-ce qu'il est 3 h ? ». Un timer persistant&lt;br&gt;
répond à « est-ce que le travail d'aujourd'hui a été fait ? ». La première question est facile à&lt;br&gt;
poser et n'apporte aucune garantie ; la seconde est la seule qui m'intéresse.&lt;/p&gt;

&lt;p&gt;Ma maintenance tourne maintenant tous les jours, y compris les nuits où le PC dort. Et comme&lt;br&gt;
elle est enfin visible dans &lt;code&gt;journalctl&lt;/code&gt;, elle a arrêté d'être un rituel silencieux pour&lt;br&gt;
devenir une tâche comme les autres.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>automation</category>
      <category>tools</category>
    </item>
    <item>
      <title>Gmail refuse une sauvegarde de 329 Mo — et relever la limite n'est pas la réparation</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Thu, 05 Nov 2026 10:18:49 +0000</pubDate>
      <link>https://dev.to/devdusud/gmail-refuse-une-sauvegarde-de-329-mo-et-relever-la-limite-nest-pas-la-reparation-91f</link>
      <guid>https://dev.to/devdusud/gmail-refuse-une-sauvegarde-de-329-mo-et-relever-la-limite-nest-pas-la-reparation-91f</guid>
      <description>&lt;p&gt;La sauvegarde faisait 329 Mo. Le message d'erreur faisait une ligne :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;552 5.3.4 Message size exceeds fixed maximum message size
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rien sur la taille autorisée, rien sur la taille réellement envoyée, rien sur ce qu'il faut&lt;br&gt;
faire. J'ai passé une heure à vérifier mon mot de passe, mon serveur SMTP et mon port avant de&lt;br&gt;
comprendre que le problème était arithmétique.&lt;/p&gt;
&lt;h2&gt;
  
  
  Les 25 Mo ne sont pas 25 Mo
&lt;/h2&gt;

&lt;p&gt;Gmail refuse un message au-delà de 25 Mo. Ce chiffre est celui du &lt;strong&gt;message encodé&lt;/strong&gt;, pas celui&lt;br&gt;
du fichier sur le disque. Et c'est là que tout se joue.&lt;/p&gt;

&lt;p&gt;Un fichier joint est transporté en base64. Cet encodage transforme 3 octets en 4 caractères :&lt;br&gt;
le volume augmente d'environ un tiers. Ajoutez les en-têtes MIME, les séparateurs de parties,&lt;br&gt;
l'encodage du nom de fichier, et un fichier de 20 Mo produit un message de 27 à 28 Mo.&lt;/p&gt;

&lt;p&gt;Autrement dit, la vraie limite utile est autour de &lt;strong&gt;18 Mo de fichier&lt;/strong&gt;, pas 25.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;taille_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taille_fichier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Estimation du message encodé : base64 (4/3) + en-têtes MIME.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taille_fichier&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Une sauvegarde de 329 Mo, c'est donc l'équivalent de 440 Mo de message. Dix-sept fois la&lt;br&gt;
limite. Il n'y avait pas de réglage à trouver : il y avait un découpage à écrire.&lt;/p&gt;
&lt;h2&gt;
  
  
  Découper, mais pas n'importe comment
&lt;/h2&gt;

&lt;p&gt;Premier réflexe : &lt;code&gt;split -b 10M&lt;/code&gt;. C'est ce que j'ai fait la première fois, et c'est une&lt;br&gt;
mauvaise idée pour trois raisons.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;split&lt;/code&gt; produit des fichiers sans aucune information sur leur ordre, leur nombre, ni leur&lt;br&gt;
contenu. Si un bloc arrive corrompu — et sur trente-trois pièces jointes, ça finit par arriver —&lt;br&gt;
vous ne le savez qu'au moment de reconstituer, sans savoir lequel. Et si un envoi échoue, vous&lt;br&gt;
ne savez pas ce qui manque.&lt;/p&gt;

&lt;p&gt;Le découpage doit produire, en plus des blocs, un &lt;strong&gt;manifeste&lt;/strong&gt;. Trente lignes de Python :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;

&lt;span class="n"&gt;SRC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/moi/backups/sauvegarde-2026-10.enc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;OUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/moi/backups/blocs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;BLOC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;

&lt;span class="n"&gt;OUT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;manifeste&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SRC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taille&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SRC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]}&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;SRC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BLOC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;nom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SRC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.part&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OUT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;nom&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;manifeste&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fichier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;octets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OUT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manifeste.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manifeste&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manifeste&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;blocs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; blocs, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;manifeste&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;taille&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; octets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Résultat : 33 blocs, dont 32 de 10 Mo et un dernier plus petit, plus un &lt;code&gt;manifeste.json&lt;/code&gt; de&lt;br&gt;
quelques kilo-octets. Chaque bloc encodé fait environ 13,3 Mo de message — largement sous les&lt;br&gt;
25 Mo, avec de la marge pour les en-têtes et pour un client mail qui décide d'ajouter sa&lt;br&gt;
signature.&lt;/p&gt;

&lt;p&gt;Le &lt;code&gt;sha256&lt;/code&gt; par bloc n'est pas du zèle. C'est ce qui permet de répondre à la seule question qui&lt;br&gt;
compte quand on restaure : « ce bloc est-il celui que j'ai envoyé ? »&lt;/p&gt;
&lt;h2&gt;
  
  
  Le manifeste part en premier
&lt;/h2&gt;

&lt;p&gt;L'ordre d'envoi a son importance. Le manifeste est le plus petit fichier et le plus important :&lt;br&gt;
il décrit tout le reste. Je l'envoie en premier, avec un objet explicite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[backup 2026-10] 00/33 manifeste — 329 Mo, 33 blocs
[backup 2026-10] 01/33 part000
...
[backup 2026-10] 33/33 part032
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trois choses dans cette convention :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;le numéro dans l'objet permet de retrouver un message égaré par une simple recherche ;&lt;/li&gt;
&lt;li&gt;le total (&lt;code&gt;/33&lt;/code&gt;) permet de savoir s'il manque quelque chose sans ouvrir le manifeste ;&lt;/li&gt;
&lt;li&gt;le nom du fichier reste dans l'objet, donc un bloc renommé par un client mail reste
identifiable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un détail qui n'en est pas un : je n'envoie jamais deux blocs dans le même message. Deux&lt;br&gt;
pièces jointes de 10 Mo, c'est 26,6 Mo encodés, donc un refus. La limite se calcule par&lt;br&gt;
message, pas par envoi.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reconstituer et vérifier
&lt;/h2&gt;

&lt;p&gt;La restauration ne doit pas dépendre de ma mémoire. Elle lit le manifeste et refuse de&lt;br&gt;
continuer si un bloc manque.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;

&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/moi/backups/blocs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manifeste.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;sortie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/moi/restaure.enc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sortie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fichier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bloc manquant: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fichier&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bloc corrompu: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fichier&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taille:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sortie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attendu:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taille&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le contrôle final est une comparaison de taille, puis un déchiffrement qui échouera de&lt;br&gt;
lui-même si un octet manque — un fichier chiffré est son propre checksum. Sur trente-trois&lt;br&gt;
blocs envoyés en trois soirs, c'est ce contrôle qui m'a évité de découvrir le problème six mois&lt;br&gt;
plus tard, au pire moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi relever la limite est toujours la mauvaise réparation
&lt;/h2&gt;

&lt;p&gt;J'ai cherché la solution évidente : un autre fournisseur, un compte payant, un envoi par lien&lt;br&gt;
de partage. Chacune de ces réponses repousse le problème sans le traiter, et en crée un autre.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La limite n'est pas une brimade.&lt;/strong&gt; Un serveur mail doit stocker, scanner et parfois
répliquer chaque message. Un plafond par message est ce qui empêche un envoi unique de
saturer une boîte partagée.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le destinataire a sa propre limite.&lt;/strong&gt; Relever la mienne ne change rien à la sienne. Un
message de 300 Mo sera refusé à l'arrivée, ou silencieusement tronqué par un relais
intermédiaire — et c'est bien pire qu'un refus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un gros message n'est pas reprenable.&lt;/strong&gt; Un envoi de 329 Mo qui casse à 90 % doit
recommencer à zéro. Trente-trois blocs de 10 Mo : on renvoie le bloc qui a échoué, en deux
minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La boîte du destinataire a un quota.&lt;/strong&gt; Une sauvegarde de 329 Mo dans une boîte de 15 Go,
c'est deux pour cent de l'espace, tous les mois, sans jamais rien supprimer. Au bout de deux
ans, la boîte est pleine et personne ne sait pourquoi.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le découpage, lui, est une contrainte qui s'applique partout : n'importe quel hébergeur, un&lt;br&gt;
disque amovible, un dépôt d'objets, une clé USB. C'est la seule version du système qui&lt;br&gt;
fonctionnera encore quand le fournisseur aura changé ses règles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que je garde
&lt;/h2&gt;

&lt;p&gt;Une fonction de découpage de trente lignes, un manifeste avec un hash par bloc, une convention&lt;br&gt;
de nommage dans l'objet du message, et un script de restauration qui refuse de mentir.&lt;/p&gt;

&lt;p&gt;La taille de 10 Mo n'est pas magique : elle laisse de la marge sous les 25 Mo encodés, elle&lt;br&gt;
tient dans une pièce jointe raisonnable, et elle permet de renvoyer un bloc sans y passer la&lt;br&gt;
soirée. Le chiffre à retenir est ailleurs : &lt;strong&gt;base64 coûte un tiers&lt;/strong&gt;. Tant qu'on l'oublie, on&lt;br&gt;
cherche un réglage là où il n'y a qu'une division.&lt;/p&gt;

</description>
      <category>python</category>
      <category>devops</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Un dossier, c'est une file d'attente : 21 articles publiés sans base de données</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Wed, 04 Nov 2026 07:41:04 +0000</pubDate>
      <link>https://dev.to/devdusud/un-dossier-cest-une-file-dattente-21-articles-publies-sans-base-de-donnees-3j1k</link>
      <guid>https://dev.to/devdusud/un-dossier-cest-une-file-dattente-21-articles-publies-sans-base-de-donnees-3j1k</guid>
      <description>&lt;p&gt;Je n'ai pas de back-office. J'ai un dossier.&lt;/p&gt;

&lt;p&gt;Quand j'ai un article à écrire, j'ouvre un fichier, je le nomme &lt;code&gt;12-mon-sujet.md&lt;/code&gt;, je le pose&lt;br&gt;
dans &lt;code&gt;devto-articles/&lt;/code&gt;. Le lendemain matin, un job prend le premier fichier par ordre&lt;br&gt;
alphabétique, le publie sur Dev.to, puis le déplace dans &lt;code&gt;publies/&lt;/code&gt;. Vingt et un articles sont&lt;br&gt;
passés par là. Aucun n'a été publié deux fois, aucun n'a été perdu, et je n'ai jamais eu à&lt;br&gt;
ouvrir un tableau de bord pour savoir où j'en étais.&lt;/p&gt;

&lt;p&gt;C'est l'architecture la plus banale qui existe — et celle que je conseille avant d'installer&lt;br&gt;
n'importe quel outil de gestion de contenu.&lt;/p&gt;
&lt;h2&gt;
  
  
  Le nom du fichier est l'ordre de publication
&lt;/h2&gt;

&lt;p&gt;Le job ne trie pas par date de modification, il ne lit pas un champ &lt;code&gt;publish_at&lt;/code&gt;, il prend&lt;br&gt;
&lt;code&gt;sorted(os.listdir(...))[0]&lt;/code&gt;. Un seul critère, et il tient parce que le nom porte un numéro à&lt;br&gt;
deux chiffres.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;11-dossier-file-attente-devto.md
12-gmail-25-mo-sauvegarde.md
13-cron-3h-pc-eteint.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le zéro de tête n'est pas décoratif. Sans lui, &lt;code&gt;10-...&lt;/code&gt; passe avant &lt;code&gt;9-...&lt;/code&gt;, et l'ordre de&lt;br&gt;
publication devient le genre de bug qu'on ne voit qu'au bout d'un mois. Trois caractères, et le&lt;br&gt;
problème n'existe pas.&lt;/p&gt;

&lt;p&gt;Deuxième conséquence, plus intéressante : &lt;strong&gt;le nom est une promesse de calendrier&lt;/strong&gt;. Un article&lt;br&gt;
par jour, donc un numéro par jour. Si je pars trois jours, je pose trois fichiers et je sais&lt;br&gt;
exactement ce qui sortira lundi, mardi, mercredi. Rien à planifier ailleurs.&lt;/p&gt;

&lt;p&gt;Le filtre compte autant que le tri. Le dossier contient aussi des notes et des brouillons : la&lt;br&gt;
sélection ne retient que les fichiers dont le nom commence par un chiffre.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CANDIDATS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RACINE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;           &lt;span class="c1"&gt;# ignore notes, brouillons, sauvegardes
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CANDIDATS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;CANDIDATS&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Un fichier = un article, et un seul
&lt;/h2&gt;

&lt;p&gt;Un dossier, c'est une file d'attente ouverte : n'importe quoi peut y tomber. Chez moi, un&lt;br&gt;
fichier de brouillons pour les réseaux sociaux s'y est retrouvé, et il a été publié avant que&lt;br&gt;
je comprenne pourquoi. C'était un fichier markdown valide, avec un contenu, sans champ &lt;code&gt;title&lt;/code&gt;.&lt;br&gt;
L'API a créé l'article. Son titre était devenu « Sans titre ».&lt;/p&gt;

&lt;p&gt;Depuis, la validation se fait avant le premier appel réseau, sur trois champs :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;valide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;titre manquant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description manquante&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published doit valoir false à la pose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un fichier refusé reste dans le dossier, avec son nom intact. Il ne bloque pas la file : le job&lt;br&gt;
le saute et prend le suivant. Un fichier invalide qui bloque toute la production, c'est une&lt;br&gt;
panne ; un fichier invalide qui est sauté, c'est un avertissement.&lt;/p&gt;

&lt;p&gt;Le &lt;code&gt;published: false&lt;/code&gt; dans le front matter n'est pas une politesse. C'est la marque « cet&lt;br&gt;
article attend son tour ». Le jour où je veux publier tout de suite, je passe par un &lt;code&gt;PUT&lt;/code&gt;&lt;br&gt;
manuel et je déplace le fichier à la main — mais le job, lui, ne publie jamais un fichier qui&lt;br&gt;
s'est déjà déclaré publié ailleurs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Le déplacement est la transaction
&lt;/h2&gt;

&lt;p&gt;C'est le point sur lequel j'ai le plus réfléchi, parce que c'est le seul endroit où le système&lt;br&gt;
peut perdre quelque chose.&lt;/p&gt;

&lt;p&gt;L'ordre est strict, et il n'est pas négociable :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;l'API répond &lt;code&gt;2xx&lt;/code&gt; avec un &lt;code&gt;id&lt;/code&gt; d'article ;&lt;/li&gt;
&lt;li&gt;le fichier est déplacé dans &lt;code&gt;publies/&lt;/code&gt; ;&lt;/li&gt;
&lt;li&gt;une ligne est ajoutée au journal.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;poster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;# lève une exception si l'API refuse
&lt;/span&gt;&lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DEST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# rename atomique, même disque
&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;JOURNAL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Si l'étape 1 échoue, rien n'a bougé. Si l'étape 2 échoue alors que l'article est en ligne, le&lt;br&gt;
fichier repart le lendemain — et l'API répond &lt;code&gt;422&lt;/code&gt; sur un titre déjà utilisé. C'est bruyant,&lt;br&gt;
c'est réparable, et ça se voit. L'inverse — déplacer avant de publier — produit un article&lt;br&gt;
perdu en silence. Entre les deux erreurs, j'ai choisi celle qui gueule.&lt;/p&gt;

&lt;p&gt;Le &lt;code&gt;shutil.move&lt;/code&gt; est sur le même disque, donc c'est un &lt;code&gt;rename&lt;/code&gt; : atomique. Pas de fichier à&lt;br&gt;
moitié copié, pas de doublon.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ce qui se passe quand le dossier est vide
&lt;/h2&gt;

&lt;p&gt;C'est le cas qui m'a coûté le plus cher, et il ne ressemble pas à une panne.&lt;/p&gt;

&lt;p&gt;Zéro fichier candidat, zéro erreur. Le script sort en &lt;code&gt;0&lt;/code&gt;, le scheduler est content, et le&lt;br&gt;
lendemain matin rien n'est en ligne. Aucun message, aucun log d'erreur, parce qu'il n'y a pas&lt;br&gt;
d'erreur : il n'y a rien à faire.&lt;/p&gt;

&lt;p&gt;Un job de publication qui n'a plus rien à publier est une alerte, pas un succès. Le script sort&lt;br&gt;
maintenant en code &lt;code&gt;3&lt;/code&gt;, avec un message explicite :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALERTE: aucun fichier en attente&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et je garde une marge de sept fichiers d'avance. Vingt et un articles plus tard, la file n'est&lt;br&gt;
jamais descendue en dessous de trois. Quand elle approche de zéro, c'est que j'ai arrêté&lt;br&gt;
d'écrire — c'est l'information la plus utile que ce système me donne.&lt;/p&gt;
&lt;h2&gt;
  
  
  Le journal, trois colonnes
&lt;/h2&gt;

&lt;p&gt;Chaque publication laisse une ligne, et cette ligne suffit :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-10-14  11-dossier-file-attente-devto.md  1874521  https://dev.to/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Je ne construis pas de statistiques dessus : &lt;code&gt;tail -5&lt;/code&gt; pour savoir ce qui est sorti cette&lt;br&gt;
semaine, &lt;code&gt;grep&lt;/code&gt; pour retrouver un article. Vingt et un articles, six cents octets de journal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi je n'ai pas ajouté de base de données
&lt;/h2&gt;

&lt;p&gt;Parce que je n'en ai pas besoin, et parce que le système de fichiers fait déjà le travail :&lt;br&gt;
lister, compter, trier, déplacer, horodater. Une table &lt;code&gt;articles&lt;/code&gt; avec une colonne &lt;code&gt;statut&lt;/code&gt;&lt;br&gt;
aurait ajouté une deuxième source de vérité à synchroniser avec les fichiers. Deux sources de&lt;br&gt;
vérité, c'est une divergence garantie et un script de réconciliation à écrire.&lt;/p&gt;

&lt;p&gt;Ici, l'état est le dossier. Un article en attente est un fichier à la racine. Un article publié&lt;br&gt;
est un fichier dans &lt;code&gt;publies/&lt;/code&gt;. Rien à interroger, rien à réparer, et une sauvegarde qui tient&lt;br&gt;
dans un &lt;code&gt;tar&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;La seule chose que j'ai ajoutée, c'est un verrou : &lt;code&gt;flock&lt;/code&gt; autour du job, pour qu'un&lt;br&gt;
déclenchement manuel et le cron ne prennent pas le même fichier à la même seconde.&lt;/p&gt;

&lt;p&gt;Deux cent cinquante lignes de Python, un dossier, un fichier de journal. Vingt et un articles&lt;br&gt;
sortis sans intervention, et un système que je peux encore lire dans trois ans sans relire de&lt;br&gt;
documentation.&lt;/p&gt;

&lt;p&gt;La leçon n'est pas « faites tout en bash ». C'est que la bonne file d'attente est souvent celle&lt;br&gt;
qu'on a déjà sous la main, à condition d'accepter deux contraintes : un ordre explicite dans le&lt;br&gt;
nom, et un déplacement qui sert de transaction. Le reste, c'est de la décoration.&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>Mes 200 tâches tiennent dans un fichier SQLite de 192 Ko</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Tue, 03 Nov 2026 18:06:48 +0000</pubDate>
      <link>https://dev.to/devdusud/mes-200-taches-tiennent-dans-un-fichier-sqlite-de-192-ko-5ff5</link>
      <guid>https://dev.to/devdusud/mes-200-taches-tiennent-dans-un-fichier-sqlite-de-192-ko-5ff5</guid>
      <description>&lt;p&gt;J'ai une centaine de tâches vivantes : un site à réparer, un client à relancer, un article à&lt;br&gt;
finir, une facture à vérifier. Pendant des années j'ai utilisé un outil en ligne, avec des&lt;br&gt;
colonnes, des étiquettes, des glisser-déposer et une application mobile.&lt;/p&gt;

&lt;p&gt;Le problème n'était pas l'outil. Le problème, c'est que je passais plus de temps à organiser&lt;br&gt;
mes tâches qu'à les faire, et qu'aucune de ces tâches ne laissait de trace vérifiable une fois&lt;br&gt;
« archivée ».&lt;/p&gt;

&lt;p&gt;Aujourd'hui, tout tient dans un fichier de 192 Ko et une commande en ligne.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ce que j'ai réellement besoin de stocker
&lt;/h2&gt;

&lt;p&gt;Quatre choses, pas plus :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;quoi (un titre),&lt;/li&gt;
&lt;li&gt;pour quel projet,&lt;/li&gt;
&lt;li&gt;pour quand (une échéance, souvent absente),&lt;/li&gt;
&lt;li&gt;et où j'en suis (un statut).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus deux champs qui ne viennent pas des outils classiques : le &lt;strong&gt;type&lt;/strong&gt; (est-ce moi qui dois&lt;br&gt;
le faire, ou un agent/une machine ?) et la &lt;strong&gt;preuve&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;missions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;projet&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;titre&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;statut&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'todo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'agent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;due_date&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;owner&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;preuve&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;note&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cree_le&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;maj_le&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_due&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;missions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;due_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;statut&lt;/code&gt; vaut &lt;code&gt;todo&lt;/code&gt;, &lt;code&gt;doing&lt;/code&gt;, &lt;code&gt;done&lt;/code&gt; ou &lt;code&gt;blocked&lt;/code&gt;. Pas de table de statuts, pas de&lt;br&gt;
métadonnées imbriquées : quatre chaînes et une contrainte applicative.&lt;/p&gt;

&lt;p&gt;Total : 190 lignes et 192 Ko pour une centaine de tâches et leur historique. La base entière&lt;br&gt;
tient dans une pièce jointe.&lt;/p&gt;
&lt;h2&gt;
  
  
  La colonne qui a tout changé : &lt;code&gt;preuve&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;J'ai eu un problème répété : des tâches marquées « faites » qui ne l'étaient pas. Un serveur&lt;br&gt;
« installé » qui n'existait pas. Un site « livré » dont personne ne retrouvait l'URL. Dans les&lt;br&gt;
deux cas, la case était cochée par quelqu'un de bonne foi, pressé, qui avait fait &lt;em&gt;presque&lt;/em&gt;&lt;br&gt;
le travail.&lt;/p&gt;

&lt;p&gt;J'ai ajouté une règle, puis une colonne pour la rendre mécanique : &lt;strong&gt;une tâche n'est terminée&lt;br&gt;
que si elle peut citer un artefact vérifiable.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- la contrainte, côté applicatif : pas de preuve, pas de clôture&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;missions&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'done'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;preuve&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;chemin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- /home/moi/livrables/rapport.pdf&lt;/span&gt;
       &lt;span class="n"&gt;maj_le&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;preuve&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;preuve&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trois formes de preuve acceptées, et trois seulement :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;un chemin de fichier qui existe et qui a été produit &lt;strong&gt;après&lt;/strong&gt; la création de la tâche ;&lt;/li&gt;
&lt;li&gt;une URL qui répond HTTP 200 testée au moment de la clôture ;&lt;/li&gt;
&lt;li&gt;un hash de commit.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 pilotage.py &lt;span class="k"&gt;done &lt;/span&gt;m-42 &lt;span class="nt"&gt;--preuve&lt;/span&gt; &lt;span class="s2"&gt;"/home/moi/livrables/rapport.pdf"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et le contrôle qui va avec, parce qu'une règle qu'on ne vérifie pas n'est pas une règle :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;sqlite3 missions.db &lt;span class="s2"&gt;"SELECT preuve FROM missions WHERE id='m-42';"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"preuve OK"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depuis, la question « où en est ce chantier ? » a une réponse en une requête, et non plus en&lt;br&gt;
une conversation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Les quatre requêtes qui servent tous les jours
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ce qui est en retard&lt;/strong&gt; — la seule requête que je regarde le matin :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;titre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;projet&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;missions&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'done'&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;due_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'+14 day'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;due_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ce que je dois faire moi&lt;/strong&gt; (par opposition à ce que les scripts font) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;titre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;projet&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;missions&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ludo'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'todo'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cette séparation est plus utile qu'elle n'en a l'air : elle évite de demander à une machine&lt;br&gt;
un geste qu'elle n'a pas le droit de faire (payer, signer, téléphoner), et elle évite de&lt;br&gt;
s'étonner qu'un chantier n'avance pas alors qu'il attend un humain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La charge par projet&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;projet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'todo'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;a_faire&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'doing'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;en_cours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'blocked'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;bloque&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;missions&lt;/span&gt;
 &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;projet&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;a_faire&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ce qui est bloqué depuis trop longtemps&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;titre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maj_le&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jours&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;missions&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'blocked'&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maj_le&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;jours&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  La commande, en 120 lignes d'argparse
&lt;/h2&gt;

&lt;p&gt;SQLite sans interface, c'est vite pénible. Un petit CLI suffit :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missions.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row_factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Row&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;

&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_subparsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cmd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_parser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;due&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--jours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_parser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--statut&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;todo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_parser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--preuve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;due&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id, titre, due_date FROM missions WHERE statut&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AND due_date IS NOT NULL AND date(due_date)&amp;lt;=date(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;now&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,?) ORDER BY due_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jours&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; day&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;due_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;titre&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En pratique je n'utilise que trois commandes : &lt;code&gt;due&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;done&lt;/code&gt;. Tout le reste, c'est du&lt;br&gt;
SQL tapé une fois par mois.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sauvegarde : &lt;code&gt;cp&lt;/code&gt; ne suffit pas
&lt;/h2&gt;

&lt;p&gt;Le premier réflexe — &lt;code&gt;cp missions.db backup/&lt;/code&gt; — est faux si un job écrit pendant la copie :&lt;br&gt;
on récupère un fichier dont la moitié des pages est incohérente. Trois réglages et une&lt;br&gt;
commande règlent le problème.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;journal_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;-- lectures concurrentes sans blocage&lt;/span&gt;
&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;busy_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;-- attendre 5 s plutôt que planter&lt;/span&gt;
&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;synchronous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NORMAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;-- bon compromis sur un poste de travail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sqlite3 missions.db &lt;span class="s2"&gt;".backup '/home/moi/backups/missions-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.db'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.backup&lt;/code&gt; utilise l'API de sauvegarde en ligne : cohérent, même si une écriture est en cours.&lt;br&gt;
Une ligne dans le cron, et le sujet est clos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que SQLite ne fait pas
&lt;/h2&gt;

&lt;p&gt;Je préfère le dire clairement, parce que la mode du « tout dans un fichier » produit aussi des&lt;br&gt;
désillusions :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pas de multi-utilisateur concurrent.&lt;/strong&gt; Un écrivain à la fois. Pour une équipe de trois qui
écrit toute la journée, ça devient un sujet. Pour un poste de travail et quelques scripts,
c'est un non-problème.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pas de synchronisation mobile.&lt;/strong&gt; Un fichier sur un disque local ne suit pas en déplacement.
Si vous avez besoin de cocher des tâches dans le train, SQLite n'est pas la réponse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pas de vue partagée.&lt;/strong&gt; Dès qu'un tiers doit &lt;em&gt;voir&lt;/em&gt; l'état, il faut exporter (un fichier
markdown, un JSON, un petit serveur) — pas donner accès au fichier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pas de sauvegarde automatique.&lt;/strong&gt; Le fichier est unique : sa perte est totale. Une
sauvegarde quotidienne n'est pas optionnelle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Et une limite plus discrète : les schémas évoluent. Ajouter une colonne est trivial&lt;br&gt;
(&lt;code&gt;ALTER TABLE ... ADD COLUMN&lt;/code&gt;), la renommer ou la supprimer ne l'est pas. Je garde donc les&lt;br&gt;
migrations dans un dossier &lt;code&gt;migrations/&lt;/code&gt; numéroté, avec la version courante dans&lt;br&gt;
&lt;code&gt;PRAGMA user_version&lt;/code&gt;. Dix lignes de code, et plus de mauvaise surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi je ne reviens pas en arrière
&lt;/h2&gt;

&lt;p&gt;Parce que ma base de tâches est maintenant une &lt;strong&gt;donnée&lt;/strong&gt;, pas une interface. Je peux la lire&lt;br&gt;
avec &lt;code&gt;sqlite3&lt;/code&gt;, l'exporter en CSV, la versionner, l'interroger depuis un script Python, la&lt;br&gt;
sauvegarder par un simple &lt;code&gt;.backup&lt;/code&gt;. Aucune de ces quatre choses n'était possible avec mon&lt;br&gt;
outil en ligne, qui me laissait au mieux un export JSON à la mise en forme douteuse.&lt;/p&gt;

&lt;p&gt;Et puis il y a le silence. Pas de notification, pas de rappel, pas d'e-mail « vous n'avez pas&lt;br&gt;
ouvert votre tableau de bord cette semaine ». Une commande, une réponse, rien d'autre.&lt;/p&gt;

&lt;p&gt;Deux cents tâches, cent quatre-vingt-douze kilo-octets, zéro euro par mois. Ce n'est pas une&lt;br&gt;
prouesse technique. C'est juste un outil qui fait le travail et se tait.&lt;/p&gt;

</description>
      <category>data</category>
      <category>tools</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Publier sur Dev.to depuis un serveur, avec 130 lignes de Python et zéro dépendance</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Mon, 02 Nov 2026 07:40:55 +0000</pubDate>
      <link>https://dev.to/devdusud/publier-sur-devto-depuis-un-serveur-avec-130-lignes-de-python-et-zero-dependance-2ec2</link>
      <guid>https://dev.to/devdusud/publier-sur-devto-depuis-un-serveur-avec-130-lignes-de-python-et-zero-dependance-2ec2</guid>
      <description>&lt;p&gt;Écrire dans un éditeur web, coller son markdown, choisir ses tags, cliquer sur publier :&lt;br&gt;
quand on écrit une fois par mois, c'est très bien. Quand on écrit deux fois par semaine, ça&lt;br&gt;
devient une corvée — et les corvées finissent par ne plus être faites.&lt;/p&gt;

&lt;p&gt;Mon besoin était simple : j'écris un fichier &lt;code&gt;.md&lt;/code&gt; dans un dossier, un job planifié le publie&lt;br&gt;
le lendemain matin à 8 h 40. Pas d'interface, pas de SDK, pas de dépendance à installer sur un&lt;br&gt;
serveur qui doit rester debout pendant des années.&lt;/p&gt;

&lt;p&gt;Voici le pipeline complet, avec les erreurs que j'ai réellement rencontrées.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ce dont l'API a besoin
&lt;/h2&gt;

&lt;p&gt;Trois choses, et la troisième est celle qu'on oublie.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;La clé API, dans l'en-tête &lt;code&gt;api-key&lt;/code&gt;. Elle se génère dans les paramètres du compte Dev.to.&lt;/li&gt;
&lt;li&gt;L'URL : &lt;code&gt;https://dev.to/api/articles&lt;/code&gt; pour créer, &lt;code&gt;https://dev.to/api/articles/&amp;lt;id&amp;gt;&lt;/code&gt; pour
modifier.&lt;/li&gt;
&lt;li&gt;Un en-tête &lt;code&gt;User-Agent&lt;/code&gt;. Sans lui, vous prenez un &lt;code&gt;403&lt;/code&gt; et vous cherchez pendant vingt
minutes pourquoi votre clé « ne marche pas ».&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La clé doit vivre dans un fichier d'environnement, jamais dans le script :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DEVTO_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxxxxxxxxxxxxxxxxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Lit un .env minimaliste : KEY=VALUE, commentaires en #.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Étape 1 — lire le front-matter sans PyYAML
&lt;/h2&gt;

&lt;p&gt;Un article, c'est un fichier markdown avec un en-tête entre deux &lt;code&gt;---&lt;/code&gt;. Pas besoin d'un&lt;br&gt;
analyseur YAML complet : trois champs suffisent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le fichier ressemble à ça :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Un titre qui donne envie de cliquer&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;La&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;phrase&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;que&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Dev.to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;affichera&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dans&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;les&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;résultats."&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python,automation,tutorial&lt;/span&gt;
&lt;span class="na"&gt;cover_image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://exemple.fr/couverture.jpg&lt;/span&gt;
&lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://mon-site.fr/mon-article&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

Le corps, en markdown normal.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Étape 2 — le piège du titre vide
&lt;/h2&gt;

&lt;p&gt;C'est l'erreur que j'ai réellement commise, et elle est publique : un fichier sans champ&lt;br&gt;
&lt;code&gt;title&lt;/code&gt; a été publié sous le titre « Sans titre ». Un fichier de brouillons de posts pour les&lt;br&gt;
réseaux sociaux s'était retrouvé dans le dossier de la file, et le script l'a pris pour un&lt;br&gt;
article.&lt;/p&gt;

&lt;p&gt;Depuis, la validation se fait &lt;strong&gt;avant&lt;/strong&gt; tout appel réseau :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fm&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SAUTÉ (pas de titre): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;           &lt;span class="c1"&gt;# on ne publie pas, on passe au suivant
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deux lignes. Elles valent leur pesant de réputation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Étape 3 — les tags valides, sinon 422
&lt;/h2&gt;

&lt;p&gt;Dev.to n'accepte que des tags de sa liste. Un tag inconnu renvoie&lt;br&gt;
&lt;code&gt;422 — tag not found&lt;/code&gt;, et l'article n'est pas créé. Le plus simple est de filtrer contre une&lt;br&gt;
liste blanche :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;VALID_TAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;javascript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;productivity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;automation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;programming&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tutorial&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;beginners&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webdev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;linux&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;career&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opensource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devops&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;performance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;testing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discuss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;showdev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watercooler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;architecture&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;codequality&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comparison&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;VALID_TAGS&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discuss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quatre tags maximum, c'est la limite de l'API. Les filtrer en silence vaut mieux qu'un échec&lt;br&gt;
de publication pour une virgule de trop.&lt;/p&gt;
&lt;h2&gt;
  
  
  Étape 4 — créer en brouillon, puis publier
&lt;/h2&gt;

&lt;p&gt;Publier directement, c'est un seul appel : &lt;code&gt;POST&lt;/code&gt; avec &lt;code&gt;"published": true&lt;/code&gt;. Ça marche… quand&lt;br&gt;
l'API va bien. Aux heures de pointe, j'ai vu passer des &lt;code&gt;503&lt;/code&gt; sur cet appel, et un &lt;code&gt;503&lt;/code&gt; sur un&lt;br&gt;
&lt;code&gt;POST&lt;/code&gt; non idempotent laisse un doute désagréable : l'article a-t-il été créé, ou pas ?&lt;/p&gt;

&lt;p&gt;Le pattern qui tient : créer le brouillon, récupérer son &lt;code&gt;id&lt;/code&gt;, puis publier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (X11; Linux x86_64) MonBot/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1) brouillon
&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;article&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body_markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;art_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# 2) publication, avec deux reprises sur 5xx
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;art_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;article&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un &lt;code&gt;PUT&lt;/code&gt; est idempotent : le rejouer ne crée rien en double. C'est toute la raison de cette&lt;br&gt;
danse en deux temps.&lt;/p&gt;
&lt;h2&gt;
  
  
  Étape 5 — un fichier d'état, sinon vous publiez deux fois
&lt;/h2&gt;

&lt;p&gt;Le job tourne une fois par jour, mais il peut tourner deux fois : un redémarrage, un&lt;br&gt;
déclenchement manuel, une reprise après panne. Un petit JSON suffit à rendre le script&lt;br&gt;
idempotent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;STATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expanduser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/.hermes/scripts/devto-publish-state.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STATE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;déjà publié aujourd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hui (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;

&lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
&lt;span class="n"&gt;todo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;fname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                      &lt;span class="c1"&gt;# un article par jour, dans l'ordre alphabétique
# … publication …
&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;STATE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.tmp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;STATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;# écriture atomique
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;os.replace&lt;/code&gt; plutôt qu'une écriture directe : si la machine s'éteint au mauvais moment, on&lt;br&gt;
garde l'ancien état au lieu d'un fichier tronqué.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le cron, et la seule ligne qui compte
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;40 8 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; flock &lt;span class="nt"&gt;-n&lt;/span&gt; /tmp/devto.lock /usr/bin/python3 &lt;span class="se"&gt;\&lt;/span&gt;
  /home/moi/.hermes/scripts/publish-devto.py &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/moi/logs/devto.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Une fois par jour, à heure fixe, avec un verrou. Et lisez ce log de temps en temps : c'est&lt;br&gt;
tout l'objet de mon article précédent sur les crons qui meurent en silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les quatre erreurs que j'ai vues en vrai
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Message&lt;/th&gt;
&lt;th&gt;Cause réelle&lt;/th&gt;
&lt;th&gt;Réaction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;403&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;En-tête &lt;code&gt;User-Agent&lt;/code&gt; absent&lt;/td&gt;
&lt;td&gt;L'ajouter, toujours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;401&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unauthorized&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clé absente, expirée ou révoquée&lt;/td&gt;
&lt;td&gt;Régénérer la clé&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;422&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tag not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tag hors liste blanche&lt;/td&gt;
&lt;td&gt;Filtrer les tags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;422&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Title has already been used in the last five minutes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Doublon de titre&lt;/td&gt;
&lt;td&gt;Attendre, ou varier le titre&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;503&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;API surchargée&lt;/td&gt;
&lt;td&gt;Réessayer avec un backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Le &lt;code&gt;422&lt;/code&gt; sur le titre m'a coûté une soirée : mes deux tentatives de reprise ré-envoyaient le&lt;br&gt;
même titre, donc échouaient de la même façon. Une reprise ne doit se déclencher que sur une&lt;br&gt;
erreur &lt;strong&gt;transitoire&lt;/strong&gt; — jamais sur une erreur de contrat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que ça donne à l'usage
&lt;/h2&gt;

&lt;p&gt;J'écris un fichier markdown, je le nomme &lt;code&gt;11-mon-sujet.md&lt;/code&gt;, je le pose dans le dossier. Le&lt;br&gt;
lendemain à 8 h 40 il est en ligne, avec ses tags et sa couverture. Le fichier est marqué&lt;br&gt;
comme publié, et il ne repartira pas.&lt;/p&gt;

&lt;p&gt;Cent trente lignes, zéro dépendance installée, et un pipeline que je peux encore lire dans&lt;br&gt;
deux ans. C'est exactement ce que je demande à un outil interne.&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Votre cron est mort depuis trois semaines et personne ne vous a rien dit</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Sun, 01 Nov 2026 19:55:11 +0000</pubDate>
      <link>https://dev.to/devdusud/votre-cron-est-mort-depuis-trois-semaines-et-personne-ne-vous-a-rien-dit-1fpc</link>
      <guid>https://dev.to/devdusud/votre-cron-est-mort-depuis-trois-semaines-et-personne-ne-vous-a-rien-dit-1fpc</guid>
      <description>&lt;p&gt;Un job planifié ne tombe pas en panne comme un site web. Un site web qui tombe, on le voit :&lt;br&gt;
les visiteurs écrivent, les statistiques s'effondrent. Un cron qui tombe, lui, produit exactement&lt;br&gt;
la même chose qu'un cron qui n'avait rien à faire : du silence.&lt;/p&gt;

&lt;p&gt;Je l'ai découvert en ouvrant le fichier des tâches planifiées de ma machine. Une publication&lt;br&gt;
automatique d'articles était en état « pause » depuis le 16 octobre à 14 h 26. Treize exécutions&lt;br&gt;
consécutives en échec avant ça, du 4 au 16 octobre. Personne n'avait été prévenu — et surtout,&lt;br&gt;
rien ne s'était arrêté : les autres jobs continuaient, la machine était en bonne santé, tout&lt;br&gt;
avait l'air normal.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pourquoi un job meurt sans bruit
&lt;/h2&gt;

&lt;p&gt;Trois causes, et dans mon cas les trois en même temps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. La panne réseau.&lt;/strong&gt; Les journaux montrent une résolution DNS impossible au moment du&lt;br&gt;
déclenchement : &lt;code&gt;URLError&lt;/code&gt;, puis &lt;code&gt;Name or service not known&lt;/code&gt;. Le job a échoué treize fois&lt;br&gt;
de suite, à la même heure, chaque matin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. L'ordonnanceur qui se protège.&lt;/strong&gt; Après un certain nombre d'échecs consécutifs, un&lt;br&gt;
scheduler correct met le job en pause. C'est la bonne décision : ça évite de marteler une API&lt;br&gt;
morte. Mais la mise en pause n'est pas forcément visible pour un humain, et rien ne garantit&lt;br&gt;
qu'on la remarque un jour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. La sortie que personne ne lit.&lt;/strong&gt; Un script qui écrit dans un fichier de log ne prévient&lt;br&gt;
personne. Un script qui sort en code 0 alors qu'il n'a rien fait prévient encore moins.&lt;/p&gt;

&lt;p&gt;Il existe un quatrième cas, plus vicieux : le script tourne, sort en 0, et ne fait rien. Le&lt;br&gt;
cas classique, c'est le dossier vide.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file vide&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# ← le drame est ici
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tout va bien. Surtout pour le monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Garde-fou 1 — un battement de cœur horodaté
&lt;/h2&gt;

&lt;p&gt;Un fichier, écrit &lt;strong&gt;uniquement en cas de succès réel&lt;/strong&gt;, avec l'horodatage à l'intérieur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;

&lt;span class="n"&gt;HEARTBEAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.local/state/mon-job-last-success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;marquer_succes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;HEARTBEAT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;HEARTBEAT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Règle : on écrit le heartbeat à la fin, après l'action réussie. Pas au début, pas « à peu&lt;br&gt;
près », pas « si tout s'est bien passé jusqu'ici ». Si le script plante au milieu, aucun&lt;br&gt;
heartbeat — et c'est précisément l'information qu'on veut.&lt;/p&gt;
&lt;h2&gt;
  
  
  Garde-fou 2 — le chien de garde qui surveille le chien de garde
&lt;/h2&gt;

&lt;p&gt;Un second job, indépendant, qui ne fait qu'une chose : mesurer l'âge du heartbeat et crier&lt;br&gt;
si l'âge dépasse le seuil.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;HB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.local/state/mon-job-last-success"&lt;/span&gt;
&lt;span class="nv"&gt;SEUIL_H&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;   &lt;span class="c"&gt;# 3 jours pour une publication quotidienne&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;notify-send &lt;span class="s2"&gt;"Job: aucun heartbeat n'a jamais été écrit"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
  exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;AGE_H&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; %Y &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AGE_H&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SEUIL_H&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;notify-send &lt;span class="s2"&gt;"Job muet depuis &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;AGE_H&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; h (seuil &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SEUIL_H&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; h)"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ok — dernier succès il y a &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;AGE_H&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; h"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ce garde-fou ne répare rien : il &lt;strong&gt;révèle&lt;/strong&gt;. Dans mon cas, il aurait parlé le 16 octobre au&lt;br&gt;
soir au lieu du 24.&lt;/p&gt;

&lt;p&gt;Le seuil est la seule décision qui compte. Trop court, on crie pour un job hebdomadaire&lt;br&gt;
parfaitement normal ; trop long, on découvre la panne quand le stock est déjà épuisé. Pour une&lt;br&gt;
publication quotidienne, trois jours laissent une marge honnête : un week-end de panne réseau&lt;br&gt;
ne déclenche rien, trois jours de silence déclenchent tout.&lt;/p&gt;

&lt;p&gt;Si vous ne voulez pas écrire ce garde-fou, les services de &lt;em&gt;dead man's switch&lt;/em&gt; existent&lt;br&gt;
(healthchecks.io, ou Uptime Kuma en auto-hébergé) : le job envoie un ping en cas de succès, le&lt;br&gt;
service crie quand le ping manque. C'est la même idée, hébergée ailleurs — et c'est plus&lt;br&gt;
robuste, puisque la surveillance ne meurt pas avec la machine surveillée.&lt;/p&gt;
&lt;h2&gt;
  
  
  Garde-fou 3 — distinguer « rien à faire » de « je n'ai pas pu faire »
&lt;/h2&gt;

&lt;p&gt;C'est tout le piège du &lt;code&gt;file vide&lt;/code&gt;. Un job qui n'avait rien à faire et un job cassé doivent&lt;br&gt;
produire deux signaux différents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALERTE file vide&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# ni succès, ni panne : problème de stock
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avec un code de sortie non nul, le scheduler marque l'exécution en échec et la remonte. Trois&lt;br&gt;
codes suffisent pour se comprendre entre humains :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Sens&lt;/th&gt;
&lt;th&gt;Réaction attendue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Succès, quelque chose a été fait&lt;/td&gt;
&lt;td&gt;Rien&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Erreur technique (réseau, API, disque)&lt;/td&gt;
&lt;td&gt;Regarder les logs, réessayer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rien à faire, et c'est anormal&lt;/td&gt;
&lt;td&gt;Recharger la file, réapprovisionner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;La différence entre &lt;code&gt;1&lt;/code&gt; et &lt;code&gt;3&lt;/code&gt; est la plus utile du lot : elle dit si on doit réparer la&lt;br&gt;
plomberie ou remplir le réservoir.&lt;/p&gt;
&lt;h2&gt;
  
  
  Garde-fou 4 — tester le contrat, pas seulement le script
&lt;/h2&gt;

&lt;p&gt;Un script testé une fois en septembre ne dit rien de son comportement en novembre. Deux&lt;br&gt;
vérifications automatiques, une fois par semaine :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1) le script est-il encore démarrable ?&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; py_compile &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.hermes/scripts/mon-job.py"&lt;/span&gt;

&lt;span class="c"&gt;# 2) a-t-il encore de quoi travailler ?&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/devto-articles"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.md 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si le second nombre vaut zéro, le job va tourner pour rien. C'est exactement ce qui s'est&lt;br&gt;
passé chez moi : les fichiers déjà publiés avaient été déplacés dans un sous-dossier&lt;br&gt;
&lt;code&gt;publies/&lt;/code&gt;, et le script ne regarde que la racine du dossier. Zéro candidat, tous les jours,&lt;br&gt;
sans la moindre erreur.&lt;/p&gt;

&lt;p&gt;Ajoutez &lt;code&gt;flock&lt;/code&gt; pour éviter deux exécutions simultanées, et &lt;code&gt;set -euo pipefail&lt;/code&gt; pour que&lt;br&gt;
l'échec d'une commande intermédiaire ne soit pas avalée :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flock &lt;span class="nt"&gt;-n&lt;/span&gt; /tmp/mon-job.lock /usr/bin/python3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.hermes/scripts/mon-job.py"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Le détail qui m'a coûté le plus de temps
&lt;/h2&gt;

&lt;p&gt;Deux runs sur treize ont échoué pour une raison différente du réseau :&lt;br&gt;
&lt;code&gt;422 — Title has already been used in the last five minutes&lt;/code&gt;. L'API refuse un titre identique&lt;br&gt;
dans une fenêtre de cinq minutes, et mon script réessayait avec le même titre. Un type d'échec&lt;br&gt;
sur deux est invisible dans un compteur global, et il suffit à casser un job.&lt;/p&gt;

&lt;p&gt;Leçon : un échec n'est pas un échec. &lt;code&gt;503&lt;/code&gt; (serveur fatigué), &lt;code&gt;422&lt;/code&gt; (contrat violé), &lt;code&gt;401&lt;/code&gt;&lt;br&gt;
(clé expirée) demandent trois réactions opposées — réessayer, corriger, réveiller un humain.&lt;br&gt;
Les traiter pareil, c'est garantir qu'aucun des trois ne sera bien traité.&lt;/p&gt;

&lt;h2&gt;
  
  
  La checklist que j'applique maintenant
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Un heartbeat écrit &lt;strong&gt;après&lt;/strong&gt; le succès, jamais avant.&lt;/li&gt;
&lt;li&gt;[ ] Un garde-fou indépendant qui mesure l'âge du heartbeat.&lt;/li&gt;
&lt;li&gt;[ ] Trois codes de sortie : &lt;code&gt;0&lt;/code&gt; succès, &lt;code&gt;1&lt;/code&gt; panne, &lt;code&gt;3&lt;/code&gt; stock vide.&lt;/li&gt;
&lt;li&gt;[ ] Un test hebdomadaire du contrat : le script compile, la file n'est pas vide.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;flock&lt;/code&gt; sur chaque job qui touche à un état partagé.&lt;/li&gt;
&lt;li&gt;[ ] La sortie du job &lt;strong&gt;lue par quelque chose&lt;/strong&gt;, jamais seulement écrite sur disque.&lt;/li&gt;
&lt;li&gt;[ ] Un job mis en pause par le scheduler déclenche une alerte, pas un silence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ce que ça change
&lt;/h2&gt;

&lt;p&gt;Rien de spectaculaire : un heartbeat, un chien de garde, trois codes de sortie, un test&lt;br&gt;
hebdomadaire. Une quarantaine de lignes en tout. Mais depuis, le silence est devenu une&lt;br&gt;
information.&lt;/p&gt;

&lt;p&gt;Un job planifié n'est pas un service qui tourne. C'est une promesse renouvelée chaque nuit —&lt;br&gt;
et une promesse non tenue ne fait aucun bruit. C'est à vous d'installer le bruit.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Sauvegarder ses fichiers importants automatiquement avec Python</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Fri, 23 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/devdusud/sauvegarder-ses-fichiers-importants-automatiquement-avec-python-271i</link>
      <guid>https://dev.to/devdusud/sauvegarder-ses-fichiers-importants-automatiquement-avec-python-271i</guid>
      <description>&lt;h2&gt;
  
  
  Le jour où on regrette
&lt;/h2&gt;

&lt;p&gt;On pense souvent aux sauvegardes trop tard. Le disque dur tombe en panne, l'ordinateur refuse de démarrer, un fichier est supprimé par erreur, ou un dossier client disparaît. À ce moment-là, on se dit : « j'aurais dû faire une copie ».&lt;/p&gt;

&lt;p&gt;La bonne nouvelle, c'est qu'une sauvegarde simple n'a pas besoin d'être compliquée. Avec Python, on peut copier automatiquement un dossier important vers un autre emplacement : un disque externe, un dossier synchronisé, ou un NAS à la maison.&lt;/p&gt;

&lt;p&gt;Ce tuto montre une sauvegarde basique, lisible, et adaptée aux débutants. Ce n'est pas une solution professionnelle complète, mais c'est déjà beaucoup mieux que rien.&lt;/p&gt;

&lt;h2&gt;
  
  
  La règle 3-2-1 en version simple
&lt;/h2&gt;

&lt;p&gt;On entend souvent parler de la règle 3-2-1 : trois copies, sur deux supports différents, dont une hors de la maison ou de l'entreprise. Pour commencer, retenez surtout ceci : une seule copie sur le même ordinateur ne suffit pas.&lt;/p&gt;

&lt;p&gt;Si votre dossier original et votre sauvegarde sont sur le même disque, une panne peut emporter les deux. L'idéal est de copier vers un support différent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le script Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;SOURCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents-importants&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DESTINATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sauvegardes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;horodatage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d_%H-%M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dossier_backup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DESTINATION&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backup_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;horodatage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;SOURCE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Le dossier source n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;existe pas : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SOURCE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copytree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SOURCE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dossier_backup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sauvegarde créée dans : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dossier_backup&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ce que fait ce script
&lt;/h2&gt;

&lt;p&gt;Le dossier &lt;code&gt;documents-importants&lt;/code&gt; est le dossier à protéger. Le dossier &lt;code&gt;sauvegardes&lt;/code&gt; est l'endroit où Python va créer une copie. À chaque lancement, le script ajoute la date et l'heure dans le nom du dossier, par exemple &lt;code&gt;backup_2026-10-11_18-30&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;C'est très pratique, car on garde plusieurs versions. Si vous avez modifié un fichier hier et que vous regrettez aujourd'hui, vous pouvez retrouver une ancienne copie.&lt;/p&gt;

&lt;p&gt;La fonction &lt;code&gt;copytree&lt;/code&gt; copie tout le dossier. Elle est simple et efficace. Pour de très gros volumes, il existe des méthodes plus avancées qui ne copient que les fichiers modifiés. Mais pour apprendre et protéger un petit dossier, cette version est parfaite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comment l'adapter
&lt;/h2&gt;

&lt;p&gt;Remplacez &lt;code&gt;documents-importants&lt;/code&gt; par le chemin de votre vrai dossier. Par exemple, cela peut être un dossier &lt;code&gt;Clients&lt;/code&gt;, &lt;code&gt;Factures&lt;/code&gt;, &lt;code&gt;Photos famille&lt;/code&gt;, ou &lt;code&gt;Administratif&lt;/code&gt;. Remplacez aussi &lt;code&gt;sauvegardes&lt;/code&gt; par votre disque externe si vous en avez un.&lt;/p&gt;

&lt;p&gt;Sur Windows, un chemin peut ressembler à &lt;code&gt;D:/Sauvegardes&lt;/code&gt;. Sur Linux ou Mac, il peut ressembler à &lt;code&gt;/media/ludo/DisqueUSB/Sauvegardes&lt;/code&gt;. Le plus important est de tester avec un petit dossier avant de viser vos vrais fichiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatiser vraiment
&lt;/h2&gt;

&lt;p&gt;Un script qu'on oublie de lancer ne sert pas à grand-chose. L'étape suivante consiste donc à le lancer automatiquement. Sur Windows, on peut utiliser le Planificateur de tâches. Sur Linux ou Mac, on peut utiliser cron ou un service équivalent.&lt;/p&gt;

&lt;p&gt;Choisissez un rythme réaliste. Pour des fichiers très importants, une sauvegarde quotidienne peut être utile. Pour des documents moins actifs, une fois par semaine suffit peut-être.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les erreurs fréquentes
&lt;/h2&gt;

&lt;p&gt;Première erreur : ne jamais vérifier les sauvegardes. Une sauvegarde doit être testée. Ouvrez quelques fichiers copiés pour confirmer qu'ils sont lisibles.&lt;/p&gt;

&lt;p&gt;Deuxième erreur : laisser le disque externe toujours branché. Si un ransomware chiffre vos fichiers, il peut aussi toucher le disque branché. Pour une vraie protection, branchez le disque, lancez ou laissez faire la sauvegarde, puis débranchez-le.&lt;/p&gt;

&lt;p&gt;Troisième erreur : confondre synchronisation et sauvegarde. Un service cloud qui synchronise supprime parfois aussi le fichier partout si vous le supprimez par erreur. C'est pratique, mais ce n'est pas toujours une sauvegarde historique.&lt;/p&gt;

&lt;h2&gt;
  
  
  À retenir
&lt;/h2&gt;

&lt;p&gt;Une sauvegarde simple aujourd'hui vaut mieux qu'un plan parfait jamais mis en place. Commencez avec un dossier important, une copie datée, et une vérification manuelle. Ensuite, vous pourrez automatiser le lancement.&lt;/p&gt;

&lt;p&gt;Python ne rend pas vos données invincibles, mais il peut vous éviter le grand moment de solitude du fichier perdu.&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Générer un PDF de rapport ou de facture sans acheter de logiciel</title>
      <dc:creator>Dev du Sud</dc:creator>
      <pubDate>Wed, 21 Oct 2026 22:14:54 +0000</pubDate>
      <link>https://dev.to/devdusud/generer-un-pdf-de-rapport-ou-de-facture-sans-acheter-de-logiciel-2ee2</link>
      <guid>https://dev.to/devdusud/generer-un-pdf-de-rapport-ou-de-facture-sans-acheter-de-logiciel-2ee2</guid>
      <description>&lt;h2&gt;
  
  
  Pourquoi générer un PDF soi-même
&lt;/h2&gt;

&lt;p&gt;Beaucoup de petites activités ont besoin de documents simples : une facture, un compte rendu d'intervention, un rapport de visite, un devis rapide, une fiche client. On peut le faire dans Word ou LibreOffice, bien sûr. Mais quand le même document revient souvent, remplir les mêmes lignes à la main devient vite pénible.&lt;/p&gt;

&lt;p&gt;Python peut générer un PDF automatiquement à partir de quelques informations : nom du client, date, lignes de prestation, total. Pas besoin d'acheter un logiciel compliqué pour comprendre le principe.&lt;/p&gt;

&lt;p&gt;Dans ce tuto, on va créer une mini facture très simple. Elle ne remplace pas un logiciel de comptabilité complet, surtout si vous avez des obligations légales précises. Mais elle montre comment produire un PDF propre, répétable, et facile à adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'outil utilisé
&lt;/h2&gt;

&lt;p&gt;On va utiliser &lt;code&gt;reportlab&lt;/code&gt;, une bibliothèque Python gratuite très connue pour créer des PDF. Si elle n'est pas installée, il faudra l'ajouter une fois sur votre ordinateur. Ensuite, le script pourra générer autant de PDF que vous voulez.&lt;/p&gt;

&lt;p&gt;Le principe est très simple : on crée une page, on écrit du texte à des positions précises, puis on sauvegarde le fichier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le script Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reportlab.lib.pagesizes&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;A4&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reportlab.pdfgen&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;canvas&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reportlab.lib.units&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Boulangerie du Port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;numero&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAC-2026-001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;lignes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Diagnostic ordinateur caisse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;49.00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sauvegarde des fichiers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;39.00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Nettoyage logiciel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;29.00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;fichier_pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;facture-exemple.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fichier_pdf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pagesize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;A4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;largeur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hauteur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;A4&lt;/span&gt;

&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Helvetica-Bold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hauteur&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Facture&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Helvetica&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hauteur&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Numéro : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;numero&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hauteur&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;3.7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Client : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hauteur&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;largeur&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hauteur&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hauteur&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;5.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;
&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prix&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lignes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;montant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quantite&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;prix&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;montant&lt;/span&gt;
    &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawRightString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;quantite&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; x &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prix&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; €&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawRightString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;montant&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; €&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;

&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;largeur&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;
&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Helvetica-Bold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawRightString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; €&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Helvetica&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Document généré automatiquement avec Python.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PDF créé : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fichier_pdf&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ce que fait le script
&lt;/h2&gt;

&lt;p&gt;La liste &lt;code&gt;lignes&lt;/code&gt; contient les prestations. Chaque ligne a trois informations : le texte, la quantité, et le prix unitaire. Python calcule le total tout seul. Si vous ajoutez une prestation, le montant final change automatiquement.&lt;/p&gt;

&lt;p&gt;Les coordonnées peuvent sembler étranges au début. Dans un PDF, on place les éléments sur une page, un peu comme sur une feuille avec une règle. &lt;code&gt;2 * cm&lt;/code&gt; signifie simplement « deux centimètres depuis le bord ». On n'a pas besoin de tout comprendre d'un coup : commencez par modifier les textes et les prix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comment l'utiliser au quotidien
&lt;/h2&gt;

&lt;p&gt;Vous pouvez transformer ce script en modèle. Par exemple, un réparateur peut changer le nom du client et les prestations après chaque intervention. Une association peut générer un rapport mensuel. Un freelance peut produire un devis simple.&lt;/p&gt;

&lt;p&gt;Le vrai intérêt arrive quand les données viennent d'un fichier. Imaginez un tableau avec tous les clients du mois. Python pourrait lire chaque ligne et générer un PDF par client. Là, le gain de temps devient énorme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attention aux obligations légales
&lt;/h2&gt;

&lt;p&gt;Une facture réelle doit respecter des règles : identité de l'entreprise, numéro unique, dates, mentions, TVA selon votre situation, conditions de paiement, et parfois d'autres informations. Ce tuto ne donne pas un modèle légal complet. Il donne une base technique.&lt;/p&gt;

&lt;p&gt;Si vous voulez l'utiliser pour une vraie activité, vérifiez les mentions obligatoires avec une source officielle ou votre comptable. L'automatisation ne dispense pas d'être carré.&lt;/p&gt;

&lt;h2&gt;
  
  
  À retenir
&lt;/h2&gt;

&lt;p&gt;Créer un PDF avec Python, ce n'est pas réservé aux grosses entreprises. Pour des documents répétitifs, c'est même une excellente porte d'entrée dans l'automatisation. Vous commencez avec une facture simple, puis vous ajoutez petit à petit votre logo, vos couleurs, vos conditions, ou vos données clients.&lt;/p&gt;

&lt;p&gt;Le but n'est pas de faire joli dès le premier jour. Le but est d'arrêter de recopier les mêmes informations à la main.&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
