<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Goovy Lab]]></title><description><![CDATA[Technology Information in french (except the title & few terms...)]]></description><link>https://blog.goovy.io/</link><image><url>https://blog.goovy.io/favicon.png</url><title>Goovy Lab</title><link>https://blog.goovy.io/</link></image><generator>Ghost 5.87</generator><lastBuildDate>Mon, 08 Jul 2024 23:16:36 GMT</lastBuildDate><atom:link href="https://blog.goovy.io/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Comment logger en go]]></title><description><![CDATA[Quelles sont les différentes stratégies de log en golang? Le langage go en natif, des frameworks tles que logrus, zerolog ou bien le slog le dernier né. Exemples de logs en Go...]]></description><link>https://blog.goovy.io/comment-logger-en-go/</link><guid isPermaLink="false">5fab89bcd2bc07000186dbca</guid><category><![CDATA[go]]></category><category><![CDATA[développement]]></category><dc:creator><![CDATA[Gerald Colin & Denis Garcia]]></dc:creator><pubDate>Sun, 05 Jun 2022 07:31:00 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2022/06/patrick-perkins-ETRPjvb0KM0-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2022/06/patrick-perkins-ETRPjvb0KM0-unsplash.jpg" alt="Comment logger en go"><p>Petit rappel &#xE9;vident mais il faut bien garder en t&#xEA;te &#xE0; quoi servent les logs. G&#xE9;n&#xE9;ralement, il s&apos;agit de traces techniques qui vous serviront &#xE0; cerner plus rapidement un probl&#xE8;me (fonctionnel, technique, de performance...) de votre service ou application. Il ne faut pas logger d&apos;informations fonctionnelles ni personnelles.</p><h2 id="en-natif-en-go">En natif en go</h2><p>Par d&#xE9;faut le langages offre au moins 2 possibilit&#xE9;s pour ajouter des logs :</p><ol><li>utiliser le package &quot;fmt&quot; et printX comme par exemple <code>fmt.Printf(&quot;erreur de chargement de %s&quot;, maVariable)</code><br>Il est &#xE9;vident que cela n&apos;est pas tenable sur le long terme au fur et &#xE0; mesure que la base de code augmente, mais qui ne l&apos;a jamais fait... ;-)</li><li>utiliser le package &quot;log&quot; et printX. Le package log natif est d&#xE9;j&#xE0; tr&#xE8;s riche comme beaucoup d&apos;autres packages natifs et &#xE9;vitent de se charger en d&#xE9;pendances externes. </li></ol><pre><code class="language-go">package main

import (
	&quot;fmt&quot;
	&quot;log&quot;
)

func main() {
	fmt.Printf(&quot;Exemple pour logger :\n&quot;)
	log.Printf(&quot;mon premier log&quot;)
}</code></pre><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2022/06/image.png" class="kg-image" alt="Comment logger en go" loading="lazy" width="475" height="87"></figure><p>Le package log vous permet &#xE9;galement de sp&#xE9;cifier la sortie avec la m&#xE9;thode <code>SetOutput(w io.Writer)</code> et vous pouvez d&#xE9;finir aussi votre propre logger avec <code>func New(out io.Writer, prefix string, flag int) *Logger</code>.</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2022/06/image-1.png" class="kg-image" alt="Comment logger en go" loading="lazy" width="1320" height="601" srcset="https://blog.goovy.io/content/images/size/w600/2022/06/image-1.png 600w, https://blog.goovy.io/content/images/size/w1000/2022/06/image-1.png 1000w, https://blog.goovy.io/content/images/2022/06/image-1.png 1320w" sizes="(min-width: 720px) 720px"></figure><p>et le fichier logs.txt :</p><pre><code class="language-txt">2022/06/20 12:06:51 mon premier log dans le fichier
INFO : 2022/06/20 12:06:51.716196 main.go:21: un log personnalis&#xE9;</code></pre><p>Vous pouvez aussi ajouter l&apos;initialisation de votre syst&#xE8;me de log dans un r&#xE9;pertoire utils &#xA0;et une fonction <code>init()</code> par exemple :</p><pre><code class="language-go">package logging

import (
	&quot;log&quot;
	&quot;os&quot;
)

var (
	Info     *log.Logger
	Warning  *log.Logger
	Error    *log.Logger
	Critical *log.Logger
)

func init() {

	Info = log.New(os.Stdout, &quot;INFO: &quot;, log.Ldate|log.Ltime|log.Lshortfile)
	Warning = log.New(os.Stdout, &quot;WARNING: &quot;, log.Ldate|log.Ltime|log.Lshortfile)
	Error = log.New(os.Stderr, &quot;ERROR: &quot;, log.Ldate|log.Ltime|log.Lshortfile)
	Critical = log.New(os.Stderr, &quot;CRITICAL: &quot;, log.Ldate|log.Ltime|log.Lshortfile)
}</code></pre><p>Cela reste int&#xE9;ressant pour des petits services ou utilitaires mais tr&#xE8;s vite il va manquer une rotation des fichiers, de la distribution pour un usage multi services...</p><h2 id="avec-des-librairies-externes">Avec des librairies externes</h2><p>Une des plus connues est certainement <a href="https://github.com/sirupsen/logrus?ref=blog.goovy.io">logrus</a>. Seulement elle est pass&#xE9;e en mode maintenance. Cela n&apos;emp&#xEA;che pas de l&apos;utiliser ni qu&apos;elle soit utilis&#xE9;e dans de gros projets tel que Docker :</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/moby/moby/blob/7b9275c0da707b030e62c96b679a976f31f929d3/container/health.go?ref=blog.goovy.io#L7"><div class="kg-bookmark-content"><div class="kg-bookmark-title">moby/health.go at 7b9275c0da707b030e62c96b679a976f31f929d3 &#xB7; moby/moby</div><div class="kg-bookmark-description">Moby Project - a collaborative project for the container ecosystem to assemble container-based systems - moby/health.go at 7b9275c0da707b030e62c96b679a976f31f929d3 &#xB7; moby/moby</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.com/fluidicon.png" alt="Comment logger en go"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">moby</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/28e6f506414da668d0c8b294e17ece275011be6683fbd142c9d0688f5957644e/moby/moby" alt="Comment logger en go"></div></a></figure><p>ou encore si vous utilisez datadog, ils <a href="https://www.datadoghq.com/blog/go-logging/?ref=blog.goovy.io">privil&#xE9;gient logrus</a>.</p><p><a href="https://github.com/golang/glog?ref=blog.goovy.io">glog </a> n&apos;est &#xE9;galement plus vraiment maintenue. C&apos;est pourquoi les &#xE9;quipes de Kubernetees ont fait un fork pour leur projet :</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/kubernetes/kubernetes/tree/609db7ed0b1f2839e414c17d29fe4d76edc994bd/vendor/k8s.io/klog/v2?ref=blog.goovy.io"><div class="kg-bookmark-content"><div class="kg-bookmark-title">kubernetes/vendor/k8s.io/klog/v2 at 609db7ed0b1f2839e414c17d29fe4d76edc994bd &#xB7; kubernetes/kubernetes</div><div class="kg-bookmark-description">Production-Grade Container Scheduling and Management - kubernetes/vendor/k8s.io/klog/v2 at 609db7ed0b1f2839e414c17d29fe4d76edc994bd &#xB7; kubernetes/kubernetes</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.com/fluidicon.png" alt="Comment logger en go"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">kubernetes</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/00bc6558a21f32fef59d0ba0408afa02b15c775438e337eeb25f9343a9a1bc2c/kubernetes/kubernetes" alt="Comment logger en go"></div></a></figure><p>Dans un projet interne, j&apos;ai impl&#xE9;ment&#xE9; <a href="https://github.com/rs/zerolog?ref=blog.goovy.io">zerolog</a>. Cela permet de bien structurer ses logs mais on arrive tr&#xE8;s vite &#xE0; beaucoup de code et de verbosit&#xE9;. IMHO, il devient int&#xE9;ressant &#xE0; utiliser dans des projets de monitoring et donc coupl&#xE9; &#xE0; des outils de visualisation de logs (Grafana, Kibana...) </p><p>Il y a beaucoup d&apos;autres librairies mais le point important est de bien d&#xE9;terminer ce que l&apos;on veut de ses logs...</p><p><strong>Note du 28/12/2022 :</strong></p><p>Un nouveau package est en cours et fera peut &#xEA;tre son entr&#xE9;e dans une version future de go: </p><p><a href="https://pkg.go.dev/golang.org/x/exp/slog?ref=blog.goovy.io">https://pkg.go.dev/golang.org/x/exp/slog</a></p><p>Avec ce package, vous pouvez directement &#xE9;crire un :</p><pre><code class="language-go">slog.Info(&quot;une info en stdout&quot;)

# ou bien en declarant un text handler ou json handler :

textHandler := slog.NewTextHandler(os.Stdout)
logger := slog.New(textHandler)
logger.Info(&quot;une info suppl&#xE9;mentaire&quot;, slog.Int(&quot;compteur&quot;, 10))
</code></pre><p>et il semble que les perfs soient bonnes (mais non v&#xE9;rifi&#xE9;).</p>]]></content:encoded></item><item><title><![CDATA[Installer Odoo avec docker]]></title><description><![CDATA[Comment installer Odoo version communautaire open source  v14 ou v15 avec docker et docker-compose. La méthode du hub de docker ne fonctionnait pas pour moi, voici comment j'ai fait...]]></description><link>https://blog.goovy.io/demarrer-avec-odoo/</link><guid isPermaLink="false">5f8d4b14d2bc07000186dbba</guid><category><![CDATA[tutoriaux]]></category><dc:creator><![CDATA[Gerald Colin & Denis Garcia]]></dc:creator><pubDate>Fri, 26 Nov 2021 19:46:00 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2021/11/bureaux.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2021/11/bureaux.jpg" alt="Installer Odoo avec docker"><p>Installation de sa premi&#xE8;re instance d&apos;Odoo version communautaire avec Docker. Cela a &#xE9;t&#xE9; test&#xE9; avec docker-compose et Odoo v14 et Odoo v15.</p><p>A priori vous avez une petite id&#xE9;e de ce qu&apos;est Odoo. Si ce n&apos;est pas le cas, vous pouvez toujours consulter la page <a href="https://fr.wikipedia.org/wiki/Odoo?ref=blog.goovy.io">Wikipedia</a> ou leur <a href="https://www.odoo.com/fr_FR?ref=blog.goovy.io">site web</a>.</p><p>Maintenant si vous souhaitez l&apos;installer sur votre serveur, vous pouvez reprendre le fichier docker compose fourni par la page Odoo du docker hub : <a href="https://hub.docker.com/_/odoo?ref=blog.goovy.io">https://hub.docker.com/_/odoo</a>.</p><p>Il y a d&apos;autres fa&#xE7;on de l&apos;installer et cela est bien document&#xE9;.</p><p>Voici le contenue du fichier &quot;docker-compose.yml&quot; :</p><pre><code class="language-yml">version: &apos;3.1&apos;
services:
  web:
    image: odoo:15.0
    depends_on:
      - db
    ports:
      - &quot;8069:8069&quot;
    volumes:
      - odoo-web-data:/var/lib/odoo
        # - ./config:/etc/odoo
      - ./addons:/mnt/extra-addons
    environment:
      - PASSWORD_FILE=/run/secrets/postgresql_password
      - USER=odoo
    secrets:
      - postgresql_password
  db:
    image: postgres:13
    ports:
      - &quot;5432:5432&quot;
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_PASSWORD_FILE=/run/secrets/postgresql_password
      - POSTGRES_USER=odoo
      - PGDATA=/var/lib/postgresql/data/pgdata
    volumes:
      - odoo-db-data:/var/lib/postgresql/data/pgdata
      # - ./pg_hba.conf:/var/lib/postgresql/data/pgdata/pg_hba.conf
    secrets:
      - postgresql_password
volumes:
  odoo-web-data:
  odoo-db-data:

secrets:
  postgresql_password:
    file: odoo_pg_pass</code></pre><p>Le mot de passe de la DB n&apos;est pas pass&#xE9; en tant que variable d&apos;environnement mais par un fichier ce qui &#xE9;vite de le stocker dans le fichier compose et de se retrouver dans un source control par exemple...</p><p>Ajoutez donc un fichier &quot;odoo_pg_pass&quot; avec le mot de passe de la base de donn&#xE9;e.</p><p>Il faut bien garder la db &#xE0; postgres qui est la base de donn&#xE9;e par defaut. En r&#xE9;alit&#xE9; la vraie db de Odoo sera cr&#xE9;&#xE9;e &#xE0; la premi&#xE8;re connexion avec le nom voulu. Il vaut donc mieux &#xE9;viter de la changer.</p><p>Il est aussi possible d&apos;utiliser les credentials de postgres qui sont stock&#xE9;s dans un fichier de type pgpass. Il faut donc ajouter au meme niveau que votre fichier compose, un fichier &quot;odoo_pg_pass&quot; qui contient une ligne avec les informations suivantes &quot;<em><em><code>nom_hote</code></em></em>:<em><em><code>port</code></em></em>:<em><em><code>database</code></em></em>:<em><em><code>nomutilisateur</code></em></em>:<em><em><code>motdepasse</code></em>&quot; </em>:</p><pre><code>db:5432:postgres:odoo:password1234</code></pre><p>Vous pouvez faire votre <code>docker compose up</code> (avec -d si vous le voulez en mode daemon) et si tout va bien vous avez une ligne avec <code>odoo.service.server: HTTP service (werkzeug) running on</code></p><p>Cool, je me connecte et...</p><!--kg-card-begin: html--><div style="width:100%;height:0;padding-bottom:76%;position:relative;"><iframe src="https://giphy.com/embed/xT5LMLMPdRh2VRNVLi" width="100%" height="100%" style="position:absolute" frameborder="0" class="giphy-embed" allowfullscreen></iframe></div><p><a href="https://giphy.com/gifs/season-5-the-simpsons-5x3-xT5LMLMPdRh2VRNVLi?ref=blog.goovy.io"></a></p><!--kg-card-end: html--><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2021/11/image.png" class="kg-image" alt="Installer Odoo avec docker" loading="lazy" width="989" height="183" srcset="https://blog.goovy.io/content/images/size/w600/2021/11/image.png 600w, https://blog.goovy.io/content/images/2021/11/image.png 989w" sizes="(min-width: 720px) 720px"></figure><p>Internal Server Error... On remonte &#xE0; la premi&#xE8;re ligne en erreur et on trouve <code>ERROR odoodb odoo.modules.loading: Database odoodb not initialized, you can force it with <code>-i base</code></code></p><p><em><strong>A noter </strong></em>que cela n&apos;arrive pas tout le temps et qu&apos;en r&#xE9;alit&#xE9;, si vous partez bien d&apos;un r&#xE9;pertoire vide, pas de r&#xE9;seau ni de volume docker et que vous avez bien suivi les instructions au dessus, vous devriez avoir une instance qui fonctionne directement.</p><p>A la premi&#xE8;re installation, il faut initialiser la base de donn&#xE9;e. L&apos;usage de &quot;secrets&quot; n&apos;est pas &#xE0; ce jour possible dans un container standalone.</p><p>Une fa&#xE7;on de faire est de se connecter directement dans le container <code>docker exec -it odoo-web-1 bash</code> et d&apos;executer cette commande :</p><pre><code class="language-sh"># Pour initialiser la base de donn&#xE9;e depuis le container :
$ odoo -i base -d odoo --stop-after-init --db_host=db -r odoo -w odoo</code></pre><p> &#xA0;A la suite de cela, vous devriez avoir votre instance pr&#xEA;te. Vous pouvez vous connecter sur localhost:8069 et vous devriez avoir la page de creation de base de donn&#xE9;e :</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2021/11/image-1.png" class="kg-image" alt="Installer Odoo avec docker" loading="lazy" width="628" height="846" srcset="https://blog.goovy.io/content/images/size/w600/2021/11/image-1.png 600w, https://blog.goovy.io/content/images/2021/11/image-1.png 628w"></figure>]]></content:encoded></item><item><title><![CDATA[Apache Spark 3 - Utiliser le shell Spark avec Scala]]></title><description><![CDATA[Pour démarrer avec Apache Spark en Scala : voici vos premiers pas afin de pouvoir mettre vos mains dans la manipulation de gros jeux de données avec le shell ou REPL spark. Vous trouverez quelques exemples de prototypage d'analyses de données.]]></description><link>https://blog.goovy.io/spark-scala-introduction-et-installation/</link><guid isPermaLink="false">5addc67b0d70c200019c5d08</guid><category><![CDATA[big data]]></category><category><![CDATA[développement]]></category><category><![CDATA[tutoriaux]]></category><dc:creator><![CDATA[Denis Garcia & Gerald Colin]]></dc:creator><pubDate>Mon, 08 Feb 2021 20:14:00 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2021/01/shell.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2021/01/shell.jpg" alt="Apache Spark 3 - Utiliser le shell Spark avec Scala"><p>Il existe d&#xE9;j&#xE0; beaucoup de litt&#xE9;rature de pr&#xE9;sentation &#xE0; la fois d&apos;<a href="https://fr.wikipedia.org/wiki/Apache_Spark?ref=blog.goovy.io">Apache Spark</a> et aussi de <a href="https://fr.wikipedia.org/wiki/Scala_(langage)?ref=blog.goovy.io">Scala</a>.</p><p>Juste en quelques mots, les points cl&#xE9;s qu&apos;il faut retenir:</p><ul><li><a href="https://spark.apache.org/?ref=blog.goovy.io">Apache Spark</a> est un framework con&#xE7;u pour la manipulation de gros volumes de donn&#xE9;es, et tr&#xE8;s versatile, et est g&#xE9;n&#xE9;ralement distribu&#xE9;s sur une plateforme Hadoop, mais pas que.</li><li>Dans Spark, on manipule un ensemble de donn&#xE9;es que l&apos;on appelle Datasets, qui se basent sur la notion principale de RDD : Resilient Distributed Datasets. Ce sont des blocks de donn&#xE9;es distribu&#xE9;es sur le cluster, g&#xE9;n&#xE9;ralement stock&#xE9;es en m&#xE9;moire, permettant un traitement tr&#xE8;s rapide.</li><li><a href="https://www.scala-lang.org/?ref=blog.goovy.io">Scala</a> est un langage de programmation bas&#xE9; sur la JVM et qui pr&#xE9;sente l&#x2019;int&#xE9;r&#xEA;t d&apos;&#xEA;tre moins verbeux que Java.</li><li>Apache Spark est d&#xE9;velopp&#xE9; en Scala, mais on peut aussi utiliser Spark avec Java, Python, R et SQL.</li></ul><p>Dans le reste de l&apos;article nous allons installer Spark 3 et utiliser le shell pour analyser des donn&#xE9;es. En g&#xE9;n&#xE9;ral, nous n&apos;utilisons pas le shell en production, mais c&apos;est un outil tr&#xE8;s utile pour analyser un petit ensemble de donn&#xE9;es avant de passer &#xE0; plus grand &#xE9;chelle ou bien pour faire des pocs. Le REPL spark (Read Eval Print Loop), comme dans beaucoup d&apos;autres langages, y compris Java, permet de rapidement taper des lignes de codes en ligne de commande sans passer par un &#xE9;diteur ni la phase de compilation.</p><p>Dans un autre article nous mettrons en place un projet Apache Spark dans Intellij et nous ferons des tests sur un cluster spark.</p><h2 id="t-l-charger-la-librairie-apache-spark">T&#xE9;l&#xE9;charger la librairie Apache Spark</h2><p>Allez sur la site d&apos;Apache Spark sur la <a href="https://spark.apache.org/downloads.html?ref=blog.goovy.io">page de t&#xE9;l&#xE9;chargement</a> et t&#xE9;l&#xE9;chargez une version r&#xE9;cente de spark. Au moment d&apos;&#xE9;crire cet article nous t&#xE9;l&#xE9;chargeons <a href="https://www.apache.org/dyn/closer.lua/spark/spark-3.0.1/spark-3.0.1-bin-hadoop3.2.tgz?ref=blog.goovy.io">spark 3.0.1</a></p><p>Une fois le ficher tgz t&#xE9;l&#xE9;charger, le d&#xE9;zipper o&#xF9; vous voulez sur votre disque. Puis cr&#xE9;er une variable d&apos;environnement SPARK_HOME vers ce r&#xE9;pertoire et ajouter le bin dans le path <code>export PATH=&quot;$PATH:$SPARK_HOME/bin&quot;</code> pour pouvoir d&#xE9;marrer le shell de n&apos;importe quel r&#xE9;pertoire.</p><h2 id="t-l-charger-un-jeu-de-donn-es-analyser">T&#xE9;l&#xE9;charger un jeu de donn&#xE9;es &#xE0; analyser</h2><p>Nous allons utiliser un jeu de donn&#xE9;es des valeurs fonci&#xE8;res du site : <a href="https://www.data.gouv.fr/fr/datasets/demandes-de-valeurs-foncieres-geolocalisees/?ref=blog.goovy.io">https://www.data.gouv.fr/fr/datasets/demandes-de-valeurs-foncieres-geolocalisees/</a></p><p>Voici le lien de t&#xE9;l&#xE9;chargement du jeu de donn&#xE9;es en question: <a href="https://cadastre.data.gouv.fr/data/etalab-dvf/latest/csv/2020/full.csv.gz?ref=blog.goovy.io">https://cadastre.data.gouv.fr/data/etalab-dvf/latest/csv/2020/full.csv.gz</a></p><p>T&#xE9;l&#xE9;chargez le fichier et le d&#xE9;zipper. Il s&apos;agit d&apos;un CSV de 136M dont les colonnes sont:</p><ul><li><code>id_mutation</code> : Identifiant de mutation (non stable, sert &#xE0; grouper les lignes)</li><li><code>date_mutation</code> : Date de la mutation au format ISO-8601 (YYYY-MM-DD)</li><li><code>numero_disposition</code> : Num&#xE9;ro de disposition</li><li><code>valeur_fonciere</code> : Valeur fonci&#xE8;re (s&#xE9;parateur d&#xE9;cimal = point)</li><li><code>adresse_numero</code> : Num&#xE9;ro de l&apos;adresse</li><li><code>adresse_suffixe</code> : Suffixe du num&#xE9;ro de l&apos;adresse (B, T, Q)</li><li><code>adresse_code_voie</code> : Code FANTOIR de la voie (4 caract&#xE8;res)</li><li><code>adresse_nom_voie</code> : Nom de la voie de l&apos;adresse</li><li><code>code_postal</code> : Code postal (5 caract&#xE8;res)</li><li><code>code_commune</code> : Code commune INSEE (5 caract&#xE8;res)</li><li><code>nom_commune</code> : Nom de la commune (accentu&#xE9;)</li><li><code>ancien_code_commune</code> : Ancien code commune INSEE (si diff&#xE9;rent lors de la mutation)</li><li><code>ancien_nom_commune</code> : Ancien nom de la commune (si diff&#xE9;rent lors de la mutation)</li><li><code>code_departement</code> : Code d&#xE9;partement INSEE (2 ou 3 caract&#xE8;res)</li><li><code>id_parcelle</code> : Identifiant de parcelle (14 caract&#xE8;res)</li><li><code>ancien_id_parcelle</code> : Ancien identifiant de parcelle (si diff&#xE9;rent lors de la mutation)</li><li><code>numero_volume</code> : Num&#xE9;ro de volume</li><li><code>lot_1_numero</code> : Num&#xE9;ro du lot 1</li><li><code>lot_1_surface_carrez</code> : Surface Carrez du lot 1</li><li><code>lot_2_numero</code> : Num&#xE9;ro du lot 2</li><li><code>lot_2_surface_carrez</code> : Surface Carrez du lot 2</li><li><code>lot_3_numero</code> : Num&#xE9;ro du lot 3</li><li><code>lot_3_surface_carrez</code> : Surface Carrez du lot 3</li><li><code>lot_4_numero</code> : Num&#xE9;ro du lot 4</li><li><code>lot_4_surface_carrez</code> : Surface Carrez du lot 4</li><li><code>lot_5_numero</code> : Num&#xE9;ro du lot 5</li><li><code>lot_5_surface_carrez</code> : Surface Carrez du lot 5</li><li><code>nombre_lots</code> : Nombre de lots</li><li><code>code_type_local</code> : Code de type de local</li><li><code>type_local</code> : Libell&#xE9; du type de local</li><li><code>surface_reelle_bati</code> : Surface r&#xE9;elle du b&#xE2;ti</li><li><code>nombre_pieces_principales</code> : Nombre de pi&#xE8;ces principales</li><li><code>code_nature_culture</code> : Code de nature de culture</li><li><code>nature_culture</code> : Libell&#xE9; de nature de culture</li><li><code>code_nature_culture_speciale</code> : Code de nature de culture sp&#xE9;ciale</li><li><code>nature_culture_speciale</code> : Libell&#xE9; de nature de culture sp&#xE9;ciale</li><li><code>surface_terrain</code> : Surface du terrain</li><li><code>longitude</code> : Longitude du centre de la parcelle concern&#xE9;e (WGS-84)</li><li><code>latitude</code> : Latitude du centre de la parcelle concern&#xE9;e (WGS-84)</li></ul><h2 id="utiliser-spark-shell">Utiliser spark-shell</h2><p>Dans cet article nous allons lancer spark sans cluster, juste en local. Pour cela cr&#xE9;er une variable d&apos;environnement SPARK_LOCAL comme suit.</p><pre><code>export SPARK_LOCAL_IP=&quot;127.0.0.1&quot;
</code></pre><h3 id="lancer-spark-shell">Lancer spark-shell</h3><p>Soit depuis le r&#xE9;pertoire de spark : <code>./bin/spark-shell</code> soit <code>spark-shell</code> si vous avez ajout&#xE9; les variables d&apos;environnement SPARK_HOME dans votre path.</p><h3 id="charger-un-fichier-text">Charger un fichier text</h3><pre><code>scala&gt; val donnees_brutes = spark.read.textFile(&quot;full.csv&quot;)

donnees_brutes: org.apache.spark.sql.Dataset[String] = [value: string]

</code></pre><p>Nous avons instanci&#xE9; un Dataset dont le contenu sera le contenu du fichier full.csv. Chaque ligne du fichier peut-&#xEA;tre trait&#xE9;e.</p><p>A ce point, rien n&apos;a &#xE9;t&#xE9; charg&#xE9;. Seule une action &quot;finale&quot; lancera un traitement. Par exemple, compter le nombre de ligne du Dataset lance le chargement du fichier et effectuer l&apos;action de comptage:</p><pre><code>scala&gt; donnees_brutes.count
res0: Long = 827106
</code></pre><p>Voyons maintenant un &#xE9;chantillon du fichier pour voir de quoi il est fait.</p><pre><code>scala&gt; donnees_brutes.show(3, false)

+------------+
|value       |
+------------+
|id_mutation,date_mutation,numero_disposition,nature_mutation,valeur_fonciere,adresse_numero,adresse_suffixe,adresse_nom_voie,adresse_code_voie,code_postal,code_commune,nom_commune,code_departement,ancien_code_commune,ancien_nom_commune,id_parcelle,ancien_id_parcelle,numero_volume,lot1_numero,lot1_surface_carrez,lot2_numero,lot2_surface_carrez,lot3_numero,lot3_surface_carrez,lot4_numero,lot4_surface_carrez,lot5_numero,lot5_surface_carrez,nombre_lots,code_type_local,type_local,surface_reelle_bati,nombre_pieces_principales,code_nature_culture,nature_culture,code_nature_culture_speciale,nature_culture_speciale,surface_terrain,longitude,latitude|
|2020-1,2020-01-07,000001,Vente,8000,,,FORTUNAT,B063,01250,01072,Ceyz&#xE9;riat,01,,,01072000AK0216,,,,,,,,,,,,,0,,,,,T,terres,,,1061,5.323522,46.171899                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
|2020-2,2020-01-07,000001,Vente,75000,,,RUE DE LA CHARTREUSE,0064,01960,01289,P&#xE9;ronnas,01,,,01289000AI0210,,,,,,,,,,,,,0,,,,,AB,terrains a b&#xE2;tir,,,610,5.226197,46.184538                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
+----------+
only showing top 3 rows
</code></pre><p>Nous voyons de longues lignes dont la premi&#xE8;re est le header du fichier csv puis deux autres lignes.</p><p>Nous pouvons filtrer par exemple la colonne 32 qui est &apos;nombre_pieces_principales&apos;</p><pre><code>scala&gt; val col32 = donnees_brutes.map(li =&gt; li.split(&quot;,&quot;)(32))
col32: org.apache.spark.sql.Dataset[String] = [value: string]
</code></pre><p>Nous utilisons ici le dataset initial et ajoutons un map. A ce stade aucun traitement ne se fait car il ne s&apos;agit pas d&apos;une action finale.</p><p>Le map prend une fonction lambda o&#xF9; li est une ligne et l&apos;action sera de spliter la ligne avec le s&#xE9;parateur &apos;,&apos; et extraire la colonne 32.</p><pre><code>scala&gt; col32.show(10)

col32.show(10)
+--------------------+
|               value|
+--------------------+
|nombre_pieces_pri...|
|                    |
|                    |
|                    |
|                    |
|                   5|
|                    |
|                    |
|                    |
|                    |
+--------------------+
only showing top 10 rows
</code></pre><p>On constate que toutes les entr&#xE9;es ne renseignent pas forc&#xE9;ment le nombre de pi&#xE8;ce. Essayons de montrer plus de lignes :</p><pre><code>scala&gt; col32.show(1000)

Caused by: java.lang.ArrayIndexOutOfBoundsException: Index 32 out of bounds for length 29
</code></pre><p>Nous constatons que la fonction qui split les lignes n&apos;obtient pas 32 valeurs pour certaines lignes.</p><p>En effet nous ne parsons pas le fichier tr&#xE8;s proprement dans le cas o&#xF9; le fichier est mal format&#xE9;. Chaque ligne devrait avoir 40 colonnes. Comptons le nombre de colonnes par ligne et montrons le nombre de ligne par taille</p><pre><code>scala&gt; val colsCount = donnees_brutes.map(l =&gt; l.split(&quot;,&quot;).length)
colsCount: org.apache.spark.sql.Dataset[Int] = [value: int]

scala&gt; colsCount.groupByKey(identity).count().orderBy(&quot;key&quot;).show
+---+--------+
|key|count(1)|
+---+--------+
| 31|       3|
| 40|  780120|
| 41|      12|
| 35|       4|
| 38|   39465|
| 29|    4948|
| 33|    2554|
+---+--------+
</code></pre><p><code>groupByKey</code> et <code>count</code> nous permettent de calculer le nombre d&#x2019;occurrences de colsCount.<br><code>groupByKey</code> traite chaque entr&#xE9;e, qui dans notre cas, est simplement un Integer. Nous utilisons la fonction &apos;identit&#xE9;&apos; pour utiliser cette valeur comme cl&#xE9;. La fonction count() compte le nombre d&apos;occurence.</p><p>Les Dataset nous permettent de faire des choses plus puissantes que cet exemple. Voyons plus loin.</p><h2 id="autres-manipulations-du-fichier-de-donn-es-csv">Autres manipulations du fichier de donn&#xE9;es csv</h2><p>Vous pouvez sp&#xE9;cifier plus de param&#xE8;tres au chargement d&apos;un fichier csv que dans l&apos;exemple ci-dessus.</p><p>Nous demandons &#xE0; scala de passer en &quot;paste mode&quot; pour pouvoir coller la section suivante sur plusieurs lignes.</p><pre><code>scala&gt; :paste
// Entering paste mode (ctrl-D to finish)
</code></pre><pre><code>val donnees_brutes = spark.read.format(&quot;csv&quot;)
  .option(&quot;locale&quot;, &quot;France&quot;)
  .option(&quot;sep&quot;, &quot;,&quot;)
  .option(&quot;inferSchema&quot;, &quot;true&quot;)
  .option(&quot;header&quot;, &quot;true&quot;)
  .load(&quot;full.csv&quot;)
</code></pre><p>Une fois coll&#xE9; tapez Ctrl-D.</p><p>Spark a pars&#xE9; le fichier csv avec les options fournies et &#xE0; identifi&#xE9; les colonnes du fichier. Ce qui est puissant dans ce cas est que Spark peut identifier automatiquement le schema.</p><pre><code>scala&gt; donnees_brutes.printSchema
root
 |-- id_mutation: string (nullable = true)
 |-- date_mutation: string (nullable = true)
 |-- numero_disposition: integer (nullable = true)
 |-- nature_mutation: string (nullable = true)
 |-- valeur_fonciere: double (nullable = true)
 |-- adresse_numero: integer (nullable = true)
 |-- adresse_suffixe: string (nullable = true)
 |-- adresse_nom_voie: string (nullable = true)
 |-- adresse_code_voie: string (nullable = true)
 |-- code_postal: integer (nullable = true)
 |-- code_commune: string (nullable = true)
 |-- nom_commune: string (nullable = true)
 |-- code_departement: string (nullable = true)
 |-- ancien_code_commune: string (nullable = true)
 |-- ancien_nom_commune: string (nullable = true)
 |-- id_parcelle: string (nullable = true)
 |-- ancien_id_parcelle: string (nullable = true)
 |-- numero_volume: string (nullable = true)
 |-- lot1_numero: string (nullable = true)
 |-- lot1_surface_carrez: double (nullable = true)
 |-- lot2_numero: string (nullable = true)
 |-- lot2_surface_carrez: double (nullable = true)
 |-- lot3_numero: string (nullable = true)
 |-- lot3_surface_carrez: double (nullable = true)
 |-- lot4_numero: integer (nullable = true)
 |-- lot4_surface_carrez: double (nullable = true)
 |-- lot5_numero: integer (nullable = true)
 |-- lot5_surface_carrez: double (nullable = true)
 |-- nombre_lots: integer (nullable = true)
 |-- code_type_local: integer (nullable = true)
 |-- type_local: string (nullable = true)
 |-- surface_reelle_bati: integer (nullable = true)
 |-- nombre_pieces_principales: integer (nullable = true)
 |-- code_nature_culture: string (nullable = true)
 |-- nature_culture: string (nullable = true)
 |-- code_nature_culture_speciale: string (nullable = true)
 |-- nature_culture_speciale: string (nullable = true)
 |-- surface_terrain: integer (nullable = true)
 |-- longitude: double (nullable = true)
 |-- latitude: double (nullable = true)
</code></pre><p>Notez qu&apos;il est aussi possible de sp&#xE9;cifier le schema &#xE0; spark si on connait des particularit&#xE9;s.</p><p>Remarquez &#xE9;galement que &apos;nombre_pieces_principales&apos; est bien un integer.</p><p>Si on regarde les donn&#xE9;es &#xE7;a ressemble fortement &#xE0; une requ&#xEA;te SQL</p><pre><code>scala&gt; donnees_brutes.show(3)
+---------+
|id_mutation|date_mutation|numero_disposition|nature_mutation|valeur_fonciere|adresse_numero|adresse_suffixe|    adresse_nom_voie|adresse_code_voie|code_postal|code_commune|nom_commune|code_departement|ancien_code_commune|ancien_nom_commune|   id_parcelle|ancien_id_parcelle|numero_volume|lot1_numero|lot1_surface_carrez|lot2_numero|lot2_surface_carrez|lot3_numero|lot3_surface_carrez|lot4_numero|lot4_surface_carrez|lot5_numero|lot5_surface_carrez|nombre_lots|code_type_local|type_local|surface_reelle_bati|nombre_pieces_principales|code_nature_culture|  nature_culture|code_nature_culture_speciale|nature_culture_speciale|surface_terrain|longitude| latitude|
+---------+
|     2020-1|   2020-01-07|                 1|          Vente|         8000.0|          null|           null|            FORTUNAT|             B063|       1250|       01072|  Ceyz&#xE9;riat|              01|               null|              null|01072000AK0216|              null|         null|       null|               null|       null|               null|       null|               null|       null|               null|       null|               null|          0|           null|      null|               null|                     null|                  T|          terres|                        null|                   null|           1061| 5.323522|46.171899|
|     2020-2|   2020-01-07|                 1|          Vente|        75000.0|          null|           null|RUE DE LA CHARTREUSE|             0064|       1960|       01289|   P&#xE9;ronnas|              01|               null|              null|01289000AI0210|              null|         null|       null|               null|       null|               null|       null|               null|       null|               null|       null|               null|          0|           null|      null|               null|                     null|                 AB|terrains a b&#xE2;tir|                        null|                   null|            610| 5.226197|46.184538|
|     2020-3|   2020-01-14|                 1|          Vente|        89000.0|          null|           null|           VACAGNOLE|             B112|       1340|       01024|   Attignat|              01|               null|              null|01024000AL0120|              null|         null|       null|               null|       null|               null|       null|               null|       null|               null|       null|               null|          0|           null|      null|               null|                     null|                 AB|terrains a b&#xE2;tir|                        null|                   null|            600|     null|     null|
+---------+
only showing top 3 rows
</code></pre><p>On peut m&#xEA;me s&#xE9;lectionner une colonne qui nous int&#xE9;resse</p><pre><code>scala&gt; donnees_brutes.select(&quot;nombre_pieces_principales&quot;).show
+-------------------------+
|nombre_pieces_principales|
+-------------------------+
|                     null|
|                     null|
|                     null|
|                     null|
|                        5|
|                     null|
|                     null|
|                     null|
|                     null|
|                        4|
|                     null|
|                        4|
|                        0|
|                     null|
|                     null|
|                     null|
|                     null|
|                     null|
|                        7|
|                     null|
+-------------------------+
only showing top 20 rows
</code></pre><h4 id="nombre-de-propri-t-s-vendues-par-nombre-de-pi-ces-">Nombre de propri&#xE9;t&#xE9;s vendues par nombre de pi&#xE8;ces :</h4><pre><code>scala&gt; donnees_brutes.groupBy(col(&quot;nombre_pieces_principales&quot;)).count().sort(col(&quot;nombre_pieces_principales&quot;)).show(100)
+-------------------------+------+
|nombre_pieces_principales| count|
+-------------------------+------+
|                     null|364129|
|                        0|137414|
|                        1| 33767|
|                        2| 58112|
|                        3| 77281|
|                        4| 78304|
|                        5| 47759|
|                        6| 18971|
|                        7|  6853|
|                        8|  2635|
|                        9|   960|
|                       10|   460|
|                       11|   227|
|                       12|    96|
|                       13|    42|
|                       14|    32|
|                       15|    15|
|                       16|    13|
|                       17|     7|
|                       18|     3|
|                       19|     1|
|                       20|     8|
|                       21|     2|
|                       22|     3|
|                       23|     3|
|                       25|     2|
|                       28|     2|
|                       30|     1|
|                       41|     1|
|                       55|     1|
|                       70|     1|
+-------------------------+------+
</code></pre><h4 id="nombre-de-propri-t-s-vendues-par-d-partement-">Nombre de propri&#xE9;t&#xE9;s vendues par d&#xE9;partement :</h4><pre><code>scala&gt; donnees_brutes.groupBy(&quot;code_departement&quot;).count.orderBy(&quot;code_departement&quot;).show(100)
+----------------+-----+
|code_departement|count|
+----------------+-----+
|              01| 6314|
|              02| 9217|
|              03| 1832|
|              04| 2550|
|              05|  677|
|              06|18859|
|              07| 6165|
|              08| 5256|
|              09|  563|
|              10| 4662|
|              11| 6473|
|              12|  149|
|              13| 9435|
|              14| 5474|
|              15|   81|
|              16| 3848|
|              17| 3857|
|              18| 6185|
|              19| 6346|
|              21|   23|
|              22|12335|
|              23| 5686|
|              24|11069|
|              25| 2419|
|              26| 4990|
|              27| 4136|
|              28| 9412|
|              29| 5251|
|              2A| 1983|
|              2B|   43|
|              30|18654|
|              31| 4439|
|              32| 1942|
|              33|36966|
|              34|13716|
|              35|22680|
|              36| 1096|
|              37| 6864|
|              38| 8015|
|              39| 2817|
|              40| 6351|
|              41|10449|
|              42|11995|
|              43| 4703|
|              44|34684|
|              45|13716|
|              46|  401|
|              47|10392|
|              48| 2941|
|              49|20213|
|              50|13129|
|              51|11161|
|              52|  989|
|              53| 6139|
|              54| 9418|
|              55|  119|
|              56|15274|
|              58| 4529|
|              59|29203|
|              60|13304|
|              61|  940|
|              62|14243|
|              63| 4073|
|              64| 2536|
|              65| 1289|
|              66| 1681|
|              69|16265|
|              70| 4078|
|              71| 3743|
|              72|12665|
|              73| 1118|
|              74| 6771|
|              75|23498|
|              76|12723|
|              77|24965|
|              78|23371|
|              79| 9289|
|              80| 7674|
|              81| 1137|
|              82| 5257|
|              83|11189|
|              84| 4262|
|              85|18288|
|              86| 6425|
|              87|10147|
|              88|10698|
|              89| 6259|
|              90|  383|
|              91|17785|
|              92|14624|
|              93|15855|
|              94|19177|
|              95|19635|
|             971|   31|
|             972| 1241|
|             973| 1236|
|             974|  965|
+----------------+-----+
</code></pre><p>Notez que l&apos;utilisation de col() dans les cas simples n&apos;est pas obligatoire.</p><p>On peut aussi ajouter des colonnes :</p><pre><code>scala&gt; donnees_brutes.groupBy(&quot;code_departement&quot;).agg(count(&quot;code_departement&quot;), sum(&quot;valeur_fonciere&quot;)).orderBy(&quot;code_departement&quot;).show(20)
+----------------+-----------------------+--------------------+
|code_departement|count(code_departement)|sum(valeur_fonciere)|
+----------------+-----------------------+--------------------+
|              01|                   6314| 1.742594188399994E9|
|              02|                   9217|1.3345704534799988E9|
|              03|                   1832|2.7055697240999997E8|
|              04|                   2550|3.8006166279999995E8|
|              05|                    677|       1.128379445E8|
|              06|                  18859|1.205845898618000...|
|              07|                   6165|      8.0660834657E8|
|              08|                   5256| 5.004314756300006E8|
|              09|                    563|1.3252729695000008E8|
|              10|                   4662| 8.730233068700001E8|
|              11|                   6473|1.1661764984500015E9|
|              12|                    149|       5.617143578E7|
|              13|                   9435|     3.72011788628E9|
|              14|                   5474|1.5801534871200008E9|
|              15|                     81|          4574032.25|
|              16|                   3848| 5.387546528899996E8|
|              17|                   3857| 7.421604203099996E8|
|              18|                   6185| 9.852137856099991E8|
|              19|                   6346| 5.676203941399999E8|
|              21|                     23|           2601610.0|
+----------------+-----------------------+--------------------+
only showing top 20 rows
</code></pre><h3 id="autres-requ-tes-sur-les-donn-es">Autres requ&#xEA;tes sur les donn&#xE9;es</h3><ul><li>filtrer les donn&#xE9;es pour un code postal particulier :</li></ul><pre><code>scala&gt; val data1 = donnees_brutes.filter(&quot;code_postal == 79000&quot;)
data1: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [id_mutation: string, date_mutation: string ... 38 more fields]
</code></pre><ul><li>comme il peut y avoir plusieurs communes pour un m&#xEA;me code postal, nous filtrons en plus par nom de commune, qui est typ&#xE9;e donc on met entre simple quote. Et nous mettons en cache ce qui permet de ne pas relancer les traitements &#xE0; chaque op&#xE9;ration :</li></ul><pre><code>scala&gt; data1.groupBy(&quot;nom_commune&quot;).count.show
+-----------+-----+                                                             
|nom_commune|count|
+-----------+-----+
|     Sciecq|   15|
|      Niort| 1199|
|   Bessines|   36|
+-----------+-----+

scala&gt; val data2 = data1.filter(&quot;nom_commune == &apos;Niort&apos;&quot;).cache
data2: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [id_mutation: string, date_mutation: string ... 38 more fields]
</code></pre><ul><li>nous regroupons ensuite par adresse et nous affichons les valeurs fonci&#xE8;res. Le booleen dans la fonction show permet de ne pas tronquer les noms de rues :</li></ul><pre><code>scala&gt; val data3 = data2.groupBy(&quot;valeur_fonciere&quot;).agg(collect_set(&quot;adresse_numero&quot;),collect_set(&quot;adresse_nom_voie&quot;))
data3: org.apache.spark.sql.DataFrame = [valeur_fonciere: double, collect_set(adresse_numero): array&lt;int&gt; ... 1 more field]

scala&gt; data3.show(3, false)
+---------------+---------------------------+-----------------------------------------+
|valeur_fonciere|collect_set(adresse_numero)|collect_set(adresse_nom_voie)            |
+---------------+---------------------------+-----------------------------------------+
|181400.0       |[219]                      |[RUE JEAN JAURES]                        |
|300000.0       |[65]                       |[LA JEUNE NOEMIE, RUE LOUISE MICHEL]     |
|330000.0       |[133, 47]                  |[AV DE LA VENISE VERTE, RUE DE FLEURELLE]|
+---------------+---------------------------+-----------------------------------------+
only showing top 3 rows
</code></pre><p></p><h2 id="utiliser-sparksql">Utiliser SparkSQL</h2><pre><code>scala&gt; donnees_brute.createTempView(&quot;valeurs&quot;)
scala&gt; spark.sql(&quot;SELECT code_departement, count(code_departement), format_number(sum(valeur_fonciere),2) as val from valeurs group by code_departement&quot;).show
+----------------+-----------------------+----------------+                     
|code_departement|count(code_departement)|             val|
+----------------+-----------------------+----------------+
|              07|                   6165|  806,608,346.57|
|              51|                  11161|2,602,674,825.81|
|              15|                     81|    4,574,032.25|
|              54|                   9418|3,584,392,751.82|
|              11|                   6473|1,166,176,498.45|
|              29|                   5251|  924,555,519.99|
|              69|                  16265|6,919,943,214.33|
|              42|                  11995|2,150,156,062.88|
|              73|                   1118|  322,142,860.18|
|              87|                  10147|1,252,443,498.36|
|             974|                    965|  915,366,405.44|
|              64|                   2536|  469,110,158.79|
|              30|                  18654|5,146,175,384.62|
|              34|                  13716|2,900,615,804.92|
|              59|                  29203|9,800,705,395.83|
|              01|                   6314|1,742,594,188.40|
|              22|                  12335|1,780,289,300.53|
|              28|                   9412|2,595,710,151.61|
|              85|                  18288|2,669,234,383.54|
|              16|                   3848|  538,754,652.89|
+----------------+-----------------------+----------------+
only showing top 20 rows
</code></pre><h2 id="beaucoup-de-sources-de-donn-es-disponibles">Beaucoup de sources de donn&#xE9;es disponibles</h2><p>Apache spark sait lire des tas de sources de donn&#xE9;es en natif et avec des modules open source. En voici une liste &#xE0; titre d&apos;exemple, mais en g&#xE9;n&#xE9;ral n&apos;importe quelle source est possible</p><ul><li>CSV</li><li>JSON</li><li>Parquet</li><li>ORC</li><li>JDBC/ODBC connections</li><li>Plain-text files</li><li>Cassandra</li><li>HBase</li><li>MongoDB</li><li>AWS Redshift</li><li>XML</li><li>Kafka</li><li>etc...</li></ul><h2 id="exemple-lire-un-fichier-json">Exemple : Lire un fichier JSON</h2><p>Spark ne va pas lire directement un fichier JSON, mais attend un object JSON par ligne, par example</p><pre><code>{&quot;nom&quot;:&quot;Garcia&quot;,&quot;prenom&quot;:&quot;Denis&quot;}
{&quot;nom&quot;:&quot;Colin&quot;,&quot;prenom&quot;:&quot;G&#xE9;rald&quot;}</code></pre><p>Vous pouvez pr&#xE9;parer votre fichier JSON avec un simple commande jq</p><pre><code>cat data.json | jq -c &apos;.&apos; &gt; data-ready.json</code></pre><p>Maintenant, il nous suffit de faire comme suit avec spark :</p><pre><code>scala&gt; val fichier = &quot;data-ready.json&quot;
scala&gt; val personnes = spark.read.json(fichier)

scala&gt; personnes.printSchema()
root
 |-- firstname: string (nullable = true)
 |-- name: string (nullable = true)
 
scala&gt; personnes.show()
+------+------+
|prenom|  nom |
+------+------+
| Denis|Garcia|
|G&#xE9;rald| Colin|
+------+------+</code></pre><p></p><p><em><strong>Voil&#xE0; pour cette mise en bouche rapide pour l&apos;utilisation d&apos;Apache Spark et le traitement rapide et &#xE9;ph&#xE9;m&#xE8;re de donn&#xE9;es.</strong></em></p>]]></content:encoded></item><item><title><![CDATA[Configurer HAProxy avec LetsEncrypt et plusieurs sous-domaines]]></title><description><![CDATA[Voici une méthode pour voir comment configurer haproxy et letsencrypt et gérer facilement https avec docker.]]></description><link>https://blog.goovy.io/configurer-haproxy-avec-letsencrypt-et-plusieurs-sous-domaines/</link><guid isPermaLink="false">600199845108100001016c86</guid><category><![CDATA[commandes]]></category><category><![CDATA[docker]]></category><category><![CDATA[tutoriaux]]></category><category><![CDATA[linux]]></category><dc:creator><![CDATA[Gerald Colin & Denis Garcia]]></dc:creator><pubDate>Sun, 24 Jan 2021 17:01:40 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2021/01/roads.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2021/01/roads.jpg" alt="Configurer HAProxy avec LetsEncrypt et plusieurs sous-domaines"><p>Nous avons un serveur qui h&#xE9;berge plusieurs services li&#xE9;s &#xE0; des sous-domaines diff&#xE9;rents et qui doivent tous avoir un certificat.</p><p>En pr&#xE9;requis, les entr&#xE9;es DNS des sous-domaines pointent toutes sur le m&#xEA;me serveur.</p><p>Sur un serveur qui h&#xE9;berge plusieurs services en https sur des sous-domaines diff&#xE9;rents, voici une m&#xE9;thode (parmi d&apos;autres) pour configurer <a href="https://www.haproxy.org/?ref=blog.goovy.io">HAProxy </a>avec des certificats <a href="https://letsencrypt.org/fr/?ref=blog.goovy.io">LetsEncrypt </a>qui se renouvellent automatiquement. </p><ul><li>Reprendre le fichier /etc/haproxy/haproxy.cfg ci-dessous et compl&#xE9;ter avec votre domaine et services :</li></ul><pre><code class="language-sh">global
	log /dev/log	local0
	log /dev/log	local1 notice
	chroot /var/lib/haproxy
	stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
	stats timeout 30s
	user haproxy
	group haproxy
	daemon

	# Default SSL material locations
	ca-base /etc/ssl/certs
	crt-base /etc/ssl/private
	tune.ssl.default-dh-param 2048 

	# Default ciphers to use on SSL-enabled listening sockets.
	# For more information, see ciphers(1SSL). This list is from:
	#  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
	# An alternative list with additional directives can be obtained from
	#  https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=haproxy
	ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
	ssl-default-bind-options no-sslv3

defaults
	log	global
	mode	http
	option	httplog
	option	dontlognull
        timeout connect 5000
        timeout client  50000
        timeout server  50000
	errorfile 400 /etc/haproxy/errors/400.http
	errorfile 403 /etc/haproxy/errors/403.http
	errorfile 408 /etc/haproxy/errors/408.http
	errorfile 500 /etc/haproxy/errors/500.http
	errorfile 502 /etc/haproxy/errors/502.http
	errorfile 503 /etc/haproxy/errors/503.http
	errorfile 504 /etc/haproxy/errors/504.http

frontend http-in
	bind *:80

	acl http ssl_fc,not
	http-request redirect scheme https if http

frontend https-in
	bind *:443 ssl crt-list /etc/haproxy/certs/domains_list.txt

	option forwardfor
	option forwardfor header X-Real-IP
	http-request add-header X-Real-IP %[src]
	reqadd X-Forwarded-Proto:\ https

	acl letsencrypt-acl path_beg /.well-known/acme-challenge/
	use_backend letsencrypt-back if letsencrypt-acl

	acl host_dom1 hdr_end(host) -i subd1.mydomain.com
	acl host_dom2 hdr_end(host) -i subd2.mydomain.com

	use_backend dom1-back if host_dom1
	use_backend dom2-back if host_dom2

	default_backend dom1-back

backend dom1-back
	redirect scheme https if !{ ssl_fc }
	server dom1 127.0.0.1:8080 check

backend dom2-back
	redirect scheme https if ! { ssl_fc }
	server dom2 127.0.0.1:9980 check

backend letsencrypt-back
	server letsencrypt 127.0.0.1:8888</code></pre><ul><li>Pour cr&#xE9;er un certificat la premi&#xE8;re fois, ex&#xE9;cutez la commande :</li></ul><pre><code class="language-sh">certbot certonly --standalone -d subd2.mydomain.com --email me@email.com --agree-tos --non-interactive --http-01-port=8888</code></pre><ul><li>Concat&#xE9;nez les fichiers de certificats en un seul fichier pour haproxy :</li></ul><pre><code class="language-sh">cat /etc/letsencrypt/live/subd2.domain.com/fullchain.pem /etc/letsencrypt/live/subd2.domain.com/privkey.pem | tee /etc/haproxy/certs/subd2.domain.com.pem</code></pre><ul><li>Cr&#xE9;ez ou compl&#xE9;tez le fichier /etc/haproxy/certs/domains_list.txt :</li></ul><pre><code class="language-txt">/etc/haproxy/certs/subd1.domain.com.pem subd1.domain.com
/etc/haproxy/certs/subd2.domain.com.pem subd2.domain.com</code></pre><ul><li>Red&#xE9;marrez haproxy pour prendre en compte les ajouts <code>sudo haproxy reload</code></li><li>Pour le renouvellement, copiez le script ci-dessous dans <code>/opt/certif-renewal.sh</code> et ajoutez le en crontab :</li></ul><pre><code class="language-sh">#!/bin/sh

certbot renew --force-renewal --tls-sni-01-port=8888

cat /etc/letsencrypt/live/subd1.domain.com/fullchain.pem /etc/letsencrypt/live/subd1.domain.com/privkey.pem | tee /etc/haproxy/certs/subd1.domain.com.pem
cat /etc/letsencrypt/live/subd2.domain.com/fullchain.pem /etc/letsencrypt/live/subd2.domain.com/privkey.pem | tee /etc/haproxy/certs/subd2.domain.com.pem

service haproxy reload

echo `date +&apos;%F %T&apos;` &quot; ---- Certificat renewal done ----&quot;</code></pre><p>Et voil&#xE0;...</p><h1 id="explications">Explications</h1><p><a href="https://www.haproxy.org/?ref=blog.goovy.io">HAProxy</a> est un load balancer tr&#xE8;s performant et simple de mise en oeuvre.</p><p>Dans ce mode de fonctionnement, les appels vers le serveur et les sous-domaines sont forc&#xE9;s en https par haproxy et c&apos;est lui qui g&#xE8;re les certificats. Les appels aux services backend sont alors fait en http. Cela facilite leur mise en oeuvre car vous &#xE9;vite de g&#xE9;rer des certificats, surtout quand ces services sont des containers docker.</p><p>Le probl&#xE8;me g&#xE9;n&#xE9;ral est que haproxy &#xE9;coute sur le port 80 et 443 de votre serveur et que certbot a besoin de ces ports pour cr&#xE9;er les certificats ou les renouveler. Vous pourriez stopper haproxy le temps de faire le renouvellement mais ici nous pr&#xE9;f&#xE9;rons utiliser certbot sur un port d&#xE9;di&#xE9; (8888 par exemple) et donner l&apos;instruction &#xE0; HAProxy de l&apos;utiliser quand il d&#xE9;tecte l&apos;appel &#xE0; l&apos;URL de LetsEncrypt.</p><p>Du coup, pour les commandes certbot, il faut ajouter l&apos;attribut <code>--http-01-port=8888</code> pour pr&#xE9;ciser sur quel port se connecter. Celui-ci correspond &#xE0; ce que vous param&#xE9;trez dans la configuration du backend de HAProxy.</p><p>Dans la section global, l&apos;instruction <code>tune.ssl.default-dh-param 2048</code> augmente la s&#xE9;curit&#xE9; lors de l&apos;&#xE9;change de cl&#xE9;s (1024 par d&#xE9;faut).</p>]]></content:encoded></item><item><title><![CDATA[Restitution de données et création de tableaux de bords]]></title><description><![CDATA[Metabase est un outil de restitution de données et de création de tableaux de bord orienté utilisateurs. Il se connecte à vos sources de données, vous propose de les découvrir et d'en produire des dashboards ergonomiques.]]></description><link>https://blog.goovy.io/bi-tableau-de-bord/</link><guid isPermaLink="false">5f771e7978dec10001328a3b</guid><category><![CDATA[big data]]></category><category><![CDATA[etl]]></category><category><![CDATA[tutoriaux]]></category><dc:creator><![CDATA[Gerald Colin & Denis Garcia]]></dc:creator><pubDate>Tue, 06 Oct 2020 20:05:00 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2020/10/lee-jackie-JepnSiMssrg-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2020/10/lee-jackie-JepnSiMssrg-unsplash.jpg" alt="Restitution de donn&#xE9;es et cr&#xE9;ation de tableaux de bords"><p>Dans le domaine de la Business Intelligence ou cr&#xE9;ations de dashboard m&#xE9;tiers, il existe beaucoup d&apos;outils payants et quelques uns open-source. Nous allons vous pr&#xE9;senter <a href="https://www.metabase.com/?ref=blog.goovy.io">Metabase</a> qui &#xE0; l&apos;avantage de proposer leur outil avec les principales fonctionnalit&#xE9;s en open source et gratuitement. Cette version est &#xE0; h&#xE9;berger soi-m&#xEA;me (exemple ci-dessous). Sinon ils proposent une version avec plus de s&#xE9;curit&#xE9; et d&apos;audit mais la marche est haute en terme de prix. Ils ont aussi des <a href="https://www.metabase.com/start/hosted/?ref=blog.goovy.io">offres en mode SAAS</a>.</p><h1 id="installation">Installation</h1><h3 id="pr-requis-base-de-donn-e-postgresql">Pr&#xE9;requis : base de donn&#xE9;e PostgreSQL</h3><p>Cette base de donn&#xE9;e sera utilis&#xE9;e pour la configuration de Metabase. Voici un exemple de commandes et script :</p><pre><code class="language-shell"># Ajout d&apos;un utilisateur systeme postgres sur le host et creation du repertoire des donn&#xE9;es de la base
$ sudo useradd -r postgres
$ mkdir data
$ sudo chown postgres:postgres

# Ajout du reseaux docker d&#xE9;di&#xE9;
$ docker network create MB_NET

# Execution du script start.sh ci-dessous
$ ./start.sh</code></pre><p>avec le script <code>start.sh</code> suivant :</p><pre><code class="language-sh">docker run -d --name metabaseDB \
	--restart unless-stopped \
	--network MB_NET \
	--user 999:998 \
	-v /etc/passwd:/etc/passwd:ro \
	-v $PWD/data:/var/lib/postgresql/data \
	-e POSTGRES_PASSWORD=metabasedemo \
	-e POSTGRES_USER=metabasedemo \
	-e POSTGRES_DB=metabase \
	-p 15432:5432 \
	postgres:12.4</code></pre><h3 id="image-docker-de-metabase">Image docker de Metabase</h3><p>Nous utilisons <a href="https://hub.docker.com/r/metabase/metabase?ref=blog.goovy.io">l&apos;image Docker</a> officielle avec le script suivant :</p><pre><code class="language-sh">#!/bin/sh

docker run -d -p 3000:3000 \
	--network MB_NET \
	-e &quot;MB_DB_TYPE=postgres&quot; \
	-e &quot;MB_DB_DBNAME=metabase&quot; \
	-e &quot;MB_SB_PORT=5432&quot; \
	-e &quot;MB_DB_USER=metabasedemo&quot; \
	-e &quot;MB_DB_PASS=metabasedemo&quot; \
	-e &quot;MB_DB_HOST=metabaseDB&quot; \
	-e &quot;JAVA_TIMEZONE=Europe/Paris&quot; \
	--name metabase metabase/metabase:latest</code></pre><p>et apr&#xE8;s ex&#xE9;cution de ces scripts vous pouvez aller sur localhost:3000 et vous obtenez l&#x2019;assistant de premi&#xE8;re configuration :</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2020/10/image.png" class="kg-image" alt="Restitution de donn&#xE9;es et cr&#xE9;ation de tableaux de bords" loading="lazy" width="625" height="474" srcset="https://blog.goovy.io/content/images/size/w600/2020/10/image.png 600w, https://blog.goovy.io/content/images/2020/10/image.png 625w"></figure><p>Vous avez le choix de plusieurs sources de donn&#xE9;es :</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2020/10/image-1.png" class="kg-image" alt="Restitution de donn&#xE9;es et cr&#xE9;ation de tableaux de bords" loading="lazy" width="715" height="564" srcset="https://blog.goovy.io/content/images/size/w600/2020/10/image-1.png 600w, https://blog.goovy.io/content/images/2020/10/image-1.png 715w"></figure><h1 id="overview">Overview</h1><p>La prise en main est plut&#xF4;t intuitive et simple et orient&#xE9; utilisateur (et non pas informaticien comme dans beaucoup d&apos;outils BI). </p><p>Une base de donn&#xE9;e de d&#xE9;mo est fournie ce qui permet de jouer avec l&apos;outil sans ajouter de sources externes.</p><p><a href="https://www.metabase.com/?ref=blog.goovy.io">Metabase</a> vous pr&#xE9;sente une radiographie de vos donn&#xE9;es : il a scann&#xE9; les tables et construit pour vous des premiers tableaux de bords plus ou moins pertinents en fonction de votre mod&#xE8;le.</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2020/10/image-2.png" class="kg-image" alt="Restitution de donn&#xE9;es et cr&#xE9;ation de tableaux de bords" loading="lazy" width="1805" height="229" srcset="https://blog.goovy.io/content/images/size/w600/2020/10/image-2.png 600w, https://blog.goovy.io/content/images/size/w1000/2020/10/image-2.png 1000w, https://blog.goovy.io/content/images/size/w1600/2020/10/image-2.png 1600w, https://blog.goovy.io/content/images/2020/10/image-2.png 1805w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2020/10/image-3.png" class="kg-image" alt="Restitution de donn&#xE9;es et cr&#xE9;ation de tableaux de bords" loading="lazy" width="1485" height="860" srcset="https://blog.goovy.io/content/images/size/w600/2020/10/image-3.png 600w, https://blog.goovy.io/content/images/size/w1000/2020/10/image-3.png 1000w, https://blog.goovy.io/content/images/2020/10/image-3.png 1485w" sizes="(min-width: 720px) 720px"></figure><p>En plus de la radiographie, vous avez 3 notions essentielles :</p><ul><li>les requ&#xEA;tes que vous pouvez construire de 3 mani&#xE8;re diff&#xE9;rentes, de la question simple &#xE0; la requ&#xEA;te SQL</li><li>les dashboards qui sera une composition de vos requ&#xEA;tes</li><li>les pulses qui sont des rapports automatiques que vous pouvez envoyer sur Slack ou par mail.</li></ul><p>Pour requeter les donn&#xE9;es, vous avez donc 3 niveaux en fonction de votre app&#xE9;tence technique :</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2020/10/image-4.png" class="kg-image" alt="Restitution de donn&#xE9;es et cr&#xE9;ation de tableaux de bords" loading="lazy" width="1415" height="366" srcset="https://blog.goovy.io/content/images/size/w600/2020/10/image-4.png 600w, https://blog.goovy.io/content/images/size/w1000/2020/10/image-4.png 1000w, https://blog.goovy.io/content/images/2020/10/image-4.png 1415w" sizes="(min-width: 720px) 720px"></figure><ul><li>La question simple va lire le contenu d&apos;une table choisie. Metabase fonctionnera mieux si votre mod&#xE8;le de donn&#xE9;e source est d&#xE9;j&#xE0; bien structur&#xE9;. Il ne vous permet pas de transformation interm&#xE9;diaires et de pr&#xE9;paration de donn&#xE9;es comme d&apos;autres outils de BI plus &#xE9;volu&#xE9;s. </li></ul><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2020/10/image-7.png" class="kg-image" alt="Restitution de donn&#xE9;es et cr&#xE9;ation de tableaux de bords" loading="lazy" width="1447" height="719" srcset="https://blog.goovy.io/content/images/size/w600/2020/10/image-7.png 600w, https://blog.goovy.io/content/images/size/w1000/2020/10/image-7.png 1000w, https://blog.goovy.io/content/images/2020/10/image-7.png 1447w" sizes="(min-width: 720px) 720px"></figure><p></p><ul><li>La question personnalis&#xE9;e permettra les jointures avec d&apos;autres tables, l&apos;ajout de colonnes calcul&#xE9;es, le tout de fa&#xE7;on graphique<br></li></ul><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2020/10/image-5.png" class="kg-image" alt="Restitution de donn&#xE9;es et cr&#xE9;ation de tableaux de bords" loading="lazy" width="1070" height="809" srcset="https://blog.goovy.io/content/images/size/w600/2020/10/image-5.png 600w, https://blog.goovy.io/content/images/size/w1000/2020/10/image-5.png 1000w, https://blog.goovy.io/content/images/2020/10/image-5.png 1070w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2020/10/image-8.png" class="kg-image" alt="Restitution de donn&#xE9;es et cr&#xE9;ation de tableaux de bords" loading="lazy" width="1450" height="720" srcset="https://blog.goovy.io/content/images/size/w600/2020/10/image-8.png 600w, https://blog.goovy.io/content/images/size/w1000/2020/10/image-8.png 1000w, https://blog.goovy.io/content/images/2020/10/image-8.png 1450w" sizes="(min-width: 720px) 720px"></figure><ul><li>La requ&#xEA;te native permet de taper ses propres requ&#xEA;tes SQL. Vous avez la possibilit&#xE9; d&apos;ajouter des clauses where optionnelles (entre [[ et ]] ) et aussi des variables qui pourront &#xEA;tre utilis&#xE9;es dans les dashboards.<br></li></ul><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2020/10/image-9.png" class="kg-image" alt="Restitution de donn&#xE9;es et cr&#xE9;ation de tableaux de bords" loading="lazy" width="1433" height="717" srcset="https://blog.goovy.io/content/images/size/w600/2020/10/image-9.png 600w, https://blog.goovy.io/content/images/size/w1000/2020/10/image-9.png 1000w, https://blog.goovy.io/content/images/2020/10/image-9.png 1433w" sizes="(min-width: 720px) 720px"></figure><h1 id="comparaisons-entre-p-riodes">Comparaisons entre p&#xE9;riodes</h1><p>Un cas classique est de pr&#xE9;senter des donn&#xE9;es sur une p&#xE9;riode compar&#xE9;es &#xE0; une autre p&#xE9;riode.</p><p>Une fa&#xE7;on de faire est d&apos;utiliser la question personnalis&#xE9;e. Dans la partie &quot;R&#xE9;sumer&quot;, choisir une expression personnalis&#xE9;e au lieu d&apos;une colonne existante et utiliser un op&#xE9;rateur conditionnel tel que <code>sumif</code> ou <code>countif</code>. Par exemple : <code>SumIf([Quantity], between([Created at],&quot;2020-01-01&quot;,&quot;2020-12-31&quot;))</code>.</p><h1 id="notes-et-cueils">Notes et &#xE9;cueils</h1><h3 id="alias-et-variables">Alias et variables</h3><p>Dans les requ&#xEA;tes SQL, le syst&#xE8;me n&apos;aime pas l&apos;utilisation des alias avec les variables. Un exemple qui ne fonctionne pas :</p><pre><code class="language-SQL">select p.nom, p.race 
from pet as p
where 1=1
[[ and {{ color }} ]];</code></pre><p><em>Explications</em> : </p><ul><li>nous utilisons ici une clause optionnelle d&#xE9;limit&#xE9;e entre crochet. Si celle-ci n&apos;est pas pr&#xE9;sente, la clause where se retrouve vide et la requ&#xEA;te n&apos;est plus valide, d&apos;ou l&apos;ajout du <code>1=1</code>. </li><li>la variable &quot;color&quot; rattach&#xE9;e &#xE0; la colonne du m&#xEA;me nom ne passera pas ensuite dans la requ&#xEA;te g&#xE9;n&#xE9;r&#xE9;e par Metabase et vous donnera une erreur.</li></ul><p>Notre conseil est d&apos;enlever les alias pour les tables sur lesquelles vous avez des variables.</p><h3 id="notes-sur-outils-de-bi">Notes sur outils de BI</h3><p>Nous ne ferons pas de comparaisons pouss&#xE9;es avec les gros de la BI tels que Qlik ou Tableau Software, mais le principal avantage de Metabase est sa prise main facile et rapide et son ergonomie orient&#xE9;e utilisateur final.</p>]]></content:encoded></item><item><title><![CDATA[Recherche de doublons sur Dropbox avec le SDK Dropbox]]></title><description><![CDATA[Voici un exemple d'utilisation du SDK Dropbox pour la recherche de doublons.]]></description><link>https://blog.goovy.io/recherche-de-doublons-sur-dropbox-avec-le-sdk-dropbox/</link><guid isPermaLink="false">5f5cc3f31f1af300011553ee</guid><category><![CDATA[trucs et astuces]]></category><category><![CDATA[développement]]></category><dc:creator><![CDATA[Denis Garcia & Gerald Colin]]></dc:creator><pubDate>Sat, 12 Sep 2020 21:33:01 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2020/09/maksym-kaharlytskyi-Q9y3LRuuxmg-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<h1 id="objectif">Objectif</h1><ul><li>Utiliser le SDK Dropbox</li><li>R&#xE9;cup&#xE9;rer les m&#xE9;ta donn&#xE9;es des fichiers et r&#xE9;pertoires</li><li>Utiliser le digest des fichiers (content_hash) pour identifier les doublons</li><li>G&#xE9;n&#xE9;rer une liste de doublons et calculer la taille qu&apos;on pourrait gagner en supprimant les doublons</li></ul><h1 id="avant-de-commencer">Avant de commencer</h1><ul><li> Avoir node and npm install&#xE9;s</li><li>Avoir un compte Dropbox</li></ul><h1 id="pr-paration">Pr&#xE9;paration</h1><ul><li>Cr&#xE9;ation de l&apos;espace de travail</li></ul><pre><code class="language-bash">$ mkdir espace_de_travail
$ cd espace_de_travail
$ npm init --yes
</code></pre><ul><li>installation des d&#xE9;pendances</li></ul><pre><code class="language-bash">$ npm install -s dropbox isomorphic-fetch lodash</code></pre><ul><li>G&#xE9;n&#xE9;ration du token Dropbox</li></ul><img src="https://blog.goovy.io/content/images/2020/09/maksym-kaharlytskyi-Q9y3LRuuxmg-unsplash.jpg" alt="Recherche de doublons sur Dropbox avec le SDK Dropbox"><p>Cr&#xE9;er un compte sur <a href="https://www.dropbox.com/developers/apps?ref=blog.goovy.io">https://www.dropbox.com/developers/apps</a></p><p>Cr&#xE9;er une application</p><p>R&#xE9;cup&#xE9;rer le &apos;access_token&apos;</p><ul><li>Ajouter le token &#xA0;dans le fichier token.js</li></ul><pre><code class="language-javascript">
exports.TOKEN =&apos;LE_TOKEN_GENERE&apos;;</code></pre><h1 id="r-cup-ration-des-fichiers">R&#xE9;cup&#xE9;ration des fichiers</h1><p>Nous allons maintenant travailler dans le fichier <em>recuperation-meta-donnees.js</em></p><ul><li>Imports</li></ul><pre><code class="language-javascript">const fs = require(&apos;fs&apos;);
const fetch = require(&apos;isomorphic-fetch&apos;);
const Dropbox = require(&apos;dropbox&apos;).Dropbox;
</code></pre><ul><li>Activation du SDK Dropbox</li></ul><pre><code class="language-javascript">const TOKEN = require(&apos;./token&apos;).TOKEN;
var dbx = new Dropbox({ fetch: fetch, accessToken: TOKEN });
</code></pre><ul><li>La fonction pour r&#xE9;cup&#xE9;rer les fichiers</li></ul><pre><code class="language-javascript">async function getFiles(path, process) {
  var response = await dbx.filesListFolder({
          path: path,
          recursive: true
      });
    
  processResponse(response);
  
  while(response.has_more) {
    try {
      response = await dbx.filesListFolderContinue({cursor: response.cursor});
      processResponse(response);
    }
    catch(e) {
      console.error(&apos;error&apos;, e);
      break;
    }
  }
}</code></pre><ul><li>Pr&#xE9;paration &#xA0;de notre data store local</li></ul><pre><code class="language-javascript">const entries = {};
// content_hash: Array&lt;meta_donnees_fichier&gt;
</code></pre><ul><li>Traitement des donn&#xE9;es</li></ul><pre><code class="language-javascript">async function processResponse(response) {
    
  response.entries.forEach(processEntry);
  const currentSize = Object.keys(entries).length;
}

async function processEntry(entry) {
	entries[entry.id] = entry
}</code></pre><ul><li>Action!</li></ul><p>On lance maintenant le programme, on affiche le nombre de meta donn&#xE9;es et on sauvegarde le r&#xE9;sultat dans un fichier json qu&apos;on utilisera par la suite pour trouver les doublons.</p><pre><code class="language-javascript">const path = &apos;/Caderias&apos;; // Le chemin du r&#xE9;pertoire Dropbox &#xE0; scanner. Utiliser &apos;&apos; pour tout sinon &apos;/le-chemin&apos;
getFiles(path, processResponse)
  .then(() =&gt; {
    console.log(&apos;entries&apos;, Object.keys(entries).length);
    fs.writeFileSync(&apos;db.json&apos;, JSON.stringify(entries, null, 4));
  });
</code></pre><h1 id="trouver-les-doublons">Trouver les doublons</h1><ul><li>Les imports</li></ul><pre><code class="language-javascript">const _ = require(&apos;lodash&apos;); 
const fs = require(&apos;fs&apos;); </code></pre><ul><li>Chargement de la base de donn&#xE9;es des metadata des fichiers de Dropbox</li></ul><pre><code class="language-javascript">
const fileContent = fs.readFileSync(&apos;db.json&apos;, {
  		encoding:&apos;utf8&apos;,
   		flag:&apos;r&apos;
	}
);
const db = fileContent.length &gt; 0 ? JSON.parse(fileContent) : {};

const entries = Object.values(db);
console.log(&apos;entries&apos;, entries.length);</code></pre><ul><li>Maintenant le travail commence, nous allons d&apos;abord filtrer les meta donn&#xE9;es pour ne garder que les fichiers, puis les grouper sur le champ &apos;content_hash&apos; qui &#xA0;repr&#xE9;sente &#xA0;le digeste des fichiers. Si deux fichiers ont le m&#xEA;me &apos;content_hash&apos; alors on peut consid&#xE9;rer qu&apos;ils sont identiques.</li></ul><pre><code>
// Filtrer les fichiers seulement
const files = entries.filter( e =&gt; e[&apos;.tag&apos;] === &apos;file&apos;);
console.log(&apos;files&apos;, files.length);

// Grouper les fichiers par &apos;content_hash&apos;
const grouped = _.groupBy(files, &apos;content_hash&apos;);
console.log(&apos;grouped&apos;, Object.keys(grouped).length);</code></pre><ul><li>Afficher les doublons</li></ul><p>A ce stade nous avons un Map dont la cl&#xE9; est le digest des fichiers (&apos;hash_content&apos;) et la valeur est une liste de m&#xE9;ta donn&#xE9;es des fichiers sur Dropbox</p><p>Nous allons simplement filtrer et conserver uniquement les digests qui ont une liste avec strictement plus d&apos;un &#xE9;l&#xE9;ment, car il s&apos;agit de doublons</p><pre><code class="language-javascript">// ne conserver que les digestes avec doublons
const duplicates = Object.keys(grouped)
    .filter( k =&gt; grouped[k].length &gt; 1);

// On map maintenant le nom des fichiers
const duplicatesFiles = duplicates.map(l =&gt; 
	l.map(e =&gt; e.path_display) // remplacer les m&#xE9;ta donn&#xE9;es du fichier par le nom du fichier dans la liste
);
console.log(&apos;duplicatesFiles&apos;, duplicatesFiles);</code></pre><ul><li>Calculer la taille des fichiers doublons</li></ul><pre><code>const duplicatedSize = duplicates
    .map(doublons =&gt; doublons[0].size * (doublons.length - 1))
    .reduce((a,b) =&gt; a + b);
console.log(&apos;Manque &#xE0; gagner&apos;, duplicatedSize / 1000 / 1000, &apos;mb&apos;);</code></pre><p>Nous avons maintenant une liste de doublons (donc une liste de liste).</p><p>Nous parcourons la liste des doublons, et pour chaque liste de doublons nous calculons la taille que l&apos;on peut gagner en supprimant tous les doublons (en ne conservant qu&apos;une version).</p><h1 id="r-capitulatif">R&#xE9;capitulatif</h1><p>fichier <strong>get-files.js</strong></p><pre><code class="language-javascript">const fs = require(&apos;fs&apos;);
const fetch = require(&apos;isomorphic-fetch&apos;);
const Dropbox = require(&apos;dropbox&apos;).Dropbox;

const TOKEN = require(&apos;./token&apos;).TOKEN;
const dbx = new Dropbox({ fetch: fetch, accessToken: TOKEN });

async function getFiles(path, process) {
  var response = await dbx.filesListFolder({
          path: path,
          recursive: true
      });
    
  processResponse(response);
  
  while(response.has_more) {
    try {
      response = await dbx.filesListFolderContinue({cursor: response.cursor});
      processResponse(response);
    }
    catch(e) {
      console.error(&apos;error&apos;, e);
      break;
    }
  }
}

const entries = {};
// content_hash: Array&lt;meta_donnees_fichier&gt;

async function processResponse(response) {
    
  response.entries.forEach(processEntry);
  const currentSize = Object.keys(entries).length;
}

async function processEntry(entry) {
	entries[entry.id] = entry
}

function saveDB(entries) {
  fs.writeFileSync(&apos;db.json&apos;, JSON.stringify(entries, null, 4));
}

const path = &apos;/Caderias&apos;; // Le chemin du r&#xE9;pertoire Dropbox &#xE0; scanner. Utiliser &apos;&apos; pour tout sinon &apos;/le-chemin&apos;
getFiles(path, processResponse)
  .then(() =&gt; {
    saveDB(entries);
    console.log(&apos;entries&apos;, Object.keys(entries).length);
  });
</code></pre><p>fichier <strong>analyse-files.js</strong></p><pre><code class="language-javascript">const _ = require(&apos;lodash&apos;); 
const fs = require(&apos;fs&apos;); 

const fileContent = fs.readFileSync(&apos;db.json&apos;, {encoding:&apos;utf8&apos;, flag:&apos;r&apos;});
const db = fileContent.length &gt; 0 ? JSON.parse(fileContent) : {};

const entries = Object.values(db);
console.log(&apos;entries&apos;, entries.length);

const files = entries.filter( e =&gt; e[&apos;.tag&apos;] === &apos;file&apos;);
console.log(&apos;files&apos;, files.length);

const grouped = _.groupBy(files, &apos;content_hash&apos;);
console.log(&apos;grouped&apos;, Object.keys(grouped).length);

const duplicates = Object.values(grouped)
    .filter( l =&gt; l.length &gt; 1);
console.log(&apos;duplicates&apos;, duplicates.length);

const duplicatesFiles = duplicates.map(l =&gt; l.map(e =&gt; e.path_display))
console.log(&apos;duplicatesFiles&apos;, duplicatesFiles);

const duplicatedSize = duplicates
    .map(l =&gt; l[0].size * (l.length - 1))
    .reduce((a,b) =&gt; a + b);
    // .reduce((a,b) =&gt; a.b);
console.log(&apos;duplicatedSize&apos;, duplicatedSize / 1000 / 1000, &apos;mb&apos;);

console.log(&apos;DONE&apos;);
</code></pre>]]></content:encoded></item><item><title><![CDATA[Développement d'un outil de ligne de commande en Go / Golang]]></title><description><![CDATA[Vos outils en lignes de commande préféres tels que docker, kubernetes... sont dévéloppés avec ce module Go. Pourquoi pas le votre ?]]></description><link>https://blog.goovy.io/developpement-dun-outil-de-ligne-de-commande-en-go-golang/</link><guid isPermaLink="false">5e54b2f81f1af30001155336</guid><category><![CDATA[développement]]></category><category><![CDATA[go]]></category><dc:creator><![CDATA[Gerald Colin & Denis Garcia]]></dc:creator><pubDate>Wed, 04 Mar 2020 20:02:00 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2020/03/test-gco.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2020/03/test-gco.jpg" alt="D&#xE9;veloppement d&apos;un outil de ligne de commande en Go / Golang"><p><em>Pr&#xE9;requis </em>: vous devez avoir au moins Go v1.13 (avant je n&apos;ai pas test&#xE9;).</p><p>Commencez par installer cobra:</p><pre><code class="language-go">go get -u github.com/spf13/cobra/cobra</code></pre><p>Cr&#xE9;ez votre r&#xE9;pertoire de travail (en dehors du Gopath)</p><pre><code class="language-shell">$ cd maCli
$ go mod init maCli
$ git init
$ cobra init --pkg-name maCli</code></pre><p>Pour utiliser votre programme, vous allez l&apos;appeler, puis y ajouter une commande et enfin des arguments.</p><p>Ajoutez une nouvelle commande : </p><pre><code class="language-shell">$ cobra add maCommande</code></pre><p>Votre projet contient alors un main.go avec juste un point d&apos;entr&#xE9;e qui va executer vos commandes. Vos commandes sont dans le r&#xE9;pertoire &quot;cmd&quot;. Vous avez &quot;root.go&quot; qui contient la commande par defaut et l&apos;appel &#xE0; votre programme sans commande. Vous pouvez y personnaliser votre message principal.</p><p>Dans chaque commande, vous retrouvez dans la fonction &quot;init()&quot; la gestion de vos arguments, avec 2 types : les persistens qui sont disponibles dans la commande en cours et les sous-commandes et la locale uniquement pour la commande en cours.</p><pre><code class="language-go">// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.

rootCmd.PersistentFlags().StringVar(&amp;cfgFile, &quot;config&quot;, &quot;&quot;, &quot;config file (default is $HOME/.maCli.yaml)&quot;)</code></pre><p>Par exemple, dans &quot;maCommande.go&quot; vous pouvez d&#xE9;finir dans la fonction init()</p><pre><code class="language-go">func init() {
	rootCmd.AddCommand(maCommandeCmd)

	// Here you will define your flags and configuration settings.

	// Cobra supports Persistent Flags which will work for this command
	// and all subcommands, e.g.:
	foo = maCommandeCmd.PersistentFlags().String(&quot;foo&quot;, &quot;&quot;, &quot;A help for foo&quot;)
}</code></pre><p>et dans la commande : </p><pre><code class="language-go">Run: func(cmd *cobra.Command, args []string) {
		fmt.Println(&quot;maCommande called&quot;)
		if foo!=nil {
			fmt.Printf(&quot;with foo value : %s\n&quot;, *foo)
		}
	},</code></pre><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2020/03/Capture1002.PNG" class="kg-image" alt="D&#xE9;veloppement d&apos;un outil de ligne de commande en Go / Golang" loading="lazy"></figure><p>Si vous souhaitez voir certains projets qui utilisent cela:</p><ul><li><a href="https://github.com/docker/cli?ref=blog.goovy.io">https://github.com/docker/cli</a></li><li><a href="https://github.com/kubernetes/kubernetes?ref=blog.goovy.io">https://github.com/kubernetes/kubernetes</a></li><li><a href="https://github.com/gohugoio/hugo?ref=blog.goovy.io">https://github.com/gohugoio/hugo</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Petits pièges en Go]]></title><description><![CDATA[Lors des meetups Golang Paris, nous avons vu quelques trucs et astuces et pièges courant en Go.
Voici un compte-rendu et bouts de codes.]]></description><link>https://blog.goovy.io/petits-pieges-en-go/</link><guid isPermaLink="false">5d999fbe4f48800001586005</guid><category><![CDATA[go]]></category><category><![CDATA[développement]]></category><dc:creator><![CDATA[Gerald Colin & Denis Garcia]]></dc:creator><pubDate>Sun, 06 Oct 2019 09:54:36 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2019/10/hole.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2019/10/hole.jpg" alt="Petits pi&#xE8;ges en Go"><p>Les meetups Golang Paris sont un moment privil&#xE9;gi&#xE9;s pour faire du code, avoir du retour d&apos;exp&#xE9;rience bref monter en comp&#xE9;tence et rencontrer des gens sympas. N&apos;h&#xE9;sitez pas &#xE0; vous inscrire et &#xE0; venir:</p><p><a href="https://www.meetup.com/fr-FR/Golang-Paris/?ref=blog.goovy.io">https://www.meetup.com/fr-FR/Golang-Paris/</a></p><p>et aussi &#xA0;&#xE0; consulter la page github &#xE9;galement:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/GolangParis?ref=blog.goovy.io"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Golang Paris interest group</div><div class="kg-bookmark-description">Gophers from the greater Paris area (you know... the place with that tall metal thingy) - Golang Paris interest group</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/favicon.ico" alt="Petits pi&#xE8;ges en Go"><span class="kg-bookmark-author">GitHub</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://avatars1.githubusercontent.com/u/6525860?s=280&amp;v=4" alt="Petits pi&#xE8;ges en Go"></div></a></figure><h1 id="utilisation-de-go-routines-et-assignation-de-variables">Utilisation de Go routines et assignation de variables</h1><p>Nous allons montrer un pi&#xE8;ge qui est arriv&#xE9; sur un exemple plus complexe &#xE0; la base mais qui peut se r&#xE9;sumer ainsi:</p><ul><li>Nous partons d&apos;un bout de code simple qui boucle et affiche le contenu de la variable &quot;i&quot; dans une Go routine</li></ul><pre><code class="language-Go">package main

import (
	&quot;fmt&quot;
)

func main() {
	fmt.Println(&quot;Demo on goroutines seq&quot;)

	for i := 0; i &lt; 10; i++ {
		go func() {
			fmt.Println(&quot;Val i: %d&quot;, i)
		}()
	}
}</code></pre><p>Si vous executez ce code : <a href="https://play.golang.org/p/PyMsrChN-ZD?ref=blog.goovy.io">https://play.golang.org/p/PyMsrChN-ZD</a><br>vous verrez uniquement `Demo on goroutines seq` d&apos;affich&#xE9; : le programme effectue la boucle et sort avant qu&apos;il n&apos;ait le temps de lancer les routines.</p><ul><li>Ajoutons un sleep &#xE0; la fin.</li></ul><pre><code class="language-Go">package main

import (
	&quot;fmt&quot;
	&quot;time&quot;
)

func main() {
	fmt.Println(&quot;Demo on goroutines seq&quot;)

	for i := 0; i &lt; 10; i++ {
		go func() {
			fmt.Printf(&quot;Val i: %d\n&quot;, i)
		}()
	}

	time.Sleep(500 * time.Millisecond)
}</code></pre><p>et maintenant, que devons nous voir: 10 lignes avec ` Val i: 10 `.<br>Encore une fois, les routines sont lanc&#xE9;es une fois que la boucle est termin&#xE9;e. Les routines sont cr&#xE9;&#xE9;es avec une r&#xE9;f&#xE9;rence sur la variable &quot;i&quot; et non pas une copie.</p><ul><li>Une fa&#xE7;on de corriger cela est de passer en param&#xE8;tre la variable &quot;i&quot; &#xE0; la routine:<br><a href="https://play.golang.org/p/BiSCQtXQ6wl?ref=blog.goovy.io">https://play.golang.org/p/BiSCQtXQ6wl</a></li></ul><pre><code class="language-Go">package main

import (
	&quot;fmt&quot;
	&quot;time&quot;
)

func main() {
	fmt.Println(&quot;Demo on goroutines seq&quot;)

	for i := 0; i &lt; 10; i++ {
		go func(cpt int) {
			fmt.Printf(&quot;Val i: %d\n&quot;, cpt)
		}(i)
	}

	time.Sleep(500 * time.Millisecond)
}</code></pre><p>et vous voila avec une boucle qui affiche les chiffres de 0 &#xE0; 9, mais en parall&#xE8;le!</p><h1 id="les-slices-et-leur-valeur-sous-jacente">Les slices et leur valeur sous-jacente</h1><p>Regardez l&apos;exemple de code disponible sur le repo de Meetup Golang Paris:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/GolangParis/dont-panic/blob/master/02_slices.go?ref=blog.goovy.io"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GolangParis/dont-panic</div><div class="kg-bookmark-description">Du fait des d&#xE9;r&#xE8;glements climatiques affectant le taux de reproduction de l&#x2019;esp&#xE8;ce : l&#x2019;&#xE9;tat d&#x2019;urgence a &#xE9;t&#xE9; d&#xE9;cr&#xE9;t&#xE9;. Ne pas c&#xE9;der &#xE0; la panic. - GolangParis/dont-panic</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/favicon.ico" alt="Petits pi&#xE8;ges en Go"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">GolangParis</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://avatars2.githubusercontent.com/u/6525860?s=400&amp;v=4" alt="Petits pi&#xE8;ges en Go"></div></a></figure><p>Sans revenir sur une description pouss&#xE9;e des slices, ceux-ci peuvent &#xEA;tre li&#xE9;s &#xE0; un tableau sous-jacent. Si vous ne changez pas la capacit&#xE9; du slice, il y a la m&#xEA;me r&#xE9;f&#xE9;rence que le tableau mais dans le cas d&apos;une nouvelle allocation, vous changez d&apos;emplacement.</p><p>Ce cas est &#xE0; garder en t&#xEA;te car il peut &#xEA;tre difficile &#xE0; d&#xE9;bugger et se comporter comme &quot;un bug al&#xE9;atoire&quot;.</p>]]></content:encoded></item><item><title><![CDATA[Scala pour Apache Spark - Cheatsheet]]></title><description><![CDATA[Nous présentons ici une aide-mémoire de codes en scala pour gérer des traitements de données distribués à l'aide d'Apache Spark.]]></description><link>https://blog.goovy.io/apache-spark-scala-cheatsheet/</link><guid isPermaLink="false">5d5f568f4707bf0001e06d7b</guid><category><![CDATA[big data]]></category><category><![CDATA[développement]]></category><category><![CDATA[trucs et astuces]]></category><dc:creator><![CDATA[Denis Garcia & Gerald Colin]]></dc:creator><pubDate>Wed, 28 Aug 2019 09:14:00 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2019/08/cheatsheet.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2019/08/cheatsheet.jpg" alt="Scala pour Apache Spark - Cheatsheet"><p>Vous trouverez dans cet article un ensemble de bouts de code, trucs et astuces et retours d&apos;exp&#xE9;rience sur l&apos;utilisation d&apos;<a href="https://spark.apache.org/?ref=blog.goovy.io">Apache Spark</a> en <a href="https://www.scala-lang.org/?ref=blog.goovy.io">Scala</a>.</p><h1 id="cr-er-un-objet-ex-cutable-comme-une-app">Cr&#xE9;er un objet ex&#xE9;cutable comme une app</h1><pre><code class="language-scala">object MyApp {
  def main(args: Array[String]) {
    // Le code ici sera ex&#xE9;cut&#xE9;
  }
}</code></pre><p>ou</p><pre><code class="language-scala">object MyApp extends App {
 // le code ici sera ex&#xE9;cut&#xE9;
}</code></pre><h1 id="cr-er-un-contexte-spark">Cr&#xE9;er un contexte spark</h1><pre><code class="language-scala">
  def getSC(appName: String , master: String = &quot;local&quot;): SparkContext = {
    val conf = new SparkConf()
      .setAppName(appName)
      .setMaster(master)
      .set(&quot;spark.hadoop.validateOutputSpecs&quot;, &quot;false&quot;)
    new SparkContext(conf)
  }</code></pre><h1 id="cr-er-une-session-spark">Cr&#xE9;er une session Spark</h1><pre><code class="language-scala">  def getSS(appName: String , master: String = &quot;local&quot;): SparkSession = {
    SparkSession
      .builder
      .appName(appName)
      .master(master)
      .config(&quot;spark.sql.warehouse.dir&quot;, &quot;file:///tmp&quot;) // A utiliser pour contourner un bug de Spark 2.0.0 sous Windows
      .getOrCreate()
  }
</code></pre><h1 id="cr-er-un-streaming-context">Cr&#xE9;er un streaming context</h1><pre><code class="language-scala"> def getStreamingContext(appName: String, duration: Duration, master: String = &quot;local&quot;): StreamingContext = {

    val conf = new SparkConf()
      .setAppName(appName)
      .setMaster(master)
    new StreamingContext(conf, duration)
 }
</code></pre><p><em>Note </em>: Spark Streaming ne fait pas du vrai streaming, mais plut&#xF4;t regroupe des &#xE9;v&#xE9;nements dans de micro-batch. Duration est l&apos;intervalle entre deux batches</p><h1 id="cr-er-un-rdd-partir-d-un-objet-scala-pour-tester-mon-algo-">Cr&#xE9;er un RDD &#xE0; partir d&apos;un objet scala (pour tester mon algo)</h1><pre><code class="language-scala">object MyApp extends App {
  val sc = SparkUtils.getSC(&quot;CountByValue&quot;)
  
  val list = List(&quot;value1&quot;, &quot;value2&quot;, &quot;value3&quot;, &quot;value4&quot;, &quot;value4&quot;)
  
  val rdd: RDD[String] = sc.parallelize(list)
}</code></pre><p>Le <code>parallelize</code> cr&#xE9;e un RDD qui sera distribu&#xE9; dans Spark (si vous lancez le job sur un cluster). Bien s&#xFB;r en g&#xE9;n&#xE9;ral on travaille sur de tr&#xE8;s grosses quantit&#xE9; de donn&#xE9;es que l&apos;on charge ou re&#xE7;oit d&apos;une source de donn&#xE9;es externes &#xE0; l&apos;application.</p><p>Parrallelize est tr&#xE8;s utile pendant le d&#xE9;veloppement pour tester notre algo dans une session &quot;local&quot;.</p><h1 id="charger-des-donn-es-partir-d-un-fichier">Charger des donn&#xE9;es &#xE0; partir d&apos;un fichier</h1><pre><code class="language-scala">object WordCountReduceByKey extends App {
  val sc = SparkUtils.getSC(this.getClass.getName)
  val lines: RDD[String] = sc.textFile(&quot;hdfs://spark/my-data/ddh.txt&quot;)
  // It could have been &quot;file://spark/my-data/ddh.txt&quot;
  
  println(lines.count)
}
</code></pre><p><em><em>Note 1</em></em> : le fichier texte est en g&#xE9;n&#xE9;ral plac&#xE9; sur un stockage distribu&#xE9;, type Hadoop HDFS</p><p><em>Note 2</em> : c&apos;est un super outil pour le d&#xE9;veloppement pour charger des donn&#xE9;es d&apos;un fichier local aussi</p><h1 id="countbyvalue">countByValue</h1><pre><code class="language-scala">object MyApp extends App {
  val sc = SparkUtils.getSC(&quot;CountByValue&quot;)
  
  val list = List(&quot;value1&quot;, &quot;value2&quot;, &quot;value3&quot;, &quot;value4&quot;, &quot;value4&quot;)
  val rdd = sc.parallelize(list)
  
  val countedByValue = rdd.countByValue
  countedByValue.print()
  
}</code></pre><p><em>Note 1 </em>: <code>take</code> et <code>collect</code> sont de tr&#xE8;s bons outils pour le d&#xE9;veloppement, et sont moins souvent utilis&#xE9;s en production (&#xE7;a d&#xE9;pend des cas) car ils ram&#xE8;nent tous les &#xE9;l&#xE9;ments du RDD dans le process courant. Si la collection est trop grosse Spark ne pourra pas allouer assez de m&#xE9;moire.</p><p><em>Note 2</em> : On pr&#xE9;f&#xE8;re en g&#xE9;n&#xE9;ral utiliser <code>reduceByValue</code></p><h1 id="pairrdds">PairRDDs</h1><p>Un PairRDD est un RDD &#xA0;de paires. Certaines fonctions sont propos&#xE9;es pour la manipulation des paires qui va vous simplifier la vie et am&#xE9;liorer les performances des traitements.</p><pre><code class="language-scala">object MyApp extends App {
  val sc.SparkUtils.getSC(this.getClass.getName)
  
  val lines = sc.textFile(&quot;/path/to/my/file.txt&quot;)
  val words: RDD[String] = lines.flatMap( l =&gt; l.split(&quot; &quot;))
  
  val pairRDD: RDD[(String, Int)] = words.map((word: String) =&gt; (word, 1))
  
  pairRDD.collect.foreach(println)
}
</code></pre><h1 id="groupbykey">groupByKey</h1><pre><code>object MyApp extends App {
  val sc.SparkUtils.getSC(this.getClass.getName)
  
  val lines = sc.textFile(&quot;/path/to/my/file.txt&quot;)
  val words: RDD[String] = lines.flatMap( l =&gt; l.split(&quot; &quot;))
  val pairs: RDD[(String, Int)] = words.map((word: String) =&gt; (word, 1))
  
  val groupedByKey = pairs.groupByKey()
  
  groupedByKey.collect.foreach(println)
}

// --- output ---
// [...]
// (priv&#xE9;,,CompactBuffer(1))
// (inqui&#xE9;t&#xE9;,CompactBuffer(1))
// (ou,CompactBuffer(1, 1, 1, 1, 1))
// (consentir,CompactBuffer(1))
// [...]</code></pre><p><em>Note </em>: On pr&#xE9;f&#xE8;re en g&#xE9;n&#xE9;ral <code>reduceByKey</code> qui r&#xE9;duit le volume de donn&#xE9;es &#xE9;chang&#xE9;es pendant le shuffle.</p><h1 id="reducebykey">reduceByKey</h1><pre><code class="language-scala">object MyApp extends App {
  val sc = SparkUtils.getSC(this.getClass.getName)
  val words = List(&quot;value1&quot;, &quot;value2&quot;, &quot;value3&quot;, &quot;value4&quot;, &quot;value4&quot;)
  val wordsPairs = words.map(word =&gt; (word.toLowerCase, 1))
  
  val wordCounts = wordsPairs.reduceByKey((c1: Int, c2: Int) =&gt; c1 + c2)
    .sortByKey()
    
  wordCounts.collect.foreach(println)
}

// --- output ---
// [...]
// (ordres,1)
// (express&#xE9;ment.,1)
// (priv&#xE9;,,1)
// (inqui&#xE9;t&#xE9;,1)
// [...]</code></pre><h1 id="map-filter-sur-rdd">map/filter sur RDD</h1><p>Si vous &#xEA;tes familier avec map et filter dans n&apos;importe quel langage, par exemple avec les collections Java ou Scala, c&apos;est essentiellement la m&#xEA;me chose d&apos;un point de vu programmatique. Prenons un exemple simple, qu&apos;on ne devrait pas trouver un production, mais qui donne une id&#xE9;e:</p><ul><li>Nous chargeons un fichier csv de liste d&apos;a&#xE9;roports et on split les colonnes sur &quot;,&quot;</li><li>Nous allons filtrer les a&#xE9;roports qui ont une altitude sup&#xE9;rieure &#xE0; 1500m et ne retourner que le nom et altitude des a&#xE9;roports en question.</li></ul><p>Vous noterez que comme on ne charge pas le csv tr&#xE8;s proprement en consid&#xE9;rant les chaines de caract&#xE8;res incluant le s&#xE9;parateur, etc... On filtrera la premi&#xE8;re ligne en &#xE9;liminant la cha&#xEE;ne qui commence par Airport (pas propre, mais suffisant pour notre exemple).</p><p>D&apos;autre part, le nom de l&apos;a&#xE9;roport est la premi&#xE8;re colonne de notre fichier csv et l&apos;altitude est &#xE0; la colonne 4</p><pre><code class="language-scala">object AirportsByAltitude extends App {

  val sc = SparkUtils.getSC(this.getClass.getName)

  val maxAltitude = 1500
  val airports = sc.textFile(raw&quot;C:\MyData\spark\data\airports.dat.txt&quot;)
    .filter(l =&gt; !l.startsWith(&quot;Airport&quot;)) // pas propre

    val data1 = airports
      .map(l =&gt; {
        val s = l.split(&quot;,&quot;)
        val airportName = s(1)
        val altitude = s(4)
        (airportName, altitude.toDouble)
      })

  val data2 = data1.filter(t =&gt; t._2 &gt; maxAltitude)

  data2.take(2).foreach(println)

  println(&quot;airpots above 1500m: &quot; + data2.count())
}
</code></pre><p><em>Note </em>: faire les filtres sur les donn&#xE9;es le plus t&#xF4;t possible pour r&#xE9;duire la taille des donn&#xE9;es lors des op&#xE9;rations lourdes, surtout lorsqu&apos;un shuffle va &#xEA;tre n&#xE9;cessaire. Les donn&#xE9;es filtr&#xE9;es ne sont jamais ajout&#xE9;es dans le RDD.</p><h1 id="sauvegarder-un-rdd">Sauvegarder un RDD</h1><p>On peut sauvegarder un RDD dans un fichier text (qui peut-&#xEA;tre distribu&#xE9; en &#xE9;tant sauvegarder sur HDFS par exemple) ou dans d&apos;autres format &#xE9;ventuellement plus efficace comme les s&#xE9;quences files</p><pre><code class="language-scala">[...]

  // On parse un fichier de log apache par exemple
  val logParserRegex = raw&quot;([^\:]+)[^\/]+([^\s\?]+).*&quot;.r
  def matchLog(logLine: String): (String, String) = logLine match {
    case logParserRegex(date, url) =&gt; (date, url)
    case _ =&gt; (&quot;&quot;, &quot;&quot;)
  }
  val tuples = logs.map(matchLog).distinct

  tuples.saveAsTextFile(&quot;hdfs://mon/chemin/tuples.txt&quot;)
  
[...]</code></pre><p>Il pourra &#xEA;tre recharg&#xE9; plus tard avec :</p><pre><code class="language-scala">[...]
  val sc = SparkUtils.getSC(this.getClass.getName)
  val logs = sc.textFile(&quot;hdfs://mon/chemin/tuples.txt&quot;)

[...]</code></pre><p><em>Note </em>: selon le besoin il est utile d&apos;utiliser un format sp&#xE9;cifique pour sauvegarder le RDD, par exemple un format de fichier colonnaire permet de pouvoir acc&#xE9;der de fa&#xE7;on tr&#xE8;s efficace &#xE0; un colonne enti&#xE8;re sans avoir avec lire la totalit&#xE9; du fichier comme c&apos;est le cas avec un fichier plat CSV.<br>Regardez SequenceFile, RC (Row Colomnar), ORC (Optimized Row Columnar) Avro, Parquet.</p><h1 id="mettre-un-rdd-en-cache-et-le-r-utiliser">Mettre un RDD en cache et le r&#xE9;utiliser</h1><p>Quand un RDD est cr&#xE9;&#xE9; vous pouvez vous en servir pour faire d&apos;autres op&#xE9;rations dessus. Si vous avez plusieurs op&#xE9;rations &#xE0; faire il est important de consid&#xE9;rer mettre en cache le RDD, c&apos;est &#xE0; dire qu&apos;il sera conserv&#xE9; en m&#xE9;moire. Sinon la cha&#xEE;ne compl&#xE8;te sera r&#xE9;ex&#xE9;cut&#xE9;e lors des op&#xE9;rations suivantes. </p><p>Par exemple :</p><pre><code>object AnalyseLogs extends App {
  val sc = SparkUtils.getSC(this.getClass.getName)
  val logs = sc.textFile(&quot;hdfs://mon/chemin/tuples.txt&quot;).cache // on met en cache
  
  val countSite1 = logs.filter(t =&gt; t._2.startsWith(&quot;https://monsite1.com/&quot;)).count

  val countSite2 = logs.filter(t =&gt; t._2.startsWith(&quot;https://monsite2.com/&quot;)).count

  println(&quot;count site1 &quot; + countSite1)
  println(&quot;count site2 &quot; + countSite2)
  
}</code></pre><p><em>Note </em>: sans l&apos;instruction <code>.cache</code> de la troisi&#xE8;me ligne, le fichier text serait pars&#xE9; 2 fois.</p><h1 id="map-et-flatmap">map et flatMap</h1><p><code>map</code> &#xA0;transforme chaque &#xE9;l&#xE9;ment de la collection en un autre &#xE9;l&#xE9;ment avec &#xE9;ventuellement un type diff&#xE9;rent</p><pre><code>map[U: ClassTag](f: T =&gt; U): RDD[U]</code></pre><p>flatMap transforme chaque &#xE9;l&#xE9;menet de la collection en un collection (&#xE9;ventuellement vide) qui est ensuite applitie pour donner une collection enti&#xE8;re</p><pre><code>flatMap[U: ClassTag](f: T =&gt; TraversableOnce[U]): RDD[U]

# exemple : tous les mots d&apos;un texte

val text = sc.textFile(&quot;/path/to/mon_text.txt&quot;)
val listOfWords = text.flatMap( line =&gt; line.split(&quot; &quot;)) // on retourne bien une collection de mots pour chaque ligne du fichier. Le resultat du flatMap est une collection de mots de tout le fichier

val wordCount = listOfWords.count</code></pre><h1 id="groupbykey-vs-reducebykey">GroupByKey vs ReduceByKey</h1><p>On compte le nomber de mots d&apos;un fichier</p><pre><code>object WordCountGroupByKey extends App {

  val sc = SparkUtils.getSC(this.getClass.getName)
  val lines = sc.textFile(&quot;/path/to/my_big_text.txt&quot;)
  val words = lines.flatMap( l =&gt; l.split(&quot; &quot;))
    .map(w =&gt; (w, 1) // on cr&#xE9;e un tuple2 dans la cl&#xE9; est le mot
    .cache

  # groupBy
  val groupByCount = words.groupByKey()
  groupByCount.take(10).foreach(println)
  
  # reduceByKey
  val reduceByKeyCount = pairs.reduceByKey((count1, count2) =&gt; count1 + count2 )
  // pour chaque cl&#xE9;, on fait un reduce de la valeur
  reduceByKeyCount.collect.foreach(println)
}</code></pre><p>On obtient le m&#xEA;me r&#xE9;sultat dans les deux cas, mais le reduceByKey est pr&#xE9;f&#xE9;r&#xE9; car le r&#xE9;duce est d&apos;abord fait par partition (sans shuffle / sans &#xE9;change r&#xE9;seau) alors que le reduce qui est fait par le groupBy et groupByKey est fait dans un reduce job, donc toutes les donn&#xE9;es de la collection sont &#xE9;chang&#xE9;es via un shuffle</p><h1 id="fusionner-deux-rdds">Fusionner deux RDDs</h1><pre><code class="language-scala">  val rdd1: RDD[Int] = sc.parallelize(List(1,2,3))
  val rdd2: RDD[Int] = sc.parallelize(List(4,5,6))

  val merged = rdd1.union(rdd2)
  merged.collect.foreach(println)</code></pre><h1 id="r-duction-des-logs-apache-spark">R&#xE9;duction des logs Apache Spark</h1><p>Utiliser la configuration de votre logger ou bien en d&#xE9;veloppement faites :</p><pre><code class="language-scala">Logger.getLogger(&quot;org&quot;).setLevel(Level.ERROR)
</code></pre><h1 id="ne-pas-faire-a-">Ne pas faire &#xE7;a!</h1><p>Pourquoi cela ne fonctionne-t-il pas en Prod ?</p><pre><code class="language-scala">let monRDD = ... // on charge un RDD
monRdd.foreach(println)</code></pre><p>En fait le code compile, et j&apos;arrive m&#xEA;me &#xE0; l&apos;ex&#xE9;cuter sur mon cluster, mais les println sont ex&#xE9;cut&#xE9;s sur les workers dans spark, dans un sous job. Je ne vais pas les voir appara&#xEE;tre dans le driver (mon application spark).</p><p>Ce qu&apos;il faut faire si on veut r&#xE9;cup&#xE9;rer un r&#xE9;sultat dans notre driver :</p><pre><code class="language-scala">let monRDD = ... // on charge un RDD
monRdd.collect.foreach(println)</code></pre><p>Le <code>collect</code> est une action finale qui ram&#xE8;ne tous les &#xE9;l&#xE9;ments du RDD dans une collection scala dans la JVM courante</p><h1 id="bien-pour-le-d-veloppement-et-prod">Bien pour le d&#xE9;veloppement et prod</h1><ul><li>rdd.<strong>take</strong>(num: Int): Array[T]<br>Pour r&#xE9;cup&#xE9;rer des &#xE9;l&#xE9;ments dans une collection du driver</li><li>rdd.<strong>first</strong>()<br>Pour r&#xE9;cup&#xE9;rer le premier &#xE9;l&#xE9;ment de la collection</li><li>rdd.<strong>top</strong>(num: Int): Array[T]<br>par d&#xE9;faut, comme take mais apr&#xE8;s classement de la collection avec un &quot;ordering&quot; implcit<br><br>On peut surcharger le triage comme suit :</li></ul><pre><code class="language-scala">  val customOrdering = new Ordering[Int] {
    override def compare(a: Int, b: Int) = {
      b - a
    }
  }
  rdd.top(10)(customOrdering).foreach(println)</code></pre><ul><li>rdd.<strong>sample</strong>(<br> &#xA0; &#xA0;withReplacement: Boolean, // accepte-t-on plusieurs fois le m&#xEA;me &#xE9;l&#xE9;ment<br> &#xA0; &#xA0;fraction: Double, // fraction de la collection<br> &#xA0; &#xA0;seed: Long = Utils.<em>random</em>.nextLong // initialization du g&#xE9;n&#xE9;rateur al&#xE9;atoire<br>): RDD[T] = { // note on re&#xE7;oit un RDD</li><li>un peu avec le m&#xEA;me principe, mais pour pour r&#xE9;cup&#xE9;rer en local dans le driver<br>rdd.<strong>takeSample</strong>(<br> &#xA0; &#xA0;withReplacement: Boolean,<br> &#xA0; &#xA0;num: Int,<br> &#xA0; &#xA0;seed: Long = Utils.<em>random</em>.nextLong): Array[T] // on re&#xE7;oit bien un Array, qui est une instance de collection dans la JVM du driver</li><li><strong>	</strong>rdd.<strong>randomSplit</strong>(<br> &#xA0; &#xA0;weights: Array[Double],<br> &#xA0; &#xA0;seed: Long = Utils.<em>random</em>.nextLong<br>) : Array[RDD[T]]</li></ul><h1 id="vocabulaire">Vocabulaire</h1><ul><li>RDD : Resilient Distributed Dataset</li><li>RC : Row Colomnar</li><li>ORC : Optimized Row Columnar</li></ul>]]></content:encoded></item><item><title><![CDATA[Code snippet : comment récupérer les paramètres des nombres décimaux sous  windows en Java]]></title><description><![CDATA[Bout de code pour lire les paramètres d’affichage avancés du poste windows en Java.]]></description><link>https://blog.goovy.io/code-snippet-java-windows-nombres/</link><guid isPermaLink="false">5d5bea884707bf0001e06cb1</guid><category><![CDATA[développement]]></category><category><![CDATA[java]]></category><dc:creator><![CDATA[Gerald Colin & Denis Garcia]]></dc:creator><pubDate>Sun, 18 Aug 2019 12:41:00 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2019/08/caracs.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2019/08/caracs.jpg" alt="Code snippet : comment r&#xE9;cup&#xE9;rer les param&#xE8;tres des nombres d&#xE9;cimaux sous  windows en Java"><p>Ceci est un bout de code qui permet de lire les pr&#xE9;f&#xE9;rences du poste utilisateur sous Windows et de trouver les param&#xE8;tres d&apos;affichage des nombres d&#xE9;cimaux.</p><p>&#xC9;videmment, cela ne fonctionne que sous windows.</p><p>Listons d&#x2019;abord les propri&#xE9;t&#xE9;s syst&#xE8;mes disponibles par d&#xE9;faut :</p><pre><code class="language-Java">System.getProperties().forEach(
	(o, o2) -&gt; System.out.printf(&quot;Prop : %s / %s\n&quot;, o, o2)
);</code></pre><p>et vous obtenez :</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.goovy.io/content/images/2019/08/image.png" class="kg-image" alt="Code snippet : comment r&#xE9;cup&#xE9;rer les param&#xE8;tres des nombres d&#xE9;cimaux sous  windows en Java" loading="lazy"><figcaption>Liste des propri&#xE9;t&#xE9;s syst&#xE8;me</figcaption></figure><p>Nous voyons que nous acc&#xE9;dons facilement &#xE0; la propri&#xE9;t&#xE9; <code>user.language</code>. Seulement cette propri&#xE9;t&#xE9; donne la langue du poste mais sous windows, il est possible de changer des param&#xE8;tres avanc&#xE9;s tel que les formats des dates ou des nombres d&#xE9;cimaux :</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/08/image-2.png" class="kg-image" alt="Code snippet : comment r&#xE9;cup&#xE9;rer les param&#xE8;tres des nombres d&#xE9;cimaux sous  windows en Java" loading="lazy"></figure><p>Si on veut afficher des nombres d&#xE9;cimaux, nous pouvons simplement utiliser les locales default et nous obtenons:</p><pre><code class="language-Java">System.out.println(&quot;Locale: &quot; + Locale.getDefault().toString());
NumberFormat nf = NumberFormat.getNumberInstance(Locale.getDefault());
DecimalFormat df = (DecimalFormat)nf;
System.out.println(&quot;Locale default : &quot; + df.format(123456.789));</code></pre><p>et on obtient un r&#xE9;sultat qui ne prend pas en compte notre param&#xE9;trage du poste :</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/08/image-3.png" class="kg-image" alt="Code snippet : comment r&#xE9;cup&#xE9;rer les param&#xE8;tres des nombres d&#xE9;cimaux sous  windows en Java" loading="lazy"></figure><p>Du coup si vous souhaitez exporter des nombres d&#xE9;cimaux selon la pr&#xE9;f&#xE9;rence de l&#x2019;utilisateur, un moyen de faire est d&apos;aller lire ces donn&#xE9;es via le Kernel32 (cette m&#xE9;thode est tr&#xE8;s fortement inspir&#xE9;e de ce qui est <a href="https://github.com/JetBrains/intellij-community/blob/master/platform/util/src/com/intellij/util/text/DateFormatUtil.java?ref=blog.goovy.io">fait dans IntelliJ</a>)</p><pre><code class="language-java">import com.sun.jna.Native;

(...)

private interface Kernel32 extends com.sun.jna.win32.StdCallLibrary {
        int LOCALE_SDECIMAL = 0x0000000E;
        int LOCALE_STHOUSAND = 0x0000000F;

        int GetLocaleInfoEx(String localeName, int lcType, char[] lcData, int dataSize);
        int GetLastError();
}

static void getNbFormat() {
        Kernel32 kernel32 = (Kernel32) Native.loadLibrary(&quot;Kernel32&quot;, Kernel32.class);
        int bufferSize = 128, rv;
        char[] buffer = new char[bufferSize];

        rv = kernel32.GetLocaleInfoEx(null, Kernel32.LOCALE_SDECIMAL, buffer, bufferSize);
        if (rv &lt; 2) throw new IllegalStateException(&quot;GetLocaleInfoEx: &quot; + kernel32.GetLastError());
        System.out.println(&quot;Decimal sep: &quot; + new String(buffer, 0, rv - 1));

        rv = kernel32.GetLocaleInfoEx(null, Kernel32.LOCALE_STHOUSAND, buffer, bufferSize);
        if (rv &lt; 2) throw new IllegalStateException(&quot;GetLocaleInfoEx: &quot; + kernel32.GetLastError());
        System.out.println(&quot;Thousands sep: &quot; + new String(buffer, 0, rv - 1));
}</code></pre><p>Il faut surement rajouter dans votre pom.xml :</p><pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;com.sun.jna&lt;/groupId&gt;
    &lt;artifactId&gt;jna&lt;/artifactId&gt;
    &lt;version&gt;3.0.9&lt;/version&gt;
&lt;/dependency&gt;</code></pre><p>et vous obtenez</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/08/image-4.png" class="kg-image" alt="Code snippet : comment r&#xE9;cup&#xE9;rer les param&#xE8;tres des nombres d&#xE9;cimaux sous  windows en Java" loading="lazy"></figure><p>&#xE0; partir desquelles vous pouvez red&#xE9;finir le pattern d&apos;affichage de vos nombres d&#xE9;cimaux qui prendra bien en compte les param&#xE8;tres du poste utilisateur (comme le fait Excel par exemple).</p><h3 id="ressources-">Ressources :</h3><p><strong> - </strong>Cover photo by <a href="https://unsplash.com/@brunus?utm_medium=referral&amp;utm_campaign=photographer-credit&amp;utm_content=creditBadge&amp;ref=blog.goovy.io">Bruno Martins</a></p>]]></content:encoded></item><item><title><![CDATA[Utiliser les APIs Matrix en Go]]></title><description><![CDATA[Voici un exemple d'utilisation des APIs de Matrix pour se connecter à un salon de riot et intéragir avec celui-ci et créer son bot...]]></description><link>https://blog.goovy.io/utiliser-les-apis-matrix-en-go/</link><guid isPermaLink="false">5d19ee084707bf0001e06b9e</guid><category><![CDATA[développement]]></category><category><![CDATA[tutoriaux]]></category><category><![CDATA[go]]></category><dc:creator><![CDATA[Gerald Colin & Denis Garcia]]></dc:creator><pubDate>Thu, 27 Jun 2019 11:26:00 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2019/07/world.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2019/07/world.jpg" alt="Utiliser les APIs Matrix en Go"><p>Nous allons pr&#xE9;senter une d&#xE9;couverte des APIs Matrix pour communiquer dans un salon.</p><p><a href="https://matrix.org/?ref=blog.goovy.io">Matrix</a> est un syst&#xE8;me de communication distribu&#xE9; et crypt&#xE9;. Il vient d&apos;&#xEA;tre impl&#xE9;ment&#xE9; par l&apos;&#xE9;tat fran&#xE7;ais comme syst&#xE8;me de messagerie interne. Le server principal (Synapse) peut-etre auto-h&#xE9;berg&#xE9; ou bien publique. Il existe de nombreux clients et le plus connu est <a href="https://riot.im/app?ref=blog.goovy.io">riot</a>, sur lequel vous pouvez cr&#xE9;er un compte. Cette plateforme est ouverte, modulaire et pens&#xE9;e comme un socle pour &#xE9;ventuellement bien plus que de la simple communication de messages (ils ont l&apos;intention de r&#xE9;inventer le web, rien que &#xE7;a).</p><h1 id="pr-requis">Pr&#xE9;requis</h1><p>Il faut un compte sur un server Matrix. Allez sur <a href="https://riot.im/app/?ref=blog.goovy.io#/welcome">riot</a> et vous pouvez cr&#xE9;er un compte gratuit.</p><p>Dans le cadre de cet article, nous avons cr&#xE9;&#xE9; un salon d&#xE9;di&#xE9;.</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/07/image.png" class="kg-image" alt="Utiliser les APIs Matrix en Go" loading="lazy"></figure><p>Et il faut ensuite Go &gt; 1.11 install&#xE9; pour pouvoir utiliser les modules.</p><h1 id="le-code">Le code</h1><p>Vous pouvez simplement cloner le repo du projet depuis mon compte <a href="https://github.com/GeCol1/matrix-client?ref=blog.goovy.io">github</a>. Vous devez ajouter un fichier <code>riot.pass</code> contenant le mot de passe de votre compte (&#xE0; &#xE9;viter de le publier sur un compte git publique...).</p><p>Pour simplifier le code, plusieurs variables sont directement dans le code:</p><ol><li>La base_url : est l&apos;addresse du serveur Matrix sur lequel vous vous connectez. Attention, ici le seveur est <code>matrix.org</code> et le client est riot. </li></ol><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/07/image-1.png" class="kg-image" alt="Utiliser les APIs Matrix en Go" loading="lazy"></figure><p>2. L&apos;id du salon (ou room en anglais) est donc l&apos;identifiant du salon que vous retrouvez &#xE0; la fin de l&apos;adresse ci-dessus.</p><h3 id="1-la-connexion-au-server">1) La connexion au server</h3><p>Le serveur Matrix met &#xE0; disposition des API avec leur <a href="https://matrix.org/docs/api/client-server/?ref=blog.goovy.io">documentation</a> via swagger. Tous les &#xE9;changes se feront donc en API REST Json avec un &#xE9;change de token JWT que nous r&#xE9;cup&#xE9;rons &#xE0; l&apos;authentification.</p><p>Pour se connecter:</p><pre><code class="language-go">// nous lisons le mot de passe depuis le fichier riot.pass &#xE0; la racine du projet
pwd := getPasswordFromFile()

// nous pr&#xE9;parons une requete post avec en body un json contenant le login et mot de passse
	resp, err := resty.R().
		SetHeader(&quot;Content-Type&quot;, &quot;application/json&quot;).
		SetBody([]byte(`{
		&quot;identifier&quot;: {
		  &quot;type&quot;: &quot;m.id.user&quot;,
		  &quot;user&quot;: &quot;` + user + `&quot;
		},
		&quot;initial_device_display_name&quot;: &quot;Jungle Phone&quot;,
		&quot;password&quot;: &quot;` + pwd + `&quot;,
		&quot;type&quot;: &quot;m.login.password&quot;
	  }`)).
		// SetResult(&amp;AuthSuccess{}).
		Post(&quot;https://&quot; + BaseURL + &quot;/_matrix/client/r0/login&quot;)

	checkErr(err, &quot;Could not authenticate&quot;)
	// fmt.Println(resp, err)

// nous deserialisons la r&#xE9;ponse
	var lr loginResponse
	err = json.Unmarshal(resp.Body(), &amp;lr)
	checkErr(err, &quot;Could not decode json of authentication&quot;)

	// fmt.Println(lr.Access_token)
	return lr.AccessToken</code></pre><h3 id="2-lecture-des-10-derniers-messages-du-salon">2) Lecture des 10 derniers messages du salon</h3><p>Pour lire les messages, il faut envoyer une requete GET au serveur en passant le token et l&apos;identifiant du salon. On limite le nombre de messages directement dans la requ&#xEA;te:</p><pre><code class="language-go">// Point d&apos;entr&#xE9;e pour la r&#xE9;cup&#xE9;ration des messages
respMsg, err := resty.R().Get(&quot;https://&quot; + BaseURL + &quot;/_matrix/client/api/v1/rooms/&quot; + RoomID + &quot;/messages?access_token=&quot; + token + &quot;&amp;from=END&amp;dir=b&amp;limit=10&quot;)
checkErr(err, &quot;Could not get the messages from matrix API&quot;)
// fmt.Printf(&quot;%s\n\n---------------\n&quot;, respMsg)

// On parse le message re&#xE7;u &#xE0; l&apos;aide fastjson
var p fastjson.Parser
m, err := p.Parse(string(respMsg.Body()))
checkErr(err, &quot;Could not decode json of messages&quot;)
vals := m.GetArray(&quot;chunk&quot;)
for _, val := range vals {
fmt.Printf(&quot;  (%s) &gt; %s\n&quot;, val.GetStringBytes(&quot;sender&quot;), val.GetStringBytes(&quot;content&quot;, &quot;body&quot;))
}</code></pre><p>et en l&apos;executant <code>go run main.go</code> on obtient les messages (en fait ici il n&apos;y a en a eu qu&apos;un seul):</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/07/image-2.png" class="kg-image" alt="Utiliser les APIs Matrix en Go" loading="lazy"></figure><h3 id="3-envoie-de-messages-dans-le-salon">3) Envoie de messages dans le salon</h3><p>Comme c&apos;est un peu vide, nous allons envoyer des messages dans le salon. On reprend la doc des APIs et cette fois on envoie une requete en POST avec le contenu du message dans le body:</p><pre><code class="language-go">respMsg, err := resty.R().
		SetHeader(&quot;Content-Type&quot;, &quot;application/json&quot;).
		SetBody([]byte(`{&quot;msgtype&quot;:&quot;m.text&quot;, &quot;body&quot;:&quot;` + msg + `&quot;}`)).
		Post(&quot;https://&quot; + BaseURL + &quot;/_matrix/client/r0/rooms/&quot; + RoomID + &quot;/send/m.room.message?access_token=&quot; + token)

	checkErr(err, &quot;Could not post the message&quot;)
	fmt.Println(respMsg)</code></pre><p>et nous ajoutons les appels dans le main</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/07/image-3.png" class="kg-image" alt="Utiliser les APIs Matrix en Go" loading="lazy"></figure><p>Voici 2 exemples d&apos;utilisation des APIs matrix en GO mais n&apos;h&#xE9;sitez pas &#xE0; <a href="https://matrix.org/docs/api/client-server/?ref=blog.goovy.io">consulter la doc swaggers</a> pour d&#xE9;couvrir toutes les autres possibilit&#xE9;s.</p>]]></content:encoded></item><item><title><![CDATA[Resources Go]]></title><description><![CDATA[Une liste de resources, articles, librairies pour le développement en Go.]]></description><link>https://blog.goovy.io/resources-go/</link><guid isPermaLink="false">5cfe402d4707bf0001e06af8</guid><category><![CDATA[trucs et astuces]]></category><dc:creator><![CDATA[Gerald Colin & Denis Garcia]]></dc:creator><pubDate>Fri, 14 Jun 2019 00:28:27 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2019/06/disks.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2019/06/disks.jpg" alt="Resources Go"><p>Une liste de resources, articles, librairies pour le d&#xE9;veloppement en <a href="https://golang.org/?ref=blog.goovy.io">Go</a>. Nous les avons test&#xE9;s et impl&#xE9;ment&#xE9;s et avons valid&#xE9; leur int&#xE9;r&#xEA;t. Ce post sera mis &#xE0; jour au fur et &#xE0; mesure.</p><p>[NDLR] Quand on aborde une probl&#xE9;matique (parsing de json, s&#xE9;curit&#xE9;...) il y a plusieurs approches possibles qui d&#xE9;pendent du contexte : </p><ul><li>vous codez directement la solution, si possible en s&apos;appuyant sur des patterns de programmation : cela &#xE0; l&apos;avantage de garder votre code concis et minime en ne r&#xE9;pondant qu&apos;&#xE0; votre probl&#xE9;matique et vous facilite les optimisations &#xE9;ventuelles; &#xE0; contrario vous r&#xE9;inventez la roue et pour des algos plus complexes peut &#xEA;tre moins optimal qu&apos;une lib d&#xE9;velopp&#xE9;e par une &#xE9;quipe le ferait.</li><li>vous utilisez une librairie externe : cela &#xE0; l&apos;avantage de traiter plus de cas d&apos;usages en une fois (quand la lib a &#xE9;t&#xE9; &#xE9;prouv&#xE9;e) mais &#xE0; l&apos;inconv&#xE9;nient d&apos;ajouter des d&#xE9;pendances et augmente la taille de vos projets.</li></ul><hr><p>Lib pour g&#xE9;rer les param&#xE8;tres de ligne de commande mais oblige &#xE0; suivre leur paradigme. De nombreux gros projets l&apos;utilisent (docker, kubernetes, istio...)<br>- <a href="https://github.com/spf13/cobra?ref=blog.goovy.io">https://github.com/spf13/cobra</a></p><p>Lib pour charger facilement des param&#xE8;tres d&apos;un fichier de config<br>- <a href="https://github.com/joho/godotenv?ref=blog.goovy.io">https://github.com/joho/godotenv</a></p><p>Lib pour traiter le JSon plus facilement et sans cr&#xE9;er de struc:<br>- <a href="https://github.com/valyala/fastjson?ref=blog.goovy.io">https://github.com/valyala/fastjson</a></p><p>Lib pour consommer des APIs REST, resty :<br>- <a href="https://github.com/go-resty/resty?ref=blog.goovy.io">https://github.com/go-resty/resty</a></p><p>Lib qui implemente les principaux patterns de messaging (pub/sub, req/rep, push/pull...):<br>- <a href="https://github.com/nanomsg/mangos?ref=blog.goovy.io">https://github.com/nanomsg/mangos</a></p><p>Lib pour impl&#xE9;menter un r&#xE9;seau P2P:<br>- <a href="https://github.com/libp2p/go-libp2p?ref=blog.goovy.io">https://github.com/libp2p/go-libp2p</a></p><p>Lib pour construire des microservices (ou de jolis monoliths) :<br>- <a href="https://gokit.io/?ref=blog.goovy.io">https://gokit.io/</a></p><p>Framework de site web en go :<br>- <a href="https://gobuffalo.io/fr?ref=blog.goovy.io">https://gobuffalo.io/fr</a></p><p>Framework de tests de validation :<br>- <a href="https://agouti.org/?ref=blog.goovy.io">https://agouti.org/</a></p>]]></content:encoded></item><item><title><![CDATA[Les liens de Mai 2019]]></title><description><![CDATA[Un partage de quelques liens qui nous semblent interressant.]]></description><link>https://blog.goovy.io/les-liens-de-mars-2019/</link><guid isPermaLink="false">5c5c31005f815b0001676656</guid><category><![CDATA[liens]]></category><dc:creator><![CDATA[Gerald Colin & Denis Garcia]]></dc:creator><pubDate>Fri, 31 May 2019 08:09:00 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2019/05/spider06.jpg" medium="image"/><content:encoded><![CDATA[<ul><li>Les vieilles commandes sous Linux (ou ailleurs) ne sont pas toujours les plus adapt&#xE9;es et des progr&#xE8;s ont pu &#xEA;tre apport&#xE9;s depuis. Un exemple avec la commande <code>mount</code> qui peut &#xEA;tre remplac&#xE9; par <code>findmnt</code> comme l&apos;explique <a href="https://linuxhandbook.com/findmnt-command-guide/?ref=blog.goovy.io">cet article en anglais</a>.</li><li>Encore une API pour les logs en Java... mais cette fois il s&apos;agit d&apos;une tentative interne &#xE0; Google pour r&#xE9;gler leur probl&#xE8;mes de &quot;trop d&apos;APIs tuent les APIs&quot;. Ils utilisent d&#xE9;sormais <a href="https://github.com/google/flogger?ref=blog.goovy.io">Flogger</a>.</li><li>Google a annonc&#xE9; lors de son &#xE9;v&#xE9;nement annuel la possibilit&#xE9; de d&#xE9;velopper des apps pour desktop avec <a href="https://developers.googleblog.com/2019/05/Flutter-io19.html?ref=blog.goovy.io">Flutter</a>. Jusqu&apos;&#xE0; maintenant, Flutter (qui est un framework de d&#xE9;veloppement d&apos;apps mobile en langage Dart) se limitait au mobile et venait concurrencer le framework <a href="https://ionicframework.com/?ref=blog.goovy.io">ionic</a>. L&apos;int&#xE9;ret de ses plateformes est d&apos;avoir une seule source de code pour les diff&#xE9;rentes plateformes (iOS et Android principalement). L&apos;inconv&#xE9;nient est que les APIs sp&#xE9;cifiques aux OS et devices concern&#xE9;s ne sont pas toujours bien support&#xE9;s et que les perfs ne sont pas aussi bonne que du natif. Mais ces 2 points sont &#xE0; relativiser par rapport au d&#xE9;veloppement voulu. Donc Google vient d&apos;annoncer que d&#xE9;sormais son framework supportait le web desktop ce qui augmente la r&#xE9;utilisation du code. A voir o&#xF9; cela m&#xE8;ne... au <a href="https://killedbygoogle.com/?ref=blog.goovy.io">cimeti&#xE8;re des initiatives Google</a> ou bien un v&#xE9;ritable produit soutenu dans la dur&#xE9;e?</li><li><a href="https://redisson.org/?ref=blog.goovy.io">Redisson</a> est un client Java pour <a href="https://redis.io/?ref=blog.goovy.io">redis</a>. Redis est une base/cache cl&#xE9;/valeur facile &#xE0; mettre en oeuvre mais lisez bien la doc. <br>Un extrait qui devrait &#xEA;tre &#xE0; mon sens affich&#xE9; en gras sur la premi&#xE8;re page: &quot;Redis is designed to be accessed by trusted clients inside trusted environments. This means that usually it is not a good idea to expose the Redis instance directly to the internet or, in general, to an environment where untrusted clients can directly access the Redis TCP port or UNIX socket.&quot; &#xA0;et tous les d&#xE9;veloppeurs press&#xE9;s de ne pas laisser cela en libre acc&#xE8;s...<br>En tout cas <a href="https://github.com/redisson/redisson?ref=blog.goovy.io">Redisson </a>peut vous faciliter la vie si vous avez &#xE0; faire de la distribution.</li><li><a href="https://blog.angular.io/version-8-of-angular-smaller-bundles-cli-apis-and-alignment-with-the-ecosystem-af0261112a27?ref=blog.goovy.io">La version 8 d&apos;Angular est sortie</a>. Avec une release tous les 6 mois, il peut devenir difficile de suivre le rythme. A noter dans cette nouvelle version un chargement diff&#xE9;rentiel qui permet de diminuer la taille du JS charg&#xE9; par le navigateur en fonction de sa maturit&#xE9;. C&apos;est un pattern int&#xE9;ressant pour d&#xE9;precier au fur et &#xE0; mesure des vieilles parties de codes r&#xE9;serv&#xE9;es aux &quot;vieux&quot;. Vous parquez ces vieilles fonctionnalit&#xE9;s n&#xE9;cessaires pour la compatibilit&#xE9; et vous les chargez uniquement si vous d&#xE9;tectez qu&apos;elles sont n&#xE9;cessaires.</li></ul>]]></content:encoded></item><item><title><![CDATA[Un site de documentation sous Jekyll, comme github pages]]></title><description><![CDATA[Un site de documentation auto-hébergé, basé sur Jekyll avec un exemple de workflow utilisant des git-hooks serveur.]]></description><link>https://blog.goovy.io/un-site-de-documentation-jekyll-comme-github/</link><guid isPermaLink="false">5cd979cf4707bf0001e0685f</guid><category><![CDATA[docker]]></category><category><![CDATA[git]]></category><category><![CDATA[tutoriaux]]></category><dc:creator><![CDATA[Gerald Colin & Denis Garcia]]></dc:creator><pubDate>Thu, 16 May 2019 08:27:06 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2019/05/lib01.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2019/05/lib01.jpg" alt="Un site de documentation sous Jekyll, comme github pages"><p> Vous aimez bien les <a href="https://pages.github.com/?ref=blog.goovy.io">pages github</a> qui permettent d&apos;avoir des sites static (ie. sans techno server) pour de la pr&#xE9;sentation de projet ou de la documentation. Seulement vous souhaitez les garder sur un r&#xE9;seaux priv&#xE9;. Nous vous pr&#xE9;sentons ici une solution en installant <a href="https://jekyllrb.com/?ref=blog.goovy.io">Jekyll</a>, un th&#xE8;me sympa, la g&#xE9;n&#xE9;ration de la doc en PDF et un exemple de workflow pour la mise &#xE0; jour de la doc.</p><h2 id="1-installation-de-jekyll">1/ Installation de Jekyll</h2><p> Jekyll est un moteur de site statique qui permet d&apos;&#xE9;crire ses pages en <a href="https://daringfireball.net/projects/markdown/?ref=blog.goovy.io">Markdown</a>. C&apos;est aussi le moteur sous le capot des github pages.</p><p> Sur votre serveur de publication, nous construisons une image Docker &#xE0; partir de celle disponible <a href="https://hub.docker.com/r/jekyll/jekyll/?ref=blog.goovy.io">sur le hub</a> et ce afin d&apos;y ajouter quelques libs :</p><pre><code>FROM jekyll/jekyll:3.8.5

RUN echo &quot;Install extra libs&quot; \
&amp;&amp; apk --update add build-base ruby-dev \
&amp;&amp; gem install bundler \
&amp;&amp; gem install jekyll-paginate \
&amp;&amp; gem install jekyll-gist \
&amp;&amp; gem install redcarpet</code></pre><p> Puis nous lan&#xE7;ons l&apos;image construite avec la commande <code>docker build -t goovy/jekyll:1.0 .</code> par ce script:</p><pre><code class="language-sh">docker run -d \
	    	--restart=unless-stopped \
  	    	--volume=&quot;$PWD/doc-site:/srv/jekyll&quot; \
	    	-e &quot;JEKYLL_ENV=prodcution&quot; \
	    	-p 80:4000 \
		--name doc-site \
  	    	goovy/jekyll:1.0 \
	    	jekyll serve --watch</code></pre><p>Testez votre d&#xE9;ploiement en allant sur localhost et vous devriez avoir ceci:</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/05/image.png" class="kg-image" alt="Un site de documentation sous Jekyll, comme github pages" loading="lazy"></figure><h2 id="2-utilisation-d-un-th-me-sympa">2/ Utilisation d&apos;un th&#xE8;me sympa</h2><p>Il existe plein de th&#xE8;mes mais celui de <a href="https://idratherbewriting.com/documentation-theme-jekyll/index.html?ref=blog.goovy.io">idratherbewriting.com</a> est plut&#xF4;t pas mal et complet: Une topnav pour une navigation principale, un second menu contextuel sur le c&#xF4;t&#xE9; et le contenu au centre. Il explique bien l&apos;installation donc pour faire simple:</p><ul><li>clonez (ou downloadez) le th&#xE8;me et copiez l&apos;int&#xE9;gralit&#xE9; dans le volume docker (doc-site)</li><li>supprimez les 2 fichiers Gemfile et Gemfile.lock</li><li>d&#xE9;marrez l&apos;image docker</li></ul><p>et vous devriez avoir ceci :</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/05/image-1.png" class="kg-image" alt="Un site de documentation sous Jekyll, comme github pages" loading="lazy"></figure><h2 id="3-un-exemple-de-workflow-de-travail">3/ Un exemple de workflow de travail</h2><p>Maintenant que le site de la doc est en place, comment ajouter du contenu. L&apos;id&#xE9;e est de maintenir sa doc comme son code (ie. versionn&#xE9; avec git). D&#xE8;s que la documentation est valid&#xE9;e (push dans le remote) nous souhaitons avoir le site &#xE0; jour. Le probl&#xE8;me est que le site est h&#xE9;berg&#xE9; sur un serveur, que notre doc est &#xE9;crite sur des postes de travail. C&apos;est l&#xE0; que les <a href="https://www.atlassian.com/git/tutorials/git-hooks?ref=blog.goovy.io">git hooks</a> viennent &#xE0; notre secours (un hook est un crochet appel&#xE9; par le syst&#xE8;me sous-jacent, ici git, &#xE0; des &#xE9;tapes pr&#xE9;cises).</p><p>R&#xE9;sum&#xE9; du workflow:</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/05/git-doc1.PNG" class="kg-image" alt="Un site de documentation sous Jekyll, comme github pages" loading="lazy"></figure><p> Commen&#xE7;ons par la droite du schema ci-dessus en configurant la publication automatique. Sur le server d&apos;hebergement de la doc, il faut d&apos;abord cr&#xE9;er un r&#xE9;pertoire d&#xE9;di&#xE9; pour le repo git qui contient les scripts des hooks.</p><pre><code class="language-sh">// initialize the git repo folder
$ git init --bare doc-repo.git</code></pre><p>et vous voila avec le r&#xE9;pertoire ci-dessous:</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/05/image-9.png" class="kg-image" alt="Un site de documentation sous Jekyll, comme github pages" loading="lazy"></figure><p> Les hooks sont des scripts se trouvant dans le r&#xE9;pertoire &quot;hooks&quot;... Celui qui nous int&#xE9;resse est le post-receive. Il suffit d&apos;avoir ce fichier ainsi nomm&#xE9; et apr&#xE8;s un push il sera automatiquement ex&#xE9;cut&#xE9; par git.</p><p> Nous copions le code mis &#xE0; jour dans le r&#xE9;pertoire de Jekyll et affichons un message de retour &#xE0; la personne qui a push&#xE9;.</p><p>Contenu du fichier <code>post-receive</code> (<em>remplacez les &quot;xxx&quot; par votre chemin)</em> :</p><pre><code class="language-sh">#!/bin/bash
#
# Script to copy the push received into the doc-site (production) folder

while read oldrev newrev ref
do
    if [[ $ref =~ .*/master$ ]];
    then
        echo -e &quot;\nMaster ref received.  Deploying master branch to the website in production...i\n&quot;

echo &quot;      _                                _       _           _  &quot;
echo &quot;     | |                              | |     | |         | | &quot;
echo &quot;   __| | ___   ___     _   _ _ __   __| | __ _| |_ ___  __| | &quot;
echo &quot;  / _  |/ _ \ / __|   | | | | &apos;_ \ / _  |/ _  | __/ _ \/ _  | &quot;
echo &quot; | (_| | (_) | (__    | |_| | |_) | (_| | (_| | ||  __/ (_| | &quot;
echo &quot;  \__,_|\___/ \___|    \__,_| .__/ \__,_|\__,_|\__\___|\__,_| &quot;
echo &quot;                            | |                               &quot;
echo &quot;                            |_|                               &quot;


        git --work-tree=/home/xxx/Jekyll/doc-site --git-dir=/home/xxx/Jekyll/doc-repo checkout -f
    else
        echo &quot;Ref $ref successfully received.  Doing nothing: only the master branch may be deployed on this server.&quot;
    fi
done
</code></pre><p> Cot&#xE9; client, il faut ajouter un git remote vers le serveur de publication. Ainsi, lors d&apos;un git push, il va &#xE0; la fois publier vers le repository central (un gitlag, github, bitbucket...) et vers le serveur Jekyll. </p><p>Dans votre r&#xE9;pertoire de travail, vous devez avoir une copie du th&#xE8;me pr&#xE9;c&#xE9;dent. Initialisez git et ajoutez &quot;_site&quot; (version compil&#xE9;e du site) dans le fichier <code>.gitignore</code>.<br>Ajoutez le remote vers votre repository central. Vous devriez avoir ceci:</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/05/image-4.png" class="kg-image" alt="Un site de documentation sous Jekyll, comme github pages" loading="lazy"></figure><p>Ajoutez un remote vers votre serveur de documentation (localhost dans cet exemple) en push uniquement. On veut publier sur 2 serveurs en m&#xEA;me temps mais faire un fetch que du repo central (gitlab dans l&apos;exemple).</p><p>Pour ajouter le remote:</p><pre><code class="language-sh">// ajoutez l&apos;url d&#xE9;j&#xE0; pr&#xE9;sente (bizarre mais sans cela git remplace malgr&#xE9; le --add)
# git remote set-url --add --push origin git@gitlab.com:user/doc-repo.git
// ajoutez la seconde url
# git remote set-url --add --push origin ssh://user@localhost/doc-repo.git</code></pre><p>et vous pouvez v&#xE9;rifer avec <code>git-remote -v</code> :</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/05/image-6.png" class="kg-image" alt="Un site de documentation sous Jekyll, comme github pages" loading="lazy"></figure><p>Modifiez ensuite un fichier dans votre r&#xE9;pertoire de travail (par exemple le fichier index.md), committez et pushez et vous obtiendrez ce message et le site sera &#xE9;galement &#xE0; jour !</p><figure class="kg-card kg-image-card"><img src="https://blog.goovy.io/content/images/2019/05/image-7.png" class="kg-image" alt="Un site de documentation sous Jekyll, comme github pages" loading="lazy"></figure><h2 id="4-g-n-ration-de-pdf">4/ G&#xE9;n&#xE9;ration de PDF</h2><p>Nous utilisons <a href="https://www.princexml.com/?ref=blog.goovy.io">Prince</a> qui utilise des feuilles de styles CSS pour la mise en page et qui sont d&#xE9;j&#xE0; prise en compte dans notre th&#xE8;me Jekyll.</p><p>Il y a aussi des scripts shell pour l&apos;utilisation de Prince mais nous allons les adapter pour les utiliser avec le container. Nous g&#xE9;n&#xE9;rons le pdf dans un process &#xE0; part avec une instance diff&#xE9;rente du site :</p><ol><li>pour cela nous stoppons le site</li><li>nous d&#xE9;marrons une instance d&#xE9;di&#xE9;e &#xE0; la g&#xE9;n&#xE9;ration du PDF</li><li>nous lan&#xE7;ons la g&#xE9;n&#xE9;ration du PDF</li><li>nous revenons ensuite sur le site de d&#xE9;part.</li></ol><p>L&apos;instance d&#xE9;di&#xE9;e pour le PDF se lance avec le script suivant:</p><pre><code class="language-sh">#!/bin/sh

docker run      -d \
                -v $PWD/doc-site:/srv/jekyll \
                -p 4010:4010 \
                --name doc-site-pdf \
                -e &quot;JEKYLL_ENV=production&quot; \
                goovy/jekyll:1.0 jekyll serve --config _config.yml,pdfconfigs/_config_mydoc_pdf.yml</code></pre><p>Pour la g&#xE9;n&#xE9;ration via PrinceXML, nous devons cr&#xE9;er une image docker avec ce Dockerfile</p><pre><code># Container for PrinceXML (tools to generate PDF file)
# Used with Jekyll for the documentation website
FROM ubuntu:16.04
VERSION=prince_12.5-1_ubuntu16.04_amd64.deb
RUN \
        DEBIAN_FRONTEND=noninteractive apt-get update &amp;&amp; \
        apt-get install -y wget &amp;&amp; \
        wget -q https://www.princexml.com/download/$VERSION &amp;&amp; \
        dpkg -i --force-depends $VERSION &amp;&amp; \
        apt-get install -yf

# Define default command.
CMD [&quot;prince&quot;]</code></pre><p>que nous ex&#xE9;cutons ensuite par ce script:</p><pre><code class="language-sh">#!/bin/sh

docker run      -it --rm \
                -v $PWD/doc-site:/data \
                --link doc-site-pdf:localhost \
                goovy/princexml \
                prince --javascript --input-list=/data/_site/pdfconfigs/prince-list.txt -o /data/pdf/mydoc.pdf</code></pre><p>N&apos;oubliez pas de stopper l&apos;instance d&#xE9;di&#xE9;e au PDF et de red&#xE9;marrer celle normale. Il doit &#xEA;tre possible de lancer les 2 en m&#xEA;me temps et d&apos;automatiser mieux cette partie, ce qui vous fera un bon exercice.</p>]]></content:encoded></item><item><title><![CDATA[Docker - Quick MySQL / PHPMyAdmin or AdMiner]]></title><description><![CDATA[Un fichier docker-compose pour un mysql sur votre poste de développement avec des outils d'admin.]]></description><link>https://blog.goovy.io/docker-quick-mysql-phpmyadmin-or-adminer/</link><guid isPermaLink="false">5ccfcbb6a8621f00018b0dd1</guid><category><![CDATA[docker]]></category><category><![CDATA[commandes]]></category><dc:creator><![CDATA[Denis Garcia & Gerald Colin]]></dc:creator><pubDate>Mon, 06 May 2019 06:08:49 GMT</pubDate><media:content url="https://blog.goovy.io/content/images/2019/05/command-01.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.goovy.io/content/images/2019/05/command-01.jpg" alt="Docker - Quick MySQL / PHPMyAdmin or AdMiner"><p>Voici un petit docker-compose que j&apos;utilise r&#xE9;guli&#xE8;rement en d&#xE9;veloppement pour importer des donn&#xE9;es dans un MySQL local pour faire une analyse rapide des donn&#xE9;es ou plus.</p><p>Il vous faudra pr&#xE9;alablement avoir install&#xE9; docker et docker-compose</p><p>Le docker compose cr&#xE9;e un volume persistent docker pour MySQL, ce qui permet de pouvoir red&#xE9;marrer le docker compose tout en ayant conserv&#xE9; les donn&#xE9;es.</p><p>Puis le script d&#xE9;marre 3 composants :</p><ul><li>MySQL</li><li>Adminer</li><li>PHPMyAdmin<br>mais vous devrez choisir si vous voulez utiliser AdMiner (petit utilitaire tr&#xE8;s simple pour voir la base de donn&#xE9;es et faire des requ&#xEA;tes) ou PHPMyAdmin. On vous rappelle que pour des raisons &#xE9;videntes de s&#xE9;curit&#xE9;, il ne faut pas avoir de PHPMyAdmin en prod!</li></ul><pre><code># mysql-stack.yml

version: &apos;3.7&apos;

services:

  db:
    image: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_DATABASE: testDB
      MYSQL_USER: testDB
      MYSQL_PASSWORD: testDB
      MYSQL_RANDOM_ROOT_PASSWORD: &apos;1&apos;
    volumes:
      - testDB:/var/lib/mysql
    ports:
      - 3306:3306

  adminer:
    image: adminer
    restart: always
    ports:
      - 8085:8080
      
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    restart: always
    ports:
      - 8086:80

volumes:
  testDB:</code></pre><p><strong>Pour lancer les composants</strong></p><blockquote>docker-compose -f mysql-stack.yml up -d</blockquote><p><strong>Pour arr&#xEA;ter les composants</strong></p><p>docker-compose -f mysql-stack.yml down</p><p><strong>Note 1</strong></p><p>J&apos;expose le port 3306 de MySQL localement pour pouvoir m&apos;y connecter avec mon code en d&#xE9;veloppement</p><p><strong>Note 2</strong></p><p>Lorsque vous supprimez le container, si vous voulez supprimer les donn&#xE9;es aussi n&apos;oubliez pas de supprimer le volume. Dans notre cas d&apos;exemple :</p><blockquote>docker volume rm testDB</blockquote><p>Puis pour v&#xE9;rifier</p><blockquote>docker volume ls</blockquote>]]></content:encoded></item></channel></rss>