Compare commits

..

10 Commits

Author SHA1 Message Date
maurane 5a93d0d67b 🐛fix(classifier): remove store code error pattern
remove store code error, its related fixture (`CR_store_code_not_found`) and mentionned in fixtures `only_relevant_logs.txt` and `specific_logs.txt`
2026-09-02 18:21:25 +02:00
maurane b2b68e1072 📝docs(project): add CHANGELOG.md 2026-09-02 14:17:09 +02:00
maurane 21fda4103e 🔧chore(project): remove unecessary files
remove unecessary files from repository
2026-09-02 14:06:55 +02:00
maurane bfa5fa600b 📝docs(docs): add documentation for README.md
add documentation in README.md, usage.md, architecture.md, update python version based on the version used in development
2026-09-02 11:52:27 +02:00
maurane a43afb9df7 ✨feat(project): add logging in project
add logging in modules classier.py, parser.py, reporter.py. Update logging in cli.
2026-09-01 17:21:17 +02:00
maurane 24a77363d0 💄style(project): turn all restructure docstrings into google docstrings 2026-09-01 15:38:27 +02:00
maurane b6dab13bd1 ✨feat(cli): add cli features
add cli using typer, its option --input-dir, --input-files and --output-dir, add related tests, add logging, add and update of the package version in `logwatcher/__init__.py`, correct templates in `reporter.py`
2026-09-01 14:53:18 +02:00
maurane ee118b531d ♻️ refactor(cli): rename app module by cli for clarity 2026-08-28 15:24:16 +02:00
maurane 8e98a44a7f 💄style(logging): rename logging by logging_config
naming the logging module of logwatcher like the built-in python package is confusing. By naming it `logging_config`, everything is explicit.
2026-08-28 14:58:08 +02:00
maurane 9c06222f37 ✅test(reporter): add tests for reporter module
add tests, reformat code, add an output variable in writing function. All reading and writing functions must read in windows-1252 encoding because original logs are windows-1252 encoded
2026-08-27 17:34:22 +02:00
26 changed files with 1450 additions and 6433 deletions
+3 -1
View File
@@ -4,8 +4,10 @@ src/logwatcher/__pycache__/
*.log *.log
__pycache__ __pycache__
docs/convention*
docs/conventions.yml
docs/resultats.json docs/resultats.json
.vscode/ .vscode/
.idea/ .idea/
.coverage .coverage
+10
View File
@@ -0,0 +1,10 @@
# Changelog
## [0.0.1] - 2026-09-02
### Added
- Pipeline complet : parsing, classification N2, génération de rapports
- CLI avec options `--input-files`, `--input-dir`, `--output-dir`, `--version`
- Logging applicatif (jalons, compteurs)
- Tests unitaires et d'intégration (~95% de couverture)
- Documentation (README, usage, architecture)
+83
View File
@@ -0,0 +1,83 @@
# logwatcher
Logwatcher est un outil sur-mesure d'analyse et de tri automatique des logs LAME MDC pour le support N2.
Les techniciens N2 reçoivent quotidiennement des compte-rendus de suivi
d'imports NOSYMAG contenant des centaines de lignes d'erreurs. La plupart
ne les concernent pas. Logwatcher analyse ces compte-rendus, identifie
les erreurs pertinentes pour le support N2, et génère trois rapports
(n2, autres, tout) prêts à être transmis.
---
## 📦 Installation
### Prérequis
- Python >= 3.14
- `uv` (recommandé) ou `pip`
### Procédure
#### uv
```bash
uv pip install -e ".[dev]"
```
#### pip
```bash
pip install -e ".[dev]"
```
---
## 🚀 Commandes
### Analyser un fichier de logs
```bash
logwatcher --input-files chemin/vers/CR_fichier.txt
```
### Analyser tous les logs d'un répertoire
```bash
logwatcher --input-dir chemin/vers/dossier
```
### Afficher la version
```bash
logwatcher --version
```
Le guide complet se trouve dans [usage](docs/usage.md).
---
## 🏗️ Architecture
Le détail de l'architecture se trouve dans [architecture](docs/architecture.md).
---
## 🧪 Tests
Les tests se trouvent dans `tests/`. Le projet est couvert à ~95% par les tests unitaires et d'intégration.
### Lancer tous les tests
```bash
pytest
```
### Lancer un test spécifique (ex: `test_parser.py`)
```bash
pytest tests/test_parser.py
```
### Générer un rapport de couverture
```bash
pytest --cov=logwatcher --cov-report=term-missing
```
---
## 📄 Licence
MIT — voir [LICENSE](LICENSE).
+105
View File
@@ -0,0 +1,105 @@
# Architecture
## Vue d'ensemble
Logwatcher est un outil en ligne de commande Python qui analyse des fichiers
de logs LAME MDC, identifie les erreurs pertinentes pour le support N2, et
génère des rapports texte.
Le traitement suit un pipeline linéaire :
```text
fichiers de logs
│
▼
parser.py lit et parse les fichiers en LogEntry
│
▼
classifier.py classe chaque entrée : N2 ou hors N2
│
▼
reporter.py génère les trois rapports (all, n2, other)
│
▼
rapports .log
```
## Structure du package
```text
src/logwatcher/
├── __init__.py # expose __version__
├── __main__.py # point d'entrée : python -m logwatcher
├── cli.py # interface en ligne de commande (Typer)
├── config.py # constantes : chemins, formats
├── logging_config.py # configuration du logging
├── models.py # LogEntry : structure de données
├── parser.py # lecture et parsing des fichiers
├── classifier.py # classification N2 / hors N2
└── reporter.py # génération des rapports texte
```
### Modules
#### cli.py — point d'entrée
* Définit `app = typer.Typer()` et la commande principale.
* Valide les arguments (`--input-files` / `--input-dir` mutuellement exclusifs).
* Orchestre le pipeline : parsing → classification → rapports.
* Gère les erreurs utilisateur (`BadParameter`, code de sortie 2) et
* les erreurs d'exécution (code de sortie 1).
* Expose `--version` (eager, affiche et quitte).
#### models.py — LogEntry
Dataclass immuable (par convention) représentant une ligne de log parsée.
Champs principaux :
* `server_ip`, `mdc_server_name` : origine du log
* `store_name` : magasin concerné
* `start_time`, error_time : horodatages
* `error_message` : message d'erreur complet
* `error_name` : nom du pattern N2 correspondant (rempli par le classifier)
* `raw_line` : ligne brute d'origine
* `ligne` : numéro de ligne dans le fichier source
LogEntry est créée uniquement par le parser. Le classifier l'enrichit
(remplit `error_name`). Le modèle lui-même ne contient pas de logique métier.
[`parser.py`]() — lecture et parsing
* `LOG_PATTERN` : expression régulière du format d'une ligne LAME MDC.
* `parse_log_file()` : lit un fichier, retourne list[LogEntry].
* Les lignes non conformes (système, en-têtes, corrompues) sont ignorées silencieusement.
#### classifier.py — classification
* N2_PATTERNS : dictionnaire des motifs d'erreurs pertinents pour le N2, fournis par les techniciens.
* classify_log_entries() : sépare les entrées en deux listes (relevant, irrelevant) et remplit entry.error_name pour les entrées pertinentes.
#### reporter.py — rapports
* Templates texte (`string.Template`) : `BASE_TEMPLATE`, `N2_SUPPORT_TEMPLATE`, `OTHER_TEMPLATE`, `ERROR_TEMPLATE`.
* `build_reports()` : construit les trois rapports (`dict n2, other, all`).
* `write_log_report()` : écrit les rapports sur disque en Windows-1252.
* `_get_period()` : calcule les bornes min/max des error_time.
#### logging_config.py — logging
* `setup_logging()` : configure le logger racine.
* Handler console (`INFO`, ou `DEBUG` si verbose) + handler fichier.
* Le fichier de log `logwatcher.log` est horodaté et écrit dans le dossier de sortie.
#### config.py — constantes
* Chemins (`OUTPUT_PATH`, `FIXTURE_PATH` ...).
* Formats de date (`DATETIME_FORMAT`).
## Dépendances
| Package | Rôle |
|---------|------|
| `typer` | Interface en ligne de commande |
| `pytest` | Tests (dev) |
| `pytest-cov` | Couverture (dev) |
| `ruff` | Linting/formatage (dev) |
| `mypy` | Typage statique (dev) |
-5831
View File
File diff suppressed because one or more lines are too long
+142
View File
@@ -0,0 +1,142 @@
# Guide d'utilisation
## Introduction
Logwatcher analyse les comptes-rendus de suivi d'imports NOSYMAG (fichiers
`CR_*.txt` et autres logs LAME MDC) pour identifier les erreurs pertinentes
pour le support N2. Il génère trois rapports : un pour les erreurs N2, un
pour les erreurs hors périmètre N2, et un rapport complet.
---
## Entrées
### Analyser un fichier de logs
```bash
logwatcher --input-files chemin/vers/CR_fichier.txt
```
### Analyser plusieurs fichiers de logs
L'option `--input-files` accepte plusieurs fichiers. Répéter l'option pour
chaque fichier :
```bash
logwatcher --input-files fichier1.txt --input-files fichier2.txt
```
### Analyser tous les logs d'un répertoire
```bash
logwatcher --input-dir chemin/vers/dossier
```
L'option `--input-dir` analyse tous les fichiers `.log`, `.txt` ou sans
extension contenus dans le répertoire. Les autres fichiers sont ignorés.
> ### Règle d'exclusivité
> `--input-files` et `--input-dir` sont mutuellement exclusifs. Fournir
les deux options lève une erreur et le traitement est interrompu. Ne fournir
aucune des deux lève également une erreur.
## Sorties
### Emplacement des rapports
Par défaut, les rapports sont écrits dans le répertoire `output/` du dossier
courant. Un autre emplacement peut être fourni avec `--output-dir` :
```bash
logwatcher --input-dir chemin/vers/dossier --output-dir chemin/vers/sortie
```
### Les rapports générés
Chaque analyse produit trois rapports différents : `all.log`, `n2.log` et `other.log`.
- `all.log` : Toutes les erreurs analysées, N2 et hors N2.
- `n2.log` : Uniquement les erreurs pertinentes pour le support N2.
- `other.log` : Uniquement les erreurs hors périmètre N2
#### Contenu d'un rapport
Chaque rapport contient un en-tête avec la période analysée, le nombre de
fichiers lus et le nombre total d'erreurs, suivi du détail des erreurs.
Un rapport se présente ainsi :
```text
RAPPORT D'ANALYSE DE LOGS
=========================
Période : 01/09/2026 11:07:53 -> 01/09/2026 15:04:18
Fichier(s) lu(s) : 1
Nombre total d'erreur(s) : 3
=================================
ERREURS SUPPORT N2
=================================
Nombre d'erreurs : 2
[1] BL_AUTO_BESTSELLER_REJECTED
Magasin: DUMAS-DELAGE
MDC: MDC_340
Serveur: 192.168.13.23
Heure: 28/07/2026 08:05:27
Raison: Erreur : 1099_2622609667_DESADV : / 00098/000 ... ne correspond
```
## Options
- `--output-dir` : Répertoire de sortie des rapports. Par défaut : `output/`.
```bash
logwatcher --input-dir logs/ --output-dir rapports/2026-09-01/
```
- `--version` : Affiche la version du package et quitte.
```bash
logwatcher --version
# logwatcher version: 0.0.1
```
## Cas particuliers
### Fichier vide
Un fichier vide est lu sans erreur. Les trois rapports sont générés avec `Nombre total d'erreur(s) : 0`.
### Répertoire vide
Un répertoire vide est traité sans erreur. Les trois rapports sont générés
avec `Fichier(s) lu(s) : 0 et Nombre total d'erreur(s) : 0`.
### Logs non reconnus
Les lignes qui ne correspondent pas au format LAME MDC (lignes système,
en-têtes de répertoire, lignes corrompues) sont ignorées silencieusement
et ne produisent pas d'entrée dans les rapports.
### Encodage des fichiers
Les fichiers d'entrée sont lus en Windows-1252 (encodage natif des logs
LAME MDC). Les rapports générés sont également écrits en Windows-1252.
## Exemple complet
Analyser tous les logs d'un répertoire et écrire les rapports dans un
dossier dédié :
```bash
logwatcher --input-dir logs/2026-09-01/ --output-dir rapports/2026-09-01/
```
Après exécution, les fichiers suivants sont créés dans `rapports/2026-09-01/` :
```text
all.log # toutes les erreurs
n2.log # erreurs pertinentes pour le N2
other.log # erreurs hors périmètre N2
```
Le rapport `n2.log` peut ensuite être transmis aux techniciens N2, et
`other.log` aux équipes concernées par les autres erreurs.
+5 -4
View File
@@ -1,11 +1,11 @@
[project] [project]
name = "logwatcher" name = "logwatcher"
version = "0.1.0" version = "0.0.1"
description = "Trier, classifier et extraire automatiquement les entrées pertinentes d'un fichier de logs applicatif." description = "Trier, classifier et extraire automatiquement les entrées pertinentes d'un fichier de logs applicatif."
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.14"
dependencies = [ dependencies = [
"pytest-cov>=7.1.0", "typer>=0.27.2",
] ]
[dependency-groups] [dependency-groups]
@@ -13,10 +13,11 @@ dev = [
"ruff>=0.16.0", "ruff>=0.16.0",
"mypy>=2.3.0", "mypy>=2.3.0",
"pytest>=9.1.1", "pytest>=9.1.1",
"pytest-cov>=7.1.0",
] ]
[project.scripts] [project.scripts]
logwatcher = "logwatcher.app:app" logwatcher = "logwatcher.cli:app"
[build-system] [build-system]
+5 -2
View File
@@ -1,4 +1,7 @@
from logwatcher.config import * from importlib.metadata import version
from logwatcher.logging import setup_logging
from logwatcher.config import *
from logwatcher.logging_config import setup_logging
__version__ = version("logwatcher")
logger = setup_logging() logger = setup_logging()
+1 -1
View File
@@ -1,4 +1,4 @@
from logwatcher.app import app from logwatcher.cli import app
def main(): def main():
-9
View File
@@ -1,9 +0,0 @@
from logwatcher import logger
def app():
logger.info("logwatcher starts its work.")
logger.info('logwatcher has finished its task.')
if __name__ == "__main__":
app()
+28 -15
View File
@@ -1,28 +1,28 @@
import re import re
from logwatcher import logger
from logwatcher.models import LogEntry from logwatcher.models import LogEntry
# ============================================================ # ============================================================
# Atomic patterns — reusable building blocks (no named groups) # Atomic patterns — reusable building blocks (no named groups)
# ============================================================ # ============================================================
_GRP = r"GRP_\d+" # store group _GRP = r"GRP_\d+" # store group
_STORE = r"\d{5}/\d{3}" # store code _STORE = r"\d{5}/\d{3}" # store code
_SAP = r"\d{4}" # SAP code (4 digits) _SAP = r"\d{4}" # SAP code (4 digits)
_ORD = r"\w+" # order number (alphanumeric) _ORD = r"\w+" # order number (alphanumeric)
_BL = rf"{_SAP}_{_ORD}_DESADV" # BL number _BL = rf"{_SAP}_{_ORD}_DESADV" # BL number
_SIZE = r"SizeRange \d+\.\d+" # size range _SIZE = r"SizeRange \d+\.\d+" # size range
_BRACKETS = r"(?:\[[^\]]+\])+" # one or more [Tag] blocks _BRACKETS = r"(?:\[[^\]]+\])+" # one or more [Tag] blocks
# Composite prefixes — reused across multiple patterns # Composite prefixes — reused across multiple patterns
_STORE_PREFIX_ERR = rf"(?P<store_group>{_GRP}) / (?P<store_code>{_STORE}) (?P<sap_code>{_SAP})_(?P<order_number>{_ORD}) ERREUR" _STORE_PREFIX_ERR = rf"(?P<store_group>{_GRP}) / (?P<store_code>{_STORE}) (?P<sap_code>{_SAP})_(?P<order_number>{_ORD}) ERREUR"
_STORE_PREFIX_WRN = rf"(?P<store_group>{_GRP}) / (?P<store_code>{_STORE}) (?P<sap_code>{_SAP})_(?P<order_number>{_ORD}) WARNING" _STORE_PREFIX_WRN = rf"(?P<store_group>{_GRP}) / (?P<store_code>{_STORE}) (?P<sap_code>{_SAP})_(?P<order_number>{_ORD}) WARNING"
_BL_PREFIX = rf"(?P<bl_number>{_BL}) : / (?P<store_code>{_STORE}) {_BL}" _BL_PREFIX = rf"(?P<bl_number>{_BL}) : / (?P<store_code>{_STORE}) {_BL}"
# error message patterns # error message patterns
N2_PATTERNS = { N2_PATTERNS = {
# high priority errors # high priority errors
"STORE_CODE_NOT_FOUND": r"Code magasin non trouvé : \d{5}/\d{3}",
"FEDAS_CODE_NOT_FOUND": r"Exception : Code Fedas non trouvé : (?P<fedas_code>\d+)", "FEDAS_CODE_NOT_FOUND": r"Exception : Code Fedas non trouvé : (?P<fedas_code>\d+)",
"CLOTHES_SIZE_RANGE_OUT_OF_BOUND": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+))(?P<brand_code>\w{3})?) / SetTailleTrav -> SizeRange (?P<size_range>\d+\.\d+) / Dépassement du nombre de tailles travaillées \(28 tailles maximum\)", "CLOTHES_SIZE_RANGE_OUT_OF_BOUND": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+))(?P<brand_code>\w{3})?) / SetTailleTrav -> SizeRange (?P<size_range>\d+\.\d+) / Dépassement du nombre de tailles travaillées \(28 tailles maximum\)",
"NOT_SAVED_DB_READ_ONLY": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\d{10}) ERREUR \d+ \w+ / DoTarifAchat -> SizeRange \d+\.\d+ / SetArtPrixAchat_Base -> (?: \[[^\]]+\])+attempted update on read-only database", "NOT_SAVED_DB_READ_ONLY": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\d{10}) ERREUR \d+ \w+ / DoTarifAchat -> SizeRange \d+\.\d+ / SetArtPrixAchat_Base -> (?: \[[^\]]+\])+attempted update on read-only database",
@@ -35,37 +35,50 @@ N2_PATTERNS = {
"PARTIAL_INTEGRATION": r"Erreur : (?P<bl_number>(?P<sap_code>\d{4})_(?P<order_number>\w+)_DESADV) : ISC ERROR CODE:(?P<isc_error_code>\d+) \(attention intégration partielle du BL ou d'une commande\)", "PARTIAL_INTEGRATION": r"Erreur : (?P<bl_number>(?P<sap_code>\d{4})_(?P<order_number>\w+)_DESADV) : ISC ERROR CODE:(?P<isc_error_code>\d+) \(attention intégration partielle du BL ou d'une commande\)",
"SAME_LINE_EXCEPT_DISCOUNT": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+))(?P<brand_code>\w{3})) / SetCOMBCDE -> OrderNumber : \d+ / Une commande ne peut pas contenir plusieurs lignes avec le même modèle, la même collection, la même date de livraison et avoir des remises différentes", "SAME_LINE_EXCEPT_DISCOUNT": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+))(?P<brand_code>\w{3})) / SetCOMBCDE -> OrderNumber : \d+ / Une commande ne peut pas contenir plusieurs lignes avec le même modèle, la même collection, la même date de livraison et avoir des remises différentes",
"MULTIPLE_ROWS_IN_SINGLETON_SELECT": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+))(?P<brand_code>\w{3})) / SizeRange (?P<size_range>\d+\.\d+) / SetMrkFourn -> (?: \[[^\]]+\])+multiple rows in singleton select", "MULTIPLE_ROWS_IN_SINGLETON_SELECT": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+))(?P<brand_code>\w{3})) / SizeRange (?P<size_range>\d+\.\d+) / SetMrkFourn -> (?: \[[^\]]+\])+multiple rows in singleton select",
# low priority errors # low priority errors
"SIZE_NOT_FOUND": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING (\d+ \w+) / SizeRange (?P<size_range>\d+\.\d+) - Columnx \d+ / Taille Inexistante", "SIZE_NOT_FOUND": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING (\d+ \w+) / SizeRange (?P<size_range>\d+\.\d+) - Columnx \d+ / Taille Inexistante",
"UNSYNCED_ORD_SD_FILES": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+)(?P<brand_code>\w{3}))) / OrderNumber : \d+ / Le modèle n'existe pas dans le fichier SD correspondant", "UNSYNCED_ORD_SD_FILES": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+)(?P<brand_code>\w{3}))) / OrderNumber : \d+ / Le modèle n'existe pas dans le fichier SD correspondant",
"CODE_MODEL_SIZE_OUT_OF_BOUND": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING [\w ]+ \w+ / SetArticle -> SizeRange \d+\.\d+ / (?: \[[^\]]+\])+-\w+\. Données trop volumineuses pour la variable (?: \[[^\]]+\]). Long. max. = \[\d+\], long. réelle = \[\d+\] Conseil : attribuez une valeur supérieure au paramètre TFDParam.Size", "CODE_MODEL_SIZE_OUT_OF_BOUND": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING [\w ]+ \w+ / SetArticle -> SizeRange \d+\.\d+ / (?: \[[^\]]+\])+-\w+\. Données trop volumineuses pour la variable (?: \[[^\]]+\]). Long. max. = \[\d+\], long. réelle = \[\d+\] Conseil : attribuez une valeur supérieure au paramètre TFDParam.Size",
"USER_NOT_CREATED_ON_NOSYMAG": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR / OrderNumber : \d+ / Utilisateur : configuration incorrecte \(valeur 0\) - vérifier dans IF_Config", "USER_NOT_CREATED_ON_NOSYMAG": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR / OrderNumber : \d+ / Utilisateur : configuration incorrecte \(valeur 0\) - vérifier dans IF_Config",
"INCORRECT_MDC_XML_FILES": r"Exception : Impossible de convertir le variant de type \(Null\) en type \(OleStr\)" "INCORRECT_MDC_XML_FILES": r"Exception : Impossible de convertir le variant de type \(Null\) en type \(OleStr\)",
} }
# compiled all patterns # compiled all patterns
_COMPILED_N2_PATTERNS = {error_name: re.compile(pattern) for error_name, pattern in N2_PATTERNS.items()} _COMPILED_N2_PATTERNS = {
error_name: re.compile(pattern) for error_name, pattern in N2_PATTERNS.items()
}
def _match_n2_pattern(log_entry: LogEntry) -> str|None:
def _match_n2_pattern(log_entry: LogEntry) -> str | None:
for error_code, pattern in _COMPILED_N2_PATTERNS.items(): for error_code, pattern in _COMPILED_N2_PATTERNS.items():
if pattern.search(log_entry.error_message): if pattern.search(log_entry.error_message):
return error_code return error_code
return None return None
def classify_log_entries(log_entries: list[LogEntry]) -> tuple[list[LogEntry], list[LogEntry]]:
def classify_log_entries(
log_entries: list[LogEntry],
) -> tuple[list[LogEntry], list[LogEntry]]:
""" """
Separate logs that require N2 intervention from those that don't. Separate logs that require N2 intervention from those that don't.
""" """
relevant_log_entries = [] relevant_log_entries = []
irrelevant_log_entries = [] irrelevant_log_entries = []
logger.info("\tclassification job started.")
for log_entry in log_entries: for log_entry in log_entries:
error_name = _match_n2_pattern(log_entry) error_name = _match_n2_pattern(log_entry)
if error_name: if error_name:
logger.info(
f"\t\tError '{error_name}' associated with log entry '{log_entry.error_message}'. This is a n2 log entry"
)
log_entry.error_name = error_name log_entry.error_name = error_name
relevant_log_entries.append(log_entry) relevant_log_entries.append(log_entry)
else: else:
irrelevant_log_entries.append(log_entry) irrelevant_log_entries.append(log_entry)
logger.info(
f"\t\tn2 logs: {len(relevant_log_entries)}, other logs: {len(irrelevant_log_entries)}"
)
logger.info("\tclassification job completed.")
return relevant_log_entries, irrelevant_log_entries return relevant_log_entries, irrelevant_log_entries
+124
View File
@@ -0,0 +1,124 @@
from pathlib import Path
from typing import Annotated
import typer
from logwatcher import __version__, logger
from logwatcher.classifier import classify_log_entries
from logwatcher.models import LogEntry
from logwatcher.parser import parse_log_file
from logwatcher.reporter import write_log_report
app = typer.Typer()
def version_callback(value: bool):
"""
Print the version of package.
Stop the CLI after printing version.
Args:
value: True if '--version' is provided, False otherwise.
"""
if value:
print(f"logwatcher version: {__version__}")
raise typer.Exit()
def _is_log_file(path: Path) -> bool:
"""
Return True if file is a log file (.txt, .log, or extensionless).
Args:
path: Path of a file
Returns:
True if file has .txt or .log extension or extensionless
False otherwise.
"""
if not path.is_file():
return False
return path.suffix in {"", ".txt", ".log"}
def _get_files_from_dir(input_dir: Path) -> list[Path]:
if not input_dir.exists():
raise FileNotFoundError(f"No such directory at '{input_dir}'")
return [file for file in input_dir.iterdir() if _is_log_file(file)]
def _files_to_log_entries(input_files: list[Path]) -> list[LogEntry]:
log_entries = []
for file in input_files:
if not file.is_file():
raise ValueError(f"'{file}' is not a file")
log_entries.extend(parse_log_file(file))
return log_entries
@app.command()
def cli(
output_dir: Annotated[
Path,
typer.Option(
"--output-dir", help="Output directories where reports will be stored."
),
] = Path("output"),
input_files: Annotated[
list[Path] | None, typer.Option("--input-files", help="Log files to scan.")
] = None,
input_dir: Annotated[
Path | None,
typer.Option("--input-dir", help="Directories containing log files."),
] = None,
version: Annotated[
bool,
typer.Option(
"--version",
help="Get the version of the package",
callback=version_callback,
is_eager=True,
),
] = False,
) -> None:
"""
Entry point for logwatcher.
Convert log files into two reports, one of n2 related logs, one without any n2 related logs.
These reports are sent by mail to n2 support technicians.
These log entries are used to generate incidents on Ivanti using its API.
Args:
input_files: Log files to scan. Either input_files or input_dir must be provided.
input_dir: Directories containing log files. Either input_files or input_dir must be provided.
output_dir: Required. Output directories where reports will be stored.
version: Option that returns the version of package
Returns:
None
"""
logger.info("============ logwatcher started ============")
if input_files and input_dir:
raise typer.BadParameter(
"Only one of input_files or input_dir must be provided."
)
if input_files:
logger.info(f"input_files option selected. input_files: {input_files}")
log_files = input_files
elif input_dir:
logger.info(f"input_dir option selected with value {input_dir}")
log_files = _get_files_from_dir(input_dir)
else:
raise typer.BadParameter("Either input_files or input_dir must be provided")
logger.info(f"reports will be located at '{output_dir.cwd()}'")
log_entries = _files_to_log_entries(log_files)
relevant, irrelevant = classify_log_entries(log_entries)
write_log_report(relevant, irrelevant, len(log_files), output_dir)
logger.info("============ logwatcher ended ============")
if __name__ == "__main__":
app()
+26 -21
View File
@@ -36,53 +36,58 @@ effective_file_path = Path.cwd() / "output/effective.log"
def parse_log_line(log_line: str) -> LogEntry | None: def parse_log_line(log_line: str) -> LogEntry | None:
"""Parse a raw log line into a LogEntry object. """Parse a raw log line into a LogEntry object.
Args: Args:
log_line: a line in a log file received by N2 technicians log_line: a line in a log file received by N2 technicians
Returns: Returns:
LogEntry if the line is a valid line, None if LogEntry if the line is a valid line, None if
it is empty or not a valid line. it is empty or not a valid line.
""" """
match = LOG_PATTERN.search(log_line) match = LOG_PATTERN.search(log_line)
if not match: if not match:
logger.debug(f"Unrecognize format. Ignoring line {log_line}.")
return None return None
data = match.groupdict() data = match.groupdict()
return LogEntry( return LogEntry(
server_ip=data["server_ip"], server_ip=data["server_ip"],
mdc_server_name=data["mdc_server_name"], mdc_server_name=data["mdc_server_name"],
start_time=datetime.strptime(data["start_time"], DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE), start_time=datetime.strptime(data["start_time"], DATETIME_FORMAT).astimezone(
FRENCH_TIMEZONE
),
store_name=data["store_name"], store_name=data["store_name"],
error_time=datetime.strptime(data["error_time"], DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE), error_time=datetime.strptime(data["error_time"], DATETIME_FORMAT).astimezone(
FRENCH_TIMEZONE
),
error_message=data["error_message"], error_message=data["error_message"],
raw_line=log_line raw_line=log_line,
) )
def parse_log_file(log_file_path: Path) -> list[LogEntry]: def parse_log_file(log_file_path: Path) -> list[LogEntry]:
"""Transforms the content of a log file into a list of LogEntry. """Transforms the content of a log file into a list of LogEntry.
Uses `parse_log_line` to parse each line of the choosen log file. Uses `parse_log_line` to parse each line of the choosen log file.
Raise exception if a log line encounters an error. Raise exception if a log line encounters an error.
Args: Args:
log_file_path: Path of a log file log_file_path: Path of a log file
Returns: Returns:
A list of LogEntry A list of LogEntry
""" """
log_entry_list: list[LogEntry] = [] log_entry_list: list[LogEntry] = []
with open(log_file_path, 'r', encoding="windows-1252") as log_file: with open(log_file_path, "r", encoding="windows-1252") as log_file:
logger.info(f"\tparsing log file '{log_file_path.name}' started.")
for index, log_line in enumerate(log_file): for index, log_line in enumerate(log_file):
try: try:
log_entry = parse_log_line(log_line) log_entry = parse_log_line(log_line)
except ValueError as error: except ValueError as error:
logger.error(f"Error while parsing the log file {log_file.name} at {log_line}: {error}") logger.error(
f"\t\tline {index}: error while parsing the log file '{log_file.name}': {error}"
)
else: else:
if log_entry: if log_entry:
log_entry_list.append(log_entry) log_entry_list.append(log_entry)
logger.debug(f"Line {index} was successfully parsed.") logger.info(f"\t\t{len(log_entry_list)} lines were parsed.")
else: logger.info("\tparsing completed.")
logger.debug(f"Line {index} is empty. Skip.")
return log_entry_list return log_entry_list
+120 -79
View File
@@ -1,7 +1,8 @@
import json
from datetime import datetime from datetime import datetime
from pathlib import Path
from string import Template from string import Template
from logwatcher import logger
from logwatcher.config import DATETIME_FORMAT, RESULT_PATH from logwatcher.config import DATETIME_FORMAT, RESULT_PATH
from logwatcher.models import LogEntry from logwatcher.models import LogEntry
@@ -9,31 +10,33 @@ from logwatcher.models import LogEntry
BASE_TEMPLATE = Template(""" BASE_TEMPLATE = Template("""
RAPPORT D'ANALYSE DE LOGS RAPPORT D'ANALYSE DE LOGS
========================= =========================
Période\t : $start_date → $end_date Période\t : $start_date -> $end_date
Fichiers lu\t : $nb_files Fichier(s) lu(s)\t : $nb_files
Nombre total d'erreurs\t: $nb_errors Nombre total d'erreur(s)\t: $nb_errors
$sub_content $sub_content
""") """)
N2_SUPPORT_TEMPLATE = Template(""" N2_SUPPORT_TEMPLATE = Template("""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ =========================
ERREURS SUPPORT N2 ERREURS SUPPORT N2
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ =========================
Nombre d'erreurs\t : $nb_errors Nombre d'erreur(s)\t : $nb_errors
$error_list $error_list
""") """)
OTHER_TEMPLATE = Template(""" OTHER_TEMPLATE = Template("""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ =========================
AUTRES ERREURS AUTRES ERREURS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ =========================
Nombre d'erreurs\t : $nb_errors Nombre d'erreur(s)\t : $nb_errors
$error_list $error_list
""") """)
ERROR_TEMPLATE = Template(""" ERROR_TEMPLATE = Template("""
[$index] $error_name [$index] $error_name
\tMagasin: $store_name \tMagasin: $store_name
\tMDC: $mdc
\tIP du MDC: $server_ip
\tHeure: $error_time \tHeure: $error_time
\tRaison: $error_message \tRaison: $error_message
""") """)
@@ -45,99 +48,137 @@ def _get_period(entries: list[LogEntry]) -> tuple[str, str]:
times: list[datetime] = [e.error_time for e in entries] times: list[datetime] = [e.error_time for e in entries]
return min(times).strftime(DATETIME_FORMAT), max(times).strftime(DATETIME_FORMAT) return min(times).strftime(DATETIME_FORMAT), max(times).strftime(DATETIME_FORMAT)
def write_log_report(relevant: list[LogEntry], irrelevant: list[LogEntry], nb_files: int):
def write_log_report(
relevant: list[LogEntry],
irrelevant: list[LogEntry],
nb_files: int,
output_dir: Path = RESULT_PATH,
) -> None:
""" """
Write each relevant, irrelevant and general reports in their respective output Write each relevant, irrelevant and general reports in their respective output
file. file.
Use the range date of relevant and irrelevant lists to get the period of time Use the range date of relevant and irrelevant lists to get the period of time
the logs were generated. the logs were generated.
Args:
relevant: List of n2 log entry
irrelevant: List of not n2 log entry
nb_files: Number of files scanned
output_dir: Location where all reports will be written
Returns:
None
""" """
end_date, start_date = _get_period(relevant + irrelevant) logger.info("\twriting reports job started.")
start_date, end_date = _get_period(relevant + irrelevant)
logger.info(f"\t\tperiod found: {start_date} -> {end_date}")
reports_dict = build_reports(relevant, irrelevant, start_date, end_date, nb_files) reports_dict = build_reports(relevant, irrelevant, start_date, end_date, nb_files)
for target, content in reports_dict.items(): for target in reports_dict:
target_file = RESULT_PATH / str(target + ".log") logger.info(f"\t\tgenerating report '{target}'...")
target_file.write_text(content, encoding="windows-1252") output_dir.mkdir(parents=True, exist_ok=True)
logger.debug(f"\t\t\t{output_dir} exists.")
target_file = output_dir / str(target + ".log")
target_file.write_text(reports_dict[target], encoding="windows-1252")
logger.debug(f"\t\t\treport was written in {target_file.name}")
logger.info(f"\t\treport '{target}' was generated.")
logger.info("\twriting reports job completed.")
def generate_json_report(relevant: list[LogEntry]) -> str: # TODO: put it in a serializer module def build_reports(
relevant: list[LogEntry], irrelevant: list[LogEntry], start_date, end_date, nb_files
) -> dict[str, str]:
""" """
Serialize from a Python LogEntry object to a JSON object. Build the three output reports: n2, other, and all.
Used in the Ivanti API calls "all" report contains both "n2" and "other" sections.
""" "n2" and "other" are two unique sections.
return json.dumps(relevant)
Args:
def build_reports(relevant: list[LogEntry], irrelevant: list[LogEntry], start_date, end_date, nb_files) -> dict[str, str]: relevant: List of n2 log entry
""" irrelevant: List of not n2 log entry
Build the three output reports: n2, other, and all. start_date: Date of the oldest log entry in relevant + irrelevant list
"all" report contains both "n2" and "other" sections. end_date: Date of the newest log entry in relevant + irrelevant list
"n2" and "other" are two unique sections. nb_files: number of files scanned
Returns:
Dictionary of reports in string format
""" """
logger.info("\t\tbuilding reports job started.")
relevant_report = _render_target_report( relevant_report = _render_target_report(
log_entries=relevant, log_entries=relevant, target_template=N2_SUPPORT_TEMPLATE
target_template=N2_SUPPORT_TEMPLATE
) )
irrelevant_report = _render_target_report( irrelevant_report = _render_target_report(
log_entries=irrelevant, log_entries=irrelevant, target_template=OTHER_TEMPLATE
target_template=OTHER_TEMPLATE
) )
logger.info("\t\t\tn2 and other reports were generated.")
logger.info("\t\tbuilding reports job completed.")
return { return {
"n2": BASE_TEMPLATE.substitute( "n2": BASE_TEMPLATE.substitute(
start_date=start_date, start_date=start_date,
end_date=end_date, end_date=end_date,
nb_files=nb_files, nb_files=nb_files,
nb_errors=len(relevant), nb_errors=len(relevant),
sub_content=relevant_report, sub_content=relevant_report,
), ),
"other": BASE_TEMPLATE.substitute( "other": BASE_TEMPLATE.substitute(
start_date=start_date, start_date=start_date,
end_date=end_date, end_date=end_date,
nb_files=nb_files, nb_files=nb_files,
nb_errors=len(irrelevant), nb_errors=len(irrelevant),
sub_content=irrelevant_report, sub_content=irrelevant_report,
), ),
"all": BASE_TEMPLATE.substitute( "all": BASE_TEMPLATE.substitute(
start_date=start_date, start_date=start_date,
end_date=end_date, end_date=end_date,
nb_files=nb_files, nb_files=nb_files,
nb_errors=len(relevant + irrelevant), nb_errors=len(relevant + irrelevant),
sub_content=relevant_report + "\n" + irrelevant_report, sub_content=relevant_report + "\n" + irrelevant_report,
) ),
} }
# build templates
def _render_target_report(
log_entries: list[LogEntry], target_template: Template
) -> str:
"""
Render report using either N2_SUPPORT_TEMPLATE or OTHER_TEMPLATE.
Call _render_entries() and include rendered log entries in the
new report.
Args:
log_entries: List of log entries
target_template: Template to use for N2_SUPPORT_TEMPLATE
Returns:
content of report in string format
"""
error_list = _render_entries(log_entries)
return target_template.substitute(nb_errors=len(log_entries), error_list=error_list)
def _render_entries(log_entries: list[LogEntry]) -> str: def _render_entries(log_entries: list[LogEntry]) -> str:
""" """
Render a list of log entries using the `ERROR_TEMPLATE`. Render a list of log entries using the `ERROR_TEMPLATE`.
Used in generated report files in the `output` directory Used in generated report files in the `output` directory
Args:
log_entries: List of log entries
Returns:
error report in string format
""" """
error_list = [] error_list = []
for index, log_entry in enumerate(log_entries, start=1): for index, log_entry in enumerate(log_entries, start=1):
error_list.append( error_list.append(
ERROR_TEMPLATE.substitute( ERROR_TEMPLATE.substitute(
index=index, index=index,
error_name=log_entry.error_name, error_name=log_entry.error_name,
store_name=log_entry.store_name, store_name=log_entry.store_name,
error_time=log_entry.get_error_time(), error_time=log_entry.get_error_time(),
error_message=log_entry.error_message error_message=log_entry.error_message,
) mdc=log_entry.mdc_server_name,
server_ip=log_entry.server_ip,
) )
)
return "".join(error_list) return "".join(error_list)
# build templates
def _render_target_report(log_entries: list[LogEntry], target_template) -> str:
"""
Render report using either N2_SUPPORT_TEMPLATE or OTHER_TEMPLATE.
Call _render_entries() and include rendered log entries in the
new report.
"""
error_list = _render_entries(log_entries)
return target_template.substitute(
nb_errors=len(log_entries),
error_list=error_list
)
View File
+1 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
import pytest import pytest
from logwatcher.config import FIXTURE_PATH from logwatcher.config import FIXTURE_PATH
from logwatcher.logging import setup_logging from logwatcher.logging_config import setup_logging
# original log directory # original log directory
ORIGINAL_LOG_DIR = FIXTURE_PATH / "original_logs" ORIGINAL_LOG_DIR = FIXTURE_PATH / "original_logs"
-238
View File
@@ -1,238 +0,0 @@
Répertoire scanné : \\192.168.60.40\e$\MDC_1110\Logs
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731084952.txt [31/07/2026 08:50:00] DOSSIER EN COURS : FLAMMIER [31/07/2026 08:50:23] Erreur FTP commande : Requested action not taken
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731084952.txt [31/07/2026 08:50:00] DOSSIER EN COURS : FLAMMIER [31/07/2026 08:50:26] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731084952.txt [31/07/2026 08:58:50] DOSSIER EN COURS : GARRIGOU [31/07/2026 08:59:05] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731084952.txt [31/07/2026 09:15:54] DOSSIER EN COURS : HANOT [31/07/2026 09:17:12] Erreur FTP commande : Requested action not taken
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731084952.txt [31/07/2026 09:15:54] DOSSIER EN COURS : HANOT [31/07/2026 09:17:30] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731084952.txt [31/07/2026 09:36:31] DOSSIER EN COURS : PIGUET [31/07/2026 09:36:35] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731094952.txt [31/07/2026 09:50:01] DOSSIER EN COURS : FLAMMIER [31/07/2026 09:50:34] Erreur FTP commande : Requested action not taken
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731094952.txt [31/07/2026 09:50:01] DOSSIER EN COURS : FLAMMIER [31/07/2026 09:50:35] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731094952.txt [31/07/2026 09:51:07] DOSSIER EN COURS : GARRIGOU [31/07/2026 09:51:14] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731094952.txt [31/07/2026 09:56:24] DOSSIER EN COURS : HANOT [31/07/2026 09:56:39] Erreur FTP commande : Requested action not taken
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731094952.txt [31/07/2026 09:56:24] DOSSIER EN COURS : HANOT [31/07/2026 09:56:39] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.40\e$\MDC_1110\Logs\26\07\31\20260731094952.txt [31/07/2026 09:58:22] DOSSIER EN COURS : PIGUET [31/07/2026 09:58:26] Erreur FTP SDUpdatePrixISF : Requested action not taken
Répertoire scanné : \\192.168.60.41\e$\MDC_2110\Logs
\\192.168.60.41\e$\MDC_2110\Logs\26\07\31\20260731091707.txt [31/07/2026 09:17:17] DOSSIER EN COURS : VINET [31/07/2026 09:17:27] Erreur FTP commande : Requested action not taken
\\192.168.60.41\e$\MDC_2110\Logs\26\07\31\20260731091707.txt [31/07/2026 09:17:17] DOSSIER EN COURS : VINET [31/07/2026 09:17:44] Erreur FTP commande : Erreur : le fichier [1834_4508457998_SD.XML] est vide sur le serveur FTP !
\\192.168.60.41\e$\MDC_2110\Logs\26\07\31\20260731091707.txt [31/07/2026 09:17:17] DOSSIER EN COURS : VINET [31/07/2026 09:18:18] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.41\e$\MDC_2110\Logs\26\07\31\20260731091707.txt [31/07/2026 10:07:17] DOSSIER EN COURS : GRAND-EST-COLL-NOSYMAG [31/07/2026 10:07:20] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.41\e$\MDC_2110\Logs\26\07\31\20260731091707.txt [31/07/2026 10:08:27] DOSSIER EN COURS : TAILLET-BIS [31/07/2026 10:08:34] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.41\e$\MDC_2110\Logs\26\07\31\20260731101158.txt [31/07/2026 10:12:09] DOSSIER EN COURS : VINET [31/07/2026 10:12:27] Erreur FTP commande : Requested action not taken
\\192.168.60.41\e$\MDC_2110\Logs\26\07\31\20260731101158.txt [31/07/2026 10:12:09] DOSSIER EN COURS : VINET [31/07/2026 10:12:27] Erreur FTP commande : Erreur : le fichier [1834_4508457998_SD.XML] est vide sur le serveur FTP !
\\192.168.60.41\e$\MDC_2110\Logs\26\07\31\20260731101158.txt [31/07/2026 10:12:09] DOSSIER EN COURS : VINET [31/07/2026 10:12:27] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.41\e$\MDC_2110\Logs\26\07\31\20260731101158.txt [31/07/2026 10:40:31] DOSSIER EN COURS : GRAND-EST-COLL-NOSYMAG [31/07/2026 10:40:33] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.41\e$\MDC_2110\Logs\26\07\31\20260731101158.txt [31/07/2026 10:40:33] DOSSIER EN COURS : TAILLET-BIS [31/07/2026 10:40:37] Erreur FTP SDUpdatePrixISF : Requested action not taken
Répertoire scanné : \\192.168.60.42\e$\MDC_3110\Logs
\\192.168.60.42\e$\MDC_3110\Logs\26\07\31\20260731091401.txt [31/07/2026 09:14:10] DOSSIER EN COURS : RIHOUET [31/07/2026 09:33:29] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.42\e$\MDC_3110\Logs\26\07\31\20260731091401.txt [31/07/2026 09:14:10] DOSSIER EN COURS : RIHOUET [31/07/2026 09:34:41] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.42\e$\MDC_3110\Logs\26\07\31\20260731103040.txt [31/07/2026 10:30:52] DOSSIER EN COURS : RIHOUET [31/07/2026 10:36:05] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.42\e$\MDC_3110\Logs\26\07\31\20260731103040.txt [31/07/2026 10:30:52] DOSSIER EN COURS : RIHOUET [31/07/2026 10:36:11] Erreur FTP SDUpdatePrixISF : Requested action not taken
Répertoire scanné : \\192.168.60.43\e$\MDC_4110\Logs
\\192.168.60.43\e$\MDC_4110\Logs\26\07\31\20260731080409.txt [31/07/2026 08:04:17] DOSSIER EN COURS : JOANNIN-NOSYMAG [31/07/2026 09:20:14] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.43\e$\MDC_4110\Logs\26\07\31\20260731100809.txt [31/07/2026 10:08:18] DOSSIER EN COURS : JOANNIN-NOSYMAG [31/07/2026 10:11:44] Erreur FTP SDUpdatePrixISF : Requested action not taken
Répertoire scanné : \\192.168.60.44\e$\MDC_5110\Logs
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731080152.txt [31/07/2026 08:02:01] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 08:02:17] Erreur FTP commande : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731080152.txt [31/07/2026 08:02:01] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 08:02:20] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731080152.txt [31/07/2026 08:32:05] DOSSIER EN COURS : NIVAULT-NOSYMAG [31/07/2026 08:32:28] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731084855.txt [31/07/2026 08:49:04] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 08:50:12] Erreur FTP commande : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731084855.txt [31/07/2026 08:49:04] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 08:50:12] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731084855.txt [31/07/2026 08:50:24] DOSSIER EN COURS : NIVAULT-NOSYMAG [31/07/2026 08:50:30] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731091856.txt [31/07/2026 09:19:05] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 09:22:27] Erreur FTP commande : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731091856.txt [31/07/2026 09:19:05] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 09:22:28] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731091856.txt [31/07/2026 09:31:42] DOSSIER EN COURS : NIVAULT-NOSYMAG [31/07/2026 09:33:30] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731094856.txt [31/07/2026 09:49:04] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 09:49:34] Erreur FTP commande : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731094856.txt [31/07/2026 09:49:04] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 09:49:34] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731094856.txt [31/07/2026 09:55:48] DOSSIER EN COURS : NIVAULT-NOSYMAG [31/07/2026 09:56:18] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731094856.txt [31/07/2026 09:55:48] DOSSIER EN COURS : NIVAULT-NOSYMAG [31/07/2026 10:00:10] Erreur : GRP_57 / 04414/000 2782_M00058837 WARNING 5H1300 B02BUYN46 / Art_id : 297707604 - Fusart_id : 293032470 / Impossible de créer un lien en automtique, la grille de taille est différente entre l'article d'origne et l'article fusionné
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731101857.txt [31/07/2026 10:19:06] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 10:20:15] Erreur FTP commande : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731101857.txt [31/07/2026 10:19:06] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 10:20:16] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731101857.txt [31/07/2026 10:25:42] DOSSIER EN COURS : NIVAULT-NOSYMAG [31/07/2026 10:26:16] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731104858.txt [31/07/2026 10:49:07] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 10:49:38] Erreur FTP commande : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731104858.txt [31/07/2026 10:49:07] DOSSIER EN COURS : LAPINI-LEGRAND [31/07/2026 10:49:39] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.44\e$\MDC_5110\Logs\26\07\31\20260731104858.txt [31/07/2026 10:55:58] DOSSIER EN COURS : NIVAULT-NOSYMAG [31/07/2026 10:56:29] Erreur FTP SDUpdatePrixISF : Requested action not taken
Répertoire scanné : \\192.168.60.45\e$\MDC_6110\Logs
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731070128.txt [31/07/2026 07:01:38] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 07:01:40] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731073128.txt [31/07/2026 07:31:39] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 07:31:41] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731080129.txt [31/07/2026 08:01:37] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 08:01:50] Erreur FTP commande : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731080129.txt [31/07/2026 08:01:37] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 08:01:56] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731080129.txt [31/07/2026 08:12:43] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 08:12:48] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731080129.txt [31/07/2026 08:12:43] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 08:13:14] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731083746.txt [31/07/2026 08:37:55] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 08:37:57] Erreur FTP commande : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731083746.txt [31/07/2026 08:37:55] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 08:37:57] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731083746.txt [31/07/2026 08:38:14] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 08:38:20] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731083746.txt [31/07/2026 08:38:14] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 08:38:20] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731090747.txt [31/07/2026 09:07:56] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 09:07:58] Erreur FTP commande : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731090747.txt [31/07/2026 09:07:56] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 09:07:58] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731090747.txt [31/07/2026 09:07:59] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 09:08:23] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731090747.txt [31/07/2026 09:07:59] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 09:08:24] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731093747.txt [31/07/2026 09:37:57] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 09:37:59] Erreur FTP commande : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731093747.txt [31/07/2026 09:37:57] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 09:37:59] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731093747.txt [31/07/2026 09:38:00] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 09:38:40] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731093747.txt [31/07/2026 09:38:00] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 09:38:45] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731100747.txt [31/07/2026 10:07:58] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 10:08:01] Erreur FTP commande : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731100747.txt [31/07/2026 10:07:58] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 10:08:02] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731100747.txt [31/07/2026 10:08:28] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 10:09:07] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731100747.txt [31/07/2026 10:08:28] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 10:09:15] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731103748.txt [31/07/2026 10:37:56] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 10:38:00] Erreur FTP commande : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731103748.txt [31/07/2026 10:37:56] DOSSIER EN COURS : NEUKIRCH-NOSYMAG [31/07/2026 10:38:00] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731103748.txt [31/07/2026 10:38:43] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 10:39:58] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.45\e$\MDC_6110\Logs\26\07\31\20260731103748.txt [31/07/2026 10:38:43] DOSSIER EN COURS : LECLERC-NOSYMAG [31/07/2026 10:40:05] Erreur FTP SDUpdatePrixISF : Requested action not taken
Répertoire scanné : \\192.168.60.46\e$\MDC_7110\Logs
\\192.168.60.46\e$\MDC_7110\Logs\26\07\31\20260731072401.txt [31/07/2026 07:24:09] DOSSIER EN COURS : ANDALI [31/07/2026 07:24:14] Erreur FTP Réception : Requested action not taken
\\192.168.60.46\e$\MDC_7110\Logs\26\07\31\20260731075402.txt [31/07/2026 07:54:11] DOSSIER EN COURS : ANDALI [31/07/2026 07:54:16] Erreur FTP Réception : Requested action not taken
\\192.168.60.46\e$\MDC_7110\Logs\26\07\31\20260731082402.txt [31/07/2026 08:24:11] DOSSIER EN COURS : ANDALI [31/07/2026 08:24:20] Erreur FTP Réception : Requested action not taken
\\192.168.60.46\e$\MDC_7110\Logs\26\07\31\20260731085403.txt [31/07/2026 08:54:12] DOSSIER EN COURS : ANDALI [31/07/2026 08:54:18] Erreur FTP Réception : Requested action not taken
\\192.168.60.46\e$\MDC_7110\Logs\26\07\31\20260731092403.txt [31/07/2026 09:24:12] DOSSIER EN COURS : ANDALI [31/07/2026 09:27:04] Erreur FTP Réception : Requested action not taken
\\192.168.60.46\e$\MDC_7110\Logs\26\07\31\20260731092403.txt [31/07/2026 09:40:53] DOSSIER EN COURS : ROLLAND [31/07/2026 09:49:27] Erreur : GRP_112 / 04507/000 2759_M00054781 WARNING MB153 01058Q26C / Art_id : 282646256 - Fusart_id : 197203919 / Impossible de créer un lien en automtique, la grille de taille est différente entre l'article d'origne et l'article fusionné
\\192.168.60.46\e$\MDC_7110\Logs\26\07\31\20260731095658.txt [31/07/2026 09:57:09] DOSSIER EN COURS : ANDALI [31/07/2026 09:57:52] Erreur FTP Réception : Requested action not taken
\\192.168.60.46\e$\MDC_7110\Logs\26\07\31\20260731102658.txt [31/07/2026 10:27:08] DOSSIER EN COURS : ANDALI [31/07/2026 10:28:57] Erreur FTP Réception : Requested action not taken
Répertoire scanné : \\192.168.60.47\e$\MDC_8110\Logs
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731070611.txt [31/07/2026 07:06:21] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 07:06:28] Erreur FTP Réception : Requested action not taken
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731080611.txt [31/07/2026 08:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 08:07:04] Erreur FTP Réception : Requested action not taken
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:10:13] Erreur FTP commande : Requested action not taken
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:10:33] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:10:34] Erreur FTP Réception : Requested action not taken
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:26:12] Code magasin non trouvé : 04929/000
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:26:12] Code magasin non trouvé : 04929/000
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:26:12] Code magasin non trouvé : 04929/000
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:26:12] Code magasin non trouvé : 04929/000
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:34:14] DOSSIER EN COURS : RUE [31/07/2026 09:34:26] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731100612.txt [31/07/2026 10:06:23] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 10:09:50] Erreur FTP commande : Requested action not taken
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731100612.txt [31/07/2026 10:06:23] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 10:09:50] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731100612.txt [31/07/2026 10:06:23] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 10:09:51] Erreur FTP Réception : Requested action not taken
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731100612.txt [31/07/2026 10:13:19] DOSSIER EN COURS : RUE [31/07/2026 10:13:26] Erreur FTP SDUpdatePrixISF : Requested action not taken
Répertoire scanné : \\192.168.60.48\e$\MDC_9110\Logs
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731081227.txt [31/07/2026 08:36:59] DOSSIER EN COURS : CHENNEVIER-NOSYMAG [31/07/2026 08:37:02] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731081227.txt [31/07/2026 08:38:12] DOSSIER EN COURS : FRONTERA-NOSYMAG [31/07/2026 08:38:29] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731085432.txt [31/07/2026 08:54:43] DOSSIER EN COURS : BARIAUD-NOSYMAG [31/07/2026 09:01:50] Erreur : 2804_4506738503 -> Exception : Déclaration xml non valide.
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731085432.txt [31/07/2026 09:05:10] DOSSIER EN COURS : CHENNEVIER-NOSYMAG [31/07/2026 09:05:14] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731085432.txt [31/07/2026 09:05:15] DOSSIER EN COURS : FRONTERA-NOSYMAG [31/07/2026 09:05:17] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731092432.txt [31/07/2026 09:24:43] DOSSIER EN COURS : BARIAUD-NOSYMAG [31/07/2026 09:25:50] Erreur : 2804_4506738503 -> Exception : Déclaration xml non valide.
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731092432.txt [31/07/2026 09:29:06] DOSSIER EN COURS : CHENNEVIER-NOSYMAG [31/07/2026 09:29:08] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731092432.txt [31/07/2026 09:29:09] DOSSIER EN COURS : FRONTERA-NOSYMAG [31/07/2026 09:29:11] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731095433.txt [31/07/2026 09:54:43] DOSSIER EN COURS : BARIAUD-NOSYMAG [31/07/2026 09:59:45] Erreur : 2804_4506738503 -> Exception : Déclaration xml non valide.
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731095433.txt [31/07/2026 10:02:59] DOSSIER EN COURS : CHENNEVIER-NOSYMAG [31/07/2026 10:03:01] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731095433.txt [31/07/2026 10:03:01] DOSSIER EN COURS : FRONTERA-NOSYMAG [31/07/2026 10:03:06] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731102433.txt [31/07/2026 10:24:43] DOSSIER EN COURS : BARIAUD-NOSYMAG [31/07/2026 10:26:18] Erreur : 2804_4506738503 -> Exception : Déclaration xml non valide.
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731102433.txt [31/07/2026 10:29:31] DOSSIER EN COURS : CHENNEVIER-NOSYMAG [31/07/2026 10:29:33] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.48\e$\MDC_9110\Logs\26\07\31\20260731102433.txt [31/07/2026 10:29:34] DOSSIER EN COURS : FRONTERA-NOSYMAG [31/07/2026 10:29:40] Erreur FTP SDUpdatePrixISF : Requested action not taken
Répertoire scanné : \\192.168.60.64\e$\MDC_10110\Logs
\\192.168.60.64\e$\MDC_10110\Logs\26\07\31\20260731081125.txt [31/07/2026 08:11:34] DOSSIER EN COURS : ROSEC-NOSYMAG [31/07/2026 08:11:51] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.64\e$\MDC_10110\Logs\26\07\31\20260731081125.txt [31/07/2026 08:30:46] DOSSIER EN COURS : SAVARIEAU-NOSYMAG [31/07/2026 08:30:59] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.64\e$\MDC_10110\Logs\26\07\31\20260731084359.txt [31/07/2026 08:44:08] DOSSIER EN COURS : ROSEC-NOSYMAG [31/07/2026 08:44:11] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.64\e$\MDC_10110\Logs\26\07\31\20260731084359.txt [31/07/2026 08:48:34] DOSSIER EN COURS : SAVARIEAU-NOSYMAG [31/07/2026 08:48:36] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.64\e$\MDC_10110\Logs\26\07\31\20260731091400.txt [31/07/2026 09:14:10] DOSSIER EN COURS : ROSEC-NOSYMAG [31/07/2026 09:14:13] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.64\e$\MDC_10110\Logs\26\07\31\20260731091400.txt [31/07/2026 09:18:51] DOSSIER EN COURS : SAVARIEAU-NOSYMAG [31/07/2026 09:18:55] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.64\e$\MDC_10110\Logs\26\07\31\20260731094401.txt [31/07/2026 09:44:11] DOSSIER EN COURS : ROSEC-NOSYMAG [31/07/2026 09:44:13] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.64\e$\MDC_10110\Logs\26\07\31\20260731094401.txt [31/07/2026 09:48:43] DOSSIER EN COURS : SAVARIEAU-NOSYMAG [31/07/2026 09:48:45] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.64\e$\MDC_10110\Logs\26\07\31\20260731101401.txt [31/07/2026 10:14:11] DOSSIER EN COURS : ROSEC-NOSYMAG [31/07/2026 10:14:17] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.64\e$\MDC_10110\Logs\26\07\31\20260731101401.txt [31/07/2026 10:20:10] DOSSIER EN COURS : SAVARIEAU-NOSYMAG [31/07/2026 10:20:14] Erreur FTP SDUpdatePrixISF : Requested action not taken
Répertoire scanné : \\192.168.60.65\e$\MDC_11110\Logs
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731071009.txt [31/07/2026 07:14:51] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 07:14:52] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731074010.txt [31/07/2026 07:42:27] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 07:42:28] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731081010.txt [31/07/2026 08:10:18] DOSSIER EN COURS : BOBILLIER_NOSYMAG [31/07/2026 08:10:26] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731081010.txt [31/07/2026 08:16:35] DOSSIER EN COURS : CHABAUTY-NOSYMAG [31/07/2026 08:16:59] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731081010.txt [31/07/2026 08:32:14] DOSSIER EN COURS : CUNTZMANN [31/07/2026 08:32:18] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731081010.txt [31/07/2026 08:34:06] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 08:34:08] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731081010.txt [31/07/2026 08:34:06] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 08:34:11] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731084011.txt [31/07/2026 08:40:18] DOSSIER EN COURS : BOBILLIER_NOSYMAG [31/07/2026 08:40:24] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731084011.txt [31/07/2026 08:40:25] DOSSIER EN COURS : CHABAUTY-NOSYMAG [31/07/2026 08:40:31] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731084011.txt [31/07/2026 08:44:33] DOSSIER EN COURS : CUNTZMANN [31/07/2026 08:44:35] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731084011.txt [31/07/2026 08:44:35] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 08:44:37] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731084011.txt [31/07/2026 08:44:35] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 08:44:38] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731091012.txt [31/07/2026 09:10:19] DOSSIER EN COURS : BOBILLIER_NOSYMAG [31/07/2026 09:11:22] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731091012.txt [31/07/2026 09:11:22] DOSSIER EN COURS : CHABAUTY-NOSYMAG [31/07/2026 09:11:39] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731091012.txt [31/07/2026 09:16:00] DOSSIER EN COURS : CUNTZMANN [31/07/2026 09:16:04] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731091012.txt [31/07/2026 09:16:05] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 09:16:06] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731091012.txt [31/07/2026 09:16:05] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 09:16:07] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731094012.txt [31/07/2026 09:40:20] DOSSIER EN COURS : BOBILLIER_NOSYMAG [31/07/2026 09:40:26] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731094012.txt [31/07/2026 09:40:27] DOSSIER EN COURS : CHABAUTY-NOSYMAG [31/07/2026 09:40:58] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731094012.txt [31/07/2026 09:46:23] DOSSIER EN COURS : CUNTZMANN [31/07/2026 09:46:26] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731094012.txt [31/07/2026 09:46:27] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 09:46:29] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731094012.txt [31/07/2026 09:46:27] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 09:46:30] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731101012.txt [31/07/2026 10:10:20] DOSSIER EN COURS : BOBILLIER_NOSYMAG [31/07/2026 10:10:27] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731101012.txt [31/07/2026 10:10:28] DOSSIER EN COURS : CHABAUTY-NOSYMAG [31/07/2026 10:10:49] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731101012.txt [31/07/2026 10:16:17] DOSSIER EN COURS : CUNTZMANN [31/07/2026 10:16:20] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731101012.txt [31/07/2026 10:16:21] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 10:16:26] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731101012.txt [31/07/2026 10:16:21] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 10:16:26] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731104012.txt [31/07/2026 10:40:20] DOSSIER EN COURS : BOBILLIER_NOSYMAG [31/07/2026 10:40:50] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731104012.txt [31/07/2026 10:42:30] DOSSIER EN COURS : CHABAUTY-NOSYMAG [31/07/2026 10:42:57] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731104012.txt [31/07/2026 10:48:03] DOSSIER EN COURS : CUNTZMANN [31/07/2026 10:48:08] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731104012.txt [31/07/2026 10:48:08] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 10:48:13] Erreur FTP commande réassort : Requested action not taken
\\192.168.60.65\e$\MDC_11110\Logs\26\07\31\20260731104012.txt [31/07/2026 10:48:08] DOSSIER EN COURS : MAITRE-ROSSI [31/07/2026 10:48:14] Erreur FTP SDUpdatePrixISF : Requested action not taken
Répertoire scanné : \\192.168.60.52\e$\MDC_12110\Logs
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731081539.txt [31/07/2026 08:17:11] DOSSIER EN COURS : DELEGLISE [31/07/2026 08:17:12] Erreur FTP MajModele : Erreur : le fichier [Majmodele_v20250723t010101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731081539.txt [31/07/2026 08:17:11] DOSSIER EN COURS : DELEGLISE [31/07/2026 08:17:13] Erreur FTP SDUpdate : Erreur : le fichier [Sdupdate_v20250918t170101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731081539.txt [31/07/2026 08:18:31] DOSSIER EN COURS : WALTER [31/07/2026 08:18:41] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731081539.txt [31/07/2026 08:23:27] DOSSIER EN COURS : ROUSSARIE-PELOU [31/07/2026 08:23:36] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731084539.txt [31/07/2026 08:45:50] DOSSIER EN COURS : DELEGLISE [31/07/2026 08:45:52] Erreur FTP MajModele : Erreur : le fichier [Majmodele_v20250723t010101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731084539.txt [31/07/2026 08:45:50] DOSSIER EN COURS : DELEGLISE [31/07/2026 08:45:52] Erreur FTP SDUpdate : Erreur : le fichier [Sdupdate_v20250918t170101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731084539.txt [31/07/2026 08:45:53] DOSSIER EN COURS : WALTER [31/07/2026 08:45:58] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731084539.txt [31/07/2026 08:45:58] DOSSIER EN COURS : ROUSSARIE-PELOU [31/07/2026 08:46:00] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731091540.txt [31/07/2026 09:15:51] DOSSIER EN COURS : DELEGLISE [31/07/2026 09:15:53] Erreur FTP MajModele : Erreur : le fichier [Majmodele_v20250723t010101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731091540.txt [31/07/2026 09:15:51] DOSSIER EN COURS : DELEGLISE [31/07/2026 09:15:53] Erreur FTP SDUpdate : Erreur : le fichier [Sdupdate_v20250918t170101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731091540.txt [31/07/2026 09:15:54] DOSSIER EN COURS : WALTER [31/07/2026 09:15:58] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731091540.txt [31/07/2026 09:15:59] DOSSIER EN COURS : ROUSSARIE-PELOU [31/07/2026 09:16:01] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731094540.txt [31/07/2026 09:45:53] DOSSIER EN COURS : DELEGLISE [31/07/2026 09:45:54] Erreur FTP MajModele : Erreur : le fichier [Majmodele_v20250723t010101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731094540.txt [31/07/2026 09:45:53] DOSSIER EN COURS : DELEGLISE [31/07/2026 09:45:55] Erreur FTP SDUpdate : Erreur : le fichier [Sdupdate_v20250918t170101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731094540.txt [31/07/2026 09:45:55] DOSSIER EN COURS : WALTER [31/07/2026 09:46:06] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731094540.txt [31/07/2026 09:47:02] DOSSIER EN COURS : ROUSSARIE-PELOU [31/07/2026 09:47:06] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731101540.txt [31/07/2026 10:15:53] DOSSIER EN COURS : DELEGLISE [31/07/2026 10:15:55] Erreur FTP MajModele : Erreur : le fichier [Majmodele_v20250723t010101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731101540.txt [31/07/2026 10:15:53] DOSSIER EN COURS : DELEGLISE [31/07/2026 10:15:56] Erreur FTP SDUpdate : Erreur : le fichier [Sdupdate_v20250918t170101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731101540.txt [31/07/2026 10:15:56] DOSSIER EN COURS : WALTER [31/07/2026 10:16:04] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731101540.txt [31/07/2026 10:16:30] DOSSIER EN COURS : ROUSSARIE-PELOU [31/07/2026 10:16:33] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731104540.txt [31/07/2026 10:45:52] DOSSIER EN COURS : DELEGLISE [31/07/2026 10:45:54] Erreur FTP MajModele : Erreur : le fichier [Majmodele_v20250723t010101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731104540.txt [31/07/2026 10:45:52] DOSSIER EN COURS : DELEGLISE [31/07/2026 10:45:54] Erreur FTP SDUpdate : Erreur : le fichier [Sdupdate_v20250918t170101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731104540.txt [31/07/2026 10:45:55] DOSSIER EN COURS : WALTER [31/07/2026 10:46:04] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.52\e$\MDC_12110\Logs\26\07\31\20260731104540.txt [31/07/2026 10:46:09] DOSSIER EN COURS : ROUSSARIE-PELOU [31/07/2026 10:46:14] Erreur FTP SDUpdatePrixISF : Requested action not taken
Répertoire scanné : \\192.168.60.53\e$\MDC_13110\Logs
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:37:17] DOSSIER EN COURS : PRIOU-NOSYMAG [31/07/2026 09:37:29] Erreur FTP commande : Erreur : le fichier [1283_4509336380_ORD.XML] est vide sur le serveur FTP !
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:37:17] DOSSIER EN COURS : PRIOU-NOSYMAG [31/07/2026 09:37:29] Erreur FTP commande : Erreur : le fichier [1283_4509337060_SD.XML] est vide sur le serveur FTP !
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:37:17] DOSSIER EN COURS : PRIOU-NOSYMAG [31/07/2026 09:38:04] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:54:03] DOSSIER EN COURS : SCHIEVER-BS-C-NOSYMAG [31/07/2026 09:54:08] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 09:58:00] Erreur MAJ : Configuration incorrecte -> (12/3) valeur 0
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 09:58:00] Erreur MAJ : Type comptable : configuration incorrecte (valeur 0)
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 10:00:48] Erreur MAJ : Code Fedas non trouvé : 799999
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 10:00:48] Erreur : Exception : Code Fedas non trouvé : 799999
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 10:00:48] Erreur : Type comptable : configuration incorrecte (valeur 0)
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 10:00:51] Erreur : Type comptable : configuration incorrecte (valeur 0)
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 10:01:34] DOSSIER EN COURS : TOSI-NOSYMAG [31/07/2026 10:01:51] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 10:01:34] DOSSIER EN COURS : TOSI-NOSYMAG [31/07/2026 10:01:52] Erreur : Exception : Code Fedas non trouvé : 799999
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731102208.txt [31/07/2026 10:22:17] DOSSIER EN COURS : PRIOU-NOSYMAG [31/07/2026 10:23:17] Erreur FTP commande : Erreur : le fichier [1283_4509336380_ORD.XML] est vide sur le serveur FTP !
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731102208.txt [31/07/2026 10:22:17] DOSSIER EN COURS : PRIOU-NOSYMAG [31/07/2026 10:23:17] Erreur FTP commande : Erreur : le fichier [1283_4509337060_SD.XML] est vide sur le serveur FTP !
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731102208.txt [31/07/2026 10:22:17] DOSSIER EN COURS : PRIOU-NOSYMAG [31/07/2026 10:23:18] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731102208.txt [31/07/2026 10:26:38] DOSSIER EN COURS : SCHIEVER-BS-C-NOSYMAG [31/07/2026 10:26:48] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731102208.txt [31/07/2026 10:26:49] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 10:26:49] Erreur MAJ : Configuration incorrecte -> (12/3) valeur 0
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731102208.txt [31/07/2026 10:26:49] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 10:26:49] Erreur MAJ : Type comptable : configuration incorrecte (valeur 0)
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731102208.txt [31/07/2026 10:26:49] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 10:26:50] Erreur MAJ : Code Fedas non trouvé : 799999
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731102208.txt [31/07/2026 10:26:49] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 10:26:50] Erreur : Exception : Code Fedas non trouvé : 799999
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731102208.txt [31/07/2026 10:26:50] DOSSIER EN COURS : TOSI-NOSYMAG [31/07/2026 10:26:54] Erreur FTP SDUpdatePrixISF : Requested action not taken
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731102208.txt [31/07/2026 10:26:50] DOSSIER EN COURS : TOSI-NOSYMAG [31/07/2026 10:26:55] Erreur : Exception : Code Fedas non trouvé : 799999
Répertoire scanné : \\192.168.60.77\e$\MDC_15110\Logs
Accès refusé.\\192.168.60.77\e$\MDC_15110\Logs
-1
View File
@@ -4,4 +4,3 @@
\\192.168.60.46\e$\MDC_7110\Logs\26\07\30\20260730115342.txt [30/07/2026 11:59:11] DOSSIER EN COURS : ROLLAND [30/07/2026 12:00:16] Erreur : GRP_112 / 04507/000 2759_M00054781 WARNING MB153 01058Q26C / Art_id : 282646256 - Fusart_id : 197203919 / Impossible de créer un lien en automtique, la grille de taille est différente entre l'article d'origne et l'article fusionné \\192.168.60.46\e$\MDC_7110\Logs\26\07\30\20260730115342.txt [30/07/2026 11:59:11] DOSSIER EN COURS : ROLLAND [30/07/2026 12:00:16] Erreur : GRP_112 / 04507/000 2759_M00054781 WARNING MB153 01058Q26C / Art_id : 282646256 - Fusart_id : 197203919 / Impossible de créer un lien en automtique, la grille de taille est différente entre l'article d'origne et l'article fusionné
\\192.168.60.64\e$\MDC_10110\Logs\26\07\30\20260730101103.txt [30/07/2026 10:11:12] DOSSIER EN COURS : ROSEC-NOSYMAG [30/07/2026 10:11:54] Erreur : GRP_58 / 00185/000 1168_4509983556 ERREUR HJ3791 006B4UO81 / OrderNumber : 4509983556 - ColorNumber 006B4U - Taille 07 / GRP_58 / 00185/000 1168_4509983556 ERREUR HJ3791 006B4UO81 / SetCOMCDEL -> OrderNumber : 4509983556 - ColorNumber 006B4U - Taille 07 / [FireDAC][Phys][IB]lock conflict on no wait transaction \\192.168.60.64\e$\MDC_10110\Logs\26\07\30\20260730101103.txt [30/07/2026 10:11:12] DOSSIER EN COURS : ROSEC-NOSYMAG [30/07/2026 10:11:54] Erreur : GRP_58 / 00185/000 1168_4509983556 ERREUR HJ3791 006B4UO81 / OrderNumber : 4509983556 - ColorNumber 006B4U - Taille 07 / GRP_58 / 00185/000 1168_4509983556 ERREUR HJ3791 006B4UO81 / SetCOMCDEL -> OrderNumber : 4509983556 - ColorNumber 006B4U - Taille 07 / [FireDAC][Phys][IB]lock conflict on no wait transaction
\\192.168.60.52\e$\MDC_12110\Logs\26\07\27\20260727153934.txt [27/07/2026 15:39:49] DOSSIER EN COURS : WALTER [27/07/2026 15:40:07] Erreur : GRP_332 / 04601/000 2605_M00009978 WARNING CDL/1/BMX3/FIT/3 00017ZCF2 / / TVA à 0 pour le modèle - Intégration impossible \\192.168.60.52\e$\MDC_12110\Logs\26\07\27\20260727153934.txt [27/07/2026 15:39:49] DOSSIER EN COURS : WALTER [27/07/2026 15:40:07] Erreur : GRP_332 / 04601/000 2605_M00009978 WARNING CDL/1/BMX3/FIT/3 00017ZCF2 / / TVA à 0 pour le modèle - Intégration impossible
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:26:12] Code magasin non trouvé : 04929/000
-6
View File
@@ -7,14 +7,8 @@
\\192.168.60.52\e$\MDC_12110\Logs\26\07\27\20260727153934.txt [27/07/2026 15:39:49] DOSSIER EN COURS : WALTER [27/07/2026 15:40:07] Erreur : GRP_332 / 04601/000 2605_M00009978 WARNING CDL/1/BMX3/FIT/3 00017ZCF2 / / TVA à 0 pour le modèle - Intégration impossible \\192.168.60.52\e$\MDC_12110\Logs\26\07\27\20260727153934.txt [27/07/2026 15:39:49] DOSSIER EN COURS : WALTER [27/07/2026 15:40:07] Erreur : GRP_332 / 04601/000 2605_M00009978 WARNING CDL/1/BMX3/FIT/3 00017ZCF2 / / TVA à 0 pour le modèle - Intégration impossible
\\192.168.60.52\e$\MDC_12110\Logs\26\07\27\20260727183937.txt [27/07/2026 18:39:50] DOSSIER EN COURS : DELEGLISE [27/07/2026 18:39:52] Erreur FTP MajModele : Erreur : le fichier [Majmodele_v20250723t010101.zip] est vide sur le serveur FTP ! \\192.168.60.52\e$\MDC_12110\Logs\26\07\27\20260727183937.txt [27/07/2026 18:39:50] DOSSIER EN COURS : DELEGLISE [27/07/2026 18:39:52] Erreur FTP MajModele : Erreur : le fichier [Majmodele_v20250723t010101.zip] est vide sur le serveur FTP !
\\192.168.60.52\e$\MDC_12110\Logs\26\07\27\20260727183937.txt [27/07/2026 18:39:50] DOSSIER EN COURS : DELEGLISE [27/07/2026 18:39:52] Erreur FTP SDUpdate : Erreur : le fichier [Sdupdate_v20250918t170101.zip] est vide sur le serveur FTP ! \\192.168.60.52\e$\MDC_12110\Logs\26\07\27\20260727183937.txt [27/07/2026 18:39:50] DOSSIER EN COURS : DELEGLISE [27/07/2026 18:39:52] Erreur FTP SDUpdate : Erreur : le fichier [Sdupdate_v20250918t170101.zip] est vide sur le serveur FTP !
\\192.168.13.23\e\MDC_320\Logs\26\07\28\20260728103943.txt [28/07/2026 10:39:58] DOSSIER EN COURS : VERGNE [28/07/2026 10:48:26] Code magasin non trouvé : 04299/000
\\192.168.13.23\e\MDC_320\Logs\26\07\28\20260728103943.txt [28/07/2026 10:39:58] DOSSIER EN COURS : VERGNE [28/07/2026 10:48:26] Code magasin non trouvé : 04299/000
\\192.168.60.44\e$\MDC_5110\Logs\26\07\30\20260730101515.txt [30/07/2026 10:15:24] DOSSIER EN COURS : LAPINI-LEGRAND [30/07/2026 10:20:31] Erreur : 1097_2622656794_DESADV : / 00093/000 1097_2622656794_DESADV ERREUR M_NONISF_1B_NONISF_1 / BL AUTO : 2622656794 / Package : / EAN : 5715871624921 / SizeRange S_NONISF_1 / La quantité dépasse le reste à livrer de la ligne de commande \\192.168.60.44\e$\MDC_5110\Logs\26\07\30\20260730101515.txt [30/07/2026 10:15:24] DOSSIER EN COURS : LAPINI-LEGRAND [30/07/2026 10:20:31] Erreur : 1097_2622656794_DESADV : / 00093/000 1097_2622656794_DESADV ERREUR M_NONISF_1B_NONISF_1 / BL AUTO : 2622656794 / Package : / EAN : 5715871624921 / SizeRange S_NONISF_1 / La quantité dépasse le reste à livrer de la ligne de commande
\\192.168.60.44\e$\MDC_5110\Logs\26\07\30\20260730101515.txt [30/07/2026 10:15:24] DOSSIER EN COURS : LAPINI-LEGRAND [30/07/2026 10:20:36] Erreur : 1838_0012238055_DESADV : / 00708/000 1838_0012238055_DESADV ERREUR DOUBLON L45477200 021CJSQ24 / SetRelationArticle -> BL AUTO : 0012238055 / Package : 330354962027088798 / EAN : 2050002995901 / SizeRange 35.02 / Correspondance manquante pour l'article fusionné (Couleur/taille) \\192.168.60.44\e$\MDC_5110\Logs\26\07\30\20260730101515.txt [30/07/2026 10:15:24] DOSSIER EN COURS : LAPINI-LEGRAND [30/07/2026 10:20:36] Erreur : 1838_0012238055_DESADV : / 00708/000 1838_0012238055_DESADV ERREUR DOUBLON L45477200 021CJSQ24 / SetRelationArticle -> BL AUTO : 0012238055 / Package : 330354962027088798 / EAN : 2050002995901 / SizeRange 35.02 / Correspondance manquante pour l'article fusionné (Couleur/taille)
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:26:12] Code magasin non trouvé : 04929/000
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:26:12] Code magasin non trouvé : 04929/000
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:26:12] Code magasin non trouvé : 04929/000
\\192.168.60.47\e$\MDC_8110\Logs\26\07\31\20260731090612.txt [31/07/2026 09:06:22] DOSSIER EN COURS : ELLIS-LEREGENT [31/07/2026 09:26:12] Code magasin non trouvé : 04929/000
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 09:58:00] Erreur MAJ : Configuration incorrecte -> (12/3) valeur 0 \\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 09:58:00] Erreur MAJ : Configuration incorrecte -> (12/3) valeur 0
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 09:58:00] Erreur MAJ : Type comptable : configuration incorrecte (valeur 0) \\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 09:58:00] Erreur MAJ : Type comptable : configuration incorrecte (valeur 0)
\\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 10:00:48] Erreur MAJ : Code Fedas non trouvé : 799999 \\192.168.60.53\e$\MDC_13110\Logs\26\07\31\20260731093707.txt [31/07/2026 09:56:29] DOSSIER EN COURS : CHARALAMBAKIS-NOSYMAG [31/07/2026 10:00:48] Erreur MAJ : Code Fedas non trouvé : 799999
+1 -1
View File
@@ -104,7 +104,7 @@ def test_classify_log_entries_none_relevant(valid_log_dir: Path):
""" """
Must return an empty relevant log entry list Must return an empty relevant log entry list
""" """
log_file = valid_log_dir / "no_relevant_error.txt" log_file = valid_log_dir / "no_relevant_logs.txt"
log_entries = parse_log_file(log_file) log_entries = parse_log_file(log_file)
# all entries are relevant # all entries are relevant
+423
View File
@@ -0,0 +1,423 @@
import re
from pathlib import Path
import pytest
from typer.testing import CliRunner
from logwatcher import __version__
from logwatcher.cli import _get_files_from_dir, _is_log_file, app
runner = CliRunner()
@pytest.fixture(name="output_path")
def get_output_path_fixture(tmp_path: Path):
"""
Fixture that return output directory path where all logs and output files
will be located during tests.
Args:
tmp_path: Temporary root path for testing results
Returns:
None
"""
return tmp_path / "output"
def get_argument_from_files(log_dir: Path, output_path: Path) -> list[str]:
"""
Utility function that implement an argument list to call cli
Args:
log_dir: Path to the logwatcher directory
output_path: Temporary path for all output generated during testing
Returns:
List of arguments in string format
"""
files = [
str(file) for file in log_dir.iterdir() if file.suffix in ("", ".txt", ".log")
]
arguments = ["--output-dir", str(output_path)]
for file in files:
arguments += ["--input-files", file]
return arguments
def test_cli_help():
"""
Test the help command and assert that all options are listed
Returns:
None
"""
results = runner.invoke(app, ["--help"])
assert results.exit_code == 0
assert "Usage: cli [OPTIONS]" in results.output
assert "--input-dir" in results.output
assert "--input-files" in results.output
assert "--output-dir" in results.output
assert "--version" in results.output
def test_version_cli():
"""
Test the version option. The version must be printed
Returns:
None
"""
result = runner.invoke(app, ["--version"])
assert result.exit_code == 0
assert f"logwatcher version: {__version__}" in result.output
def test_is_log_file(tmp_path: Path):
"""
Assert only .log, .txt or file without extensions are valid
Args:
tmp_path: Temporary root path for testing results
Returns:
None
"""
# set up files
Path(tmp_path / "a.log").touch()
Path(tmp_path / "a.txt").touch()
Path(tmp_path / "a.csv").touch()
Path(tmp_path / "a.py").touch()
Path(tmp_path / "a").touch()
Path(tmp_path / "a.pdf").touch()
# test
assert _is_log_file(Path(tmp_path / "a.log"))
assert _is_log_file(Path(tmp_path / "a.txt"))
assert _is_log_file(Path(tmp_path / "a")) # without extensions
assert not _is_log_file(Path(tmp_path / "a.csv"))
assert not _is_log_file(Path(tmp_path / "a.py"))
assert not _is_log_file(Path(tmp_path / "a.pdf"))
def test_get_files_from_dir_empty_dir(tmp_path: Path):
"""
Assert that a non-existing directory (or wrong path) cannot be
given as a working input directory.
Assert that everything works even if the directory is empty.
Args:
tmp_path: Temporary root path for testing results
Returns:
None
"""
with pytest.raises(FileNotFoundError):
_get_files_from_dir(tmp_path / "input_test")
input_test_dir = tmp_path / "input_test"
input_test_dir.mkdir()
assert input_test_dir.is_dir()
assert _get_files_from_dir(input_test_dir) == []
def test_get_files_from_dir_not_empty_dir(tmp_path: Path):
"""
Assert the right files are retrieved if the directory is not empty.
Args:
tmp_path: Temporary root path for testing results
Returns:
None
"""
# create directory
input_test_dir = tmp_path / "input_test"
input_test_dir.mkdir()
# populate it with files
Path(input_test_dir / "a.log").touch()
Path(input_test_dir / "a.txt").touch()
Path(input_test_dir / "a.csv").touch()
Path(input_test_dir / "a.py").touch()
Path(input_test_dir / "a").touch()
Path(input_test_dir / "a.pdf").touch()
assert input_test_dir.is_dir()
file_list = _get_files_from_dir(input_test_dir)
assert file_list != []
assert len(file_list) == 3
assert {file.name for file in file_list} == {"a.log", "a.txt", "a"}
def test_cli_input_files_and_input_dir(output_path: Path):
"""
Assert that input files and input dir options cannot be provided in the same command.
Args:
tmp_path: Temporary root path for testing results
Returns:
None
"""
results = runner.invoke(
app,
[
"--input-files",
"a.log",
"--input-dir",
"logs",
"--output-dir",
str(output_path),
],
)
assert results.exit_code == 2
assert "Only one of input_files or input_dir must be provided." in results.output
assert not output_path.exists()
def test_cli_neither_input_files_nor_input_dir():
"""
Test calling cli without providing input files or input dir options.
Must return an error on provided parameters (BadParameter = exit code 2)
Returns:
None
"""
results = runner.invoke(app)
assert results.exit_code == 2
assert "Either input_files or input_dir must be provided" in results.output
def test_cli_bad_input_file(tmp_path: Path, output_path: Path, invalid_log_dir: Path):
"""
Test calling cli with bad input file (directory instead of file)
Must return an exit code at 1 and the ValueError exception raised
while `_files_to_log_entries()` was running.
Args:
tmp_path: Temporary root path for testing results
output_path: Temporary path for all output generated during testing
invalid_log_dir: Fixture directory of invalid logs
Returns:
None
"""
directory = str(tmp_path)
results = runner.invoke(
app,
["--input-files", directory, "--output-dir", str(output_path)],
)
assert results.exit_code == 1
assert isinstance(results.exception, ValueError)
assert f"'{directory}' is not a file" in results.exception.args[0]
assert (
not output_path.exists()
) # would be created if `directory` was a valid parameter
def test_cli_empty_input_files(output_path: Path, invalid_log_dir: Path):
"""
Test calling cli with empty input files.
Must pass and generate reports that mention reading a file only.
All other parameters must be at 0.
Args:
output_path: Temporary path for all output generated during testing
invalid_log_dir: Fixture directory of invalid logs
Returns:
None
"""
empty_file = str(next(invalid_log_dir.glob("empty_file.txt")))
results = runner.invoke(
app,
["--input-files", empty_file, "--output-dir", str(output_path)],
)
assert results.exit_code == 0
# check files
nb_files = 1 # only empty_file.txt should have been read
file_list = list(output_path.iterdir())
assert len(file_list) == 3
for file in file_list:
content = file.read_text(encoding="windows-1252")
assert file.name in ("all.log", "n2.log", "other.log")
assert f"Fichier(s) lu(s)\t : {nb_files}" in content
assert "Nombre total d'erreur(s)\t: 0" in content
def test_cli_input_files_or_input_dir(
tmp_path: Path, output_path: Path, original_log_dir: Path
):
"""
Assert that calling the CLI with either input files or input dir options
is a valid process.
Args:
tmp_path: Temporary root path for testing results
output_path: Temporary path for all output generated during testing
original_log_dir: Fixture directory of initial logs received by n2 technicians
Returns:
None
"""
# output for input-files option
input_files_output = output_path / "input_files"
# save number of files from original log directory for future assertion test
nb_files = len(list(original_log_dir.iterdir()))
# test with `input files` option provided only
result = runner.invoke(
app, get_argument_from_files(original_log_dir, input_files_output)
)
assert result.exit_code == 0
assert result.exception is None
output_file_list = list(input_files_output.iterdir())
assert len(output_file_list) == 3
# check files
for file in output_file_list:
assert file.name in ("all.log", "n2.log", "other.log")
assert file.stat().st_size != 0 # must contain basic execution information
assert f"Fichier(s) lu(s)\t : {nb_files}" in file.read_text(
encoding="windows-1252"
)
# test with `input dir` option provided only
# output for input-dir option
input_dir_output = output_path / "input_dir"
result = runner.invoke(
app,
[
"--input-dir",
str(original_log_dir),
"--output-dir",
str(input_dir_output),
],
)
assert result.exit_code == 0
assert result.exception is None
output_file_list = list(input_dir_output.iterdir())
assert len(output_file_list) == 3
# check files
for file in output_file_list:
assert file.name in ("all.log", "n2.log", "other.log")
assert file.stat().st_size != 0 # must contain basic execution information
assert f"Fichier(s) lu(s)\t : {nb_files}" in file.read_text(
encoding="windows-1252"
)
def test_cli_input_files_one_file(valid_log_dir: Path, output_path: Path):
mixed_logs_file = valid_log_dir / "mixed_logs.txt"
result = runner.invoke(
app, ["--input-files", str(mixed_logs_file), "--output-dir", str(output_path)]
)
assert result.exit_code == 0
assert result.exception is None
output_file_list = list(output_path.iterdir())
assert len(output_file_list) == 3
# check file
date_pattern = r"\d{1,2}/\d{1,2}/\d{4} \d{2}:\d{2}:\d{2}"
for file in output_file_list:
content = file.read_text(encoding="windows-1252")
assert re.search(f"Période\t : {date_pattern} -> {date_pattern}\n", content)
assert file.name in ("all.log", "n2.log", "other.log")
assert "Fichier(s) lu(s)\t : 1" in file.read_text(encoding="windows-1252")
assert "Nombre total d'erreur(s)\t: 0" not in content
def test_cli_empty_input_directory(tmp_path: Path, output_path: Path):
"""
Assert that CLI can run on empty directory without error and
generates reports that indicates no file read.
Args:
tmp_path: Temporary root path for testing results
output_path: Temporary path for all output generated during testing
Returns:
None
"""
input_dir = Path(tmp_path / "input_test")
input_dir.mkdir(parents=True)
result = runner.invoke(
app,
[
"--input-dir",
str(input_dir),
"--output-dir",
str(output_path),
],
)
assert result.exit_code == 0
output_file_list = list(output_path.iterdir())
assert len(output_file_list) == 3
# check files
for file in output_file_list:
content = file.read_text(encoding="windows-1252")
assert file.name in ("all.log", "n2.log", "other.log")
assert file.stat().st_size != 0 # must contain basic execution information
assert "Période : -> " in content
assert "Fichier(s) lu(s)\t : 0" in content
assert "Nombre total d'erreur(s)\t: 0" in content
def test_cli_invalid_logs(output_path: Path, invalid_log_dir: Path):
"""
On invalid logs (empty files, log from different sources than MDC LAME...),
CLI should run without error and creates report files with metrics at 0.
Args:
output_path: Temporary path for all output generated during testing
invalid_log_dir: Fixture directory of invalid logs
Returns:
None
"""
arguments = get_argument_from_files(invalid_log_dir, output_path)
results = runner.invoke(app, arguments)
assert results.exit_code == 0
assert output_path.exists()
nb_files = len(list(invalid_log_dir.iterdir()))
output_file_list = list(output_path.iterdir())
assert len(output_file_list) == 3
for file in output_file_list:
content = file.read_text(encoding="windows-1252")
assert "Période : -> " in content
assert f"Fichier(s) lu(s)\t : {nb_files}" in content
assert "Nombre total d'erreur(s)\t: 0" in content
def test_cli_valid_logs(output_path: Path, valid_log_dir: Path):
"""
On valid test, everything should work successfully. Metrics
should not be at 0.
Args:
output_path: Temporary path for all output generated during testing
valid_log_dir: Fixture directory of valid logs
Returns:
None
"""
arguments = get_argument_from_files(valid_log_dir, output_path)
results = runner.invoke(app, arguments)
assert results.exit_code == 0
assert output_path.exists()
nb_files = len(list(valid_log_dir.iterdir()))
date_pattern = r"\d{1,2}/\d{1,2}/\d{4} \d{2}:\d{2}:\d{2}"
output_file_list = list(output_path.iterdir())
assert len(output_file_list) == 3
for file in output_file_list:
content = file.read_text(encoding="windows-1252")
assert re.search(f"Période\t : {date_pattern} -> {date_pattern}\n", content)
assert f"Fichier(s) lu(s)\t : {nb_files}" in content
assert "Nombre total d'erreur(s)\t: 0" not in content
+1 -1
View File
@@ -14,7 +14,7 @@ from logging import DEBUG, ERROR, INFO, WARNING, FileHandler, StreamHandler, get
import pytest import pytest
from logwatcher import logger from logwatcher import logger
from logwatcher.logging import _setup_formatter, _setup_handler, setup_logging from logwatcher.logging_config import _setup_formatter, _setup_handler, setup_logging
def test_setup_handler(tmp_log_file): def test_setup_handler(tmp_log_file):
+294
View File
@@ -0,0 +1,294 @@
from datetime import datetime
from pathlib import Path
import pytest
from logwatcher.classifier import classify_log_entries
from logwatcher.config import DATETIME_FORMAT, FIXTURE_PATH
from logwatcher.models import LogEntry
from logwatcher.parser import parse_log_file
from logwatcher.reporter import (
N2_SUPPORT_TEMPLATE,
OTHER_TEMPLATE,
_get_period,
_render_entries,
_render_target_report,
build_reports,
write_log_report,
)
# path of all CR_* files used in `get_all_log_entries_fixture`
CR_LOG_FILES = list(FIXTURE_PATH.glob("*/CR_*"))
def _get_classified_log_entries(
log_file: Path,
) -> tuple[list[LogEntry], list[LogEntry]]:
"""Parse a log file and split its entries into relevant and irrelevant lists."""
log_entries = parse_log_file(log_file)
return classify_log_entries(log_entries)
@pytest.fixture(name="nb_files")
def get_nb_log_files_used_fixture():
"""Return the number of CR_* log files used by the other fixtures."""
return len(CR_LOG_FILES)
@pytest.fixture(name="get_all_log_entries")
def get_all_log_entries_fixture() -> tuple[list[LogEntry], list[LogEntry]]:
"""Parse and classify every CR_* fixture, returning (relevant, irrelevant)."""
all_entries = []
for log_file in CR_LOG_FILES:
all_entries.extend(parse_log_file(log_file))
relevant_list, irrelevant_list = classify_log_entries(all_entries)
return relevant_list, irrelevant_list
@pytest.fixture(name="get_only_relevant_log_entries")
def get_only_relevant_log_entries_fixture(valid_log_dir: Path):
"""Return (relevant, irrelevant) for a file containing only N2-relevant errors."""
log_file = valid_log_dir / "only_relevant_logs.txt"
return _get_classified_log_entries(log_file)
@pytest.fixture(name="get_only_irrelevant_log_entries")
def get_only_irrelevant_log_entries_fixture(valid_log_dir: Path):
"""Return (relevant, irrelevant) for a file containing only non-N2 errors."""
log_file = valid_log_dir / "no_relevant_logs.txt"
return _get_classified_log_entries(log_file)
def test_get_period_empty_list():
"""An empty entry list yields an empty (start, end) period."""
assert _get_period([]) == ("", "")
def test_get_period_format(
get_all_log_entries: tuple[list[LogEntry], list[LogEntry]],
):
"""Period bounds are formatted according to the configured datetime format."""
period = _get_period(get_all_log_entries[0] + get_all_log_entries[1])
for period_bound in period:
try:
assert datetime.strptime(period_bound, DATETIME_FORMAT) # noqa: DTZ007
except ValueError:
pytest.fail(f"Wrong date format for {period_bound}")
def test_render_entries_all(get_all_log_entries: tuple[list[LogEntry], list[LogEntry]]):
"""Rendered entries expose every useful field but never the raw line."""
for log_entries in get_all_log_entries:
error_report = _render_entries(log_entries)
if log_entries:
for index, log_entry in enumerate(log_entries, start=1):
assert f"[{index}]" in error_report
assert log_entry.error_name in error_report
assert log_entry.store_name in error_report
assert log_entry.get_error_time() in error_report
try:
assert datetime.strptime( # noqa: DTZ007
log_entry.get_error_time(), DATETIME_FORMAT
)
except ValueError:
pytest.fail(f"Wrong date format for {log_entry}")
assert log_entry.error_message in error_report
assert log_entry.server_ip in error_report
assert log_entry.mdc_server_name in error_report
# must not be in report file
assert log_entry.raw_line not in error_report
elif not log_entries:
assert error_report == ""
else:
pytest.fail("`log_entries` must be defined")
def test_render_entries_relevant(
get_only_relevant_log_entries: tuple[list[LogEntry], list[LogEntry]],
):
"""Only-relevant input renders a non-empty relevant block and an empty other block."""
error_report_relevant = _render_entries(get_only_relevant_log_entries[0])
error_report_irrelevant = _render_entries(get_only_relevant_log_entries[1])
assert error_report_relevant != "", error_report_irrelevant == ""
def test_render_entries_irrelevant(
get_only_irrelevant_log_entries: tuple[list[LogEntry], list[LogEntry]],
):
"""Only-irrelevant input renders an empty relevant block and a non-empty other block."""
error_report_relevant = _render_entries(get_only_irrelevant_log_entries[0])
error_report_irrelevant = _render_entries(get_only_irrelevant_log_entries[1])
assert error_report_relevant == "", error_report_irrelevant != ""
def test_render_target_report_relevant(
get_only_relevant_log_entries: tuple[list[LogEntry], list[LogEntry]],
):
"""The N2 section header, count, and rendered entries are all present."""
relevant = get_only_relevant_log_entries[0]
target_report = _render_target_report(relevant, N2_SUPPORT_TEMPLATE)
assert "ERREURS SUPPORT N2" in target_report
assert target_report.find("Nombre d'erreur(s)\t : " + str(len(relevant))) != -1
assert _render_entries(relevant) in target_report
def test_render_target_report_irrelevant(
get_only_irrelevant_log_entries: tuple[list[LogEntry], list[LogEntry]],
):
"""The "other" section header, count, and rendered entries are all present."""
irrelevant = get_only_irrelevant_log_entries[1]
target_report = _render_target_report(irrelevant, OTHER_TEMPLATE)
assert "AUTRES ERREURS" in target_report
assert target_report.find("Nombre d'erreur(s)\t : " + str(len(irrelevant))) != -1
assert _render_entries(irrelevant) in target_report
def test_render_target_report_all(
get_all_log_entries: tuple[list[LogEntry], list[LogEntry]],
):
"""Each target template renders the expected section header."""
relevant, irrelevant = get_all_log_entries
relevant_report = _render_target_report(relevant, N2_SUPPORT_TEMPLATE)
irrelevant_report = _render_target_report(irrelevant, OTHER_TEMPLATE)
assert "ERREURS SUPPORT N2" in relevant_report
assert "AUTRES ERREURS" in irrelevant_report
def test_build_reports(
get_all_log_entries: tuple[list[LogEntry], list[LogEntry]], nb_files
):
"""The three reports are built with correct headers, counts, and section separation."""
start_date, end_date = _get_period(get_all_log_entries[0] + get_all_log_entries[1])
reports = build_reports(
get_all_log_entries[0], get_all_log_entries[1], start_date, end_date, nb_files
)
assert "all" in reports and "n2" in reports and "other" in reports
assert reports["all"].find(f"Période\t : {start_date} -> {end_date}") != -1
assert reports["all"].find(f"Fichier(s) lu(s)\t : {nb_files}") != -1
assert (
reports["all"].find(
f"Nombre total d'erreur(s)\t: {len(get_all_log_entries[0] + get_all_log_entries[1])}"
)
!= -1
)
assert "ERREURS SUPPORT N2" in reports["all"] or "AUTRES ERREURS" in reports["all"]
assert (
"AUTRES ERREURS" not in reports["n2"] and "ERREURS SUPPORT N2" in reports["n2"]
)
assert (
"ERREURS SUPPORT N2" not in reports["other"]
and "AUTRES ERREURS" in reports["other"]
)
assert (
reports["n2"].find(f"Nombre total d'erreur(s)\t: {len(get_all_log_entries[0])}")
!= -1
)
assert (
reports["other"].find(
f"Nombre total d'erreur(s)\t: {len(get_all_log_entries[1])}"
)
!= -1
)
def _test_write_log_report(
log_entries_tuple: tuple[list[LogEntry], list[LogEntry]],
nb_files: int,
target_name_empty: str,
output_dir: Path,
):
"""Write the reports, then verify files exist and their content matches build_reports."""
results_path = output_dir / "results"
relevant, irrelevant = log_entries_tuple
write_log_report(relevant, irrelevant, nb_files, results_path)
# check if files were created
all_file, n2_file, other_file = (
(results_path / "all.log"),
(results_path / "n2.log"),
(results_path / "other.log"),
)
assert all_file.exists()
assert n2_file.exists()
assert other_file.exists()
# check content
start_date, end_date = _get_period(relevant + irrelevant)
reports = build_reports(relevant, irrelevant, start_date, end_date, nb_files)
nb_errors = {
"all": len(relevant + irrelevant),
"n2": len(relevant),
"other": len(irrelevant),
}
assert reports["all"] == all_file.read_text(encoding="windows-1252")
assert (
all_file.read_text(encoding="windows-1252").find(
f"Nombre total d'erreur(s)\t: {nb_errors['all']}"
)
!= -1
)
assert reports["n2"] == n2_file.read_text(encoding="windows-1252")
assert (
n2_file.read_text(encoding="windows-1252").find(
f"Nombre total d'erreur(s)\t: {nb_errors['n2']}"
)
!= -1
)
assert reports["other"] == other_file.read_text(encoding="windows-1252")
assert (
other_file.read_text(encoding="windows-1252").find(
f"Nombre total d'erreur(s)\t: {nb_errors['other']}"
)
!= -1
)
assert nb_errors[target_name_empty] == 0 # the target list must be empty
def test_write_log_report_relevant(
get_only_relevant_log_entries: tuple[list[LogEntry], list[LogEntry]],
nb_files: int,
tmp_path: Path,
):
"""Only-relevant input writes the three files with an empty "other" list."""
_test_write_log_report(get_only_relevant_log_entries, nb_files, "other", tmp_path)
def test_write_log_report_irrelevant(
get_only_irrelevant_log_entries: tuple[list[LogEntry], list[LogEntry]],
nb_files: int,
tmp_path: Path,
):
"""Only-irrelevant input writes the three files with an empty "n2" list."""
_test_write_log_report(get_only_irrelevant_log_entries, nb_files, "n2", tmp_path)
def test_write_log_report_all_logs(
get_all_log_entries: tuple[list[LogEntry], list[LogEntry]],
nb_files: int,
tmp_path: Path,
):
"""The full fixture writes three files whose content matches build_reports exactly."""
results_path = tmp_path / "results"
write_log_report(
get_all_log_entries[0], get_all_log_entries[1], nb_files, results_path
)
# check if files were created
all_file, n2_file, other_file = (
(results_path / "all.log"),
(results_path / "n2.log"),
(results_path / "other.log"),
)
assert all_file.exists()
assert n2_file.exists()
assert other_file.exists()
# check content
start_date, end_date = _get_period(get_all_log_entries[0] + get_all_log_entries[1])
reports = build_reports(
get_all_log_entries[0], get_all_log_entries[1], start_date, end_date, nb_files
)
assert reports["all"] == all_file.read_text(encoding="windows-1252")
assert reports["n2"] == n2_file.read_text(encoding="windows-1252")
assert reports["other"] == other_file.read_text(encoding="windows-1252")
Generated
+69 -213
View File
@@ -1,11 +1,20 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.10" requires-python = ">=3.14"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.15'", "python_full_version >= '3.15'",
"python_full_version < '3.15'", "python_full_version < '3.15'",
] ]
[[package]]
name = "annotated-doc"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
]
[[package]] [[package]]
name = "ast-serialize" name = "ast-serialize"
version = "0.6.0" version = "0.6.0"
@@ -62,65 +71,6 @@ version = "7.15.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/70/b052a519a584663a7bd052841a2debe11c8309ec49a7786340003f9c0a02/coverage-7.15.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0be6daac4cce6b8c8dc65886bae1b082ddbca4da8e5cbb5e15166acf253e264", size = 222245, upload-time = "2026-08-06T13:46:55.253Z" },
{ url = "https://files.pythonhosted.org/packages/67/39/892fa511aba3d1c3c8f49509a0ff5c71eab9f9f88d08e1a38da395821660/coverage-7.15.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b24e078eabcd6a9caa8b0713f9bc1eeb310bcc960a29d45a3b4fcd4b16d5b11d", size = 222762, upload-time = "2026-08-06T13:46:57.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/95/b2c724ce1e64bc23cb5b1d7eeffa9548dc3d811f7a6297b2d01607f4e062/coverage-7.15.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe20cc8cf8821d4fe54f89106cbf06aa27f37b5bbe3535568065a81539b4150", size = 249498, upload-time = "2026-08-06T13:46:59.012Z" },
{ url = "https://files.pythonhosted.org/packages/0b/4f/b1973f67a1382af65b572a31ed692f8e490a6ad707191eab59148376832a/coverage-7.15.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83cf06cdd687677742caff1a9134833b7a8b75f111519d2cb0e0ba1b9a851e15", size = 251328, upload-time = "2026-08-06T13:47:00.764Z" },
{ url = "https://files.pythonhosted.org/packages/a2/09/03efa6722a132abcac91b32a60b64b240dd707c189c64eee697e48992c96/coverage-7.15.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fa4de68e2a752468ff14b4e15db7def689a71be759e826a31ccecbef69c5fd0", size = 253194, upload-time = "2026-08-06T13:47:01.976Z" },
{ url = "https://files.pythonhosted.org/packages/45/63/8299201d9c80fb65551ce99c966cab83d706ec4066ac999bef08201346de/coverage-7.15.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4dff9daa47d83120c3ec38ce921214242944a832aa04e903e50b5b7ebac8972d", size = 255106, upload-time = "2026-08-06T13:47:03.281Z" },
{ url = "https://files.pythonhosted.org/packages/ee/16/26fd8a691eb8d9a230128685f6d23309d7402cb030aa553001788c8c50fc/coverage-7.15.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a093fd37229918976f602aa07aa59e0973cde82186f220c8e197f721f5be0ce4", size = 250177, upload-time = "2026-08-06T13:47:04.713Z" },
{ url = "https://files.pythonhosted.org/packages/ad/ef/3c7556f33783a0a566e01443ca62bd8eb2cdfe22d271efdc02e08beb5654/coverage-7.15.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:317db01a2cb02552fd67e2b1cca77a4b528a2a277176c5e0bf2cecbb639d3f54", size = 251234, upload-time = "2026-08-06T13:47:06.104Z" },
{ url = "https://files.pythonhosted.org/packages/29/49/640a34043edac950738f36a3567832db5731d4cb2ed84b59cdb89c6bccbf/coverage-7.15.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8ee3838dcb656602c3b51e16aed9bfb0822f8d8d6d1c5966d32ec8c104be8e20", size = 249237, upload-time = "2026-08-06T13:47:07.467Z" },
{ url = "https://files.pythonhosted.org/packages/48/f5/e80f212669dd1be954ff844f883ef11a437ef4fd0089c6e0effc7b66b15d/coverage-7.15.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:425920379052ff1fe465268f3361d35804a241bbdd5a1b592c8cb60df4c52325", size = 253050, upload-time = "2026-08-06T13:47:08.748Z" },
{ url = "https://files.pythonhosted.org/packages/c7/e9/e5da0fe39f7fde1bca9edc09c60921bb5fdba4cec7db5bbad41ddfd8c230/coverage-7.15.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:69bb2400abef928e365ea7d4d9925169ada78ed2295546780002d4b65de3df88", size = 249508, upload-time = "2026-08-06T13:47:10.072Z" },
{ url = "https://files.pythonhosted.org/packages/7d/38/41bf25774a0c8bba6b467f917cb1c9a0a2605e02dc93aad489fc7050ed59/coverage-7.15.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:81661f82d302484e3119e7c80c519c02fa9bcc2a6b339baf67d67bc89c580f04", size = 250110, upload-time = "2026-08-06T13:47:11.35Z" },
{ url = "https://files.pythonhosted.org/packages/89/6e/26f2e54b79acc29d179ee4272922625aedb69198c4eb61f7ff4f098f3c78/coverage-7.15.4-cp310-cp310-win32.whl", hash = "sha256:cb476b2e828ecb71cb6b6a928d23fd20a7ddb501188022dae1c37499149cc338", size = 224294, upload-time = "2026-08-06T13:47:12.753Z" },
{ url = "https://files.pythonhosted.org/packages/7b/06/9a318fc3ae040d4d6cb2d86101c6aa963fab20899a5c58666adf52cde0ca/coverage-7.15.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fc2130bf37df31852a8384f12601563a45a0024bccc6624f38355cba7a8d360", size = 224919, upload-time = "2026-08-06T13:47:14.17Z" },
{ url = "https://files.pythonhosted.org/packages/2a/66/edcec7d7a0b524aa8923e22925fde6fe50ce005a113dca13ae1581455c4c/coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490", size = 222367, upload-time = "2026-08-06T13:47:15.578Z" },
{ url = "https://files.pythonhosted.org/packages/e6/c6/ab8de429e2e8548faf58ec7e1674a4ce00414b4113942d3fe87109cf0f68/coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e", size = 222874, upload-time = "2026-08-06T13:47:16.961Z" },
{ url = "https://files.pythonhosted.org/packages/be/c4/3b7b49587e8a6b9af79b3eb468d443d6042b6d65b47aa26586846a0d6566/coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7", size = 253287, upload-time = "2026-08-06T13:47:18.291Z" },
{ url = "https://files.pythonhosted.org/packages/fb/65/ec03b743a2a229c72cc1eff3e57be9d3564e9c6b4d5aba2d70744a3fc0d8/coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6", size = 255199, upload-time = "2026-08-06T13:47:19.765Z" },
{ url = "https://files.pythonhosted.org/packages/41/4b/5163729e4b6582d61975cfd3ccab45b4ec53e21cf156d9941cb025188468/coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d", size = 257308, upload-time = "2026-08-06T13:47:21.206Z" },
{ url = "https://files.pythonhosted.org/packages/86/08/2167a0f08fb87d702fa423a48578a32865464b7c9e1db3911ad7812ab414/coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce", size = 259268, upload-time = "2026-08-06T13:47:22.503Z" },
{ url = "https://files.pythonhosted.org/packages/1e/e5/68eebae3053dbd48508edea559c21b23fbdf3460784f91370c83a86a6acd/coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7", size = 253392, upload-time = "2026-08-06T13:47:23.88Z" },
{ url = "https://files.pythonhosted.org/packages/1a/46/fd4ced40a2b691c774e515c9b69500bfa64c7960b67fcee4b2f6fad97fc3/coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b", size = 255001, upload-time = "2026-08-06T13:47:25.469Z" },
{ url = "https://files.pythonhosted.org/packages/53/25/ae2e5fa710bb6957a9aadeb9e3598d3b3e4af6587ce857ad42e8639a3f30/coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc", size = 253061, upload-time = "2026-08-06T13:47:26.845Z" },
{ url = "https://files.pythonhosted.org/packages/d7/31/67ddc0365db2c6e93ac8580bc4bbc50f65273262f973f63ebcdbc15c0495/coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571", size = 256831, upload-time = "2026-08-06T13:47:28.217Z" },
{ url = "https://files.pythonhosted.org/packages/f6/78/82b8fd18f57fb13f12d98fe874995bb2c4f9f17be8aff762c426323fdb96/coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719", size = 252781, upload-time = "2026-08-06T13:47:29.712Z" },
{ url = "https://files.pythonhosted.org/packages/0a/eb/6c74ef4dd12b252e573c49bdef9e2ac265bf3dbb79b8d7feb3266e084e9e/coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7", size = 253692, upload-time = "2026-08-06T13:47:31.192Z" },
{ url = "https://files.pythonhosted.org/packages/5a/66/eb9aed1c3fd2d36ee00eb173f434b14fa607fc056739c9a89ff4244010ea/coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e", size = 224461, upload-time = "2026-08-06T13:47:32.572Z" },
{ url = "https://files.pythonhosted.org/packages/e2/6d/81fa4161dfb3ed9d74e40d58647eff83a56b7612e78352581280fce2f477/coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc", size = 224937, upload-time = "2026-08-06T13:47:34.205Z" },
{ url = "https://files.pythonhosted.org/packages/5b/c1/d8dacf683c6cad3cf85ce68fd3774a6774ec402128822fdfaed920f11e6a/coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890", size = 224479, upload-time = "2026-08-06T13:47:36.118Z" },
{ url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" },
{ url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" },
{ url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" },
{ url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" },
{ url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" },
{ url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" },
{ url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" },
{ url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" },
{ url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" },
{ url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" },
{ url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" },
{ url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" },
{ url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" },
{ url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" },
{ url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" },
{ url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" },
{ url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" },
{ url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" },
{ url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" },
{ url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" },
{ url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" },
{ url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" },
{ url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" },
{ url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" },
{ url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" },
{ url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" },
{ url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" },
{ url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" },
{ url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" },
{ url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" },
{ url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" },
{ url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" },
@@ -184,23 +134,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" },
] ]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.3.0" version = "2.3.0"
@@ -216,58 +149,6 @@ version = "0.13.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/89/2f/ec5241c38e7fa0fe6c26bfc450e78b9489a6c3c08b394b85d2c10e506975/librt-0.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5", size = 148654, upload-time = "2026-07-08T12:24:30.622Z" },
{ url = "https://files.pythonhosted.org/packages/a5/1a/d651e18d3ee7aa2879322368c4f278bb7ecaa6b90caadfdec4ebfa8389f3/librt-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547", size = 153537, upload-time = "2026-07-08T12:24:31.773Z" },
{ url = "https://files.pythonhosted.org/packages/45/18/10bff2122577246009d9619b6569596daf69b7648812f997ca9ca0426f60/librt-0.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2", size = 494336, upload-time = "2026-07-08T12:24:33.079Z" },
{ url = "https://files.pythonhosted.org/packages/67/69/87dfee871b852970f137fdeae8e2ca356c5ab38e6f21d2a3299535fc3159/librt-0.13.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929", size = 485393, upload-time = "2026-07-08T12:24:34.324Z" },
{ url = "https://files.pythonhosted.org/packages/e9/d5/625447a8c0441ff5f15f4ac5e1d323fb9d4d256ebfde7a3c8e003f646057/librt-0.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a", size = 515382, upload-time = "2026-07-08T12:24:35.575Z" },
{ url = "https://files.pythonhosted.org/packages/8d/d8/1c8c49ea04235960426444deece9092a6b3a9587a850a81bae2335317411/librt-0.13.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac", size = 509483, upload-time = "2026-07-08T12:24:36.923Z" },
{ url = "https://files.pythonhosted.org/packages/6f/65/f1760fc48050e215201a03506c32b7270159088d01f64557b53e39e74a45/librt-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7", size = 532503, upload-time = "2026-07-08T12:24:38.203Z" },
{ url = "https://files.pythonhosted.org/packages/18/1b/793e281dcf494879eff99f642b63ebc9c7c58694a1c2d1e93362a22c7041/librt-0.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40", size = 537027, upload-time = "2026-07-08T12:24:39.34Z" },
{ url = "https://files.pythonhosted.org/packages/69/45/0801bbb40c9eea795d3dd3ce91c4c5f3fe7d42d23ec4be3e8cb283bcc754/librt-0.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a", size = 517100, upload-time = "2026-07-08T12:24:40.907Z" },
{ url = "https://files.pythonhosted.org/packages/a1/6c/eb5f514f8e29d4924bc0ff4601dd7b4175557e182e7c0721e84cffa39b8a/librt-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde", size = 558653, upload-time = "2026-07-08T12:24:42.359Z" },
{ url = "https://files.pythonhosted.org/packages/b4/bf/f140100d1b59fe87ff40b5ecbb4e27924335b189a784e230ee465452f6c2/librt-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8", size = 104402, upload-time = "2026-07-08T12:24:43.668Z" },
{ url = "https://files.pythonhosted.org/packages/22/7c/57e40fef7cfb61869341cb28bdcefe8a950bebcbecca74a397bae14dce4a/librt-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc", size = 125002, upload-time = "2026-07-08T12:24:44.793Z" },
{ url = "https://files.pythonhosted.org/packages/89/25/a6498964cfeec270c468cffdc118f69c29b412593610d55fa1327ca51ff4/librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082", size = 148029, upload-time = "2026-07-08T12:24:45.961Z" },
{ url = "https://files.pythonhosted.org/packages/78/59/dc86d1bffd8e0c2818bace29d9f7783cfbb8e0673bf3673b5bbd5bbe0420/librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14", size = 153036, upload-time = "2026-07-08T12:24:47.257Z" },
{ url = "https://files.pythonhosted.org/packages/29/3f/b923826660f02f286186cd9303d52bb05ced0a13708edc104dc8480920e3/librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79", size = 493062, upload-time = "2026-07-08T12:24:48.483Z" },
{ url = "https://files.pythonhosted.org/packages/88/87/6c0980a9c9b1302cb68d108906697b89eceb55889bb1dcf77c109aa56ca5/librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176", size = 485510, upload-time = "2026-07-08T12:24:49.727Z" },
{ url = "https://files.pythonhosted.org/packages/32/81/795ae3b9df5dd94079fb807e38191855e023e8c6249014ae6bc3f0d9a490/librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89", size = 515909, upload-time = "2026-07-08T12:24:51.135Z" },
{ url = "https://files.pythonhosted.org/packages/20/e5/182de15abce8907108a6fdb41487de65beb5099b74dc5841b19b099168db/librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f", size = 508620, upload-time = "2026-07-08T12:24:52.358Z" },
{ url = "https://files.pythonhosted.org/packages/32/03/33978d32db76e1f66377e8f78e42a2ca3c162143331677d1f50bbad36cfb/librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d", size = 530363, upload-time = "2026-07-08T12:24:53.503Z" },
{ url = "https://files.pythonhosted.org/packages/e6/f5/b291fbd2d00f7d8287bcbf67b5aa0c6afed4bc26cef23e079629c47a2c04/librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd", size = 534209, upload-time = "2026-07-08T12:24:55.138Z" },
{ url = "https://files.pythonhosted.org/packages/3e/03/6f41f17939d191bc21609f220da8509316bc62797f078545fe83be522e78/librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588", size = 514254, upload-time = "2026-07-08T12:24:56.276Z" },
{ url = "https://files.pythonhosted.org/packages/af/c2/2e4befa5410a7443019c14abccc94ff619797171f6b72013635fb87f31d7/librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1", size = 557611, upload-time = "2026-07-08T12:24:57.561Z" },
{ url = "https://files.pythonhosted.org/packages/ab/54/8b69f81448417adbc040a2185f4e2eece1e1994b7dcfaeed4662b30f98a5/librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21", size = 104906, upload-time = "2026-07-08T12:24:58.806Z" },
{ url = "https://files.pythonhosted.org/packages/76/5a/f4aaf37b50f2fde12c8c663b83fdd499cdc24f957f19543d7414bfcc9e25/librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b", size = 125852, upload-time = "2026-07-08T12:25:00.065Z" },
{ url = "https://files.pythonhosted.org/packages/f2/99/bf1820e6feeabc2f218c24450ec0c995d6a91e8ba0fd3caf042c9e8adb2a/librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c", size = 111832, upload-time = "2026-07-08T12:25:01.148Z" },
{ url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" },
{ url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" },
{ url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" },
{ url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" },
{ url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" },
{ url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" },
{ url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" },
{ url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" },
{ url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" },
{ url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" },
{ url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" },
{ url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" },
{ url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" },
{ url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" },
{ url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" },
{ url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" },
{ url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" },
{ url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" },
{ url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" },
{ url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" },
{ url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" },
{ url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" },
{ url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" },
{ url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" },
{ url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" },
{ url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" },
{ url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" },
{ url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" },
@@ -299,29 +180,52 @@ wheels = [
[[package]] [[package]]
name = "logwatcher" name = "logwatcher"
version = "0.1.0" version = "0.0.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "pytest-cov" }, { name = "typer" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "mypy" }, { name = "mypy" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" },
{ name = "ruff" }, { name = "ruff" },
] ]
[package.metadata] [package.metadata]
requires-dist = [{ name = "pytest-cov", specifier = ">=7.1.0" }] requires-dist = [{ name = "typer", specifier = ">=0.27.2" }]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "mypy", specifier = ">=2.3.0" }, { name = "mypy", specifier = ">=2.3.0" },
{ name = "pytest", specifier = ">=9.1.1" }, { name = "pytest", specifier = ">=9.1.1" },
{ name = "pytest-cov", specifier = ">=7.1.0" },
{ name = "ruff", specifier = ">=0.16.0" }, { name = "ruff", specifier = ">=0.16.0" },
] ]
[[package]]
name = "markdown-it-py"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "2.3.0" version = "2.3.0"
@@ -331,39 +235,10 @@ dependencies = [
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" }, { name = "mypy-extensions" },
{ name = "pathspec" }, { name = "pathspec" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/09/f2f5f45dae0c9a0891e4751a73312730e009395102e5d72a22a976cca41f/mypy-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc", size = 14927774, upload-time = "2026-07-13T11:28:38.224Z" },
{ url = "https://files.pythonhosted.org/packages/56/b9/345367effd3a6877275a94d481614bfca983f45e028c6290e2cc54603811/mypy-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3", size = 14000127, upload-time = "2026-07-13T11:30:19.57Z" },
{ url = "https://files.pythonhosted.org/packages/99/6c/a10b7a7b9f0a755fb94e27ae834d4cea9ad6c5221f9325eef8f182641feb/mypy-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805", size = 14229437, upload-time = "2026-07-13T11:28:17.765Z" },
{ url = "https://files.pythonhosted.org/packages/d9/bd/a26a602acb1bbf849fa4bdac4bc657ee2f11c0c2a764a2cc87a5304e865c/mypy-2.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117", size = 15171457, upload-time = "2026-07-13T11:29:01.834Z" },
{ url = "https://files.pythonhosted.org/packages/7f/14/124f462bef69bcbc90b9358088460b6091954a3e004852fcd9948db617a5/mypy-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5", size = 15478281, upload-time = "2026-07-13T11:32:23.413Z" },
{ url = "https://files.pythonhosted.org/packages/db/a4/8bdca6a8ac8d856d82ed049144af2721245a135c2e8001d3890c93975852/mypy-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494", size = 11148008, upload-time = "2026-07-13T11:34:17.332Z" },
{ url = "https://files.pythonhosted.org/packages/83/41/490eea348e60ba50decec20bc750605444149a5d7a8cc560042f90ba2c75/mypy-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee", size = 10142329, upload-time = "2026-07-13T11:32:52.116Z" },
{ url = "https://files.pythonhosted.org/packages/e6/b9/d75b3082b05f1b3028828aeb18e74ae5ab0a0936051bbf1f32f59f654747/mypy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329", size = 14838725, upload-time = "2026-07-13T11:32:44.655Z" },
{ url = "https://files.pythonhosted.org/packages/a9/50/79a65c6ea6e115bc73296038a4543b2d5c91f07912b918a2c616a2514bba/mypy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f", size = 13911128, upload-time = "2026-07-13T11:32:02.021Z" },
{ url = "https://files.pythonhosted.org/packages/90/48/e11ed7716c26953ca321f726e452e374dbf81a6f2b8b212ec02af29b6b8f/mypy-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a", size = 14146742, upload-time = "2026-07-13T11:33:03.313Z" },
{ url = "https://files.pythonhosted.org/packages/06/72/6807565b1c4861ef66f7fdd98b51c61556356eab80235717b46c53bb8627/mypy-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36", size = 15081418, upload-time = "2026-07-13T11:31:13.899Z" },
{ url = "https://files.pythonhosted.org/packages/00/80/1ea14c5d80e589e415973db3e47c78c2219a305b808b2b506395342c1d79/mypy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461", size = 15328164, upload-time = "2026-07-13T11:31:35.723Z" },
{ url = "https://files.pythonhosted.org/packages/37/28/8223157404a3d51920078459c37f80fbdc590e1d8ea049dc5ce48643022a/mypy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd", size = 11136472, upload-time = "2026-07-13T11:27:37.018Z" },
{ url = "https://files.pythonhosted.org/packages/6f/cc/ea27e5959c5f258585a756b252031f3b313583d81b5064b2bebc41d3706b/mypy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568", size = 10135800, upload-time = "2026-07-13T11:30:08.92Z" },
{ url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" },
{ url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" },
{ url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" },
{ url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" },
{ url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" },
{ url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" },
{ url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" },
{ url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" },
{ url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" },
{ url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" },
{ url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" },
{ url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" },
{ url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" },
{ url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" },
{ url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" },
{ url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" },
@@ -433,12 +308,10 @@ version = "9.1.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" }, { name = "iniconfig" },
{ name = "packaging" }, { name = "packaging" },
{ name = "pluggy" }, { name = "pluggy" },
{ name = "pygments" }, { name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [ wheels = [
@@ -450,7 +323,7 @@ name = "pytest-cov"
version = "7.1.0" version = "7.1.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "coverage", extra = ["toml"] }, { name = "coverage" },
{ name = "pluggy" }, { name = "pluggy" },
{ name = "pytest" }, { name = "pytest" },
] ]
@@ -459,6 +332,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
] ]
[[package]]
name = "rich"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.16.0" version = "0.16.0"
@@ -485,57 +371,27 @@ wheels = [
] ]
[[package]] [[package]]
name = "tomli" name = "shellingham"
version = "2.4.1" version = "1.5.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, ]
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, [[package]]
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, name = "typer"
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, version = "0.27.2"
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, source = { registry = "https://pypi.org/simple" }
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, dependencies = [
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, { name = "annotated-doc" },
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, { name = "rich" },
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, { name = "shellingham" },
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, ]
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" }
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, { url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" },
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
] ]
[[package]] [[package]]