IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Python Discussion :

SqlAlchemy : lever une exception sur erreur d'encodage


Sujet :

Python

  1. #1
    Membre régulier
    Profil pro
    Inscrit en
    Février 2012
    Messages
    48
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2012
    Messages : 48
    Points : 105
    Points
    105
    Par défaut SqlAlchemy : lever une exception sur erreur d'encodage
    Bonjour.

    Je travaille depuis une base MySql qui a de gros problèmes d'encodages. La base est déclaré en utf-8, mais les données à l'intérieur sont à 90% issu de données codés en windows-1252 passés tel quel en utf-8. 9% sont réellement de l'utf-8 et 1% sont du latin-1 (et je soupçonne qu'il y est encore d'autres encodages dans de rare cas...).
    J'aimerais être averti quand une "erreur" est rencontré (quand je parle d'erreur, c'est un cas autre qu'un texte encodé en utf-8 qui présente des caractères windows-1252. Une string en utf-8 correctement encodé est une erreur...).
    Pour me signaler l'erreur, j'utilise le module logging qui m'envoie un mail. Pour extraire les données dont j'ai besoin pour la suite de mon programme, j'utilise sqlalchemy.

    Ce qui me pose problème, c'est de me signaler se situe exactement l'erreur d'encodage, c'est à dire la table, la colonne et sous quelle référence. J'ai vraiment beaucoup de mal à comprendre (ou plutôt, à me mettre à la place de celui qui l'a écrit) la documentation de sqlalchemy.

    Pour corriger les problèmes d'encodage et avoir du unicode "relativement" propre dans la suite du programme, j'hérite de TypeDecorator pour créer mon propre type sqlalchemy.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
     
    from sqlalchemy import types
    @add_logger
    class FakeUtf8String(types.TypeDecorator):
        """ La base de données est déclaré en utf-8, mais contient du
        windows-1252. Il faut donc passer par une étape intermédiaire, pour obtenir
        des strings unicodes qui puissent être exportable vers tout type 
        d'encodages """
        impl = types.String
     
        def process_result_value(self, value, dialect):
            """ Cette étape intervient juste après l'extraction des données dans la
            BDD, mais juste avant qu'il ne soit passé à l'objet python représentant
            cette base. """
            if value:
                try:
                    return value.encode('windows-1252').decode('windows-1252')
                except UnicodeError:
                    self.logger.exception('Problème encodage sur ')
            return value
    # Commenter ou décommenter pour 'écraser' la classe String fourni avec SqlAlchemy
    String = FakeUtf8String
    @add_logger est un décorator qui ajouter un objet logger à un autre objet.
    C'est un squelette, faut que j'écrive pour les autres types d'encodage, mais l'idée est là. Et comme vous pouvez le voir, je bloque sur self.logger.exception, où j'aimerai indiquer où se situe le problème. Je pense que je dois lever l'exception jusqu'à ce qu'elle remonte à l'objet contenant cette instance FakeUtf8String, qui contiendra certainement le nom de la colonne, mais je n'arrive pas à comprendre la doc de sqlalchemy...

    Merci de m'avoir lu !

  2. #2
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 357
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 357
    Points : 36 886
    Points
    36 886
    Par défaut
    Salut,

    Quelle version de Python et de SQLAlchemy utilisez vous?

    A vue de nez, "process_result_value" sera appelé avec une value de type str (i.e. Unicode sous Python3 et je ne sais pas côté Python2).
    Comment "value.encode('windows-1252').decode('windows-1252')" fera plus qu'assurer que value (Unicode) sera sérialisable en 'windows-1252' (latin-1)?
    Ca n'aide pas à traiter le cas qui semble vous préoccuper.

    Désolé si je n'ai pas tout compris mais s'il faut écrire du code pour valider que vos hypothèses de base tiennent la route, çà n'aide pas.

    - W

  3. #3
    Membre régulier
    Profil pro
    Inscrit en
    Février 2012
    Messages
    48
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2012
    Messages : 48
    Points : 105
    Points
    105
    Par défaut
    Bonjour wiztricks.

    J'utilise python2.7 et la version 0.8 de sqlalchemy. Mais vu que je me suis farci énormément de problème d'encodage divers et varié de par le passé, j'utilie aussi from __futur__ import unicode_literals (je compte bientôt passer à python3) et je ne travaille qu'avec des strings unicode en interne. Donc, j'ai configuré sqlalchemy pour qu'il me donne des unicode à manger.

    Le problème n'est pas au niveau de l'encodage ou du décodage, je me débrouille pour ça. En réalité, c'est fait actuellement plus loin dans le programme (typiquement, il est construit selon le modèle MVC, et la levée des erreurs sur l'encodage se fait dans la vue, ce qui n'est pas vraiment propre. Avant, il était dans le model, quand j'utilisais mysqldb, mais j'ai dû passer à sqlalchemy car les données devenaient trop chiante à représenter sans abstraction). Le problème est où intercepter la levée d'exception pour que je puisse fournir suffisamment d'informations à propos de l'erreur.

    Partons sur une base et un exemple plus simple. Une base de donnée qui contient uniquement du windows-1252. Sauf pour une case bien précise de la BDD. Je n'ai pas configuré sqlalchemy pour qu'il me donne de l'unicode, je préfère le faire à la main :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
     
    import logging
     
    class MyString(types.TypeDecorator):
        impl = types.String
        logger = logging.getLogger('MyString')
     
        def process_result_value(self, value, dialect):
            try:
                return value.decode('windows-1252')
             except UnicodeError:
                self.logger.exception('Problème encodage sur ')  # <= Mon problème
                raise
     
    class MyModel(Base):  # Base est un objet sqlalchemy, pour construire la représentation de la table
        __table__ = 'MYTABLESQL'
        id = Column(Integer(), primary_key=True)
        title = Column(MyString(50))
        # 50 autres Column(MyString()) qui représente le bordel de la table
    Par exemple, le problème d'encodage se situe à l'id 500 ayant pour titre "Mon super livre mal encod€ Vol.3", sur le titre justement. Où puis-je intercepter l'erreur levé dans process_result_value pour récupérer l'id et la table qui pose problème (et continuer quand même la suite du programme en laissant l'erreur d'encodage) ? C'est logique que je ne puisse l'avoir dans process_result_value, je suis d'accord. Mais fallait bien que je fasse le décodage quelque part et commencer la levée de l'erreur.

    En espérant avoir été plus clair.

  4. #4
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 357
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 357
    Points : 36 886
    Points
    36 886
    Par défaut
    Salut,

    Un exemple, c'est du code qui fonctionne et pas un pseudo-code ouvert à de nombreuse interprétation.
    Par exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    import logging
    logging.basicConfig(level=logging.DEBUG)
    log = logging.getLogger('test')
     
    import sys
    PyVER = (sys.version_info.major, sys.version_info.minor)
    PyV3 = PyVER >= (3, 1)
    PyV2 = PyVER == (2, 7)
    import sqlalchemy
    assert sqlalchemy.__version__.startswith('0.8')
     
    import inspect
    def trace():
        frames = inspect.getouterframes(inspect.currentframe())
        for f in frames:
            log.debug(str(f))
     
     
    if PyV2:
        import sqlalchemy.types as types
     
        class MyString(types.TypeDecorator):
            impl = types.String
            def process_bind_param(self, value, dialect):
                if value:
                    log.debug('*** process_bind_param: got type=%s' % type(value))
                    assert isinstance(value, str)
                    value = value.decode('utf-8')
                    trace()
                return value
     
            def process_result_value(self, value, dialect):
                if value:
                    log.debug('*** process_result_value: got type=%s' % type(value))
                    assert isinstance(value, unicode)
                    value = value.encode('utf-8')
                return value
     
     
    if PyV3:
        import sqlalchemy.dialects.sqlite as dialect
        class MyString(dialect.VARCHAR):
     
            def bind_processor(self, dialect):
                impl = super().bind_processor(dialect) or (lambda value: value)
                def process(value):
                    log.debug('*** bind_processor: got type=%s' % type(value))
                    value = impl(value)
                    log.debug('*** bind_processor: returns type=%s' % type(value))
                    return value
                return process
     
            def result_processor(self, dialect, coltype):
                impl = super().result_processor(dialect, coltype) or (lambda value: value)
                def process(value):
                    log.debug('*** result_processor: got type=%s' % type(value))
                    value = impl(value)
                    log.debug('*** result_processor: return type=%s' % type(value))
                    return value
                return process
     
     
    if __name__ == '__main__':
        import os
        from sqlalchemy import MetaData, Table, create_engine, Column, select, String
     
        path = None
     
        meta = MetaData()
        if PyV2:
            test_table = Table('test_table', meta,
                  Column('name', MyString(50)),
                  Column('data', String(255)))
        elif PyV3:
            test_table = Table('test_table', meta,
                  Column('name', MyString(50, convert_unicode='force')),
                  Column('data', String(255)))
     
        if path is not None and os.path.exists(path):
            os.remove(path)
        else:
            path = ':memory:'
     
        engine = create_engine('sqlite:///%s' % path)
        meta.bind = engine
        test_table.create()
        test_table.insert().execute(name='foo', data='some data')
        rows = select(
                 [ test_table.c.name, test_table.c.data ]
            ).execute(
            ).fetchall()
        for name, data in rows:
            log.debug ('name: %s, data=%s' % (name, data))
    Vous constatez que ce qui fonctionne en 2.7 ne marche pas du tout en 3.x, il faut le faire "ailleurs" et "autrement".

    Par exemple, le problème d'encodage se situe à l'id 500 ayant pour titre "Mon super livre mal encod€ Vol.3", sur le titre justement. Où puis-je intercepter l'erreur levé dans process_result_value pour récupérer l'id et la table qui pose problème (et continuer quand même la suite du programme en laissant l'erreur d'encodage) ?
    Si vous levez un exception, elle sera d'abord traitée par SQLAlchemy. Impossible pour lui de laisser passer sauf à compromettre l'intégrité des données. Par contre, vous pouvez regarder ce qu'il y a dans la pile d'appel et y piocher des informations: c'est ce que fait "trace". C'est très bourrin.

    - W
    PS:Il serait quand même plus sage de nettoyer la BDD.

  5. #5
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 357
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 357
    Points : 36 886
    Points
    36 886
    Par défaut
    Salut,
    Si la route d'examen des "frames" est "hasardeuse": il y a peut être à gratter du côté "event".
    L'idée serait:
    • on définit un callback qui sera appelé lors de la création d'une instance d'une classe en relation avec une table (plus généralement un selectable),
    • le callback agit suivant les strings Ok/Ko qu'il trouve: mais on peut avoir des informations sur la table, la ligne, ... et éventuellement "forcer" un update.

    => Le hook qui (de)serialise s/classe "str" ou "unicode" et positionne l'attribut OK/KO.
    Si j'ai du temps(pas évident) , je mettrais à jour l'exemple précédent pour montrer ce que çà donne.
    - W

  6. #6
    Membre régulier
    Profil pro
    Inscrit en
    Février 2012
    Messages
    48
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2012
    Messages : 48
    Points : 105
    Points
    105
    Par défaut
    Bonjour.

    Je reviens à la charge sur ce problème. J'avais laissé de coté cette partie sur mon code, mais je suis en train de le finaliser, et je dois me pencher sur ce problème.
    J'ai essayé la méthode inspect, et effectivement, c'est bourrin. J'ai vu en milieu de semaine votre proposition d'utilisation des events, et je profite de ce long week-end pour me pencher sur la question.

    J'ai aussi découvert la partie wiki de sqlalchemy. En particulier un exemple de code qui se rapproche de ce que je souhaite :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
     
    from sqlalchemy import Column, Integer, String, DateTime
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy import event
    import datetime
     
    Base= declarative_base()
     
    def validate_int(value):
        if isinstance(value, basestring):
            value = int(value)
        else:
            assert isinstance(value, integer)
        return value
     
    def validate_string(value):
        assert isinstance(value, basestring)
        return value
     
    def validate_datetime(value):
        assert isinstance(value, datetime.datetime)
        return value
     
    validators = {
        Integer:validate_int,
        String:validate_string,
        DateTime:validate_datetime,
    }
     
    @event.listens_for(Base, 'attribute_instrument')
    def configure_listener(class_, key, inst):
        if not hasattr(inst.property, 'columns'):
            return
        @event.listens_for(inst, "set", retval=True)
        def set_(instance, value, oldvalue, initiator):
            validator = validators.get(inst.property.columns[0].type.__class__)
            if validator:
                return validator(value)
            else:
                return value
     
     
    class MyObject(Base):
        __tablename__ = 'mytable'
     
        id = Column(Integer, primary_key=True)
        svalue = Column(String)
        ivalue = Column(Integer)
        dvalue = Column(DateTime)
     
     
    m = MyObject()
    m.svalue = "ASdf"
     
    m.ivalue = "45"
     
    m.dvalue = "not a date"
    Voici la page, je n'ai fais que copier-coller l'exemple

    Le code fonctionne bien. Très bien même. Mais en l'adaptant pour mes modèles, je me suis rendu compte que la fonction _set du listener @event.listens_for(inst, "set", retval=True) n'est pas appelé lorsque l'InstrumentedAttribute est crée depuis la base de donnée. Il est appelé si on le définit dans le programme (avant de faire un update ou un commit par exemple), mais pas aux chargements des données.
    Et quand je regarde les événements documentés sur cette page, je ne vois que des appels "set", "append" et "remove". Pas de "init" par exemple.

    Voici la partie extraite de mon code:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
     
    @add_logger
    class Sanatizer(object):
        regex_blank_string = re.compile('^(\s*&nbsp;\s*)+$')
        @classmethod
        def sanatize_String(cls, string):
            """ 
            La base de données est déclaré en utf-8, mais contient du
            windows-1252. Il est donc nécéssaire d'encoder/decoder correctement
            les données.
            C'est aussi ici que sont 'nettoyé' les données vide, comme une chaine
            ne contenant que des espaces ou des '&nbsp;'.
            
            Parameters
            ----------
            string : unicode
                C'est cette string qui sera nettoyé.
            
            Raise
            -----
            TypeError : unicode non fourni en paramètre
            
            Returns
            -------
            value : str
                Logiquement, la même chaine de caractère passée en argument,
                mais correctement encodée et nettoyée.
            """
            print('EVENT CALL')
            if not isinstance(string, unicode):
                raise TypeError
            try:
                string = string.encode('windows-1252').decode('window-1252')
            except UnicodeError:
                # TODO : Récuperer l'exception dans un logger
                pass
            return cls.regex_blank_string.sub('', string.rstrip().lstrip())
     
    @event.listens_for(Base, 'attribute_instrument')
    def set_events_on_models(class_, key, inst):
        if not hasattr(inst.property, 'columns'):
            return
        # Je récupère le nom de la classe. Le résultat donne "String", "Integer", etc...
        # donc les types de "base" de sqlalchemy.
        type_name = inst.property.columns[0].type.__class__.__name__
        func_sanatize = getattr(Sanatizer, 'sanatize_%s' % type_name, None)
        if not func_sanatize:
            return
        print("sanatize for %s" % inst.property) # Ici, ça s'affiche, comme attendu pour les attributs qui sont de type String
        @event.listens_for(inst, "set", active_history=True)
        # Ici, ça ne s'affiche plus
        def sanatize_event(instance, value, oldvalue, initiator):
            print('Dans EVENT %s' % inst.property)
            print(value, oldvalue)

  7. #7
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 357
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 357
    Points : 36 886
    Points
    36 886
    Par défaut
    Salut,
    event.listen(MyObject, 'load', callback) appeller callback à chaque instanciation de MyObject.
    - W

  8. #8
    Membre régulier
    Profil pro
    Inscrit en
    Février 2012
    Messages
    48
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2012
    Messages : 48
    Points : 105
    Points
    105
    Par défaut
    Mmmmh.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     
    @event.listens_for(Base, 'attribute_instrument')
    def set_events_on_models(class_, key, inst):
        if not hasattr(inst.property, 'columns'):
            return
        type_name = inst.property.columns[0].type.__class__.__name__
        func_sanatize = getattr(Sanatizer, 'sanatize_%s' % type_name, None)
        if not func_sanatize:
            return
    #    print("sanatize for %s" % inst.property)
        @event.listens_for(class_, "load")
        def sanatize_event(target, context):
            setattr(target, key, func_sanatize(getattr(target, key)))
    Mmmmh. Je viens d'écrire la dernière ligne et ça fonctionne. Mais je ne comprend pas la logique derrière tout ça. L'event load s'applique à une seule classe, il devrait donc être seul et unique par classe. Hors, il est bien appelé plusieurs fois et "écrase" les anciens attributs, comme attendu. Le seul truc qui varie, c'est l'event qui défini le second event, en ayant un inst différent, qui se retrouve dans le corps du second event, mais pas dans la signature de la fonction (ce n'est pas très clair...).


    Et j'en profite aussi. Pour la base de donnée, je reverrais de la corriger, mais je n'ai pas les droits nécessaire pour cela car en théorie, ce n'est pas mon boulot. Et le problème d'encodage servait de fil rouge, mais n'était pas le principal souci. En réalité, y a des problèmes de partout avec cette base. Par exemple, je traite principalement du xml qui est contenu dans cette base, et les chevrons, esperluette qui devraient être échappé ne le sont pas (ils le sont avant d'être dans la base, le type qui s'occupe de l'insérer les "dés-échappent". Je sais, c'est idiot. Et c'est vraiment très chiant à corriger). Et la structure de la base de données est lolilol pour un truc qui pourrait être simple.

    En tout cas, je vous remercie énormément pour votre aide apporté, wiztrick.

  9. #9
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 357
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 357
    Points : 36 886
    Points
    36 886
    Par défaut
    Salut,
    Je n'ai pas de réponses à vos questions.
    Ce domaine de SQLAchemy est très Pythonique. L'architecture entre la classe que vous construisez et l'infrastructure de metaclass, event, décorateurs associés n'est pas si compliquée mais il faut passer du temps dans les sources pour espérer la comprendre un peu.

    En réalité, y a des problèmes de partout avec cette base. Par exemple, je traite principalement du xml qui est contenu dans cette base, et les chevrons, esperluette qui devraient être échappé ne le sont pas (ils le sont avant d'être dans la base, le type qui s'occupe de l'insérer les "dés-échappent". Je sais, c'est idiot. Et c'est vraiment très chiant à corriger). Et la structure de la base de données est lolilol pour un truc qui pourrait être simple.
    Une modèle de donnée à l'inconvénient d'avoir un cycle de vie bien plus grand que l'ensemble des applications qui devront s'en servir.
    Même en respectant les règles de l'art, impossible de tout anticiper.
    Les mises à jours pouvant casser ce qui marche déjà, on se retrouve à passer pas mal de temps pour trouver comment ajouter des fonctionnalités sans que tout s'écroule.
    Cà passe par quantifier la dette technique de la mise en production d'un truc pas fini et taxer les entités métiers qui ont "poussé" pour avoir le truc livré trop tôt - un peu comme la taxe carbone.

    - W

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Créer une exception sur un dossier en local uniquement
    Par nicolas.pied dans le forum Apache
    Réponses: 1
    Dernier message: 19/02/2008, 18h24
  2. [Validation] Perte d'une variable sur erreur
    Par kindjal dans le forum Struts 2
    Réponses: 2
    Dernier message: 13/02/2008, 12h04
  3. Recupération d'une exception sur Job Talend
    Par tioneb369 dans le forum Développement de jobs
    Réponses: 2
    Dernier message: 18/10/2007, 10h05
  4. lever une EXCEPTION pour 2 blocs séparés
    Par atruong dans le forum Oracle
    Réponses: 2
    Dernier message: 05/05/2006, 10h27
  5. [SQL]Lever une exception sans planter le code
    Par Titouf dans le forum Oracle
    Réponses: 2
    Dernier message: 25/01/2006, 15h28

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo