Aller au contenu

Python⚓︎

Voisinage⚓︎

🐣 2022-08

Lorsqu’on parcoure une liste, il est parfois utile d’avoir accès à l’élément précédent et au suivant, pour faire par exemple des liens suivant/précédent entre des billets de blog.

Cette implémentation permet de spécifier le premier et le dernier item de la liste au besoin, ce qui permet de lier vers d’autres contenus par exemple.

neighborhood.py
def neighborhood(iterable, first=None, last=None):  # (1)!
    """
    Yield the (previous, current, next) items given an iterable.
    You can specify a `first` and/or `last` item for bounds.
    """
    iterator = iter(iterable)
    previous = first
    current = next(iterator)  # Throws StopIteration if empty.
    for next_ in iterator:
        yield (previous, current, next_)
        previous = current
        current = next_
    yield (previous, current, last)
  1. 🙋‍♂️ iterable peut être une liste, un set ou toute structure… itérable !

Tout se passe au moment du yield, on garde en mémoire le précédent et le courant lors du parcours de l’itérateur et… on n’oublie pas le dernier !

Lister des dossiers/fichiers⚓︎

🐣 2022-08

Il est courant en Python de devoir parcourir des fichiers ou des dossiers. Ces deux méthodes permettent de récupérer les chemins des dossiers ou des fichiers à partir d’une racine.

each-from.py
import Path


def each_file_from(source_dir, pattern="*", exclude=None):  # (1)!
    """Walk across the `source_dir` and return the md file paths."""
    for path in _each_path_from(source_dir, pattern=pattern, exclude=exclude):
        if path.is_file():
            yield path


def each_folder_from(source_dir, exclude=None):
    """Walk across the `source_dir` and return the folder paths."""
    for path in _each_path_from(source_dir, exclude=exclude):
        if path.is_dir():
            yield path


def _each_path_from(source_dir, pattern="*", exclude=None):
    for path in sorted(Path(source_dir).glob(pattern)):
        if exclude is not None and path.name in exclude:
            continue
        yield path
  1. Le paramètre de pattern est bien pratique pour ne récupérer que des *.md par exemple.

Générer un md5⚓︎

🐣 2022-08

Parce que j’oublie à chaque fois comment faire…

md5.py
import hashlib


def generate_md5(content):
    """Generate a md5 string from a given string.

    >>> generate_md5("foo")
    'acbd18db4cc2f85cedef654fccc4a4d8'
    """
    return hashlib.md5(content.encode()).hexdigest()

Il y aurait probablement de meilleurs algorithmes à utiliser selon les cas.

Générer une chaîne de 128 bits⚓︎

🐣 2022-08

Pour avoir des slugs dans une URL par exemple.

128bits-string.py
import base64
import uuid


def generate_128bits():
    """Generate a 128bits string, useful for URLs.

    >>> generate_128bits()
    'LQ0HB7ksTdesCuSms-I98Q'
    """
    return base64.urlsafe_b64encode(uuid.uuid4().bytes)[:22].decode()

Pour un usage réel, il faudrait exclure certains caractères qui peuvent prêter à confusion. Mais qui dicte des URLs ?

🐣 2022-12

Une autre façon de générer des tokens aléatoires :

$ python3 -c 'print(__import__("secrets").token_hex(16))'
fcaef3ee71f32009fe240661fffa693c

Générer une data URI⚓︎

🐣 2022-08

Il est possible de transformer des images en chaîne de caractère afin de les insérer directement dans le HTML.

data-uri.py
import base64
import mimetypes


def img_to_data(path):
    """Convert a file (specified by a path) into a data URI."""
    mime, _ = mimetypes.guess_type(path)
    with open(path, "rb") as fp:
        data = fp.read()
        data64 = "".join(base64.encodestring(data).splitlines())
        return f"data:{mime};base64,{data64}"

Utiliser des ULID (vs. UUID)⚓︎

🐣 2022-09

Pour obtenir de l’unicite (pour des clés primaires par exemple), mieux que des UUID, il y a les ULID :

Récupérer la valeur epoch depuis une date ISO⚓︎

Parce que j’ai été surpris que ce soit aussi compliqué. Je suis peut-être passé à côté d’un truc plus élégant…

to-epoch.py
import time
from datetime import date


def iso8601toEpoch(iso8601):
    """The name says it all, there has to be a better way.

    >>> iso8601toEpoch('2021-07-11')
    1625976000.0
    """
    return time.mktime(
        date(*[int(part) for part in iso8601.split("-")]).timetuple()
    )  # fmt: off (1)
  1. Désactivation de black : on veut faire rentrer le code dans cette page manuellement.

TODO

Configurer black pour avoir des lignes plus courtes par défaut.

📱 Mot de passe à usage unique⚓︎

🐣 2022-10

Un bout de code qui vient de SourceHut pour générer un mot de passe à usage unique basé sur le temps.

totp-2fa.py
import base64
import hashlib
import hmac
import struct
import time


def totp(secret, token):
    tm = int(time.time() / 30)
    key = base64.b32decode(secret)

    for ix in range(-2, 3):
        b = struct.pack(">q", tm + ix)
        hm = hmac.HMAC(key, b, hashlib.sha1).digest()
        offset = hm[-1] & 0x0F
        truncatedHash = hm[offset : offset + 4]
        code = struct.unpack(">L", truncatedHash)[0]
        code &= 0x7FFFFFFF
        code %= 1000000
        if token == code:
            return True

    return False

Je ne comprends pas tout mais j’espère pouvoir m’en resservir un jour (et le comprendre à ce moment là…).

La version anglaise que je ne me risque pas à frangliser :

The algorithm is as follows:

  1. Divide the current Unix timestamp by 30
  2. Encode it as a 64-bit big endian integer
  3. Write the encoded bytes to a SHA-1 HMAC initialized with the TOTP shared key
  4. Let offs = hmac[-1] & 0xF
  5. Let hash = decode hmac[offs .. offs + 4] as a 32-bit big-endian integer
  6. Let code = (hash & 0x7FFFFFFF) % 1000000
  7. Compare this code with the user’s code

You’ll need a little dependency to generate QR codes with the otpauth:// URL scheme, a little UI to present the QR code and store the shared secret in your database, and a quick update to your login flow, and then you’re good to go.

This implementation has a bit of a tolerance added to make clock skew less of an issue, but that also means that the codes are longer-lived.

Installer un paquet depuis une branche distante avec pip⚓︎

🐣 2023-01

Il est possible d’installer un paquet depuis une branche distante avec l’utilitaire pip avec cette ligne de commande :

pip install https://github.com/<user>/<repo>/archive/refs/heads/<branch>.zip

C’est très pratique pour donner du feedback sur une pull-request par exemple :

pip install https://github.com/simonw/datasette-insert/archive/refs/heads/cors.zip --upgrade

Ou pour installer la dernière version de la branche principale :

pip install https://github.com/buriy/python-readability/archive/refs/heads/master.zip --upgrade

Faire des requêtes HTTP en Python sans lib⚓︎

🐣 2023-01

J’ai découvert cela grâce au AdvancedRestClient qui propose des exemples de code en Python pour reproduire les requêtes réalisées via l’outil, dont une version avec les batteries inclues :

native-http-request.py
import http.client

headers = {
    "content-type": "application/json",
    "authorization": "Bearer json-token",
}
body = """{
    "query": "query($user:ID!){foo}",
    "variables": {"user": "bar"}
}"""

conn = http.client.HTTPSConnection("example.com")
conn.request("POST", "/graphql", body, headers)
res = conn.getresponse()

data = res.read()
print(res.status, res.reason)
print(data.decode("utf-8"))
print(res.getheaders())

Je ne l’ai pas encore utilisé car je favorisais jusqu’à présent HTTPX qui est maintenu par des personnes en qui j’ai confiance.

Tâches asynchrones⚓︎

🐣 2022-09

Surtout des liens vers les projets existants dont j’ai toujours du mal à me rappeler :

  • RQ (Redis Queues) : is a simple Python library for queueing jobs and processing them in the background with workers.
  • Procrastinate : PostgreSQL-based Task Queue for Python
  • wakaq : Distributed background task queue for Python backed by Redis, a super minimal Celery
  • Dramatiq : Dramatiq is a background task processing library for Python with a focus on simplicity, reliability and performance.
  • huey : a lightweight alternative.
  • Nameko : A microservices framework for Python that lets service developers concentrate on application logic and encourages testability.
  • Background Tasks : Starlette includes a BackgroundTask class for in-process background tasks. A background task should be attached to a response, and will run only once the response has been sent.
  • Chard : is a very simple async/await background task queue for Django. One process, no threads, no other dependencies.

🗃 Forcer un environnement virtuel⚓︎

🐣 2023-10

J’avais l’habitude de mettre dans mes Makefile :

python3 -m pip install --editable .

Si on veut s’assurer que ce ne sera pas fait en dehors d’un environnement virtuel, il est possible d’ajouter :

python3 -m pip --require-virtualenv install --editable .

Il est aussi possible de configurer pip pour le faire de manière globale sur son système :

python3 -m pip config set global.require-virtualenv True

Sous macOS, cela va créer un fichier ~/.config/pip/pip.conf avec le contenu suivant :

[global]
require-virtualenv = True

Retirer les accents d’une chaîne de caractères⚓︎

🐣 2023-10

On utilise la lib standard avec unicodedata qui permet de découper les caractères sous leur forme normale.

On exclue les caractères qui sont dans la catégorie des Nonspacing_Mark (Mn), correspondant à des marqueurs de caractères diacritiques (soyons honnête, je n’ai pas tout compris mais ça semble marcher…).

strip-accents.py
import unicodedata


def strip_accents(text):
    # See https://stackoverflow.com/a/518232
    return "".join(
        c
        for c in unicodedata.normalize("NFD", text)
        if unicodedata.category(c) != "Mn"  # fmt: off (1)
    )
  1. Désactivation de black : on veut faire rentrer le code dans cette page manuellement.

Retirer la ponctuation d’une chaîne de caractères⚓︎

🐣 2023-10

J’aime bien cette solution car elle utilise au maximum la bibliothèque standard 🐍🔋.

strip-punctuation.py
import string


def strip_punctuation(text):
    # See https://stackoverflow.com/a/266162 (adapted for French texts).
    # The two last characters are NBSP and NNBSP.
    additional_punctuation = "«»’“”…•·—–  "  # (1)
    # string.punctuation == !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~
    punctuation = string.punctuation + additional_punctuation
    return text.translate(str.maketrans(punctuation, " " * len(punctuation)))  # (2)
  1. Ces caractères additionnels sont intéressants pour des textes en français, vous pouvez les adapter.
  2. Ici on remplace chaque caractère de ponctuation par une espace simple, à adapter aussi !

Extraire les mots signifiants d’un texte⚓︎

🐣 2023-10

filter-words.py
def filter_words(text):
    # French stopwords from https://github.com/stopwords-iso/stopwords-fr
    # Walrus operator (:=) requires Python 3.8+.
    return set(
        word_strip_lower
        for word in text.split(" ")
        if (word_strip := word.strip())  # (1)
        and (word_strip_lower := word_strip.lower()) not in STOP_WORDS
        and not word_strip.isdigit()
    )
  1. C’est la première fois que je me permettais d’utiliser le walrus operator sur un projet. Pas sûr que ce soit vraiment plus lisible hein 😅

L’idée est de combiner les trois précédents bouts de code ainsi :

example.py
1
2
3
4
data = f"{description} {mots_cles} {solutions}"
data = strip_punctuation(data)
data = " ".join(filter_words(data))
data = strip_accents(data)

Voir aussi cette entrée JavaScript pour avoir la logique côté client.

Comparer des chaînes de caractères⚓︎

🐣 2023-12

James Bennett explique en détail pourquoi et je découvre au passage str.casefold() qui est bien plus pertinent qu’un str.lower() pour des comparaisons :

compare-strings.py
1
2
3
4
5
from unicodedata import normalize


def compare_strings(s1: str, s2: str) -> bool:
    return normalize("NFKC", s1).casefold() == normalize("NFKC", s2).casefold()

Télécharger une feuille Google en CSV⚓︎

🐣 2023-10

C’est une chose qui m’arrive très souvent car j’apprécie de travailler avec des personnes qui ne sont pas techniques et je ne veux pas (immédiatement :p) leur imposer mes outils de saisie de données.

download-googlesheet-as-csv.py
from pathlib import Path
from urllib.parse import urlencode, urlparse, urlunsplit

import httpx


def convert_sheet_url_to_csv_download_url(urlstring):
    """
    From:
    https://docs.google.com/spreadsheets/d/ID/edit
    To:
    https://docs.google.com/spreadsheets/d/ID/export?format=csv&id=ID
    """
    parse_result = urlparse(urlstring)
    # /spreadsheets/d/ID/edit
    # => ID
    gsheet_id = parse_result.path.split("/")[3]
    url_path = parse_result.path
    url_path = url_path.replace("/edit", "/export")
    params = {"format": "csv", "id": gsheet_id}
    url = urlunsplit(
        (parse_result.scheme, parse_result.netloc, url_path, urlencode(params), "")
    )
    return url


def download_googlesheet_as_csv():
    sheet_url = "https://docs.google.com/spreadsheets/d/ID/edit"  # (1)
    csv_url = convert_sheet_url_to_csv_download_url(sheet_url)
    response = httpx.get(csv_url, follow_redirects=True)
    response.raise_for_status()
    (Path() / "data.csv").write_text(response.text)
  1. À passer en paramètre de la fonction au besoin.

Ce n’est pas destiné à être trop robuste mais de toute façon les URLs de Google sont encore moins pérennes…

Générer un ZIP de fichiers tout en mémoire⚓︎

🐣 2023-11

Info

Je mets la vue Django complète extraite du code de uMap car ça donne un cas concrêt.

Ici on veut boucler sur les geojson des cartes et en faire un zip à télécharger. Mais sans pour autant stocker les fichiers geojson ni le zip final donc tout passe par des objets BytesIO et StringIO.

zip-in-memory.py
import io
import json
import zipfile

from django.http import HttpResponse


def render_to_response(self, context, *args, **kwargs):
    zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
        for map_ in self.get_maps():
            map_geojson = map_.generate_geojson(self.request)
            geojson_file = io.StringIO(json.dumps(map_geojson))
            file_name = f"umap_backup_{map_.slug}.umap"
            zip_file.writestr(file_name, geojson_file.getvalue())

    response = HttpResponse(zip_buffer.getvalue(), content_type="application/zip")
    response["Content-Disposition"] = 'attachment; filename="umap_backup_complete.zip"'
    return response

J’étais bien content de pouvoir faire ça en quelques lignes car j’avais un doute sur la faisabilité.

Si vous avez besoin de le faire côté client en JavaScript.

Script Python autonome avec uv⚓︎

🐣 2024-10

Un moyen de rendre n’importe quel script Python exécutable sous réserve qu’il y ait uv d’installé sur la machine (ce qui est moins contraignant/fastidieux que d’expliquer ce qu’est un virtualenv…). Merci Simon Willison !

uv-script.py
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "flask==3.*",
# ]
# ///
from flask import Flask

app = Flask(__name__)


@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

Et ensuite :

chmod 755 uv-script.py
./uv-script.py # (1)!
  1. Le nom du fichier n’est bien évidemment pas significatif.

C’est vraiment un des avantages de uv de pouvoir se passer de la partie virtualenv dans certains contextes, notamment pédagogiques.

Pour jouer⚓︎

🐣 2022-11