Mots-clé : python

Django : un modèle sur mesure : DaysField étape 1/3

Le principe

Je voulais un modèle de données qui stocke les jours d’une semaine que l’utilisateur désire, et qui les stocke sous forme de chaîne simple, avec des numéros qui correspondent aux jours de la semaine.
J’ai appelé ce modèle DaysField.
Les valeurs sont stockées par rapport aux jours choisis. Par exemple, 1,3 signifie que l’utilisateur a choisi lundi et mercredi parmi les jours. 6,7 correspondent à samedi et dimanche. Pour finir, tous les jours de la semaine cochés serait donc : 1,2,3,4,5,6,7

Toujours descendre d’un modèle classique

J’ai retenu ces principes et cela semble suffire pour fonctionner : on descend du modèle qu’on veut écrire en base de données. Ici, je voulais écrire une chaîne de caractères dans la base ⇒ on descend de CharField(), en modifiant la description au passage :
class DaysField(models.CharField):
    description = _("Comma-separated integers between 1 and 7")

Conversion de « sources » différentes en données Python

Pour résumer : les modèles ont deux « arrivées » de données :

  • quand on les données viennent de la base = from_db_value() ;
  • quand on les données viennent d’un formulaire (ou autre, mais en gros quand ça vient pas de la base de données mais du code Python lui-même) = to_python().

Il faut donc gérer ces deux cas, qui, dans les deux cas, doivent renvoyer quelque chose au format Python qui nous intéresse.
Ici, c’est un tableau d’entiers compris entre 1 et 7 qui correspond aux jours de la semaine.

J’ai centralisé la gestion des deux « arrivées » (base et Python) dans une seule fonction : value_to_array() :

    @staticmethod
    def value_to_array(value):
        if value is None:
            return None
        try:
            if isinstance(value, list):
                return [int(a) for a in value]
            elif isinstance(value, str):
                return [int(a) for a in value.split(',')]
        except (TypeError, ValueError):
            raise ValidationError(_("Unexpected value"))
        raise ValidationError(_("Unexpected value"))
    @staticmethod
    def from_db_value(value, expression, connection):
        return DaysField.value_to_array(value)
    def to_python(self, value):
        return DaysField.value_to_array(value)

Peut-être existe-t-il des cas où la provenance du code Python doit être gérée différemment de la provenance de la base de données… ici ce n’est pas le cas – et si vous avez des exemples de gestion différentes pour des entrées de base et de code, laissez-moi un commentaire, car je n’en vois pas…

Conversion de données Python ⇒ données pour la base

C’est la fonction get_prep_value().

Dans le cas DaysField, c’est très simple :
    def get_prep_value(self, value):
        return ','.join([str(a) for a in value]) \
            if value is not None else None

On prend toutes les valeurs du tableau, on les concatène avec la virgule , comme séparateur. C’est plus long de l’expliquer que de le coder !

Django rest framework : Got AttributeError when attempting to get a value for field `my_field` on serializer `UserSerializer`.

Si vous voulez faire un mapping avec un modèle c’est ultra simple. Si vous voulez ajouter un champ custom, là ça devient compliqué…

Aucune réponse viable sur stackoverflow, vous risquez de chercher longtemps.

Si vous avez fait un code comme cela, pour rajouter un champ « custom » :

class UserSerializer(serializers.ModelSerializer):
    my_field = serializers.CharField(allow_blank=True)
    class Meta:
        model = User
        fields = ['url', 'email', 'password', 'my_field']

Eh bien cela ne marchera jamais, avec une erreur qui ne vous met absolument pas sur la voie :

Got AttributeError when attempting to get a value for field `my_field` on serializer `UserSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `User` instance.
Original exception text was: 'User' object has no attribute 'my_field'.

Voici la solution : autoriser le null :

class UserSerializer(serializers.ModelSerializer):
    my_field = serializers.CharField(allow_blank=True, allow_null=True)
    class Meta:
        model = User
        fields = ['url', 'email', 'password', 'my_field']

Python : hints & cheats

Son propre fichier basique de log

import datetime
msg = u"Test de log"
dt = datetime.datetime.now()
with open("monlogfile.log", 'a+') as f:
    f.write(u'{:02}:{:02} - {}\n'.format(dt.hour, dt.minute, msg))

Teaching Python

Here

Découpages Python : formation

Checkio

Demystifying Two Factor Auth

Two-factor auth

Python Open Source Projects of the Year

Here!

Thanks to Dan Bader

  • Python Parallel Computing (in 60 Seconds or less): here
  • Python Decorators: A Step-By-Step Introduction: here
  • Interfacing Python and C: Advanced “ctypes” Features: here
  • Working with Random Numbers in Python » ici
  • Face detection
  • Différentes manières de testers plusieurs flags en même temps en Python :
    x, y, z = 0, 1, 0
    if x == 1 or y == 1 or z == 1:
        print('ok')
    if 1 in (x, y, z):
        print('ok')
    # si l'un d'eux n'est pas vide:
    if x or y or z:
        print('ok')
    if any((x, y, z)):
        print('ok')
  • Comment trier un dictionnaire Python par ses valeurs :
    »»» xs = {'a': 4, 'b': 3, 'c': 2, 'd': 1}
    »»» sorted(xs.items(), key=lambda x: x[1])

    ou bien :
    »»» import operator
    »»» sorted(xs.items(), key=operator.itemgetter(1))
  • Mesurer le temps d’exécution de petits morceaux de code Python :
    »»» import timeit
    »»» timeit.timeit('"-".join(str(n) for n in range(100))',
                      number=10000)
    0.3412662749997253
    »»» timeit.timeit('"-".join([str(n)
                                 for n in range(100)])',
                      number=10000)
    0.2996307989997149
    »»» timeit.timeit('"-".join(map(str, range(100)))',
                      number=10000)
    0.24581470699922647
  • Comment utiliser la classe namedtuples :
    # namedtup1e est une classe :
    »»» from collections import namedtuple
    »»» Car = namedtup1e('Car' , 'color mileage')
    # Our new "Car" class works as expected:
    »»» my_car = Car('red', 3812.4)
    »»» my_car.color
    'red'
    »»» my_car.mileage
    3812.4
    # Une belle représentation repr avec :
    »»» my_car
    Car(color='red' , mileage=3812.4)
    # Comme les tuples, les namedtuples sont immuables :
    »»» my_car.color = 'blue'
    AttributeError: "can't set attribute"
  • « is » vs « == » :
    »»» a = [1, 2, 3]
    »»» b = a
    »»» a is b
    True
    »»» a == b
    True
    »»» c = list(a)
    »»» a == c
    True
    »»» a is c
    False

    is est True si deux variables pointent vers le même objet ;
    == est True si les variables contenues dans les objets sont identiques.

  • Titre Titre  :
    »»» Code code code

Learning Python in minutes
https://learnxinyminutes.com/docs/python3/

How to Send an Email With Python
https://dbader.org/blog/python-send-email

Python Decorators From the Ground Up
https://pabloariasal.github.io/python-decorators-from-the-ground-up/

How — and why — you should use Python Generators
https://medium.freecodecamp.org/how-and-why-you-should-use-python-generators-f6fb56650888

Download information on all your gmail emails and the body text to either csv or json. I developed this to download my 100K + emails stored over several years on gmail.
https://teklern.blogspot.fr/2017/11/download-all-your-email-information.html

Memoization in Python: How to Cache Function Results
https://dbader.org/blog/python-memoization

Implementing a Neural Network from Scratch in Python – An Introduction
https://www.datasciencecentral.com/profiles/blogs/implementing-a-neural-network-from-scratch-in-python-an

—–

Mailtrap – Sending Emails

—–

Introduction to NumPy and Pandas – A Simple Tutorial

https://cloudxlab.com/blog/numpy-pandas-introduction

Fastest way to uniquify a list in Python >=3.6

https://www.peterbe.com/plog/fastest-way-to-uniquify-a-list-in-python-3.6

8 Python Modules For Files Handling
http://devarea.com/8-python-modules-for-files-handling/

How do async for loops work in Python? Using asynchronous for loops in Python
https://quentin.pradet.me/blog/using-asynchronous-for-loops-in-python.html

How to use Python and Flask to build a web app — an in-depth tutorial
https://medium.freecodecamp.org/how-to-use-python-and-flask-to-build-a-web-app-an-in-depth-tutorial-437dbfe9f1c6

Framework ultra simple pour faire des micro-services en Json
Falcon is a bare-metal Python web API framework for building very fast app backends and microservices.
http://falconframework.org

How to break a CAPTCHA system in 15 minutes with Machine Learning
https://medium.com/@ageitgey/how-to-break-a-captcha-system-in-15-minutes-with-machine-learning-dbebb035a710

Python Exceptions: An Introduction
https://realpython.com/python-exceptions/

Python Metaclasses
https://realpython.com/python-metaclasses/

Building a Simple Web App with Bottle, SQLAlchemy, and the Twitter API
https://realpython.com/blog/python/building-a-simple-web-app-with-bottle-sqlalchemy-twitter-api/

Python – Regular Expressions Practical Guide
http://devarea.com/python-regular-expressions-practical-guide/#.Wki2nN_iZhE

A fast high-level screen scraping and web crawling framework.
https://scrapy.org

A fast high-level screen scraping and web crawling framework.
https://pyfiddle.io/

Python : faire un « beep » sur Debian

Le code est simple, il fautt avoir paplay d’installé (je ne sais pas si c’est installé par défaut dans Debian).

A partir de là voici un exemple de code qui joue un fichier ogg :

# python3
>>> import subprocess
>>> subprocess.Popen([
... "paplay",
... "/home/olivier/.local/blah/Mission_Failed.ogg"
... ]).poll()
>>>

Python » PyPI

Contribuer à une librairie Python

Principes

  • Faire un dossier (ex : source)
  • Dans source, cloner la librairie à laquelle on veut contribuer via
    git clone [repo concerné]
  • Faire un dossier pour l’environnement pip (ex : pip_env_lib)
  • Dans pip_env_lib, installer l’environnement pip via pipenv install
  • Dans pip_env_lib, lancer le « shell pip » via pip shell
  • Rarement documenté et important :
    – aller dans le dossier source
    – faire pip install -e .

pip install -e . (n’oubliez pas le . qui change tout, cela indique le répertoire à utiliser) signifie « si jamais le shell Python en cours essaie d’accéder à la librairie à laquelle on veut contribuer, aller la chercher dans source ». En fait, concrètement parlant, pip fait simplement un lien symbolique entre le dossier source d’installation de pip (ici c’est pip_env_lib), et le dossier source. N’oubliez pas que cela ne concerne le shell Python en cours (ici pip_env_lib).

L’avantage pratique de ce « lien symbolique », c’est qu’il est directement relié au code source. Donc, lorsqu’on modifie le code source de la librairie elle-même (dossier source), elle est immédiatement disponible. Si on fait donc des fichiers de tests unitaires, il suffit de les relancer, les modifications du code source sont toujours instantanément prises en compte. Toujours dans le shell Python en cours (ici pip_env_lib).

Résumé : exemple de code

mkdir [source]
cd [source]
git clone [repo concerné]
cd ..
mkdir [pip_env]
cd [pip_env]
pipenv --python 3 install
pip shell
pip install -e .

.. et les modifications dans [source] peuvent commencer !


PyPI : nouveau package / contribution

La documentation détaillée en anglais se trouve ici.
Bien meilleure que ce qui suit, mais longue, longue…

Voici les notes prises à la volées, d’un contributeur plutôt habitué à faire des contributions dans le monde Python.
Toute nouvelle note / correction éventuelle est la bienvenue.

Il y a trois choses différentes qu’on a tendance à confondre, mais qui n’ont fondamentalement aucun rapport. On a tendance à les confondre parce qu’on essaie d’utiliser, pour des raisons pratiques, le même nom, mais dans trois cadres différents.
Ces trois choses à bien dissocier sont :
le nom du package PyPI : dans le fichier de déploiement setup.py, il est défini dans le paramètre « name » de la commande setup(). C’est ce qui est vu dans PyPI.
le nom du projet qu’on met dans github : c’est le répertoire qui contient tous les fichiers du projet (= il peut être totalement différent, en général on s’arrange pour les appeler d’un nom différent) = c’est ce qu’on clone dans l’ordre git clone [repo concerné]
– le développeur final qui fait un « import » dans son code : ce module là peut être aussi différent, car c’est du Python. En général, c’est le répertoire de premier niveau du package (il doit contenir, bien sûr, « __init__.py » pour être vu comme un package et importable par Python). En général : « from [ma lib] import [quelque chose]« .

En général, on essaie de s’arranger pour que le nom du package sur PyPI soit le même que le nom du package dans le code Python même.
Mais s’il est déjà pris (dans un des trois contextes), il faut forcément utiliser un autre nom, et c’est là qu’il faut faire attention à ne pas se mélanger les pinceaux.

Pour faire un nouveau package sur PyPI le plus homogène possible :

  1. essayer de faire un projet « vide »
  2. faites une version squelette « alpha1 »
  3. faites une mise à jour sur PyPI : si ça fonctionne, alors vous pouvez continuer. Sinon, il faut utiliser un autre nom

Versionning

Dans PyPI, les versions sont obligatoirement du type «a.b.c».
En général :
[a].[b].[c]
[a] = fonctionnalités totalement nouvelles / incompatibilité
[b] = améliorations / nouvelles "petites" fonctionnalités
[c] = correctifs bogues

setup.py

Lorsque vous faites une nouvelle contribution, vous devez obligatoirement fournir un fichier setup.py.
Dans ce fichier, setup.py, il y a une fonction, ici aussi, obligatoire : setup().
Cette fonction, parmi tous les paramètres, a un paramètre particulier : classifiers qui permet au niveau de PyPI de dire « ce package est rangé dans telle(s) catégorie(s) ». La liste des expressions disponibles se trouve ici.

Django 2.1 : les autorisations de l’administration grandement facilitées

Voici le (petit) souci que tout le monde a forcément dès qu’on commence à vouloir un peu pousser la customization de l’administration Django : on aimerait avoir la possibilité d’accéder à certains modèles, mais uniquement en lecture seule. J’ai des tonnes d’exemples simples, mais très souvent, ce sont des modèles avec des choix « fixes » qui ne risquent pas d’évoluer dans le temps (exemple : sexe « féminin »/ »masculin »). Seulement (1) on ne veut pas le voir apparaître dans l’admin (2) si on ne veut pas le voir apparaître, il ne faut pas le déclarer, et Django ne veut pas accéder à un modèle qui n’est pas déclaré (= erreur), donc on en arrive toujours à (3) on déclare le modèle, on applique une solution « bidouille » que je ne décris pas ici mais même si le modèle n’apparaît pas dans l’interface, en tapant à la main la bonne URL (facile à deviner qui plus est), on peut tout de même modifier ce modèle. Heureusement la version 2.1 de Django pallie à ce manque !

Eh oui, dans la classe interne «Meta», ils ont fait évoluer default_permissions, qui est écrit en base. Donc en passant à Django 2.1 il vous faudra faire le fameux makemigrations migrate.

Là ou cela devient intéressant, c’est que dans les options vous avez default_permissions et elles sont ('add', 'change', 'delete', 'view'). Le plus important et le nouveau est donc 'view' qui pallie parfaitement au problème expliqué en haut !

Il vous suffit de déclarer votre modèle, de faire la classe dérivée de ModelAdmin et de préciser uniquement ('view',).
Attention, je n’ai pas testé, si quelqu’un veut tester et confirmer que ce code fonctionne (ou pas), je suis preneur !

class SexeAdmin(models.ModelAdmin):
    class Meta:
        default_permissions = ('view',)

Plein de code en moins, plus aucune bidouille ici, plus de lisibilité, bref, de mieux en mieux !

Django Async Roadmap

Petite parenthèse sur l’évolution de Django. Aujourd’hui, ce qui commence à importer plus que la vitesse, c’est la parallélisation des choses.
Donc avoir un serveur Web asynchrone, c’est censé être mieux qu’un serveur Web synchrone (= bloquant).

  • Django 2.1 : rien d’asynchrone. Aucun travail dessus.
  • Django 2.2 : travail initial pour ajouter la possibilité de faire de l’asynchrone sur l’ORM et les vues, sachant que c’est une option et que tout sera synchrone par défaut. Le support async sera principalement basé sur des pools de threads.
  • Django 3.0 : Ré-écriture de la gestion interne des requêtes pour être 100% asynchrone, ajout de la gestion asynchrone des middleware, forms, cache, sessions, auth. Début du process de dépréciation pour toutes les APIs prévues pour être uniquement asynchrones.
  • Django 3.1 : amélioration du support async, peut-être changement du templating
  • Django 3.2 : Fin du process de dépréciation et Django sera presque entièrement asynchrone.

Django : autorisations sur mesure – custom authorizations

Voici comment créer manuellement des groupes + des autorisations personnalisées :

from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
new_group, created = Group.objects.get_or_create(
    name ='co_branding_trainer')
ct = ContentType.objects.get_for_model(User)
permission_co_branding_trainer = Permission.objects.create(
    codename='co_branding_trainer',
    name='Co-branding trainer', content_type=ct)
new_group.permissions.add(permission_co_branding_trainer)

Une fois cela fait, on a deux possibilités :

  • aller dans le template et se servir de
    {% if perms.app_label.can_do_something %}
    {% endif %}
  • ou modifier le code vue dans le fichier Python. Dans ce cas, on n’a rien dans le template et si la personne connectée n’a pas les droits, la vue fait un forbidden

Comme je préfère la seconde (qui évite d’avoir dans le template des « if » un peu partout), voici comment faire : on crée un mixin qui se chargera de vérifier si, dans la vue, on a les droits : il suffit de chercher PermissionRequiredMixin dans le code de Django, et vous verrez que tout est simple, à partir de là vous pourrez faire descendre vos vues génériques de PermissionRequiredMixin.

Django Channels sous Windows : attention !

Bah oui !

Sous Linux, Django Channels est compatible Python 3.5 et plus : c’est vrai.
Pof, comme d’habitude, sudo apt-get install blabla, pip3 install blabla et tout fonctionne.
Sous Windows, ce n’est pas le cas (cela changera sûrement dans les semaines à venir et cet article va rapidement ne plus être vrai), mais pour l’instant, Django Channels ne fonctionne pas sous Windows avec Python 3.6, mais uniquement avec Python 3.5.

De plus, vous devrez télécharger des Gigas et des Gigas de données Microsoft pour avoir le compilateur C++ 14 (si ma mémoire est bonne).

Parce que mon ami Microsoft © a retiré les liens (et si vous les avez je suis preneur) du compilateur en ligne C++, et vous devez télécharger Visual Studio et toutes ses immondices qui vont autour, « téléchargez la version pro », « payez un abonnement », ou le pire : « vous devez être enregistré pour pouvoir lancer Visual Studio » (ce qui, à mon sens, est totalement inadmissible et fait partie des wagons (trains entiers ?) de choses qui font que je prône Linux).

2,8 Go chez moi pour Visual Studio Community ainsi que 5,3 Go pour Microsoft SDKs.

Bref, sous Windows :

  • Installez Python 3.5 (toujour à la racine « C:\Python35« ) pour éviter les problèmes de droits quand il est dans « Program Files« 
  • Installez via la ligne de commande classique « channels » (sans passer par PyCharm) : python.exe -m pip install -U channels

Les requêtes « à la » Django : howto / principes

Voici une requête « à la Django ».
« objects » est un objet statique destiné à faire les requêtes
p = un objet du modèle « Person » = un modèle base de données que j’ai fait
g = un objet du modèle « Game » = un modèle base de données que j’ai fait

J’ai crée un modèle intermédiaire « PersonGame » qui lie les deux tables en n/n. Pour comprendre l’idée :
Q(person=p) signifie « dont la personne == p »
~Q(person=p) signifie « dont la personne != p »

Si on met deux « __ » cela signifie « faire une jointure entre les deux modèles », par exemple « person__user » signifie « LEFT JOIN PERSON p ON p.id_user = user.id »

Donc pour tout reprendre :
« Aller chercher dans PersonGame toutes les personnes qui ne sont pas le joueur p »
PersonGame.objects.filter(~Q(person=p), game=g)

…et dont le username vaut « u »
.get(person__user__username=u)

Ce qui donne au final :
PersonGame.objects.filter(
    ~Q(person=p),
    game=g).get(person__user__username=u)

Et voici la requête qu’il aurait fallu écrire « à la main » :

SELECT * FROM PersonGame pg
WHERE pg.id_person != p.id
AND pg.id_game = g.id
LEFT JOIN Person pe on pe.id = p.id
LEFT JOIN User us on us.id = pe.id_user
WHERE us.username = u
LIMIT 1;

En fait ça peut paraître rébarbatif, ou surprenant, mais :
– ça tient en une ligne ;
– quand on a fait 3-4 requêtes comme ça :
  – on arrive à faire n’importe quelle requête un peu complexe très très vite ;
  – on arrive à lire très facilement n’importe quelle requête ;
  – le générateur de requêtes est incroyablement mieux optimisé que celui de Symfony (en fait, techniquement, il est parfait, il génère les LEFT JOIN exactement comme il faut et dans l’ordre le plus efficace en fonction des clés qu’on a précisées dans le modèle, voire il fait des requêtes plus efficaces que ce qu’on aurait éventuellement fait à la main (cela m’est arrivé par deux fois)).

Conclusion : ici aussi, en termes de maintenance = ce qui coûte le plus cher, c’est exceptionnellement rentable.