From 738597ee5af4685ee4922c9595fb4465c234c846 Mon Sep 17 00:00:00 2001 From: Johan Date: Tue, 16 Dec 2025 14:15:24 +0100 Subject: [PATCH] TP done --- .gitignore | 4 + poetry.lock | 32 +++++- pyproject.toml | 1 + src/tp_multiprocessing/process.py | 166 +++++++++++++++++------------- 4 files changed, 130 insertions(+), 73 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..becfc5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/resources/hash/ +/resources/output_images/ +/resources/input_images/ +/.idea/ \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 914eeab..6c130ad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "colorama" @@ -282,6 +282,22 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "pandas-stubs" +version = "2.3.3.251201" +description = "Type annotations for pandas" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pandas_stubs-2.3.3.251201-py3-none-any.whl", hash = "sha256:eb5c9b6138bd8492fd74a47b09c9497341a278fcfbc8633ea4b35b230ebf4be5"}, + {file = "pandas_stubs-2.3.3.251201.tar.gz", hash = "sha256:7a980f4f08cff2a6d7e4c6d6d26f4c5fcdb82a6f6531489b2f75c81567fe4536"}, +] + +[package.dependencies] +numpy = ">=1.23.5" +types-pytz = ">=2022.1.1" + [[package]] name = "pillow" version = "11.3.0" @@ -519,6 +535,18 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "types-pytz" +version = "2025.2.0.20251108" +description = "Typing stubs for pytz" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "types_pytz-2025.2.0.20251108-py3-none-any.whl", hash = "sha256:0f1c9792cab4eb0e46c52f8845c8f77cf1e313cb3d68bf826aa867fe4717d91c"}, + {file = "types_pytz-2025.2.0.20251108.tar.gz", hash = "sha256:fca87917836ae843f07129567b74c1929f1870610681b4c92cb86a3df5817bdb"}, +] + [[package]] name = "tzdata" version = "2025.2" @@ -534,4 +562,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "7d05498c899519f4f05c0a6105d4ef33c146dd410b14138f08cfb020e7406590" +content-hash = "6de1bd5af7aeda6180258cc60a110094b35ddfc0e6b455215b90de2c864ed745" diff --git a/pyproject.toml b/pyproject.toml index 7ce5481..f1a9adf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ requires-python = ">=3.13" python = "^3.13" pandas = "^2.3.1" pillow = "^11.3.0" +pandas-stubs = "~=2.3.1" [tool.poetry.group.dev.dependencies] pytest = "^8.4.1" diff --git a/src/tp_multiprocessing/process.py b/src/tp_multiprocessing/process.py index 2d15485..de220e4 100644 --- a/src/tp_multiprocessing/process.py +++ b/src/tp_multiprocessing/process.py @@ -106,17 +106,23 @@ def resize_single_image(args: Tuple[Path, int, str]) -> str: str: un message de statut indiquant le résultat de l'opération (succès, échec, ignoré). """ source_file, output_width, fmt = args - # TODO : Partie 1 - implémenter la logique de redimensionnement. - # 1. Ouvrir l'image source avec Pillow (`Image.open`). Utiliser un bloc `with`. - # 2. Récupérer la largeur et la hauteur originales de l'image. - # 3. Vérifier si `output_width` est >= à la largeur originale. Si c'est le cas, retourner un message d'information. - # 4. Calculer la nouvelle hauteur en utilisant la fonction `calculate_new_height`. - # 5. Redimensionner l'image avec `img.resize`, en utilisant `Image.Resampling.LANCZOS`. - # 6. Construire le chemin de sortie du fichier (ex: f"{source_file.stem}_{output_width}x{new_height}.{fmt}"). - # 7. Sauvegarder l'image redimensionnée. - # 8. Retourner un message de succès. - # 9. Encadrer la logique dans un bloc `try...except Exception` pour capturer les erreurs et retourner un message d'erreur. - pass + try: + with Image.open(source_file) as img: + orig_width, orig_height = img.size + if output_width >= orig_width: + return f"Ignoré (trop grand) : {source_file.name} pour la largeur {output_width}px" + + new_height = calculate_new_height(orig_width, orig_height, output_width) + img_resized = img.resize((output_width, new_height), Image.Resampling.LANCZOS) + + output_filename = f"{source_file.stem}_{output_width}x{new_height}.{fmt}" + output_path = TARGET_DIRECTORY / output_filename + img_resized.save(output_path, format=fmt.upper()) + return f"Traité : {source_file.name} -> {output_filename}" + except Exception as e: + error_msg = f"Erreur lors du traitement de {source_file.name} pour {output_width}px ({fmt}): {e}" + logging.error(error_msg) + return error_msg def run_sequential(worker_function: Callable[[Any], Any], tasks: List[Any], description: str = "") -> Tuple[List[Any], float]: @@ -130,16 +136,19 @@ def run_sequential(worker_function: Callable[[Any], Any], tasks: List[Any], desc description (str, optional): une description de la tâche globale pour l'affichage. Retourne: - float: durée totale d'exécution en secondes. + tuple (Tuple[List[Any], float]) : un tuple contenant la liste des résultats de chaque tâche et la durée totale + d'exécution en secondes. """ - # TODO : Partie 3 - implémenter l'exécuteur séquentiel. - # 1. Logguer le message de démarrage. - # 2. Enregistrer le temps de début (`time.time()`). - # 3. Exécuter les tâches avec une list comprehension (ou boucle for): `[worker_function(task) for task in tasks]`. - # 4. Enregistrer le temps de fin. - # 5. Calculer la durée et logguer le message de fin. - # 6. Retourner la liste des résultats et la durée. - pass + logging.info("Démarrage de '%s' (%d tâches) en mode séquentiel...", description, len(tasks)) + + start_t = time.time() + # Utilise une list comprehension pour une exécution simple et directe + results = [worker_function(task) for task in tasks] + end_t = time.time() + + duration = end_t - start_t + logging.info("Tâche '%s' terminée en %.2f secondes.", description, duration) + return results, duration def run_parallel(worker_function: Callable[[Any], Any], tasks: List[Any], description: str = "") -> Tuple[List[Any], float]: @@ -153,21 +162,23 @@ def run_parallel(worker_function: Callable[[Any], Any], tasks: List[Any], descri description (str, optional): une description de la tâche globale pour l'affichage. Retourne: - float: durée totale d'exécution en secondes. + tuple (Tuple[List[Any], float]): un tuple contenant la liste des résultats (dans l'ordre des tâches) + et la durée totale d'exécution en secondes. """ - # TODO : Partie 3 - implémenter l'exécuteur parallèle. - # 1. Déterminer le nombre de workers (`multiprocessing.cpu_count()`). - # 2. Logguer le message de démarrage. - # 3. Enregistrer le temps de début. - # 4. Utiliser un `ProcessPoolExecutor` dans un bloc `with`, en passant `initializer=setup_logging`. - # 5. Appeler `executor.map(worker_function, tasks)` et convertir le résultat en liste. - # 6. Enregistrer le temps de fin. - # 7. Calculer la durée et logguer le message de fin. - # 8. Retourner la liste des résultats et la durée. - pass + workers = max(1, multiprocessing.cpu_count() - 1) + logging.info("Démarrage de '%s' (%d tâches) avec %d processus...", description, len(tasks), workers) + + start_t = time.time() + with ProcessPoolExecutor(max_workers=workers, initializer=setup_logging) as executor: + results = list(executor.map(worker_function, tasks)) + end_t = time.time() + + duration = end_t - start_t + logging.info("Tâche '%s' terminée en %.2f secondes.", description, duration) + return results, duration -def get_hashes_from_csv() -> Dict[str, str]: +def get_hashes_from_csv(): """ Charge les hashes de fichiers précédemment calculés depuis un fichier CSV. Permet de comparer les hashes actuels aux anciens pour détecter les fichiers modifiés. @@ -180,24 +191,32 @@ def get_hashes_from_csv() -> Dict[str, str]: sont leurs hashes SHA3-512 (str). Retourne un dictionnaire vide si le fichier CSV n'existe pas. """ - # TODO : Partie 4 - implémenter la lecture du CSV de hashes. - # 1. Vérifier si `HASHES_CSV_PATH` existe. Si non, retourner un dictionnaire vide. - # 2. Utiliser un bloc `try...except` pour lire le CSV avec `pd.read_csv`. - # 3. Convertir le DataFrame en dictionnaire (ex: `pd.Series(df[HASH_COL].values, index=df[FILENAME_COL]).to_dict()`). - # 4. Retourner le dictionnaire. En cas d'erreur (fichier vide...), retourner un dictionnaire vide. - pass + if not HASHES_CSV_PATH.exists(): + return {} + try: + df = pd.read_csv(HASHES_CSV_PATH) + return pd.Series(df[HASH_COL].values, index=df[FILENAME_COL]).to_dict() + except (FileNotFoundError, pd.errors.EmptyDataError) as e: + logging.warning("Impossible de lire le fichier de hash %s: %s. Un nouveau sera créé.", HASHES_CSV_PATH.name, e) + return {} def main() -> None: """ Point d'entrée principal du script. - Orchestre le processus de traitement d'images. + Orchestre le processus de traitement d'images : + 1. sélectionne le mode d'exécution (séquentiel ou parallèle). + 2. calcule le hash des images sources pour détecter les changements. + 3. filtre les images qui sont nouvelles ou ont été modifiées. + 4. redimensionne les images filtrées dans plusieurs tailles et formats. + 5. affiche un résumé des temps d'exécution. """ setup_logging() # --- Initialisation --- TARGET_DIRECTORY.mkdir(parents=True, exist_ok=True) HASHES_CSV_PATH.parent.mkdir(parents=True, exist_ok=True) + total_start_time = time.time() # --- Sélection du mode d'exécution --- @@ -206,58 +225,63 @@ def main() -> None: elif EXECUTION_MODE == "sequential": runner = run_sequential else: - logging.critical("Mode d'exécution inconnu : '%s'.", EXECUTION_MODE) + logging.critical("Mode d'exécution inconnu : '%s'. Choisissez 'parallel' ou 'sequential'.", EXECUTION_MODE) raise ValueError(f"Mode d'exécution inconnu : '{EXECUTION_MODE}'.") + logging.info("--- Mode d'exécution sélectionné : %s ---", EXECUTION_MODE.upper()) - # TODO : Partie 1 - lister les fichiers sources dans SOURCE_DIRECTORY. - # Utiliser `SOURCE_DIRECTORY.iterdir()` avec une list comprehension (ou boucle for) pour ne garder que les fichiers (exclusion des dossiers) dans `all_source_files`. - all_source_files = [] + all_source_files = [f for f in SOURCE_DIRECTORY.iterdir() if f.is_file()] logging.info("Trouvé %d fichier(s) dans '%s'.", len(all_source_files), SOURCE_DIRECTORY.name) - # La logique de Hachage et Filtrage sera implémentée en Partie 4. - # --- Hachage --- - # hash_tasks = [(f,) for f in all_source_files] - # hash_results, hash_duration = runner(compute_sha3_512, hash_tasks, "Calcul des hashes") + # --- Étape 1: Hachage --- + hash_tasks = [(f,) for f in all_source_files] + hash_results, hash_duration = runner(compute_sha3_512, hash_tasks, "Calcul des hashes") - # --- Filtrage --- + # --- Étape 2: Filtrage --- logging.info("Filtrage des fichiers modifiés...") - # Tant qu'on n'a pas implémenté le hachage, on traite tous les fichiers. - files_to_process = all_source_files - # TODO : Partie 4 - implémenter la logique de filtrage. - # 1. Charger les anciens hashes depuis le fichier CSV en appelant `get_hashes_from_csv`. - # old_hashes = get_hashes_from_csv() - # files_to_process: List[Path] = [] - # new_hash_records: List[Dict[str, str]] = [] - # - # 2. Itérer sur `hash_results` pour comparer les hashes et remplir `files_to_process`. - # for result in hash_results: - # file_hash: Optional[str] = result[0] - # filepath: Path = result[1] - # ... - # 3. Préparer `new_hash_records` pour la sauvegarde. - #if new_hash_records: - # 4. Sauvegarder les nouveaux hashes dans le CSV avec pandas. - # logging.info("-> %d fichier(s) à traiter.", len(files_to_process)) + old_hashes = get_hashes_from_csv() + files_to_process: List[Path] = [] + new_hash_records: List[Dict[str, str]] = [] - # --- Redimensionnement --- + for result in hash_results: + file_hash: Optional[str] = result[0] + filepath: Path = result[1] + + if file_hash is None: continue + filename = filepath.name + new_hash_records.append({FILENAME_COL: filename, HASH_COL: file_hash}) + if old_hashes.get(filename) != file_hash: + files_to_process.append(filepath) + + if new_hash_records: + pd.DataFrame(new_hash_records).to_csv(HASHES_CSV_PATH, index=False) + logging.info("Fichier de hash '%s' mis à jour.", HASHES_CSV_PATH.name) + + logging.info("-> %d fichier(s) à traiter.", len(files_to_process)) + + # --- Étape 3: Redimensionnement --- resize_duration = 0.0 if files_to_process: - # TODO : Partie 2 - préparer les tâches de redimensionnement (triple boucle : chaque fichier d'entrée, chaque largeur, chaque format). - resize_tasks = [] - resize_duration = runner(resize_single_image, resize_tasks, "Redimensionnement des images") + resize_tasks = [ + (file, width, fmt) + for file in files_to_process + for width in RESIZE_WIDTHS + for fmt in FORMATS + ] + _, resize_duration = runner(resize_single_image, resize_tasks, "Redimensionnement des images") else: logging.info("Aucun fichier à redimensionner.") # --- Résumé Final --- total_duration = time.time() - total_start_time logging.info("--- Résumé des temps ---") - #logging.info(f"Temps de hachage : {hash_duration:.2f} secondes") + logging.info(f"Temps de hachage : {hash_duration:.2f} secondes") logging.info(f"Temps de redimensionnement : {resize_duration:.2f} secondes") logging.info("--------------------------") logging.info(f"Traitement complet terminé en {total_duration:.2f} secondes.") if __name__ == "__main__": + # Pour s'assurer que le logging fonctionne correctement avec multiprocessing multiprocessing.freeze_support() - main() \ No newline at end of file + main()