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
Il peut être lancé en ligne de commande.
python -m random 1.0
python -m random 10
python -m random rouge vert bleu
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
- Formattage et validation de code : documentation
- Formattage de code : documentation
- Validation de code : documentation
- Organisation des imports : documentation
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
.
tablib
Manipulation de données tabulaires (JSON, YAML, CSV, XLS…) : documentation.
import tablib
data = tablib.Dataset(headers=['First Name', 'Last Name', 'Age'])
for i in [('Kenneth', 'Reitz', 22), ('Bessie', 'Monke', 21)]:
data.append(i)
data.dict
data = tablib.Dataset()
with open('excel_file.xlsx', 'rb') as fh:
data.load(fh, 'xlsx')
data.export('csv')
data.export('json')
data.export('yaml')
pydantic
Validation de données : documentation.
Pydantic gère la validation et la serialisation des données grâce aux annotations de type.
from datetime import datetime
from pydantic import BaseModel, PositiveInt, ValidationError
class User(BaseModel):
id: int
name: str = 'John Doe'
signup_ts: datetime | None
tastes: dict[str, PositiveInt]
external_data = {
'id': 123,
'signup_ts': '2019-06-01 12:22',
'tastes': {
'wine': 9,
b'cheese': 7,
'cabbage': '1',
},
}
user = User(**external_data)
print(user.model_dump())
"""
{
'id': 123,
'name': 'John Doe',
'signup_ts': datetime.datetime(2019, 6, 1, 12, 22),
'tastes': {'wine': 9, 'cheese': 7, 'cabbage': 1},
}
"""
external_data = {'id': 'not an int', 'tastes': {}}
try:
User(**external_data)
except ValidationError as e:
print(e.errors())
"""
[
{
'type': 'int_parsing',
'loc': ('id',),
'msg': 'Input should be a valid integer, unable to parse string as an integer',
'input': 'not an int',
'url': 'https://errors.pydantic.dev/2/v/int_parsing',
},
{
'type': 'missing',
'loc': ('signup_ts',),
'msg': 'Field required',
'input': {'id': 'not an int', 'tastes': {}},
'url': 'https://errors.pydantic.dev/2/v/missing',
},
]
"""
Gestion de configuration : documentation.
Pydantic Settings gère le chargement et la validation de la configuration depuis la ligne de commande, un fichier ou des variables d'environnement.
from typing import Any, Callable, Set
from pydantic import AliasChoices, AmqpDsn, BaseModel, Field, ImportString, PostgresDsn, RedisDsn
from pydantic_settings import BaseSettings, SettingsConfigDict
class SubModel(BaseModel):
foo: str = 'bar'
apple: int = 1
class Settings(BaseSettings):
auth_key: str = Field(validation_alias='my_auth_key')
api_key: str = Field(alias='my_api_key')
redis_dsn: RedisDsn = Field(
'redis://user:pass@localhost:6379/1',
validation_alias=AliasChoices('service_redis_dsn', 'redis_url'),
)
special_function: ImportString[Callable[[Any], Any]] = 'math.cos'
# to override domains:
# export my_prefix_domains='["foo.com", "bar.com"]'
domains: Set[str] = set()
# to override more_settings:
# export my_prefix_more_settings='{"foo": "x", "apple": 1}'
more_settings: SubModel = SubModel()
model_config = SettingsConfigDict(env_prefix='my_prefix_')
print(Settings().model_dump())
"""
{
'auth_key': 'xxx',
'api_key': 'xxx',
'redis_dsn': Url('redis://user:pass@localhost:6379/1'),
'special_function': math.cos,
'domains': set(),
'more_settings': {'foo': 'bar', 'apple': 1},
}
"""
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 :
- Naviguer dans l'arbre avec
parents
,children
,next_siblings
… - Rechercher dans l'arbre avec
find_parents
,find_children
,find_next_siblings
… - Modifier l'arbre avec
append
,insert
,extract
,clear
…
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 :
- ancien module
jupiter
compatible avec l'extension Jupyter de Visual Studio Code et PyCharm. Interface web :jupyter notebook
- 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)