Python : les modules

nora.nckm.eu

Illustration
Table des matières

Cette page présente une sélection de modules Python. D'autres mémos sont disponibles dans les pages suivantes : les bases, l'orienté objet et l'environnement.

Certains modules sont distribués avec Python. Il s'agit des modules de la bibliothèque standard listés sur la page Python Module Index. De nombreux modules supplémentaires sont distribués par la communauté et sont disponibles sur le site : Python Package Index.

Python Module Index

Les modules de la bibliothèque standard (Python Module Index) peuvent être importés directement dans du code Python.

datetime

Manipulation des dates et des heures : documentation et liste des codes de formatage.

Le module time fournit des fonctions liées au temps.

import time

time.time() # 1649927737.9162598
time.sleep(3)

Le module datetime permet de manipuler les dates et les heures.

from datetime import date, time, datetime

## Constructeurs
date(year=2022, month=2, day=22)
time(hour=22, minute=22, second=22)
datetime(year=2022, month=2, day=22, hour=22, minute=22, second=22)

today = date.today()
now = datetime.now()

## Formatage et parsing
datetime.fromtimestamp(1645568542)
datetime.fromisoformat("2022-02-22T22:22:22")

datetime.strptime("22/02/2022 22:22:22", "%d/%m/%Y %H:%M:%S")
now.strftime("%d/%m/%Y %H:%M:%S") # '22/02/2022 22:22:22'

Il faut distinguer les dates naive (sans fuseau horaire) et aware (avec fuseau horaire). On ne peut pas comparer des dates si elles ne sont pas du même type. De manière générale les opérations entre des dates doivent être faites sur l'échelle de temps universelle UTC.

from zoneinfo import ZoneInfo

now_in_paris = datetime.now(tz=ZoneInfo("Europe/Paris"))
now_in_tokyo = now_in_paris.astimezone(ZoneInfo("Asia/Tokyo"))
now_in_tokyo.tzinfo() # 'Asia/Tokyo'

now_utc = now_in_paris.astimezone(ZoneInfo("UTC"))

Pour faire des opérations sur les dates on utilise timedelta qui représente une durée.

from datetime import timedelta

now_in_10_days_minus_5_hours = now + timedelta(days=15, hours=-5)

random

Opérations aléatoires : documentation.

import random

# Nombres aléatoires
random.uniform(0, 1) # tire un nombre décimal entre 0 et 1
random.randint(1, 10) # tire un nombre entre 1 et 10
random.randrange(10) # tire un nombre entre 0 et 9
random.randrange(1, 10, 2) # tire un nombre entre 1 et 9 avec un pas de 2

# Opérations aléatoires
couleurs = ["rouge", "vert", "bleu"]
random.choice(couleurs) # tire un objet dans une liste
random.choices(couleurs, 2) # tire un échantillon d'objets (tirage avec remise, avec ou sans pondération)
random.sample(couleurs, 2) # tire un échantillon d'objets (tirage sans remise)
random.shuffle(couleurs) # mélange une collection

decimal et fractions

Nombres décimaux et nombres rationnels.

from decimal import Decimal
from fractions import Fraction

0.1 + 0.1 + 0.1 # 0.30000000000000004
Decimal("0.1") + Decimal("0.1") + Decimal("0.1") # Decimal('0.3')
Decimal('0.1') * 3 # Decimal('0.3')

Fraction(1, 3) + Fraction(1, 3) # Fraction(2, 3)
Fraction(1, 3) * 2 # Fraction(2, 3)

Pour accéder aux fonctions mathématiques, se référer au module math. Pour les fonctions statistiques, se référer au module statistics.

re

Expressions régulières : documentation.

Une expression régulière est définie dans une chaîne de caractères brute r"" pour échapper les caractères spéciaux.

import re

# Recherche une correspondance au début de la chaîne
m = re.match(r".*", "Hello World")
print(m.group()) # 'Hello World'

m = re.match(r"(\w+)\s(\w+)", "Hello World !")
print(m.group(0)) # 'Hello World'
print(m.group(1)) # 'Hello'
print(m.group(2)) # 'World'
m.groups() # ('Hello', 'World')

m = re.match(r"(?P<mot1>\w+)\s(?P<mot2>\w+)", "Hello World !")
print(m.group("mot2")) # 'Hello'
print(m.group("mot2")) # 'World'
m.groupdict() # {'mot1': 'Hello', 'mot2': 'World'}

# Recherche une correspondance n'importe où dans la chaîne
m = re.search(r"\s", "Hello World")

# Recherche toutes les correspondances et retourne une liste
m = re.findall(r"\w", "Hello World") # ['Hello', 'World']

# Recherche toutes les correspondances et retourne un itérateur
m = re.finditer(r"\w", "Hello World") # iter(['Hello', 'World'])

# Remplace les correspondances
m = re.sub(r"\s", "_", "Hello World") # 'Hello_World'
m = re.sub(r"(\w) (\w)", r"\2 \1", "Hello World") # 'World Hello'

# Sépare la chaîne selon l'expression régulière
m = re.split(r" \| | - ", "item1 | item2 - item3 | item4")
m # ['item1', 'item2', 'item3', 'item4']

pathlib

Gestion des chemins : documentation.

Le module pathlib gère les chemins de système de fichiers de façon orientée objet contrairement aux modules os, shutil, glob.

from pathlib import Path

Path.home() # répertoire utilisateur
Path.cwd() # répertoire courant

Path("./index.tar.gz").resolve() # Transforme un chemin relatif en chemin absolu
Path("~/index.tar.gz").expanduser() # Transforme un chemin avec répertoire utilisateur en chemin absolu

# Informations sur un chemin
p = Path("/home/user/index.tar.gz")
p.parent # PosixPath('/home/user')
p.name # 'index.tar.gz'
p.stem # 'index.tar'
p.suffix # '.gz'
p.suffixes # ['.tar', '.gz']
p.parts # ('/', 'home', 'user', 'index.tar.gz')
p.exists() # True
p.is_dir() # False
p.is_file() # True

# Concaténation de chemins
p.joinpath("dir", "main.py")
p / "dir" / "main.py"
(p / "dir" / "main.py").suffix

# Créer et supprimer des fichiers
p.touch() # créer un fichier
p.unlink() # supprimer un fichier
p.mkdir() # créer un répertoire (paramètres : exist_ok=True, parents=True)
p.mkdir(exist_ok=True) # créer un répertoire sans erreur s'il existe
p.mkdir(parents=True) # créer un répertoire et ses parents
p.rmdir() # supprimer un répertoire
shutil.rmtree(p) # supprimer un répertoire non vide
p.rename(p_dest) # renommer un fichier

# Lire et écrire dans un fichier
p.read_text()
p.write_text("Bonjour")

# Scanner un répertoire
p.iterdir() # liste des fichiers et répertoires
p.glob("*.png") # liste des fichiers avec un filtre
p.rglob("*.png") # liste des fichiers avec recherche récursive dans les sous-répertoires

[f for f in p.iterdir() if f.is_dir()]
[f for f in p.iterdir() if f.is_file()]
[f for f in p.glob("*.png")]

# Exemple de constantes d'un projet
SOURCE_FILE = Path(__file__).resolve()
SOURCE_DIR = SOURCE_FILE.parent
ROOT_DIR = SOURCE_DIR.parent
DATA_DIR = SOURCE_DIR / "data"

logging

Journalisation : documentation.

import logging

logging.basicConfig(level=logging.DEBUG,
        filename="app.log",
        filemode="a",
        format="%(asctime)s %(levelname)s %(message)s")

logging.debug("message de débogage")
logging.info("message d'information")
logging.warn("message d'avertissement")
logging.error("message d'erreur")
logging.critical("message d'erreur critique")

configparser

Fichiers de configuration : documentation.

Attention : il est maintenant recommandé d'utiliser tomllib pour la configuration.

import configparser

config = configparser.ConfigParser()

# Lire la configuration
config.read("config.ini")
config["DEFAULT"].get("title", "My Title")

# Écrire la configuration
config["DEFAULT"]["title"] = "My New Title"
with open('config.ini', 'w') as file:
    config.write(file)

Contenu du fichier config.ini :

[DEFAULT]
title = My New Title

tomllib

Fichiers de configuration : documentation.

import tomllib

with open("pyproject.toml", "rb") as f:
    data = tomllib.load(f)

{'project': {
    'name': 'myproject',
    'version': 3,
    'beta': True,
    'release': datetime.date(2022, 6, 1),
    'peps': [657, 654, 678],
    'urls': {
        'home': 'https://example.com/',
        'source': 'https://example.com/'}}}

Contenu du fichier pyproject.toml :

[project]
name = "myproject"
version = 3
beta = true
release = 2022-06-01
peps = [657, 654, 678]

[project.urls]
home = "https://example.com/"
source = "https://example.com/"

csv

Fichiers CSV : documentation.

import csv
chemin = "/home/user/fichier.csv"

# Lecture du fichier
with open(chemin, "r") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(f"{row.["col1"]}, {row.["col2"]}, {row.["col3"]}")

# Écriture du fichier
with open(chemin, 'w') as f:
    writer = csv.DictWriter(f, ["col1", "col2", "col3"])
    writer.writeheader()
    writer.writerow({"col1": "a", "col2": "1", "col3": "100"})
    writer.writerow({"col1": "b", "col2": "2", "col3": "120"})

json

Fichiers JSON : documentation.

import json
chemin = "/home/user/fichier.json"

# Lecture du fichier
with open(chemin, "r") as f:
    data = json.load(f)

# Modification des données json
data.get("key")
data.["key"] = "value"
data.append("value")

# Écriture du fichier
with open(chemin, "w") as f:
    json.dump(data, f, indent=4)

sqlite3

Base de données relationnelle : documentation.

La base est enregistrée dans un fichier binaire.

import sqlite3

conn = sqlite3.connect("database.db")
c = conn.cursor()

# Création d'une table
c.execute("""
    create table if not exists USER(
        name text,
        age number
    )
""")

# Insertions
d = {"name": "John", "age": 25}
c.execute("insert into USER values(:name, :age)", d)

# Sélections
c.execute("select * from USER where name=:name", {"name": "John"})
user = c.fetchone()

c.execute("select * from USER where age<:age", {"age": 18})
users = c.fetchall()

# Mises à jour
d = {"name": "John", "age": 26}
c.execute("update USER set age=:age where name=:name", d)

# Suppressions
c.execute("delete from USER where name=:name", {"name": "John"})

conn.commit()

conn.close()

webbrowser

Contrôle du navigateur : documentation.

Le module ouvre une URL dans le navigateur.

import webbrowser

webbrowser.open("https://www.python.org")

Il peut être lancé en ligne de commande.

python -m webbrowser "https://www.python.org"

Python Package Index

Les modules de la communauté (Python Package Index) doivent d'abord être installés sur le système avant de pouvoir être importés dans du code Python.

arrow

Manipulation des dates et des heures : documentation et liste des codes de formatage.

Le module arrow remplace avantageusement le module datetime de la bibliothèque standard. Il ajoute des fonctionnalités telles que le timezone UTC par défaut, l'humanisation des dates, le décalage par mois...

import arrow

# Constructeurs
arrow.get(1645568542) # timestamp
arrow.get(2022, 2, 22, 22, 22, 22) # numbers
arrow.get("2022-02-22T22:22:22") # iso parse
arrow.get("2022-02-22 22:22:22", "YYYY-MM-DD HH:mm:ss") # parse
arrow.get("June was born in May 2020", "MMMM YYYY") # string search

utc = arrow.utcnow()
local = utc.to("Europe/Paris")
local = arrow.now()
local = arrow.now("Europe/Paris")

# Formatage
local.format() # '2022-02-22 22:22:22 +02:00'
local.format("YYYY-MM-DD HH:mm:ss ZZ") # '2022-02-22 22:22:22 +02:00'
local.format(arrow.FORMAT_ATOM) # '2022-02-22 22:22:22+02:00'

# Humanize
local.humanize() # 'an hour ago'
local.humanize(locale='fr') # 'il y a une heure'
future.humanize(present, granularity=["hour", "minute"]) # 'in an hour and 6 minutes'

earlier = local.dehumanize("2 days ago")

# Opérations
utc.shift(hours=-1)
utc.span("hour") # 2022-02-22T00:00:00+00:00, 2022-02-22T23:59:59.999999+00:00
utc.floor("hour") # 2022-02-22T00:00:00+00:00
utc.ceil("hour") # 2022-02-22T23:59:59.999999+00:00

ruff, black, flake8, isort

Le programme ruff combine les fonctionnalités des autres outils : formattage, validation, organisation des imports… Le formatteur black fonctionne bien en tandem avec le linter flake8 qui vérifie la conformité du code avec les conventions de codage de la PEP 8. Il peuvent être lancés en ligne de commande (idéalement dans un pre-commit) ou intégrés à un éditeur de code.

ruff check {source_file_or_directory}
ruff format {source_file_or_directory}

black {source_file_or_directory}
flake8 {source_file_or_directory}
isort {source_file_or_directory}

Configuration dans pyproject.toml :

[tool.ruff]
line-length = 100

[tool.black]
line-length = 100

[flake8]
max-line-length = 100

cookiecutter

Génération de projets à partir de modèles prédéfinis : documentation.

Exemple avec cookiecutter-django :

cookiecutter https://github.com/cookiecutter/cookiecutter-django

pytest

Les tests unitaires : documentation.

D'autres modules sont disponibles dans la bibliothèque standard : unittest et doctest. Ce dernier permet de réaliser les tests directement dans la docstring.

En général les tests sont écrits dans des fichiers test_<module>.py situés dans le répertoire tests du projet. Les fonctions doivent commencer par le préfixe test_.

import pytest
from module import add

def test_add_with_two_numbers():
    assert add(1, 2) == 3

def test_add_with_two_letters():
    assert add("a", "b") == "ab"

def test_add_with_two_none():
    with pytest.raises(TypeError):
        add(None, None)

Une fixture est définie avec l'annotation @pytest.fixture. Des fixtures sont définies par défaut comme tmp_path qui retourne un répertoire temporaire unique.

import pytest
from module import Counter

@pytest.fixture
def counter():
    return Counter(count=10)

def test_counter_inc(counter):
    counter.inc(1)
    assert counter.count == 11

def test_counter_dec(counter):
    counter.dec(1)
    assert counter.count == 9

def test_counter_dec():
    counter = Counter(count=10)
    with pytest.raises(ValueError):
        counter.dec(11)

La fixture spéciale monkeypatch aide à créer des mocks temporaires annulés à la fin du test. On peut ainsi remplacer des fonctions, des dictionnaires ou des variables d'environnement.

import pytest

def test_func(monkeypatch):
    monkeypatch.setattr(obj, name, value)
    monkeypatch.setitem(mapping, name, value)
    monkeypatch.setenv(name, value)
    assert ...

Exécution des tests et génération des rapports : il est possible de générer des rapports d'exécution avec pytest-html et de couverture de code avec coverage.

pytest tests
pytest tests -v --html=report.html
coverage run -m pytest tests
coverage report
coverage html

faker

Génération de données aléatoires : documentation.

from faker import Faker

fake = Faker(locale="fr_FR")

# Exemples de providers
fake.name()
fake.first_name()
fake.last_name()
fake.phone_number()
fake.email()
fake.address()
fake.job()
fake.text()
fake.date()
fake.rgb_color()
fake.hex_color()
fake.image()
fake.json()
fake.csv()

# Ajout d'options
fake.file_path(depth=5, category="video")

# Génération d'éléments uniques
fake.unique.random_int()

# Génération d'éléments choisis parmi une collection
fake.random_element(elements=("a", "b", "c", "d"))

# Génération de chaînes avec des caractères numériques / alphanumériques selon un template
fake.numerify(text="%%%-#-%%%%-%%%%-%%%-##")
fake.bothify(text="Product number: ????-########")

typer

Programmes en ligne de commande : documentation.

Un argument est positionnel alors qu'une option est nommée avec les caractères --.

import typer

app = typer.Typer()

def main(param: str, # Argument requis
        param: str = typer.Argument(..., help="Argument requis"),
        param: str = typer.Argument("txt", help="Argument avec valeur par défaut"),
        opt: bool = False, # Option avec valeur par défaut
        opt: bool = typer.Option(..., help="Option requise"),
        opt: bool = typer.Option(False, help="Option avec valeur par défaut"),
        ):
    """
    Description du programme.
    """

    # prompt et echo
    param = typer.prompt("Quelle est la valeur du paramètre param ?")
    typer.echo(f"Le paramètre param vaut {param}.")

    # confirm et abort
    if opt:
        typer.confirm("Voulez-vous effectuer l'action ?", abort=True)

    if opt:
        confirm = typer.confirm("Voulez-vous effectuer l'action ?")
        if not confirm:
            typer.echo("Annulation de l'action.")
            raise typer.Abort()

@app.command()
def command_txt():
    main(param="txt", opt=False)

@app.command("command-png")
def command_png():
    main(param="png", opt=True)

if __name__ == "__main__":
    # Lancement sans prise en charge des commandes @app.command
    typer.run(main)
    # Lancement avec prise en charge des commandes @app.command
    app()

Lancement du programme main.py [OPTIONS] COMMAND [ARGS] :

python main.py --help
python main.py --opt param
python main.py command-txt

Le style du texte affiché peut être personnalisé :

import typer

def main():
    world = typer.style("world", bold=True, fg=typer.colors.RED, bg=typer.colors.BLUE)
    typer.echo(f"hello {world}")

    typer.secho(f"hello world", bold=True)

if __name__ == "__main__":
    typer.run(main)

Une barre de progression :

import time
import typer

def main():
    steps = range(100)
    with typer.progressbar(steps) as progress:
        for step in progress:
            time.sleep(0.05)

if __name__ == "__main__":
    typer.run(main)

tinydb

Base de données orientée documents : documentation.

La base est enregistrée dans un fichier JSON.

from tinydb import TinyDB, Query, where

db = TinyDB("data.json", indent=4)

# Sélections
User = Query()
db.get(User.name == "John") # unique
db.search(User.age < 18) # multiple
db.search(where("name") == "John")

db.contains(User.name == "John")
db.count(User.age < 18)
db.all() # sélectionne tout
len(db)

# Opérations logiques
db.search(~ (User.name == "John"))
db.search((User.name == "John") & (User.age < 18))
db.search((User.name == "John") | (User.name == "Jane"))

# Insertions
db.insert({"name": "John", "age": 25})
db.insert_multiple([
    {"name": "Jane", "age": 30}
    {"name": "Jean", "age": 16}
    {"name": "Jeanne", "age": 16}
])

# Mises à jour
db.update({"age": 26}, where("name") == "John")
db.update({"roles": ["guest"]}) # update all entries
db.update({"roles": ["admin"]}, where("name") == "John")

db.upsert({"name": "Jay", "age": 42, "roles": ["user"]},  where("name") == "Jay")

# Suppression
db.remove(where("name") == "Jay")

db.truncate() # supprime tout

# Créer plusieurs tables
users = db.table("Users")
roles = db.table("Roles")

users.insert_multiple([
    {"name": "Jean", "age": 16}
    {"name": "Jeanne", "age": 16}
])
roles.insert_multiple([
    {"name": "admin"}
    {"name": "user"}
    {"name": "guest"}
])

Astuce : utilisation d'une base en mémoire, par exemple pour des tests unitaires.

from tinydb.storages import MemoryStorage

@pytest.fixture
def setup_db():
    db = TinyDB(storage=MemoryStorage)

psycopg

Base de données relationnelle : documentation.

La base est disponible sur un serveur PostgreSQL.

import psycopg

with psycopg.connect("dbname=test user=postgres") as conn:
    with conn.cursor() as cur:

        # Création d'une table
        cur.execute("""
            CREATE TABLE test (
                id serial PRIMARY KEY,
                num integer,
                data text)
            """)

        # Insertions
        cur.execute(
            "INSERT INTO test (num, data) VALUES (%s, %s)",
            (100, "abc'def"))

        # Sélections
        cur.execute("SELECT * FROM test")

        cur.fetchone() # (1, 100, "abc'def")
        cur.fetchmany()
        cur.fetchall()

        for record in cur:
            print(record)

        conn.commit()

requests

Requêtes HTTP : documentation.

Requêtes post et get avec paramètres et authentification :

import requests

response = requests.post('https://httpbin.org/post', data={'key': 'value'})
response = requests.get("https://api.github.com/user", auth=("user", "pass"))

response.status_code # 200
response.headers["content-type"] # 'application/json; charset=utf8'
response.encoding # 'utf-8'
response.text # '{"type":"User"...'
response.json() # {'private_gists': 419, 'total_private_repos': 77, ...}

Utilisation des sessions et des exceptions :

import requests

with requests.Session() as session:
    try:
        response = session.get("https://books.toscrape.com/")
        response.raise_for_status()
    except requests.exceptions.RequestException as e:
        logging.error(f"Error on get {url}: {e}")

    response.text

Un autre module est disponible dans la bibliothèque standard : urllib.

livereload

Rechargement automatique de pages web : documentation.

from livereload import Server

server = Server()
server.watch("dir", function)
server.watch("dir/*.md", "shell command")
server.serve(root="public")

markdown

Convertir du texte au format Markdown en HTML : documentation et liste des extensions.

from markdown import Markdown

# Initialisation du parser en activant des extensions
md = Markdown(extensions=["meta", "toc", "codehilite"])

# Conversion d'une chaîne de caractères
html = md.convert("#Hello World\n\nMy first paragraphe.")
"""html=
    <h1>Hello World</h1>
    <p>My first paragraphe.</p>
"""

# Conversion d'un fichier
md.convertFile("input.md", "output.html")

Le module python-frontmatter lit les entêtes de métadonnées front matter (YAML, TOML, JSON…) : documentation.

import frontmatter

post = frontmatter.load("input.md")
post.content    # or post
post.metadata   # or post['title']

metadata, content = frontmatter.parse(file.read())
metadata['title']

beautifulsoup

Parser des fichiers HTML : documentation.

from bs4 import BeautifulSoup

# Traiter une chaîne de caractères
soup = BeautifulSoup("<html>data</html>")

# Traiter un fichier
with open("index.html") as file:
    soup = BeautifulSoup(file, "html.parser")  # or "html5lib"

# Afficher l'arbre
soup.prettify()

# Extraire des objets
soup.title
soup.p
soup.p.name
soup.p.string
soup.p["class"]
soup.p.get("class")

# Extraire les textes
soup.get_text()

# Rechercher des objets
soup.find("h1")
soup.find_all("a", title=True, class_="class")

# Rechercher des objets avec un sélecteur CSS
soup.selectone("h1")
soup.select("a[title].class")

Autres fonctions utiles :

Astuce : selectolax est un parser plus rapide.

from selectolax.parser import HTMLParser

tree = HTMLParser(response.text)
tree.css("a.class")
tree.css_first("a.class")

Pour aller plus loin, scrapy est un framework de scraping permettant d'extraire des données depuis des sites web.

feedparser

Parser des flux RSS ou Atom : documentation.

import feedparser

feedUrl = "http://feedparser.org/docs/examples/atom10.xml"
data = feedparser.parse(feedUrl)

# Deux notations : data["feed"] ou data.feed
title = data["feed"]["title"]
for entry in data.entries:
    print(f"{entry.title}: {entry.link}")

qrcode

Génération de codes QR : documentation.

import qrcode
from qrcode.image.pure import PyPNGImage
from qrcode.image.svg import SvgPathImage

# format PNG
img = qrcode.make("Some data", image_factory=PyPNGImage)
img.save("file.png")

# format SVG
img = qrcode.make("Some data", image_factory=SvgPathImage)
img.to_string(encoding="unicode")

pydenticon

Génération d'identicons : documentation.

import pydenticon

generator = pydenticon.Generator(5, 5)

# format PNG
identicon_png = generator.generate("email@example.com", 200, 200, output_format="png")

# format ASCII
identicon_ascii = generator.generate("email@example.com", 200, 200, output_format="ascii")

pillow

Traitement d'images : documentation.

from PIL import Image, ImageDraw, ImageEnhance, ImageOps, ImageFont
import piexif

im = Image.open("image.png")

# conversion (couche alpha)
im1 = im.convert("RGB")
im2 = Image.new("RGB", im.size, color="red")
im2.paste(im, im)
compare(im, im1, im2)

im2.save("image-red.jpg")

# transposition, rotation
im1 = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
im2 = im.rotate(45, fillcolor="green", expand=True)
compare(im, im1, im2)

# noir et blanc
couches = im.split()
im1 = im.convert("L")
compare(*couches, im1)

# saturation, contrast, netteté, luminosité
images = []
for i in range(5, 21, 5):
    im_filtre = ImageEnhance.Color(im).enhance(i / 10)
    im_filtre = ImageEnhance.Contrast(im).enhance(i / 10)
    im_filtre = ImageEnhance.Sharpness(im).enhance(i / 10)
    im_filtre = ImageEnhance.Brightness(im).enhance(i / 10)
    images.append(im_filtre)
compare(*images)

# filtre sépia
im1 = im.convert("L")
im2 = ImageOps.colorize(im1, (132, 84, 129), (240, 176, 113))
im3 = ImageEnhance.Contrast(im2).enhance(3)
im4 = ImageEnhance.Color(im3).enhance(0.5)
compare(im, im1, im2, im3, im4)

# filtre dégradé
gradient = Image.open("gradient.png")
gradient = gradient.resize(im.size)
images = []
for i in range(1, 5):
    im_filtre = Image.blend(im, gradient, i / 10)
    images.append(im_filtre)
compare(*images)

# redimensionnement
facteur = 3
taille = (round(im.size[0] / facteur)), (round(im.size[1] / facteur))
im.resize(taille).show()

# vignettage (ratio conservé, métadonnées supprimées)
im.thumbnail((300, 300))
im.show()

# affichage d'un logo et d'un filigrane
logo = Image.open("logo.png")
font_path = "roboto.ttf"
copyright_logo(im, logo, "hd", 20)
copyright_watermark(im, "nora.nckm.eu", font_path, 0.3, 30)

# métadonnées
exif_dict = piexif.load(im.info["exif"])
exif_dict = piexif.load("image.png")
exif_dict["0th"][272] = "Canon 5D"
exif_bytes = piexif.dump(exif_dict)
im.save("image.png", exif_bytes=exif_bytes)

Fonctions utilisées par le script précédent :

def compare(*args):
    largeur, hauteur = zip(*(i.size for i in args))

    largeur_totale = sum(largeur)
    hauteur_maximale = max(hauteur)

    image_composite = Image.new("RGB", (largeur_totale, hauteur_maximale))

    offset_x = 0
    for im in args:
        image_composite.paste(im, (offset_x, 0))
        offset_x += im.size[0]

    image_composite.show()

def copyright_logo(image, logo, position, marge):
    largeur, hauteur = image.size
    logo_largeur, logo_hauteur = logo.size
    coord = {"hg": (0 + marge, 0 + marge),
            "bg": (0 + marge, hauteur - marge - logo_hauteur),
            "hd": (largeur -  marge - logo_largeur, 0 + marge),
            "bd": (largeur -  marge - logo_largeur, hauteur - marge - logo_hauteur)}

    image = image.convert("RGBA")
    logo = logo.convert("RGBA")

    image.paste(logo, coord[position], logo)
    image.show()

def copyright_watermark(image, texte, font_path, opacity=1.0, rotation=30):
    image = image.convert("RGBA")
    texte_image = Image.new("RGBA", image.size, (255,255,255,0))

    font_size = 1
    font = ImageFont.truetype(font_path, font_size)
    while font.getsize(texte)[0] < image.size[0]:
        font_size += 1
        font = ImageFont.truetype(font_path, font_size)

    texte_height = font.getsize(texte)[1]
    pos = (0, (image.size[1] / 2) - texte_height / 2)

    draw = ImageDraw.Draw(texte_image)
    draw.text(pos, texte, fill=(255, 255, 255, round(opacity * 255)), font=font)

    texte_image = texte_image.rotate(rotation)

    Image.alpha_composite(image, texte_image).show()

pandas

Analyse de données : documentation.

Data Science

Plusieurs possibilités pour installer l'environnement de développement Jupyter :

  1. ancien module jupiter compatible avec l'extension Jupyter de Visual Studio Code et PyCharm. Interface web : jupyter notebook
  2. nouveau module jupyterlab. Interface web : jupyter-lab

Pandas utilise deux structures de données : un DataFrame constitué de Series. Un DataFrame est une structure en deux dimensions (tableau contenant des lignes et des colonnes). Une Series est une structure en une dimension (map contenant des clés et des valeurs).

import pandas as pd

# Lecture d'un fichier
df = pd.read_csv("data.csv")

# Analyse du dataframe
df.head(10) # premières lignes
df.tail(10) # dernières lignes
df.shape # nombre de lignes et colonnes
df.dtypes # type des colonnes
df.columns # colonnes; liste avec tolist()
df.index # index des lignes (numéros de lignes)
df.set_index("email") # modifier l'index (colonne "email")

# Sélection de données
df["email"]
df.email
df[10:20] # lignes avec une position entre 10 et 19
df.loc[10:20] # lignes avec un index entre 10 et 20

df_email = df.set_index("email")
df_email.loc['john@example.com']
df_email.loc['john@example.com'].values
df_email.loc['john@example.com', 'jane@example.com']

# Filtrage de données
df.gender == "Male" # Series
df[df.gender == "Male"] # Dataframe
df[df.country.isin(("France", "Canada"))]
df[df.price_paid >= 5]
df[df.duration.str.contains("min")].duration.str.replace(" min", "")

# Suppression de colonnes
del df["ip_address"]
df.drop("ip_address", axis=1, inplace=True)
df.drop(["first_name", "last_name"], axis=1, inplace=True)
df.set_index("gender", inplace=True)
df.drop("Male", axis=0, inplace=True)

# Création et modification de colonnes
df.price_paid = df.price_paid.apply(lambda x: x.replace("$", ""))
df.price_paid = df.price_paid.astype(float)
df["price_total"] = df["price_paid"] * (1 - df["tax"] / 100)

countries = {"United States": "US", "France": "FR", "Canada": "CA"}
df["country_code"] = df["country"].map(countries)

# Valeurs manquantes
df.isnull()
df.notnull()
df[df.tax.notnull()]
df.tax.fillna(0)
df.tax.fillna(method='bfill')
df.tax.dropna()
df.dropna(subset=["tax"])

# Analyse des données
df.describe()
df.price_paid.describe()
df.price_paid.mean()
df.price_paid.sum()
df.price_paid.min()
df.price_paid.max()
df.price_paid.map(int)
df.country.unique()
df.country.value_counts()
df.country.value_counts(normalize=True))
df.country.value_counts().sort_index(ascending=False)

df.groupby("country").mean()
df.groupby("gender")["price_total"].mean()
df.groupby(["gender", "country"]).mean()

# Graphiques avec matplotlib
df.groupby("country")["price_total"].sum().plot(figsize=[20, 10])
df.groupby("country")["price_paid"].sum().plot.bar(rot=45, legend=True)
df.groupby("country")["price_paid"].sum().plot.pie(legend=True)

Le paramètre inplace permet de modifier le dataframe directement au lieu d'en retourner une copie. Les deux lignes suivantes sont équivalentes :

df = df.set_index("col")
df.set_index("col", inplace=True)

stripe

Paiement en ligne : documentation.

Il faut créer un compte sur Stripe. Ensuite la documentation explique comment implémenter le paiement en ligne dans differents langages de programmation.

Attention : les développement doivent être réalisés avec le mode test de Stripe. De plus le client Stripe doit être installé pour rediriger les évènements vers le serveur Web local.

stripe login
stripe status
stripe listen --forward-to localhost:8000/store/stripe-webhook/

Implémentation avec le framework Django. Les clés de sécurité STRIPE_API_KEY et STRIPE_WEBHOOK_KEY sont définies dans un fichier .env.

Le gabarit dans store/cart.html :

<!-- cart.html -->
<form action="{% url 'store:checkout-create-session' %}" method="POST">
    {% csrf_token %}
    <button type="submit">Procéder au paiement</button>
</form>

Les routes dans le fichier store/urls.py :

path('cart/checkout/create-session/', checkout_create_session, name='checkout-create-session'),
path('cart/checkout/success/', checkout_success, name='checkout-success'),
path('stripe-webhook/', stripe_webhook, name='stripe-webhook'),

La vue dans store/views.py :

import stripe
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from accounts.models import Shopper
from website import settings

def checkout_create_session(request):
    cart = request.user.cart
    line_items = [{
        "price_data": {
            "currency": "EUR",
            "unit_amount": int(order.product.price * 100),
            "product_data": {
                "name": order.product.name,
                "images": [request.build_absolute_uri(order.product.thumbnail.url)],
            }
        },
        "quantity": order.quantity,
    } for order in cart.orders.all()]

    checkout_data = {
        "line_items": line_items,
        "mode": 'payment',
        "payment_method_types": ['card'],
        "shipping_address_collection": {"allowed_countries": ["FR", "BE", "CH"]},
        "success_url": request.build_absolute_uri(reverse("store:checkout-success")),
        "cancel_url": request.build_absolute_uri(reverse("store:cart")),
    }

    if request.user.stripe_id:
        checkout_data["customer"] = request.user.stripe_id
    else:
        checkout_data["customer_email"] = request.user.email

    stripe.api_key = settings.STRIPE_API_KEY
    checkout_session = stripe.checkout.Session.create(**checkout_data)

    return redirect(checkout_session.url, code=303)

def checkout_success(request):
    return render(request, "store/success.html")

@csrf_exempt
def stripe_webhook(request):
    payload = request.body
    sig_header = request.META['HTTP_STRIPE_SIGNATURE']
    endpoint_secret = settings.STRIPE_WEBHOOK_KEY
    event = None

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, endpoint_secret
        )
    except ValueError as e:
        return HttpResponse("Invalid payload", status=400)
    except stripe.error.SignatureVerificationError as e:
        return HttpResponse("Invalid signature", status=400)

    if event['type'] == 'checkout.session.completed':
        session = event['data']['object']

        try:
            user = get_object_or_404(Shopper, email=session['customer_details']['email'])
        except KeyError:
            return HttpResponse("Invalid user email", status=400)

        user.stripe_id = session['customer']
        user.cart.delete()
        user.save()

    return HttpResponse(status=200)
Emojis

Un commentaire sur un de mes articles ? Commencez une discussion sur ma liste de diffusion en envoyant un email à ~nora/public-inbox@lists.sr.ht [règles]