first commit

This commit is contained in:
Johan
2025-12-15 15:58:02 +01:00
commit f042f8e3e2
13 changed files with 1507 additions and 0 deletions

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

14
.idea/m01_tp01_asyncio.iml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Poetry (m01_tp01_asyncio)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

4
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (m01_tp01_asyncio)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/m01_tp01_asyncio.iml" filepath="$PROJECT_DIR$/.idea/m01_tp01_asyncio.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

153
README.md Normal file
View File

@@ -0,0 +1,153 @@
# TP : Téléchargements synchrone et asynchrone (asyncio)
## Informations générales
**Cours** : Python Avancé > Programmation concurrente > Tâches IO-bound concurrentes avec asyncio \
**Objectifs pédagogiques** :
- Python avancé : programmation asynchrone
- Outils modernes (poetry, PyCharm)
- Bonnes pratiques de l'entreprise
---
## Prérequis
### Installation et configuration de lenvironnement
- Faites `poetry install` après import du projet (fichier lock déja présent), pour s'assurer que toutes les dépendances sont installées.
- Assurez-vous que l'interpréteur Python du projet pointe bien vers l'environnement virtuel créé par Poetry, ce qui devrait être le cas par défaut (sinon cliquer en bas à droite sur le nom de l'interpréteur puis `Add new interpreter`, ensuite `Add local interpreter...`, et sélectionner `poetry` avec Python `3.13`).
- Attention, si vous êtes derrière un proxy, pour ce qui est de la version asynchrone en particulier, vous allez peut-être devoir passer le paramètre `proxy="http://host:port"` à la méthode `session.get()`.
### Connaissances préalables
- Connaissances de base en programmation
---
## Énoncé
### Contexte
Le téléchargement de plusieurs fichiers depuis Internet est une tâche (IO-bound) courante en développement.
Cependant, la manière dont on s'y prend peut avoir un impact considérable sur la performance et la rapidité d'exécution d'un script.
Cet exercice a pour but de comparer deux approches fondamentales : le téléchargement séquentiel (synchrone) et le téléchargement parallèle (asynchrone).
Les deux scripts Python permettent de télécharger la même liste d'images.
Le premier le fera de manière traditionnelle, une image après l'autre.
Le second utilisera les capacités asynchrones de Python pour effectuer les téléchargements en parallèle, afin d'observer le gain de performance.
---
### Partie 1 : téléchargement synchrone
Dans cette première partie, vous allez créer un script qui télécharge une liste d'images de manière séquentielle en utilisant la bibliothèque `requests`.
```mermaid
graph TD
subgraph "Processus synchrone bloc par bloc"
direction TB
A[Début] --> B{Télécharger Image 1};
B --> C[Attente Réseau 1];
C --> D{Sauvegarder Image 1};
D --> E{Télécharger Image 2};
E --> F[Attente Réseau 2];
F --> G{Sauvegarder Image 2};
G --> H[...];
H --> I{Télécharger Image N};
I --> J[Attente Réseau N];
J --> K{Sauvegarder Image N};
K --> L[Fin];
end
```
#### Objectif
À partir du squelette de code fourni, écrire un script Python nommé `sync_downloader.py` qui :
1. Télécharge une liste d'images depuis des URLs.
2. Sauvegarde ces images dans un répertoire local.
3. Mesure et affiche le temps total d'exécution.
#### Cahier des charges
1. **Configuration de base** :
* Utilisez la liste d'URLs et le chemin du répertoire de sortie fournis ci-dessous.
* Le script doit créer le répertoire de sortie s'il n'existe pas. Utilisez le module `pathlib` pour une gestion propre des chemins.
2. **Logging** :
* Mettez en place un système de `logging` simple pour afficher des informations pertinentes dans la console (début/fin du script, URL en cours de téléchargement, succès, erreurs).
* Le format des logs doit inclure l'heure, le niveau du log et le message.
3. **Logique de téléchargement** :
* Utilisez la bibliothèque `requests`. Pour optimiser les connexions réseau, effectuez toutes vos requêtes à travers une `requests.Session`.
* Créez une fonction `download_and_save` qui prend en charge le téléchargement et la sauvegarde d'une seule image.
* Parcourez la liste d'URLs et appelez cette fonction pour chaque URL.
4. **Gestion des erreurs** :
* Votre script doit être robuste. Utilisez des blocs `try...except` pour gérer les erreurs potentielles comme les erreurs réseau (`requests.exceptions.RequestException`) ou les problèmes d'écriture sur le disque (`IOError`).
* Loguez les erreurs de manière explicite sans pour autant arrêter le script (une image qui ne se télécharge pas ne doit pas empêcher les autres de se télécharger).
5. **Mesure de performance** :
* Utilisez le module `time` (par exemple, `time.perf_counter()`) pour calculer la durée totale d'exécution du processus de téléchargement.
* Affichez le résultat à la fin du script.
---
### Partie 2 : téléchargement asynchrone
Maintenant que vous avez une version fonctionnelle, mais relativement lente, vous allez la réécrire en utilisant `asyncio` pour paralléliser les téléchargements et réduire le temps d'attente.
```mermaid
graph TD
A[Début] --> B{Lancement simultané des tâches};
subgraph "Exécution parallèle gérée par la boucle asyncio"
T1[Tâche 1: Get URL 1] --> W1[Attente Réseau 1] --> S1[Sauvegarde 1];
T2[Tâche 2: Get URL 2] --> W2[Attente Réseau 2] --> S2[Sauvegarde 2];
T3[...] --> W3[...] --> S3[...];
TN[Tâche N: Get URL N] --> WN[Attente Réseau N] --> SN[Sauvegarde N];
end
B --> T1;
B --> T2;
B --> T3;
B --> TN;
S1 --> G{Fin de toutes les tâches};
S2 --> G;
S3 --> G;
SN --> G;
G --> H[Fin du script];
```
#### Objectif
À partir du squelette de code fourni, compléter le script `async_downloader.py` qui accomplit la même tâche que le précédent, mais de manière asynchrone pour une performance maximale.
#### Cahier des charges
1. **Structure asynchrone** :
* Transformez votre logique en utilisant `async` et `await`. La fonction principale (`main`) et la fonction de téléchargement (`download_and_save`) doivent devenir des coroutines (`async def`).
* Utilisez `asyncio.run(main())` pour démarrer l'exécution.
2. **Logique de téléchargement asynchrone** :
* Remplacez `requests.Session` par `aiohttp.ClientSession`.
* Dans votre fonction `download_and_save`, utilisez `session.get()` pour la requête et `response.read()` pour obtenir le contenu binaire de l'image.
* Pour l'écriture des fichiers, remplacez `open()` par `aiofiles.open()` pour ne pas bloquer la boucle événementielle.
3. **Exécution en parallèle** :
* Dans votre fonction `main`, créez une liste de "tâches" (une pour chaque image à télécharger).
* Utilisez `asyncio.gather()` pour lancer toutes les tâches en parallèle et attendre qu'elles soient toutes terminées.
4. **Gestion des erreurs et logging** :
* Adaptez la gestion des erreurs pour les exceptions spécifiques à `aiohttp` (comme `aiohttp.ClientError`).
* Conservez le système de `logging`. Notez que dans un contexte asynchrone, les messages de log des différentes tâches peuvent s'entremêler, ce qui est normal.
5. **Mesure de performance** :
* Mesurez à nouveau le temps d'exécution avec `time.perf_counter()`.
* Comparez ce temps avec celui obtenu dans la Partie 1. Qu'observez-vous ?
* Si vous avez le temps, essayez de télécharger un plus grand nombre d'images, provenant éventuellement de serveurs HTTP différents, pour voir l'impact de l'asynchronisme.
#### Question de réflexion
Pourquoi l'approche asynchrone est-elle efficace pour ce type de tâche (opérations d'entrée/sortie réseau) par rapport à l'approche synchrone ?

1014
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

47
pyproject.toml Normal file
View File

@@ -0,0 +1,47 @@
[project]
name = "tp-asyncio"
version = "0.1.0"
description = "TP asyncio"
authors = [
{name = "Your Name",email = "you@example.com"}
]
readme = "README.md"
classifiers = [
"Programming Language :: Python :: 3.13",
]
keywords = ["multiprocessing", "concurrency"]
exclude = [
{ path = "tests", format = "wheel" }
]
requires-python = ">=3.13"
[tool.poetry.dependencies]
python = "^3.13"
requests = "^2.32.4"
aiohttp = "^3.12.14"
aiofiles = "^24.1.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.4.1"
pytest-cov = "^6.2.1"
coverage = { version="*", extras=["toml"]}
[tool.black]
line-length = 120
[tool.pycln]
all = true
[tool.isort]
line_length = 120
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

View File

View File

@@ -0,0 +1,126 @@
"""
Script pour télécharger une liste d'images de manière asynchrone.
Ce script utilise `aiohttp` et `aiofiles` pour effectuer des téléchargements
parallèles, ce qui est beaucoup plus rapide qu'une approche séquentielle.
Il inclut :
- L'utilisation de sessions `aiohttp` pour optimiser les performances réseau.
- Une gestion des erreurs robuste pour les opérations réseau et disque.
"""
import asyncio
import logging
import sys
import time
from pathlib import Path
from typing import List
import aiofiles
import aiohttp
from aiohttp import ClientError
# --- Configuration ---
# Une liste d'URLs d'images à télécharger
IMG_URLS: List[str] = [
"https://images.pexels.com/photos/842711/pexels-photo-842711.jpeg",
"https://images.pexels.com/photos/3408744/pexels-photo-3408744.jpeg",
"https://images.pexels.com/photos/3244513/pexels-photo-3244513.jpeg",
"https://images.pexels.com/photos/210186/pexels-photo-210186.jpeg",
"https://images.pexels.com/photos/1261728/pexels-photo-1261728.jpeg",
"https://images.pexels.com/photos/414144/pexels-photo-414144.jpeg",
"https://images.pexels.com/photos/110854/pexels-photo-110854.jpeg",
"https://images.pexels.com/photos/546819/pexels-photo-546819.jpeg",
"https://images.pexels.com/photos/1640777/pexels-photo-1640777.jpeg",
"https://images.pexels.com/photos/885880/pexels-photo-885880.jpeg",
"https://images.pexels.com/photos/5318967/pexels-photo-5318967.jpeg",
"https://images.pexels.com/photos/3464632/pexels-photo-3464632.jpeg",
"https://images.pexels.com/photos/2110951/pexels-photo-2110951.jpeg",
"https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg",
"https://images.pexels.com/photos/168927/pexels-photo-168927.jpeg",
]
# Le répertoire de sortie pour les images téléchargées
OUTPUT_DIR: Path = Path("../../images")
# --- Fonctions ---
def setup_logging() -> None:
"""
Configure le système de logging pour le script.
"""
# Sous un autre système d'exploitation que Windows, il est possible d'utiliser une blibliothèque de logging asynchrone telle que aiologger
# Lorsqu'il existe notamment des Handler de type fichiers, cela évite de ralentir la boucle asyncio
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[
logging.StreamHandler(sys.stdout),
]
)
async def download_and_save(
session: aiohttp.ClientSession, url: str, output_dir: Path
) -> None:
"""
Télécharge une image et la sauvegarde de manière asynchrone.
Paramètres:
session (aiohttp.ClientSession): la session client `aiohttp` à utiliser.
url (str): l'URL de l'image à télécharger.
output_dir (Path): le répertoire où sauvegarder l'image.
"""
# TODO : votre code ici. Similaire à la version synchrone, mais avec `async`/`await`.
# 1. Utilisez un bloc `try...except` pour gérer les erreurs (ClientError, asyncio.TimeoutError, IOError).
#
# 2. Dans le bloc `try` :
# a. Extrayez le nom du fichier et construisez le chemin de sauvegarde (comme avant).
# b. Logguez le début du téléchargement.
# c. Utilisez `async with session.get(url) as response:` pour effectuer la requête.
# d. Vérifiez le statut de la réponse avec `response.raise_for_status()`.
# e. Lisez le contenu de la réponse de manière asynchrone : `data = await response.read()`.
# f. Utilisez `async with aiofiles.open(path, "wb") as f:` pour ouvrir le fichier
# sans bloquer la boucle événementielle.
# g. Écrivez les données dans le fichier : `await f.write(data)`.
# h. Logguez la confirmation de la sauvegarde.
#
# 3. Dans les blocs `except`, logguez les erreurs spécifiques.
async def main() -> None:
"""
Fonction principale asynchrone.
Orchestre la création du répertoire de sortie et le lancement
des tâches de téléchargement en parallèle.
"""
setup_logging()
logging.info("Début du script de téléchargement asynchrone.")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
logging.info(f"Le répertoire de sortie est : {OUTPUT_DIR}")
# TODO : votre code ici. C'est ici que la magie de l'asynchronisme opère.
# 1. Créez une session client avec `async with aiohttp.ClientSession() as session:`.
#
# 2. À l'intérieur du `async with`, créez une liste de "tâches". Chaque tâche
# est un appel à votre coroutine `download_and_save`.
# Astuce : une compréhension de liste est parfaite pour ça.
# Exemple : `tasks = [download_and_save(session, url, OUTPUT_DIR) for url in IMG_URLS]`
#
# 3. Utilisez `asyncio.gather()` pour lancer toutes les tâches en parallèle et
# attendre qu'elles se terminent toutes.
# La syntaxe est : `await asyncio.gather(*tasks)`
# (L'étoile `*` dépaquette la liste de tâches en arguments pour `gather`).
if __name__ == "__main__":
# `asyncio.run()` est la manière moderne de lancer une application asyncio
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nScript interrompu par l'utilisateur.")

View File

@@ -0,0 +1,119 @@
"""
Script pour télécharger une liste d'images de manière séquentielle.
Ce script inclut :
- Un système de logging pour suivre les opérations.
- Une gestion des erreurs robuste.
- L'utilisation de sessions `requests` pour optimiser les performances réseau.
"""
import sys
import logging
import time
from pathlib import Path
from typing import List
import requests
from requests.exceptions import RequestException
# --- Configuration ---
# Une liste d'URLs d'images à télécharger.
IMG_URLS: List[str] = [
"https://images.pexels.com/photos/842711/pexels-photo-842711.jpeg",
"https://images.pexels.com/photos/3408744/pexels-photo-3408744.jpeg",
"https://images.pexels.com/photos/3244513/pexels-photo-3244513.jpeg",
"https://images.pexels.com/photos/210186/pexels-photo-210186.jpeg",
"https://images.pexels.com/photos/1261728/pexels-photo-1261728.jpeg",
"https://images.pexels.com/photos/414144/pexels-photo-414144.jpeg",
"https://images.pexels.com/photos/110854/pexels-photo-110854.jpeg",
"https://images.pexels.com/photos/546819/pexels-photo-546819.jpeg",
"https://images.pexels.com/photos/1640777/pexels-photo-1640777.jpeg",
"https://images.pexels.com/photos/885880/pexels-photo-885880.jpeg",
"https://images.pexels.com/photos/5318967/pexels-photo-5318967.jpeg",
"https://images.pexels.com/photos/3464632/pexels-photo-3464632.jpeg",
"https://images.pexels.com/photos/2110951/pexels-photo-2110951.jpeg",
"https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg",
"https://images.pexels.com/photos/168927/pexels-photo-168927.jpeg",
]
# Le répertoire de sortie pour les images téléchargées.
OUTPUT_DIR: Path = Path("../../images")
# --- Fonctions ---
def setup_logging() -> None:
"""
Configure le système de logging pour le script.
"""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[
logging.StreamHandler(sys.stdout),
]
)
def download_and_save(session: requests.Session, url: str, output_dir: Path) -> None:
"""
Télécharge une image depuis une URL et la sauvegarde dans le répertoire spécifié.
Paramètres:
session (requests.Session): La session `requests` à utiliser pour la requête.
url (str): L'URL de l'image à télécharger.
output_dir (Path): Le répertoire où sauvegarder l'image.
"""
# TODO : votre code ici. Suivez ces étapes :
# 1. Utilisez un bloc `try...except` pour gérer les erreurs potentielles
# (RequestException, IOError).
#
# 2. Dans le bloc `try` :
# a. Extrayez le nom du fichier à partir de l'URL.
# Astuce : `url.split("/")[-1]` est une méthode simple.
# b. Construisez le chemin de sauvegarde complet en utilisant `pathlib`.
# Exemple : `output_dir / nom_du_fichier`
# c. Logguez le début du téléchargement.
# d. Effectuez une requête GET avec `session.get(url)`. Pensez à ajouter un timeout.
# e. Vérifiez que la requête a réussi avec `response.raise_for_status()`.
# f. Ouvrez le fichier de destination en mode écriture binaire ('wb')
# en utilisant un `with open(...)`.
# g. Écrivez le contenu de la réponse (`response.content`) dans le fichier.
# h. Logguez la confirmation de la sauvegarde.
#
# 3. Dans les blocs `except`, logguez l'erreur de manière descriptive
# (ex: `logging.error(...)`).
def main() -> None:
"""
Fonction principale du script.
Orchestre la création du répertoire de sortie et le téléchargement
séquentiel des images.
"""
setup_logging()
logging.info("Début du script de téléchargement.")
# Crée le répertoire de sortie s'il n'existe pas.
# `parents=True` crée les répertoires parents si nécessaire.
# `exist_ok=True` évite une erreur si le répertoire existe déjà.
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
logging.info(f"Le répertoire de sortie est : {OUTPUT_DIR}")
start_time = time.perf_counter()
# TODO : votre code ici. Suivez ces étapes :
# 1. Utilisez un gestionnaire de contexte `with requests.Session() as session:`
# pour créer une session qui sera utilisée pour toutes les requêtes.
# Ceci optimise les connexions réseau.
#
# 2. À l'intérieur du `with`, faites une boucle `for` sur la liste `IMG_URLS`.
#
# 3. Dans la boucle, appelez la fonction `download_and_save()` pour chaque URL,
# en lui passant la session, l'URL et le répertoire de sortie.
if __name__ == "__main__":
main()

0
tests/__init__.py Normal file
View File