PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

La fin de l’ère du spam de sites pornos

samedi 28 juin 2014 à 03:09

Petit billet juste pour dire que cette époque où tout mail entré dans un formulaire sur un site de cul se retrouvait immédiatement noyé dans un flot d’emails publicitaires touche à sa fin.

L’industrie adulte a-t-elle soudainement une crise morale ?

Non, c’est juste que ce n’est plus efficace du tout.

La plupart des mails sont maintenant arrêtés sans efforts de l’utilisateur par tous les grands services de mails gratuits. Ils ne les voient même pas. En fait, ça en devient chiant car parfois les mails de confirmation d’ouverture de compte, les liens de validation et les reset de mots de passe y tombent aussi si le nom de domaine est trop tendancieux.

Le mail, en soit, n’a pas disparu. La mailling list est toujours d’actu, et fonctionne bien : le user peut se désabonner pour de vrai, et les intéressés (il y en a plus que le geek en moi veut l’admettre) cliquent consciemment sur les liens qu’elle inclus.

Le spam n’a pas disparu non plus. Il a changé de forme. On ne vend plus les mêmes choses, de la même façon. Mais les spams de cul sont une espèce en voie de disparition. Les derniers à le faire ont sans doute juste oublié de killer leur CRON.

RIP

J’ai commencé ma carrière dans un boîte qui faisait du spam SMS. Et ça malheureusement, ça ne se désengorge pas, j’en reçois toujours aujourd’hui. Le karma je vous dis, le karma…

flattr this!

Petite démo pragmatique d’un usage de WAMP en Python

jeudi 26 juin 2014 à 09:27

Vu que dernièrement je vous ai bien gavé avec WAMP, ça mérite un tuto non ?

Il se trouve que l’équipe derrière WAMP a publié plus tôt que prévu une version de leurs libs contenant l’API flaskesque sur laquelle on bosse. L’idée est que même si on n’a pas encore les tests unitaires, on peut déjà jouer avec.

Maintenant il me fallait un projet sexy, histoire de donner envie. Donc j’ai fouillé dans ce qui se faisait côté temps réel (essentiellement du NodeJS et du Tornado, mais pas que) pour trouver l’inspiration.

Et j’ai trouvé un truc très sympa : un player vidéo piloté à distance.

En effet, n’est-il pas chiant de regarder une vidéo en ligne sur son ordi posé sur la commode pendant qu’on est enfoncé dans le canap ? Si on veut faire pause ou changer le son, il faut se lever, arg.

Les problèmes du tiers monde, c’est du pipi de chat à côté. Ils ont de la chance, eux, ils ne connaissent pas le streaming.

Voici donc le projet :

Une page avec un player HTML 5 et un QR code.

Capture d'écran de la démo, côté player

Pour simplifier la démo, on peut cliquer sur le QR code et avoir la télécommande dans un autre tab pour ceux qui n'ont pas de smartphone ou d'app de scan de QRCode.

Si on scanne le QR code avec son téléphone, il vous envoie sur une page avec une télécommande pour contrôler le player sans bouger votre cul :

Capture d'écrand de la démo, côté contrôles

Évidement, c'est basique. Je vais pas m'amuser à faire un produit complet juste pour un truc dont le code source ne sera même pas regardé par la plupart d'enter vous. Je vous connais, bandes de feignasses !

Et vous allez voir, c’est même pas dur à faire.

Mais d’abord :

La démo

Bon, y a plus de latence que je le voudrais, mais sur un projet sérieux, il y a moyen de faire ça mieux. Ici on veut faire simple pour expliquer le principe.

Et vous pouvez télécharger le code ici.

Pour comprendre ce qui va suivre, il va vous falloir les bases en prog Javascript et Python, ainsi que bien comprendre la notion de callback. Être à l’aise avec promises peut aider.

Et pour bien digérer ce paté, rien ne vaut un peu de son :

Le Chteumeuleu

Il va nous falloir deux pages Web, une pour le player video, et une pour la télécommande.

Le player :

<!DOCTYPE html>
<html>
<head>
   <title>Video</title>
   <meta charset='utf-8'>
   <!-- Chargement des dépendances : autobahn pour WAMP
   et qrcode pour générer le QR code. Bien entendu, je
   vous invite à ne pas les hotlinker dans vos projets,
   mais pour la démo c'est plus simple. -->
  <script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz"
         type="text/javascript"></script>
   <script src="http://davidshimjs.github.com/qrcodejs/qrcode.min.js"
           type="text/javascript"></script>
 
   <style type="text/css">
      /* Quelques styles histoire que ça soit pas
         TROP moche. J'inline, hein, on va pas
         se faire chier à faire un fichier CSS
         externe juste pour ça. */
      #vid {
         /* Taille de la video */
         width:427px;
         height:240px;
      }
      /* Centrage avec la méthode Rache */
      #container {
          width:427px;
          margin:auto;
      }
      #ctrllink {
          display:block;
          width:256px;
          margin:auto;
      }
   </style>
 
</head>
<body>
<div id="container">
  <p>
 
   <!-- J'utilise le lecteur video HTML5 car c'est le plus facile
        à faire. Je ne vais pas m'attarder sur comment ça
        marche, y a plein de tutos pas trop mauvais
        sur la question -->
    <video id="vid"
           class="video-js vjs-default-skin"
           controls preload="auto"
           poster="http://media.w3.org/2010/05/sintel/poster.png" >
 
      <!-- Encore une fois, je hotlink la video, mais ne faites
       pas ça à la maison les enfants. Surtout que les perfs du
      serveur du W3C sont merdiques et ça bufferise à mort. -->
       <source id='ogv'
         src="http://media.w3.org/2010/05/sintel/trailer.ogv"
         type='video/ogg'>
       <source id='mp4'
         src="http://media.w3.org/2010/05/sintel/trailer.mp4"
         type='video/mp4'>
       <source id='webm'
         src="http://media.w3.org/2010/05/sintel/trailer.webm"
         type='video/webm'>
    </video>
  </p>
  <!-- Un élément vide pour le QR code -->
  <p>
    <a id="ctrllink" href="#" target="_blank">
      <span id="qrcode"></span>
    </a>
  </p>
 </div>
 
</body>
</html>

Et la télécommande :

<!DOCTYPE html>
<html>
<head>
  <title>Télécommande</title>
  <meta charset='utf-8'>
  <script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz"
         type="text/javascript"></script>
  <!-- Zoom du viewport sur mobile pour éviter d'avoir
       à le faire à la main. -->
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style type="text/css">
    #controls {
      width:350px;
      margin:auto;
    }
    #controls button {
      font-size: 1em;
    }
    #controls input {
      vertical-align:middle;
       width: 200px;
       height:20px;
   }
  </style>
</head>
<body>
  <p id="controls">
    <!-- Marrant de se dire qu'en 2000, le JS inline était
         considéré comme démoniaque, et maintenant avec
         angularjs et cie, c'est exactement ce que tout
         le monde fait...
         Bref, on attache le clic sur nos contrôles à des
         méthodes de notre objet qui va se charger de la
         logique. -->
 
    <button id="play" onclick="control.togglePlay()">Play</button>
    <input id="volume"
                    onchange="control.volume(this.value)"
                    type="range">
  </p>
</body>
</html>

Rien d’incroyable. C’est du HTML, un peu de CSS, on charge les dépendances en JS. Classique.

Vu qu’on utilise des ressources hotlinkées par souci de simplicité, il vous faudra être connecté à Internet.

Setup du serveur

On va travailler avec Python 2.7. Je sais, je sais, c’est pas idéal, mais pour le moment je n’ai fait l’API flaskesque que pour le backend twisted, qui est en 2.7. On ne peut pas tout faire. Est-ce que vous savez le temps que me prend juste la rédaction de ce tuto, sérieux ?

Bref, il nous faut avant tout un serveur HTTP pour servir nos fichiers HTML. Normalement crossbar peut le faire pour nous en prod, mais en dev, on va pas setuper crossbar, on va faire plus simple.

Donc, à la racine du projet, lancez la commande :

python -m SimpleHTTPServer

Vos pages Web seront ainsi servies en local sur le port 8000. Par exemple, pour afficher la page de video :

http:localhost:8000/index.html

Ensuite, faut installer de quoi faire du WAMP en Python avec pip :

pip install "autobahn[twisted]"

Pour simplifier le développement, notre app Python va lancer automatiquement un petit serveur WAMP, du coup pas besoin d’installer crossbar du tout pour cette démo.

Création de l’App WAMP côté serveur

Pour cette démo, le serveur n’a pas grand chose à faire. On pourrait en fait la faire sans aucun code serveur, mais ça va nous simplifier la vie.

En effet, on a deux problématiques que le serveur va résoudre facilement pour nous : créer un ID unique pour le player et récupérer l’IP sur le réseau local.

L’ID, c’est simplement que si plusieurs personnes lancent en même temps un player, on ne veut pas que les télécommandes puissent lancer un ordre à un autre player que le sien. On pourrait utiliser un timestamp, mais ils sont contiguës, n’importe quel script kiddies pourrait faire un script pour foutre la merde. On va donc créer un ID unique qui ne soit pas facilement prévisible. Javascript n’a rien pour faire ça en natif, et c’est un peu con de charger une lib de plus pour ça alors que Python peut le faire pour nous.

L’IP, c’est parce qu’il faut donner l’adresse de notre machine au téléphone qui va l’afficher sur la télécommande, puisque notre serveur tourne dessus. Sinon notre démo ne marchera pas. Bien sûr, en prod, on ne fera pas ça, mais en local, il faut faire avec les moyens du bord.

Cela veut dire aussi que le téléphone doit être sur le même réseau local pour que ça fonctionne. Donc mettez votre téléphone en Wifi, par en 3G.

Voilà ce que donne notre code WAMP côté serveur :

# -*- coding: utf-8 -*-
 
from autobahn.twisted.wamp import Application
 
import socket
import uuid
 
# Comme pour flask, l'objet app
# est ce qui lie tous les éléments
# de notre code ensemble. On lui donne
# un nom, ici "demo"
app = Application('demo')
# Bien que l'app va démarrer un serveur
# pour nous, l'app est bien un CLIENT
# du serveur WAMP. Le serveur démarré
# automatiquement n'est qu'une facilité
# pour le dev. En prod on utiliserait
# crossbar.
 
# Juste un conteneur pour y mettre notre IP
app._data = {}
 
# On déclare que cette fonction sera appelée
# quand l'app se sera connectée au serveur WAMP.
# Ceci permet de lancer du code juste après
# le app.run() que l'on voit en bas du fichier.
# '_' est une convention en Python pour dire
# "ce nom n'a aucune importance, c'est du code
# jetable qu'on utilisera une seule fois".
@app.signal('onjoined')
def _():
   # On récupère notre adresse IP sur le réseau local
   # C'est une astuce qui demande de se connecter et donc
   #  à une IP externe, on a besoin d'une connexion internet.
   s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
   s.connect(("8.8.8.8", 80))
   # On stocke l'adresse IP locale dans un conteneur
   # qui sera accessible partout ailleur.
   app._data['LOCAL_IP'] = s.getsockname()[0]
   s.close()
 
# On déclare que la fonction "ip()" est appelable
# via RCP. Ce qui veut dire que tout autre client
# WAMP peut obtenir le résultat de cette fonction.
# Donc on va pouvoir l'appeler depuis notre navigateur.
# Comme notre app s'appelle "demo" et notre fonction
# s'appelle "ip", un client pourra l'appeler en faisant
# "demo.ip".
@app.register()
def ip():
   # On ne fait que retourner l'IP locale. Rien de fou.
   return app._data['LOCAL_IP']
 
# Je voulais appeler cette fonction distante "uuid", mais ça
# override le module Python uuid. Ce n'est pas une bonne
# idée. Je l'appelle donc 'get_uuid' mais je déclare le
# namespace complet dans register(). Un client WAMP pourra donc
# bien l'appeler via "demo.uuid".
# Notez que ce namespace doit toujours s'écrire
# truc.machine.bidule. Pas truc/machin ou truc:machin.
# ou truc et bidule.MACHIN.
@app.register('demo.uuid')
def get_uuid():
   # Retourne un UUID, sans les tirets.
   # ex: b27f7e9360c04efabfae5ac21a8f4e3c
   return str(uuid.uuid4()).replace('-', '')
 
# On lance l'application. Ceci va lancer le serveur
# puis le client. On peut désactiver le lancement du
# serveur une fois qu'on met tout ça en prod.
if __name__ == '__main__':
    app.run(url="ws://0.0.0.0:8080/")
# On ne peut rien mettre comme code ici, il faut le
# mettre dans @app.signal('onjoined') si on veut
# entrer du code après que l'app soit lancée.

Et on lance notre app :

python app.py

Nous avons maintenant deux serveurs qui tournent : un serveur HTTP qui écoute sur le port 8000, et un serveur WAMP qui écoute sur le port 8080. En prod, crossbar peut servir à la fois HTTP et WAMP, donc pas besoin de lancer deux outils.

Le lecteur video

Il nous faut maintenant définir le comportement de notre lecteur video via Javascript. Il s’agit essentiellement de se connecter au serveur WAMP, et d’échanger des messages via RPC ou PUB/SUB :

  var player = {};
  var url;
  /* On va utiliser du pur JS histoire de pas mélanger
    des notions de jQuery dans le tas. Je ne vais
    PAS utiliser les best practices sinon vous allez
    être noyés dans des détails */
 
  /* Lancer le code une fois que la page est chargée */
  window.addEventListener("load", function(){
 
    /* Connexion au serveur WAMP. J'utilise
       les valeurs par défaut du serveur de
       dev. On ouvre explicitement la connection
       à la fin du script. */
    var connection = new autobahn.Connection({
       url: 'ws://' + window.location.hostname + ':8080/',
       realm: 'realm1'
    });
 
    /* Lancer ce code une fois que la connexion
       est réussie. Notez que je ne gère pas
       les erreurs dans dans une APP JS, c'est
       un puits sans fond. */
    connection.onopen = function (session) {
 
      /* Appel de la fonction ip() sur le serveur */
      session.call('demo.ip')
 
      /* Une fois qu'on a récupéré l'IP,
         on peut fabriquer l'URL de notre
         projet et on appelle la fonction
         get_uuid() du serveur */
      .then(function(ip){
        url = 'http://' + ip + ':8000';
        return session.call('demo.uuid');
      })
 
      /* Une fois qu'on a l'UUID, on peut commencer
         à gérer la partie télécommande */
      .then(function(uuid){
 
        /* Création du QR code avec le lien pointant
           sur la bonne URL. On met l'ID dans le hash. */
        var controlUrl = url + '/control.html#' + uuid;
        var codeDiv = document.getElementById("qrcode");
        new QRCode(codeDiv, controlUrl);
        var ctrllink = document.getElementById("ctrllink");
        ctrllink.href = controlUrl;
 
        /* Notre travail consiste essentiellement à
           manipuler cet élément */
        var video = document.getElementById("vid");
 
        /* On attache déclare 4 fonctions comme étant
           appelable à distance. Ces fonctions sont
           appelables en utilisant le nom composé
           de notre ID et de l'action qu'on souhaite
           faire. Ex:
           'b27f7e9360c04efabfae5ac21a8f4e3c.play'
           pour appeler "play" sur notre session. */
        session.register(uuid + '.play', function(){
           video.play();
        });
 
        session.register(uuid + '.pause', function(){
           video.pause();
        });
 
        session.register(uuid + '.volume', function(val){
           video.volume = val[0];
        });
 
        session.register(uuid + '.status', function(val){
          return {
            'playing': !video.paused,
            'volume': video.volume
          };
        });
 
 
 
       /* Quelqu'un peut très bien
           appuyer sur play directement sur cette page.
 
          Il faut donc réagir si l'utilisateur le fait,
          publier un événement via WAMP pour permettre
          à notre télécommande de se mettre à jour
          */
       video.addEventListener('play', function(){
         /* On publie un message indiquant que
            le player a recommencé à lire la vidéo.
            */
         session.publish(uuid + '.play');
       });
 
        video.addEventListener('pause', function(){
          session.publish(uuid + '.pause');
        });
 
        video.addEventListener('volumechange', function(){
          session.publish(uuid + '.volume', [video.volume]);
        });
 
     });
    };
 
    /* Ouverture de la connection une fois que tous les
       callbacks sont bien en place.*/
    connection.open();
  });

Code de la télécommande

La télécommande est notre 3eme client WAMP (on peut avoir des centaines de clients WAMP). Notre 1er est l’App Python, notre second est le player.

Son code a pour but d’envoyer des ordres au player HTML5, mais aussi de mettre à jour son UI si le player change d’état.

/* L'objet qui se charge de la logique de nos
   controles play/pause et changement de
   volume.
   Rien de fou, il change l'affichage
   du bouton et du slider selon qu'on
   est en pause/play et la valeur du
   volume.
   */
var control = {
   playing: false,
   setPlaying: function(val){
      control.playing = val;
      var button = window.document.getElementById('play');
      if (!val){
         button.innerHTML = 'Play'
      } else {
         button.innerHTML = 'Pause';
      }
   },
   setVolume: function(val){
      var slider = window.document.getElementById('volume');
      slider.value = val;
   }
};
window.onload = function(){
  var connection = new autobahn.Connection({
    url: 'ws://' + window.location.hostname + ':8080/',
    realm: 'realm1'
  });
 
  connection.onopen = function (session) {
 
    /* Récupération de l'ID dans le hash de l'URL */
    var uuid = window.location.hash.replace('#', '');
 
    /* Mise à jour des controles selon le status actuel
       du player grace à un appel RPC vers notre autre
       page. */
    session.call(uuid + '.status').then(function(status){
 
      control.setPlaying(status['playing']);
      control.setVolume(status['volume'])
 
      /* On attache l'appui sur les contrôles à
         un appel de la fonction play() sur le
         player distant. L'uuid nous permet
         de n'envoyer l'événement que sur le
         bon player. */
      control.togglePlay = function() {
        if (control.playing){
          session.call(uuid + '.pause');
          control.setPlaying(false);
        } else {
          session.call(uuid + '.play');
          control.setPlaying(true);
        }
      };
 
      control.volume = function(val){
        session.call(uuid + '.volume', [val / 100]);
      };
 
      /* On ajoute un callback sur les événements
         de changement de status du player. Si
         quelqu'un fait play/pause ou change le
         volume, on veut mettre à jour la page. */
      session.subscribe(uuid + '.play', function(){
        control.setPlaying(true);
      });
 
      session.subscribe(uuid + '.pause', function(){
        control.setPlaying(false);
      });
 
      session.subscribe(uuid + '.volume', function(val){
        control.setVolume(val[0] * 100);
      });
    });
  };
 
  connection.open();
};

En résumé

Voici à quoi ressemble le projet final :

.
├── app.py (client python))
├── control.html (télécommande)
├── index.html (player video)
Schéma de fonctionnement de la démo

Bien que l'app Python lance le serveur automatiquement et de manière invisible, c'est bien un composant à part.

Pour ce projet, on aura utilisé :

On n’aura pas utilisé crossbar, le serveur WAMP Python. On a utilisé un petit serveur de dev inclus dans autobahn. En prod, on utiliserait crossbar comme serveur WAMP.

Il y a pas mal de notions à prendre en compte.

D’abord, le RPC.

Cela permet à un client de dire “les autres clients peuvent appeler cette fonction à distance”. On l’utilise pour exposer ip() et get_uuid() sur notre serveur et notre javascript peut donc les appeler. Mais on l’utilise AUSSI pour qu’une des pages (le player) expose play(), pause() et volume() et que l’autre page (notre télécommande) puisse les utiliser.

La grosse différence, c’est que ip() peut être appelé par tous les clients en utilisant “demo.ip” alors que play() ne peut être appelé que par les clients qui connaissent l’ID du player, puisqu’il faut utiliser “<id>.play”.

Ensuite, il y a le PUB/SUB.

Cela permet à un client de dire “j’écoute tous les messages adressés à ce nom”. Et un autre client peut envoyer un message (on appelle ça aussi un événement, c’est pareil) sur ce nom, de telle sorte que tous les clients abonnés le reçoivent.

On l’utilise pour que notre télécommande dise “j’écoute tous les messages qui concernent les changements de status du player.” De l’autre côté, quand on clique sur un contrôle du player, on envoie un message précisant si le volume a changé, ou si on a appuyé sur play/pause. La télécommande peut ainsi mettre son UI à jour et refléter par exemple, la nouvelle valeur du volume.

Cela résume bien les usages principaux de ces deux outils :

Voici le workflow de notre projet :

Si vous virez tous les commentaires, vous verrez que le code est en fait vraiment court pour une application aussi complexe.

Encore une fois, il est possible de le faire sans WAMP, ce sera juste plus compliqué. Je vous invite à essayer de le faire pour vour rendre compte. Avec PHP, Ruby ou une app WSGI, c’est pas marrant du tout. Avec NodeJs, c’est plus simple, mais il faut quand même se taper la logique de gestion RPC et PUB/SUB à la main ou installer pas mal de libs en plus.

WAMP rend ce genre d’app triviale à écrire. Enfin triviale parce que là j’ignore tous les edge cases, évidemment. Pour un produit solide, il faut toujours suer un peu.

Les limites du truc

C’est du Python 2.7. Bientôt on pourra le faire avec asyncio et donc Python 3.4, mais malheureusement sans le serveur de dev.

Heureusement, Twisted est en cours de portage vers Python 3, et donc tout finira par marcher en 3.2+.

C’est du HTML5, mais bien entendu, rien ne vous empêche de faire ça avec du flash si ça vous amuse.

C’est du websocket, mais on peut utiliser un peu de flash pour simuler websocket pour les vieux navigateurs qui ne le supportent pas.

Non, la vraie limite c’est encore la jeunesse du projet : pas d’autoreload pour le serveur (super chiant de devoir le faire à la main à chaque fois qu’on modifie le code) et les erreurs côté serveur se lisent dans la console JS, et pas dans le terminal depuis lequel on a lancé le serveur. Plein de petits détails comme ça.

EDIT: Mise en prod

On m’a demandé à quoi ça ressemblerait si on mettais le truc en prod, alors je vais donner un exemple.

D’abord, on installe crossbar :

pip install crossbar

Ensuite, on le fait créer un dossier de configuration :

crossbar init

Ça va créer un dossier .crossbar avec un fichier config.json dedans.

On édite le fichier, de telle sorte qu’il ressemble à ça :

{
   "controller": {
   },
   "workers": [
      {
         "type": "router",
         "options": {
            "pythonpath": [".."]
         },
         "realms": [
            {
               "name": "realm1",
               "roles": [
                  {
                     "name": "anonymous",
                     "permissions": [
                        {
                           "uri": "*",
                           "publish": true,
                           "subscribe": true,
                           "call": true,
                           "register": true
                        }
                     ]
                  }
               ]
            }
         ],
         "transports": [
            {
               "type": "web",
               "endpoint": {
                  "type": "tcp",
                  "port": 8080
               },
               "paths": {
                  "/": {
                     "type": "static",
                     "directory": "../templates"
                  },
                  "ws": {
                     "type": "websocket"
                  }
               }
            }
         ]
      },
      {
         "type": "container",
         "options": {
            "pythonpath": [".."]
         },
         "components": [
            {
               "type": "class",
               "classname": "app.app",
               "realm": "realm1",
               "transport": {
                  "type": "websocket",
                  "endpoint": {
                     "type": "tcp",
                     "host": "127.0.0.1",
                     "port": 8080
                  },
                  "url": "ws://127.0.0.1/ws"
               }
            }
         ]
      }
   ]
}

Ceci n’est pas un tuto sur la mise en prod d’un projet WAMP avec crossbar, alors je ne vais pas rentrer dans les détails, mais en gros on change les ports HTTP et WAMP pour le port 8080, et on donne juste l’url /ws comme point d’entrée pour WAMP (on doit éditer le code source de la démo en conséquence). On sert les fichiers statiques en utilisant la section “transports” et notre app via la section “components”. Pour le realm, encore une fois, je skip, c’est un sujet à part, celui par défaut marche pour notre usage.

A partir de là il y a plusieurs solutions possibles :

Pour lancer la machine :

crossbar run

On met un petit script d’init pour le lancer au démarrage, et on est good.

flattr this!

D’une base à l’autre en Python

mercredi 25 juin 2014 à 15:19

Je vous avais parlé des bases dans l’article sur le binaire. En informatique, on utilise essentiellement la base 10, évidement, mais aussi 2, 8, 16 et 64. Des puissances de 2, quoi.

Cependant, il arrive parfois qu’on ait besoin d’une base personalisée. En effet, une base n’étant qu’une notation, on peut en créer une de toutes pièces. C’est parfois très utile :

Mais la plupart des outils ne permettent pas de choisir la base de représentation des données qu’ils génèrent. Pour cette raison, il peut être pratique d’implémenter une petit convertisseur d’une base à l’autre.

Voici un objet Python qui permet de convertir une string d’une base custom en entier en base 10, et inversement.

# Il est possible de passer de n'importe quelle base à
# une autre directement, mais utiliser la base 10,
# comme pivot, est ce qu'il y a de plus facile 
# à coder
class BaseConverter:
 
   def __init__(self, symboles):
      self.symboles = symboles
      self.sym2val = {l: i for i, l in enumerate(symboles)}
      self.val2sym = dict(enumerate(symboles))
 
   def to_base_10(self, string):
       i = 0
       base = len(self.sym2val)
       # On part de la gauche vers la droite,
       # donc on commence avec les valeurs les plus
       # grosses.
       # Pour chaque symbole, on ajoute la valeur
       # de celui-ci (donnée par la table) et
       # avec facteur lié à sa position.
       for c in string:
           i *= base
           i += self.sym2val[c]
       return i
 
   def from_base_10(self, number):
       """ Convert from a base 10 to the custom base"""
       array = []
       base = len(self.val2sym)
       # Division euclidienne en boucle jusqu'à ce que le
       # reste soit égal à zero.
       while number:
           number, value = divmod(number, base)
           # Le résultat est l'index du symbole.
           # On le place le plus à gauche, chaque
           # symbole ayant une valeur plus grande
           # que le précédent.
           array.insert(0, self.val2sym[value])
 
       # Ne pas oublier le zéro
       return ''.join(array) or self.symboles[0]

Maintenant imaginez la génération d’un UUID qui doit seulement contenir certains caractères ou avoir une certaine taille. Par exemple, votre client vous demande des ID en base 32 de Crockford.

uuid = uuid.uuid4()
print(uid)
## 50ab7fa9-9643-4aad-be84-548405abea08
CROCKFORD_B32 = BaseConverter("0123456789abcdefghjkmnpqrstvwxyz")
print(CROCKFORD_B32.from_base_10(uid.int))
## 2gndztk5j39apvx12mgg2tqtg8

Bon, évidément, pour toutes les bases ordinaires, int(string, base) fait ça très bien.

On peut aller plus loin et faire une seule partie de l’ID dans une base, et l’autre dans une autre base, pour des raisons de lisibilité. Le cas d’école étant les plaques d’immatriculation ou les numéros de vol.

Une fois, on m’a demandé de faire une URL de type :

/ressource/id/

Mais l’id devait être facile à recopier à la main. Le client a opté pour une notation de type “AAA111″, 3 lettres, puis 3 chiffres.

Vu qu’il fallait l’implémenter en Lua embed dans nginx, la solution la plus simple a été adoptée :

location ~ "^/([0-9A-Fa-f]{3}[0-9]{3})$" {
   set $short_id $1;
   content_by_lua '
       local redirect_url = "http://site.com/ressource/" .. string.sub(ngx.var.short_id, -3) + tonumber(string.sub(ngx.var.short_id,0,3), 16 ) * 1000
       return ngx.redirect(redirect_url.."/")
   ';
}

En gros, on prend les 3 premiers caractères, on les traite comme de l’hexa, puis on multiplie par 999, et on additionne les 3 derniers chiffres. Cela convertit le truc en base 10.

L’URL est très lisible, et facile à dicter ou recopier à la main :

http://site.com/ressource/FAC101

Bien entendu, on peut se retrouver avec des chiffres au début puisque c’est de l’hexa, et avoir une URL de type 345823, mais ça a été accepté.

Au maximum on ne peut gérer que 4 millions d’ID. Or le site n’en avait pas accumulé plus de 350 000 en 7 ans, et à moins que le trafic explose soudainement (auquel cas il aura le budget pour me payer pour changer l’algo:)), ça devrait tenir 80 ans.

Je vais sans doute ajouter ce code à Batbelt du coup.


Télécharger le code de l’article

flattr this!

Pourquoi sametmax.com utilise WordPress ?

mardi 24 juin 2014 à 08:17

WordPress est le pire des blog engines, à l’exception de tous les autres

Churchill

Il y a des solutions en Python, pourquoi vous ne les utilisez pas ?

Parce que c’est trop de taff. Si on veut une feature, il faudra la coder puisque ces blogs n’ont pas de communauté de plugins qui approche 1/1000eme de celle de WordPress.

Par exemple, depuis l’ouverture du blog, on a installé 3 plugins anti-spam, un plugin pour adapter Varnish, un pour avoir des stats sans Google analytics, un formulaire de contact, un live preview pour les commentaires, un outil de notes collaboratives, etc. On a aussi changé 10 fois de thèmes, et custo celui-là à mort (je sais, il est moche :)).

Je ne connais aucune solution de blog Python ou non qui permette de faire tout ça out of the box.

Or, durant ces 2 dernières années, on a publié plus de 742 articles. Pas un article par jour, mais pas loin. On a une vie assez remplie également derrière. Et même si on a la chance de pouvoir gérer notre emploi du temps, ce dont on use et abuse, les journées n’ont que 37h.

Je connais un super générateur de blog statique qui…

Pas de commentaires indexable par Google. Pas de formulaire de contact. Besoin de connaître un VCS pour les contributeurs. Pas de gestion de droits… J’ai failli rajouter pas de moteur de recherche mais c’est vrai que le notre est pourri.

Pourquoi pas une plateforme en ligne alors ?

On veut avoir le contrôle de nos données pour éviter de se faire fermer violemment en cas de problème.

Ok, mais pourquoi pas Dotclear, Ghost, etc ?

J’en ai essayé par mal. Aucun n’a le nombre de plugins et thèmes de WordPress. Aucun n’a autant de features. Aucun n’a autant de tutos. Et rien ne prouve qu’on aura moins de problèmes avec.

Tout va toujours bien jusqu’à ce qu’on dépasse 10 visiteurs par jour.

Alors arrêtez de vous plaindre !

Si tu ne veux pas nous entendre nous plaindre, ne nous lis pas. Il y a un petit bouton en forme de croix en haut à droite de ton tab, tu vas voir, c’est magique.

Sinon, pas envie de coder votre solution ?

Très envie. Mais contrairement à la plupart des gens, je ne pense pas que créer un moteur de blog ce soit “3 jours de boulot”.

C’est un taff énorme, et là, tout de suite, c’est mort.

Et il existe 20000 personnes qui en ont fait un, donc pour avoir une raison de coder quelque chose, il faudrait faire quelque chose de vraiment innovant :

Ça, ça serait un truc qui vaudrait le coup de se faire chier à migrer dessus.

3 jours de dev ? Mouarf. Comptez plutôt 3 ans, pendant tous ces jours de libre. Autant dire que ça n’arrivera pas.

Bref, je vous laisse vous référer à l’image d’illustration de l’article pour résumer la situation des bloggers sur la toile actuellement.

flattr this!

Prostitution masculine

lundi 23 juin 2014 à 04:11

Un de mes coloc est un ancien gogo dancer, et je le soupçonne de bien plus. Mais dans les apparences, c’est un garçon très propre sur lui, et depuis quand je rencontre de nouvelles personnes, je les imagine toujours dans des situations coquines.

Il se trouve que quand on parle de pute, on pense généralement à des nanas, pas à des gigolos, car il y en a beaucoup moins. Enfin en France, parce que quand je travaillais en Afrique, j’en ai vu des blondes venir se payer leur bois d’ébène…

Cependant, il existe une prostitution bien moins reconnue : la prostitution sociale. Coucher pour réussir, pour bénéficier du train de vie d’une personne, pour éviter des ennuis, pour ne pas perdre quelque chose, en échange d’un service, d’un bien ou d’un avantage…

Ces putes là sont très nombreuses, parfaitement socialement acceptées, absolument pas assumées, et les mecs sont beaucoup plus représentés.

Non pas qu’il n’y a pas de nanas putes sociales, elles sont légions, mais je trouve qu’on a tendance à omettre trop souvent les deux côtés de la pièce. L’homme pute est là, et pas juste métaphoriquement.

Comme un de mes potes avocat qui a fisté son batonier car c’était bon pour sa carrière.

Comme un des potes de Max qui a couché régulièrement avec un thon ce mois-ci car il avait besoin de sa voiture et qu’elle garde les chiens de sa copine (sic) pendant quelques mois.

Et qui n’a pas dans son entourage ce type qui se force à se taper sa gonzesse dont il ne veut plus pour éviter qu’elle l’emmerde, qu’elle divorce, qu’elle prenne les enfants/l’argent, etc ?

On a tous des tas de gens autour de nous, hommes ou femmes, qui utilisent leur cul pour obtenir quelque chose.

Mais ça ne se limite pas à ça.

Cette semaine, en boîte, une meuf a proposé de l’argent à Max pour qu’il rentre avec elle.

Je suis un peu jaloux, ça ne m’est jamais arrivé.

flattr this!

Error happened! 0 - count(): Argument #1 ($value) must be of type Countable|array, null given In: /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php:428 http://ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/?96 #0 /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php(999): VroumVroum_Blog->update() #1 /var/www/ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/index.php(1): require_once('...') #2 {main}