I Haz A Bug

le blog de jye.

Archive pour la catégorie ‘Python’

Ce billet fait partie d’une série : Un Digg-like sous App Engine.

La première chose à définir dans notre application sera le modèle de données sous le DataStore d’App Engine. Pour un digg-like on a besoin de seulement trois tables :

  • Item : ça représentera une URL ou un commentaire.
  • User : ça représentera l’utilisateur.
  • Vote : ça permettra de stocker les votes des user sur les items.

Contrairement à ce qu’on pourrait lire en survolant très rapidement pas mal d’articles sur App Engine. Il n’est pas interdit de concevoir des modèles relationnels. Dans certains cas, il est tout simplement impossible de faire autrement. Dans mon exemple, j’aurais très bien pu me passer de la table vote en stockant les votes à la fois dans la table item et dans la table user. Mais à mon niveau, je n’ai pas trouvé de réels intérêts à appliquer cette pratique. Si néanmoins cela vous interesse dans savoir plus sur les modèles de données idéal pour permettre une bonne scalabilité, je vous recommande cet article de Bret Taylor : How FriendFeed uses MySQL to store schema-less data.

Relation One-To-Many

Dans le cadre de la relation User/Item, nous avons une relation one to many (un user peut être l’auteur de plusieurs items). Le meilleur moyen de gérer ça sous App Engine et d’utiliser la propriété SelfReference, exemple :

class User(db.Model):
    nickname = db.StringProperty(required=True)

class Item(db.Model):
    title = db.StringProperty()
    author = db.ReferenceProperty(User, collection_name='items')

De cette manière je peux non seulement accèder à l’objet User via l’objet Item de cette façon :

user = User(nickname="Toto")
item = Item(author=user, title="Nouvel item")
print item.author.nickname
>> Toto

Mais cela permet aussi de récupérer dans l’autre sens, la liste des items dont le user est l’auteur :

user = User(nickname="Toto")
item = Item(author=user, title="Nouvel item 1")
item = Item(author=user, title="Nouvel item 2")
item = Item(author=user, title="Nouvel item 3")
for item in user.items:
  print item.title
>> Nouvel item 1
>> Nouvel item 2
>> Nouvel item 3

Relation Many-To-Many

Je n’ai pas dans mon projet besoin d’utiliser cette relation mais à titre d’information, pour mettre en place correctement une relation many to many, il est conseillé de stocker une liste de clé du côté de l’objet qui risque de comporter le moins de relation vers l’autre objet. Exemple :

class Contact(db.Model):
  name = db.StringProperty()
  groups = db.ListProperty(db.Key)

class Group(db.Model):
  name = db.StringProperty()

  @property
  def members(self):
    return Contact.gql("WHERE groups = :1", self.key())

Dans ce cas, il est plus probable qu’un contact appartienne à deux ou trois groupes, plutôt qu’un groupe ne contienne que 2 ou 3 users, un groupe aura plus de chance d’avoir 50, 100 users. Il est donc plus opportun de stocker la liste côté user que côté groupe.

Custom Property

L’extrait de code ci-dessus m’amène à parler d’un dernier point dans cet article. L’utilisation de @property pour définir des attributs personnalisés à nos objets Model. Cela va nous permettre de créer des attributs qui ne sont pas forcement liés à un champ en base de données. Exemple :

class class Item(db.Model):
  type = db.StringProperty(required=True, choices=set(["link", "comment"]))
  author = db.ReferenceProperty(User, collection_name='items')
  url = db.StringProperty()
  host = db.StringProperty()
  title = db.StringProperty()
  text = db.TextProperty()
  score = db.IntegerProperty()
  @property
  def score_number(self):
      score_num = self.score
      if score_num == 1 or score_num == 0:
          return str(score_num)+" point"
      else:
          return str(score_num)+" points"

item = Item(type="link", ..., score=5)
print item.score_number
>> 5 points

L’attribut score_number n’est relié à aucun champ en base de données. C’est un attribut custom qui me permet d’afficher le nombre de points pour l’item en tenant compte de la gestion du pluriel. On peut imaginer énormement de possibilité grâce à ce système.

Conclusion

Au final, pour mon application, et en tenant compte de ce que je vous ai présenté ci-dessus, voilà le modèle que j’ai mis en place :

class Item(db.Model):
    type = db.StringProperty(required=True, choices=set(["link", "comment"]))
    author = db.ReferenceProperty(User, collection_name='items')
    time = db.IntegerProperty()
    datetime = db.DateTimeProperty(auto_now_add=True)
    url = db.StringProperty()
    host = db.StringProperty()
    title = db.StringProperty()
    text = db.TextProperty()
    score = db.IntegerProperty()
    deleted = db.BooleanProperty()
    flag = db.BooleanProperty()
    parent_direct = db.SelfReferenceProperty()
    parent_top = db.SelfReferenceProperty(collection_name='allkids')
    @property
    def kids(self):
        return self.gql("WHERE parent_direct = :1", self.key())
    @property
    def hours(self):
        hours = tools.time_elapsed(self.time)
        if hours  24:
                days = int(hours/24)
                if days == 1 or days == 0:
                    return str(days)+" jour"
                else:
                    return str(days)+" jours"
            else:
                hrs = int(hours)
                if hrs == 1 or hrs == 0:
                    return str(hrs)+" heure"
                else:
                    return str(hrs)+" heures"
    @property
    def comments_number(self):
        # Issue if more than 1000 comments....
        comments_num = len(self.allkids.fetch(1000))
        if comments_num == 1 or comments_num == 0:
            return str(comments_num)+" commentaire"
        else:
            return str(comments_num)+" commentaires"
    @property
    def score_number(self):
        score_num = self.score
        if score_num == 1 or score_num == 0:
            return str(score_num)+" point"
        else:
            return str(score_num)+" points"
    @property
    def id(self):
        return self.key().id()
    @property
    def can_edit(self):
        now = int(time.time())
        #more than 30 minutes no edit
        if (self.time + 1800)  < now:
            return True
        else:
            return False
class User(db.Model):
    nickname = db.StringProperty(required=True)
    created = db.DateTimeProperty(auto_now_add=True)
    type = db.StringProperty(required=True, choices=set(["admin", "editor", "user"]))
    karma = db.IntegerProperty()
    weight = db.IntegerProperty()
    email = db.EmailProperty(required=True)
    password = db.StringProperty(required=True)
    @property
    def can_downvote(self):
        if self.karma > 99:
            return True
        else:
            return False
    @property
    def created_since(self):
        from datetime import datetime
        import time

        hours = tools.time_elapsed(time.mktime(self.created.timetuple()))
        if hours  24:
                days = int(hours/24)
                if days == 1 or days == 0:
                    return str(days)+" jour"
                else:
                    return str(days)+" jours"
            else:
                hrs = int(hours)
                if hrs == 1 or hrs == 0:
                    return str(hrs)+" heure"
                else:
                    return str(hrs)+" heures"
class Vote(db.Model):
    item = db.ReferenceProperty(Item)
    user = db.ReferenceProperty(User)
    time = db.IntegerProperty()
    datetime = db.DateTimeProperty(auto_now_add=True)
    type = db.StringProperty(required=True, choices=set(["up", "down"]))

Si vous avez des questions ou des remarques, n’hésitez pas, les commentaires sont là pour ça. Dans la prochaine partie, je parlerais du framework web utilisé par App Engine.

NB: Je souhaitais publier un article tous les soirs sur l’avancement de mon application, mais je n’ai vraiment pas le temps, donc mes articles arrivent avec un peu de retard. Etant donné que l’application est déjà terminée, j’en profite pour améliorer quelques points, sachant qu’à terme, le code sera disponible. J’en reparlerai bientôt.

J’espère aussi être assez clair, je ne rentre pas trop dans la profondeur afin de garder un article assez clair. Mais si vous vous avez des questions un peu plus pointus, les commentaires sont aussi là pour ça.

Un Digg-like sous App Engine

Mardi 10 mars 2009

Suite à mon précédent billet, je me suis penché (comme d’autres ici) sur la réalisation d’un équivalent à Hacker News pour le contenu français. D’un côté purement technique, j’avais le choix entre différentes solutions :

  • Utiliser le code source de reddit (open-source) qui utilise Postgresql et du Python.
  • Utiliser le code source d’Hacker News (open-source) qui est en Arc, un obscur dérivé (il me semble) de Lisp.
  • Partir de zéro.

J’ai mis en place Reddit sur mon serveur perso, mais c’est une véritable usine à gaz là ou je cherche la simplicité. Out.
Re-utiliser le code d’Hacker News aurait été l’idéal, mais codé dans un obscur langage, j’ai pas eu la moindre envie de m’y plonger. Out.

Il ne me reste plus qu’à tenter de le faire de A à Z, moi même. Pour réaliser ce petit projet, j’ai décidé d’utiliser la plate-forme App Engine. Pourquoi ? Je souhaitais ne rien avoir à gérer autre que l’application et son nom de domaine, App Engine me permet de ne pas me soucier de l’infra. Alors, bien sur, il existe d’autres solutions équivalentes, mais mon travail actuel étant tourné autour des solutions Google, je me suis dit que c’était l’occasion de tester un peu plus en profondeur ce service.

Par conséquent, nous avons les bases de cette application. App Engine oblige, elle sera donc forcement codé en Python et utilisera forcement Big Table. Le défi va être de réaliser une application dont l’implémentation permettra une bonne scalabilité, mais on verra ça sur le long terme. A court terme, le défi va surtout être de voir ce qu’offre App Engine concrètement.

Je vais donc dans les prochains billets, détailler la réalisation de cette application qui m’aura prit quelques soirées.
Screenshot de la version 1.

Vous pouvez déjà accéder à une version alpha de l’application ici (temporaire). Je suis d’ailleurs à la recherche d’un nom (français si possible) pour cette application. Je rappelle que ce site a pour but d’être un aggrégateur de contenus qui regroupe les contenus français, susceptible de satisfaire la curiosité intellectuelle de chacun. J’espère y voir naître un lieu de discussions et de découvertes entre personne intelligentes.

Tout retour est le bienvenue !