Embarquez dans le voyage Python

Embarquez dans votre voyage Python : Maîtriser les classes, l'abstraction, les packages et Poetry pour les développeurs en herbe !

Introduction

Dans ce guide, les développeurs en herbe exploreront les éléments fondamentaux de Python qui sont cruciaux pour construire des applications robustes. De la compréhension des classes et de l’abstraction à l’organisation du code avec des packages et des modules, vous acquerrez des connaissances pratiques pour structurer efficacement vos projets.

Gestion des packages et des dépendances

La gestion des packages Python est le processus de gestion des dépendances et des packages utilisés par les projets Python. Cela inclut l’installation, la mise à niveau et la suppression des packages, ainsi que le suivi de leurs versions, des exigences et la gestion des versions de Python. Les outils populaires sont : Pyenv, Poetry, UV, Conda

A - Gérer la version de Python avec Pyenv

Pyenv est un outil qui vous permet de gérer plusieurs versions de Python sur votre ordinateur. Il aide à installer et à basculer facilement entre différentes versions de Python, permettant aux développeurs de travailler avec divers projets nécessitant des versions spécifiques de Python.

Pour installer Pyenv, suivez ces étapes : Allez sur le repo Github et suivez les étapes selon votre environnement pour installer les dépendances.

Installation

curl https://pyenv.run | bash

Cela installera pyenv ainsi que quelques plugins utiles :

  • pyenv : L’application pyenv proprement dite
  • pyenv-virtualenv : Plugin pour pyenv et les environnements virtuels
  • pyenv-update : Plugin pour mettre à jour pyenv
  • pyenv-doctor : Plugin pour vérifier que pyenv et les dépendances de construction sont installés
  • pyenv-which-ext : Plugin pour rechercher automatiquement les commandes système

Lister toutes les versions disponibles de Python

Après avoir installé Pyenv, vous pouvez commencer à gérer les versions de Python sur votre machine. Pour lister toutes les versions de Python disponibles pouvant être installées avec Pyenv, exécutez :

pyenv install --list

Pour installer une version spécifique de Python, utilisez la commande install suivie du numéro de version souhaité. Par exemple, pour installer Python 3.10.8, exécutez :

pyenv install 3.13.0

Une fois installé, vous pouvez lister toutes les versions de Python disponibles sur votre système avec :

pyenv versions

Si, par exemple, vous vouliez utiliser la version 3.13.0, vous pouvez utiliser la commande globale :

pyenv global 3.13.0
python -V # pour confirmer la version de python

Avec Pyenv, la gestion des versions de Python et la création d’environnements virtuels deviennent beaucoup plus simples, vous permettant de travailler efficacement sur divers projets avec différentes exigences Python.

B - Gérer les dépendances et le packaging des projets Python avec Poetry

Poetry est un outil Python conçu pour la gestion des dépendances et le packaging des projets Python. Il vise à simplifier le processus d’installation, de gestion et de partage des packages Python en fournissant une interface en ligne de commande facile à utiliser.

Installation

Vous pouvez installer Poetry en utilisant pip ou en utilisant l’installateur officiel comme ci-dessous :

curl -sSL https://install.python-poetry.org | python3 -

Utilisation de base

Pour créer un nouveau projet nommé poetry-demo

poetry new poetry-demo
# Created package poetry_demo in poetry-demo

Cela créera le répertoire poetry-demo et le package python poetry_demo. L’arborescence du répertoire ressemble à :

poetry-demo
├── README.md
├── poetry_demo
│   └── __init__.py
├── pyproject.toml
└── tests
    └── __init__.py

Le fichier pyproject.toml est ce qui est le plus important ici. Cela orchestrera votre projet et ses dépendances. Pour l’instant, il ressemble à ceci :

[tool.poetry]
name = "poetry-demo"
version = "0.1.0"
description = ""
authors = ["James Kokou GAGLO <[email protected]>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

La version de Python est juste une déclaration, Poetry n’installera pas la version de Python mentionnée. La version de Python doit être disponible sur votre système.

Gestion des dépendances

Pour ajouter des dépendances, vous pouvez exécuter :

poetry add playwright # ajouter la dépendance playwright

Gestion des environnements virtuels

Poetry peut gérer vos environnements virtuels. Vous devez avoir l’exécutable python dans votre PATH pour pouvoir basculer entre les environnements.

poetry env use python3.12 # créer un environnement virtuel avec python3.12
poetry env list # lister tous les environnements virtuels
poetry env info

Poe, un plugin Poetry pour la gestion des tâches

Poe the Poet est un plugin utile de Poetry qui aide à exécuter des tâches.

pip install poethepoet

Nous pouvons définir des tâches dans notre pyproject.toml

[tool.poe.tasks]
py-version         = "python --version"               

Nous pouvons ensuite exécuter la tâche en utilisant la commande Poetry

poetry poe py-version  

C - Module et package Python

En Python, un module est un fichier unique qui contient du code et des définitions Python. Il peut être importé dans d’autres scripts ou programmes Python en utilisant l’instruction import. Un package, en revanche, est un répertoire qui contient un ou plusieurs modules Python ainsi qu’un fichier spécial appelé __init__.py.

Chaque module a un attribut intégré appelé __name__, qui est défini sur le nom du module. Si un script est exécuté directement, __name__ est égal à __main__. Considérez l’arborescence du package poetry_demo ci-dessous :

poetry-demo
├── README.md
├── poetry.lock
├── poetry_demo
│   ├── __init__.py
│   ├── main.py
│   ├── module1.py
│   ├── subpackage1
│   │   ├── __init__.py
│   │   └── module2.py
│   └── subpackage2
│       ├── __init__.py
│       └── module3.py
├── pyproject.toml
└── tests
  └── __init__.py

Voici les imports pour utiliser les modules des packages dans poetry_demo/main.py

# main.py
import module1
from subpackage1 import module2
from subpackage2 import module3

print(module1.func1())  # Output: Ceci est func1 de poetry_demo.module1
print(module2.func2())  # Output: Ceci est func2 de poetry_demo.subpackage1.module2
print(module3.func3())  # Output: Ceci est func3 de poetry_demo.subpackage2.module3

D - Concepts Python et POO

Héritage

Voici comment étendre les classes et remplacer les méthodes.

class Animal:
    def speak(self):
        return "Je fais un son."

class Dog(Animal):
    def speak(self):
        return "Woof!"

dog = Dog()
print(dog.speak())  # Output: "Woof!"

Utilisez super() pour accéder aux méthodes de la classe parente.

Polymorphisme

Utilisez des méthodes avec le même nom dans différentes classes :

class Cat:
    def speak(self):
        return "Meow!"

class Dog:
    def speak(self):
        return "Woof!"

animals = [Cat(), Dog()]
for animal in animals:
    print(animal.speak())

Abstraction

Le package abc (Abstract Base Classes) est utilisé pour définir les méthodes requises pour les sous-classes :

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

Méthodes de classe et méthodes statiques

@classmethod : C’est un décorateur qui indique à Python que la fonction suivante (méthode) est destinée à être utilisée comme méthode de classe. def from_string(cls) : Cela définit la méthode from_string qui appartient à la classe. Le paramètre cls est une référence à la classe elle-même, et non à une instance de la classe. Les méthodes de classe peuvent être appelées sur la classe elle-même ou sur une instance de la classe. Vous utilisez généralement les méthodes de classe lorsque vous devez opérer sur les attributs de la classe ou lorsque vous souhaitez définir une méthode de fabrique pour créer des instances de la classe.

@staticmethod : C’est un autre décorateur qui indique que la méthode suivante est destinée à être une méthode statique. def is_valid() : Cela définit la méthode is_valid qui appartient à la classe. Vous utilisez les méthodes statiques lorsque vous souhaitez regrouper des fonctions logiquement liées ou lorsque vous avez besoin d’une fonction qui ne dépend pas de l’état de la classe ou de ses instances.

class MyClass:
@classmethod
    def from_string(cls, arg):
    # Créer une instance de MyClass basée sur l'argument de chaîne
    return cls(arg)

    @staticmethod
    def is_valid(arg):
        # Effectuer une vérification de validation basée sur l'argument
        return arg > 0

E- Pydantic : une bibliothèque pour la validation des données

Pydantic est une bibliothèque de validation des données et de gestion des paramètres en Python, qui utilise les annotations de type Python pour valider les données d’entrée. Elle est largement utilisée pour créer des modèles de données dans les applications web, en particulier lorsqu’on travaille avec FastAPI, mais elle peut être utilisée dans toute application nécessitant une validation robuste des données.

Caractéristiques clés de Pydantic :

  • Validation des données : Valide automatiquement que les données correspondent aux types et contraintes attendus.
  • Annotations de type : Utilise les annotations de type Python pour définir les entrées/sorties attendues pour les modèles.
  • Sérialisation/Désérialisation : Convertit entre des objets complexes et des types de données Python natifs (par exemple, JSON).
  • Valeurs par défaut : Prend en charge les valeurs par défaut pour les champs si elles ne sont pas fournies dans les données d’entrée.
  • Validateurs personnalisés : Permet une logique de validation personnalisée en utilisant des méthodes de validation.

Définir un modèle

Les modèles Pydantic sont définis en sous-classant pydantic.BaseModel

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int
    email: str = None

Validation des données : Créer une instance du modèle avec des données d’entrée, qui seront validées

user_data = {
    "name": "Alice",
    "age": 30,
    # 'email' est facultatif car il a une valeur par défaut de None
}

try:
    user = User(**user_data)
    print(user.name)  # Output: Alice
    print(user.age)   # Output: 30
    print(user.email) # Output: None
except ValueError as e:
    print(e)

Validateurs personnalisés

La logique de validation personnalisée est ajoutée en utilisant le décorateur @validator

from pydantic import BaseModel, validator

class User(BaseModel):
    name: str
    age: int

    @validator('age')
    def check_age(cls, value):
        if value < 0:
            raise ValueError("L'âge doit être positif")
        return value

user_data = {"name": "Bob", "age": -5}

try:
    user = User(**user_data)
except ValueError as e:
    print(e)  # Output: 1 validation error for User
              # age
              #   L'âge doit être positif (type=value_error)

Sérialisation

L’instance du modèle peut être convertie en JSON ou d’autres formats.

user_dict = user.dict()
user_json = user.json()

print(user_dict)  # Output: {'name': 'Alice', 'age': 30, 'email': None}
print(user_json)  # Output: {"name": "Alice", "age": 30, "email": null}

F- Paramètres Pydantic : charger des paramètres et des configurations

Pydantic Settings fournit des fonctionnalités Pydantic optionnelles pour charger une classe de paramètres ou de configuration à partir de variables d’environnement ou de fichiers de secrets.

Définir un modèle de paramètres

Vous devrez définir un modèle en utilisant Pydantic qui représente les paramètres ou la configuration de votre application. Cela se fait en sous-classant BaseSettings.

from pydantic import BaseSettings, ValidationError

class MyAppSettings(BaseSettings):
    database_url: str
    debug_mode: bool = False  # valeur par défaut si non fournie
    max_connections: int = 10
    
    class Config:
        env_file = '.env'  # Spécifiez éventuellement un fichier .env pour charger les variables d'environnement
        env_prefix = 'MYAPP_'  # Préfixe des variables d'environnement

# Vous pouvez ajouter des validateurs ou d'autres méthodes au modèle de paramètres si nécessaire.

Charger les paramètres à partir des variables d’environnement

Avec le modèle défini, vous pouvez charger vos paramètres de configuration à partir des variables d’environnement. Pydantic mappera automatiquement les variables d’environnement en fonction des noms de champs et des préfixes spécifiés dans la sous-classe Config.

import os

# Définir des variables d'environnement d'exemple à des fins de démonstration
os.environ['MYAPP_DATABASE_URL'] = 'sqlite:///mydb.sqlite'
os.environ['MYAPP_DEBUG_MODE'] = 'True'

try:
    settings = MyAppSettings()
    print(settings.json(indent=2))  # Afficher la configuration chargée sous forme de JSON
except ValidationError as e:
    print("Erreur de chargement des paramètres :", e)

Accéder aux paramètres dans votre application

Vous pouvez ensuite utiliser ces paramètres dans toute votre application :

def main():
    try:
        settings = MyAppSettings()
        
        if settings.debug_mode:
            print("Le mode débogage est activé.")
        
        # Utiliser les paramètres dans la logique de l'application
        connect_to_database(settings.database_url)
        
    except ValidationError as e:
        print("Erreur de chargement des paramètres :", e)

def connect_to_database(url):
    print(f"Connexion à la base de données à {url} avec un maximum de connexions : {settings.max_connections}")

if __name__ == "__main__":
    main()

G- Click : package pour créer des interfaces en ligne de commande

Python-Click est une bibliothèque populaire pour créer des interfaces en ligne de commande (CLI) en Python. Elle fournit des décorateurs pour ajouter des commandes, des options et des arguments à votre application CLI avec un minimum de code standard. Un exemple simple d’utilisation de Click pour créer une application en ligne de commande de base :

import click

@click.command()
@click.option('--name', default='World', help='La personne à saluer.')
def hello(name):
    """Programme simple qui salue NAME."""
    click.echo(f'Hello {name}!')

if __name__ == '__main__':
    hello()
  • @click.command() : Ce décorateur transforme la fonction en une commande Click.
  • @click.option(…) : Cela ajoute une option à la commande. Dans ce cas, cela ajoute une option –name avec une valeur par défaut de ‘World’.
  • def hello(name) : La fonction décorée prend les arguments fournis par les options et les arguments de la ligne de commande.

Nous pouvons exécuter le programme depuis votre terminal comme ceci :

python script.py --name James
python script.py # la valeur par défaut World sera utilisée

H - SqlAlchemy : ORM SQL

SQLAlchemy ORM (Object-Relational Mapping) est un outil puissant et flexible pour travailler avec des bases de données en Python. Il permet aux développeurs de mapper des tables de base de données à des classes Python, leur permettant de travailler avec des données de manière orientée objet plutôt que d’écrire des requêtes SQL brutes.

Concepts clés :

  1. Base déclarative :

    • Vous définissez vos modèles (qui correspondent aux tables de la base de données) en sous-classant une classe de base fournie par SQLAlchemy.
    • La classe Base est généralement créée en utilisant declarative_base().
  2. Mapping :

    • Chaque modèle correspond à une table dans la base de données, et chaque instance d’un modèle représente une ligne dans cette table.
    • Les colonnes des tables sont représentées comme des attributs sur les classes de modèle.
  3. Session :

    • L’objet Session agit comme une zone de mise en scène pour tous les objets chargés ou associés à lui pendant sa durée de vie.
    • Il gère les transactions et fournit des méthodes pour interroger, ajouter, supprimer ou mettre à jour des enregistrements.
  4. Requêtes :

    • SQLAlchemy ORM utilise une API de requêtes de haut niveau qui vous permet de construire des requêtes SQL de manière pythonique.
    • Les requêtes sont exécutées par l’objet Session, qui renvoie les résultats sous forme d’instances de modèle.

Voici un exemple démontrant comment définir et utiliser des modèles avec SQLAlchemy ORM :

import datetime
from loguru import logger
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import mapped_column,Mapped
from sqlalchemy.orm import registry
from sqlalchemy.dialects.postgresql import UUID
import uuid
from sqlalchemy import create_engine
from crawler.settings import settings
from typing import Type, TypeVar, Generic
from sqlalchemy.orm import Session
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy import MetaData


_database  = create_engine(settings.db_url, echo=settings.db_debug)
mapper_registry = registry()
my_metadata = MetaData()

T = TypeVar('T', bound='PsqlBaseModel')  # Ensuring T is bound to PsqlBaseModel

class PsqlBaseModel(DeclarativeBase,Generic[T]):
    metadata = my_metadata
    id = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    created_at: Mapped[datetime.datetime] = mapped_column(server_default=func.now())
    updated_at: Mapped[datetime.datetime] = mapped_column(server_default=func.now(),server_onupdate=func.now())
    def __eq__(self, value: object) -> bool:
        if not isinstance(value, self.__class__):
            return False
        return self.id == value.id
    
    @staticmethod
    def bulk_insert(objects):
        """Bulk insert objects into the database.
        Args:
            objects (List[object]): List of objects to insert.
        """
        with Session(_database) as session:
            session.bulk_save_objects(objects)
            session.commit()
    
    @classmethod
    def get_by_hash(cls: Type[T],hash: str):
        """Get object by hash.
        Args:
            hash (str): Hash to search for.
        Returns:
            object: Object found.
        """
        logger.info(f"Searching class {cls} object with hash: {hash}")
        with Session(_database) as session:
            return session.query(cls).filter(cls.hash == hash).first()

Pour créer toutes les tables :

PsqlBaseModel.metadata.create_all(_database)

Intégrer SQLAlchemy avec des fonctions et des déclencheurs PostgreSQL peut être un moyen puissant d’étendre les capacités de votre schéma de base de données tout en maintenant une logique d’application basée sur l’ORM. Voici comment vous pouvez y parvenir :

import datetime
from typing import List, Optional
from sqlalchemy.orm import Mapped,mapped_column
from sqlalchemy import Column, String, DateTime, DECIMAL, func, event, text
from sqlalchemy.dialects.postgresql import UUID
from .category import ProductCategory
from crawler.domain.base.psql import PsqlBaseModel
from sqlalchemy.orm import DeclarativeBase

class Product(PsqlBaseModel):
    __tablename__ = "products"
    name: Mapped[str] = mapped_column(nullable=True)
    hash: Mapped[str] = mapped_column(index=True)
    price: Mapped[int] = mapped_column(index=True,type_=DECIMAL(10,2),nullable=True,default=0)
    url: Mapped[str]
    website: Mapped[str]
    image: Mapped[str] = mapped_column(nullable=True)
    website_updated_at: Mapped[datetime.datetime]
    crawler_updated_at: Mapped[datetime.datetime]

    def __str__(self):
        return repr(self)

    def __repr__(self):
        return f'Product(name={self.name}, price={self.price}, url={self.url}, website={self.website})'
    
class ProductPriceQueue(PsqlBaseModel):
    __tablename__ = "products_queue"
    id: Mapped[int] = mapped_column(primary_key=True,autoincrement=True)
    product_id = mapped_column(UUID(as_uuid=True),default=func.uuid_generate_v4(),unique=True)
    action: Mapped[str]
    status: Mapped[str] = mapped_column(default="pending")
    url: Mapped[str]
    processed_at: Mapped[datetime.datetime] = mapped_column(nullable=True)
    
# PostgreSQL function and trigger as raw SQL
detect_price_change_sql = """
CREATE OR REPLACE FUNCTION detect_price_change()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.price IS NULL OR NEW.price = 0 THEN
        INSERT INTO products_queue (product_id, action,url, status)
        VALUES (NEW.id, 'missing_price', NEW.url, 'pending')
        ON CONFLICT (product_id) DO NOTHING ;
    END IF;

    IF NEW.website_updated_at IS DISTINCT FROM OLD.website_updated_at THEN
        INSERT INTO products_queue (product_id, action, url, status)
        VALUES (NEW.id, 'update', NEW.url,'pending')
        ON CONFLICT (product_id) DO NOTHING;
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"""

create_trigger_sql = """
CREATE TRIGGER trigger_price_change
AFTER INSERT OR UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION detect_price_change();
"""

# Event listener to execute after table creation
@event.listens_for(PsqlBaseModel.metadata, "after_create")
def create_postgresql_function(target, connection, **kw):
    connection.execute(text(detect_price_change_sql))
    connection.execute(text(create_trigger_sql))

I - Alembic : SqlAlchemy migration

En cours

J - FastApi : framework web pour construire des APIs

En cours