docs(docs): 📝 update documentation files
update ruff and pyproject.toml. Sync or reinstall the package
This commit is contained in:
@@ -1,12 +1,10 @@
|
|||||||
# This file list the environment variables used
|
# This file lists the environment variables used by logwatcher.
|
||||||
# for the IMAP connection to an OWL Exchange Server
|
|
||||||
# The connection expect an Exchange account
|
|
||||||
|
|
||||||
|
## EWS connection to the OWL Exchange Server
|
||||||
EMAIL=mail@client.fr
|
EMAIL=mail@client.fr
|
||||||
PASSWORD=password123
|
PASSWORD=password123
|
||||||
EWS_URL=https://mail.client.fr/EWS/Exchange.asmx # Default mail server
|
EWS_URL=https://mail.client.fr/EWS/Exchange.asmx # Default mail server
|
||||||
|
|
||||||
# send mail report to
|
## Report email
|
||||||
N2_REPORT_RECIPIENTS=n2-support@client.fr
|
N2_REPORT_RECIPIENTS=n2-support@client.fr
|
||||||
# default report mail subject
|
|
||||||
N2_REPORT_SUBJECT=[Logwatcher] Rapport erreurs N2
|
N2_REPORT_SUBJECT=[Logwatcher] Rapport erreurs N2
|
||||||
@@ -8,6 +8,8 @@ ne les concernent pas. Logwatcher analyse ces compte-rendus, identifie
|
|||||||
les erreurs pertinentes pour le support N2, et génère trois rapports
|
les erreurs pertinentes pour le support N2, et génère trois rapports
|
||||||
(n2, autres, tout) prêts à être transmis.
|
(n2, autres, tout) prêts à être transmis.
|
||||||
|
|
||||||
|
Les compte-rendus peuvent être fournis sous deux formes : des fichiers de logs locaux, ou les emails reçus dans une boîte Exchange.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
@@ -28,25 +30,56 @@ uv pip install -e ".[dev]"
|
|||||||
pip install -e ".[dev]"
|
pip install -e ".[dev]"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
Copier `.env.example` et le renommer en `.env`.
|
||||||
|
Renseigner les variables :
|
||||||
|
- EMAIL : adresse de la boîte contenant les mails de compte-rendu
|
||||||
|
- PASSWORD : mot de passe du compte
|
||||||
|
- EWS_URL : endpoint EWS du serveur Exchange
|
||||||
|
- N2_REPORT_RECIPIENTS : destinataires du rapport n2, séparés par des virgules
|
||||||
|
- N2_REPORT_SUBJECT : objet par défaut du mail de rapport
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Commandes
|
## 🚀 Commandes
|
||||||
|
|
||||||
### Analyser un fichier de logs
|
### Analyse de fichiers
|
||||||
|
|
||||||
|
#### Analyser un fichier de logs
|
||||||
```bash
|
```bash
|
||||||
logwatcher --input-files chemin/vers/CR_fichier.txt
|
logwatcher from-files --input-files chemin/vers/CR_fichier.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Analyser tous les logs d'un répertoire
|
#### Analyser tous les logs d'un répertoire
|
||||||
```bash
|
```bash
|
||||||
logwatcher --input-dir chemin/vers/dossier
|
logwatcher from-files --input-dir chemin/vers/dossier
|
||||||
```
|
```
|
||||||
|
|
||||||
### Afficher la version
|
### Analyse de mails
|
||||||
|
|
||||||
|
#### Analyser les compte-rendus reçus par mail
|
||||||
|
```bash
|
||||||
|
logwatcher from-mails --output-dir chemin/vers/sortie
|
||||||
|
```
|
||||||
|
|
||||||
|
Les mails sont lus dans le dossier `Logs` de la boîte. Les logs sont extraits du corps du message ou de la pièce jointe `CR_*`, puis les trois rapports sont générés sur disque. Le rapport `n2.log` est envoyé automatiquement aux destinataires définis dans `.env`. Les mails traités sont déplacés dans `Logs/Analyzed`.
|
||||||
|
|
||||||
|
|
||||||
|
### Options globales
|
||||||
|
|
||||||
|
`--version` et `--verbose` sont des options globales : elles s'appliquent à toutes les commandes.
|
||||||
|
|
||||||
|
#### Affiche la version et quitte
|
||||||
```bash
|
```bash
|
||||||
logwatcher --version
|
logwatcher --version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Active les logs DEBUG sur la sortie standard
|
||||||
|
```bash
|
||||||
|
logwatcher --verbose, -v
|
||||||
|
```
|
||||||
|
|
||||||
Le guide complet se trouve dans [usage](docs/usage.md).
|
Le guide complet se trouve dans [usage](docs/usage.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -56,6 +89,12 @@ Le détail de l'architecture se trouve dans [architecture](docs/architecture.md)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Automatisation
|
||||||
|
En production, le script tourne sous Windows.
|
||||||
|
Le flux mail est déclenché par le Planificateur de tâches : tous les jours à 07h20, puis toutes les 4 heures pendant 1 jour. Détails dans docs/usage.md.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🧪 Tests
|
## 🧪 Tests
|
||||||
Les tests se trouvent dans `tests/`. Le projet est couvert à ~95% par les tests unitaires et d'intégration.
|
Les tests se trouvent dans `tests/`. Le projet est couvert à ~95% par les tests unitaires et d'intégration.
|
||||||
|
|
||||||
|
|||||||
+86
-27
@@ -9,6 +9,8 @@ génère des rapports texte.
|
|||||||
Le traitement suit un pipeline linéaire :
|
Le traitement suit un pipeline linéaire :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
Flux fichiers fichiers de logs
|
||||||
|
|
||||||
fichiers de logs
|
fichiers de logs
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -24,6 +26,29 @@ fichiers de logs
|
|||||||
rapports .log
|
rapports .log
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
Flux mail
|
||||||
|
|
||||||
|
boîte Exchange (dossier Logs)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
read_mail.py
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
perser.py
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
classifier.py
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
reporter.py
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
notifier.py
|
||||||
|
```
|
||||||
|
|
||||||
|
L'envoi du mail précède le déplacement des mails vers `Analyzed`. Si l'envoi échoue, les mails restent dans `Logs` et le rapport est régénéré au prochain passage : un rapport peut être envoyé deux fois en cas d'échec après envoi, mais jamais perdu.
|
||||||
|
|
||||||
## Structure du package
|
## Structure du package
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -31,25 +56,68 @@ src/logwatcher/
|
|||||||
├── __init__.py # expose __version__
|
├── __init__.py # expose __version__
|
||||||
├── __main__.py # point d'entrée : python -m logwatcher
|
├── __main__.py # point d'entrée : python -m logwatcher
|
||||||
├── cli.py # interface en ligne de commande (Typer)
|
├── cli.py # interface en ligne de commande (Typer)
|
||||||
├── config.py # constantes : chemins, formats
|
├── config.py # constantes : chemins, formats, source type
|
||||||
├── logging_config.py # configuration du logging
|
├── logging_config.py # configuration du logging
|
||||||
├── models.py # LogEntry : structure de données
|
├── models.py # LogEntry : structure de données
|
||||||
├── parser.py # lecture et parsing des fichiers
|
├── mail_reader.py # lecture de la boîte mail (EWS)
|
||||||
|
├── mail_utils.py # utilitaires boîte mail (dossiers, constantes)
|
||||||
|
├── notifier.py # envoi du rapport n2 par mail
|
||||||
|
├── parser.py # lecture et parsing des logs
|
||||||
├── classifier.py # classification N2 / hors N2
|
├── classifier.py # classification N2 / hors N2
|
||||||
└── reporter.py # génération des rapports texte
|
└── reporter.py # génération des rapports texte
|
||||||
```
|
```
|
||||||
|
|
||||||
### Modules
|
### Modules
|
||||||
#### cli.py — point d'entrée
|
|
||||||
|
#### [`classifier.py`](../src/logwatcher/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.
|
||||||
|
|
||||||
|
|
||||||
|
#### [`cli.py`](../src/logwatcher/cli.py) — point d'entrée
|
||||||
|
|
||||||
* Définit `app = typer.Typer()` et la commande principale.
|
* Définit `app = typer.Typer()` et la commande principale.
|
||||||
* Valide les arguments (`--input-files` / `--input-dir` mutuellement exclusifs).
|
* Valide les arguments (`--input-files` / `--input-dir` mutuellement exclusifs).
|
||||||
* Orchestre le pipeline : parsing → classification → rapports.
|
* Orchestre le pipeline : parsing → classification → rapports.
|
||||||
* Gère les erreurs utilisateur (`BadParameter`, code de sortie 2) et
|
* Gère les erreurs utilisateur (`BadParameter`, code de sortie 2) et les erreurs d'exécution (code de sortie 1).
|
||||||
* les erreurs d'exécution (code de sortie 1).
|
|
||||||
* Expose `--version` (eager, affiche et quitte).
|
* Expose `--version` (eager, affiche et quitte).
|
||||||
|
|
||||||
#### models.py — LogEntry
|
|
||||||
|
#### [`config.py`](../src/logwatcher/config.py) — constantes
|
||||||
|
|
||||||
|
* Chemins : `OUTPUT_PATH`, `RESULT_PATH`, `LOGGING_PATH`, `TEST_PATH`, `FIXTURE_PATH`
|
||||||
|
* Date et fuseau : `DATETIME_FORMAT`, `FRENCH_TIMEZONE`
|
||||||
|
* Types : `SourceType` (`FILE`, `MAIL`)
|
||||||
|
|
||||||
|
|
||||||
|
#### [`logging_config.py`](../src/logwatcher/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.
|
||||||
|
* Les loggers verbeux des dépendances (exchangelib) sont ramenés au niveau WARNING, afin de ne pas polluer `logwatcher.log`.
|
||||||
|
|
||||||
|
|
||||||
|
#### [`mail_reader.py`](../src/logwatcher/mail_reader.py) — lecture de la boîte mail
|
||||||
|
|
||||||
|
Accès à la boîte via EWS (exchangelib), en authentification NTLM.
|
||||||
|
|
||||||
|
* `connect_to_mailbox()` : lit `EMAIL`, `PASSWORD` et `EWS_URL` dans l'environnement, construit le compte et vérifie la connexion par un aller-retour serveur (accès à `account.protocol.version`).
|
||||||
|
* `fetch_log_messages()` : retourne les mails du dossier Logs.
|
||||||
|
* `extract_logs_from_mails()` : pour chaque mail, extrait les logs du corps ou de la pièce jointe `CR_*`, et les normalise en une liste de lignes. Ignore les lignes non conformes et avertit si le corps annonce une pièce jointe introuvable.
|
||||||
|
* `move_analyzed_mails()` : déplace les mails traités vers le dossier `Analyzed`.
|
||||||
|
|
||||||
|
|
||||||
|
#### [`mail_utils.py`](../src/logwatcher/mails_utils.py) — utilitaires boîte mail
|
||||||
|
* `get_or_create_folder(account, folder_name)` retourne un dossier du compte, et le crée s'il n'existe pas. Utilisé pour les dossiers `Analyzed` et `Sent`.
|
||||||
|
* Constantes : `LOG_DIR`, `ANALYZED_FOLDER`, `SENT_FOLDER`, `LOG_IN_ATTACHMENT_PATTERN`.
|
||||||
|
|
||||||
|
#### [`notifier.py`](../src/logwatcher/notifier.py) — envoi du rapport
|
||||||
|
* `send_n2_report(account, summary, n2_log_file)` : construit un message Exchange avec `MAIL_TEMPLATE` comme corps, attache `n2_log_file`, et l'envoie aux destinataires de `N2_REPORT_RECIPIENTS`. Une copie du message envoyé est conservée dans `Logs/Sent` (copy_to_folder). L'envoi passe par les services Exchange (message.send) : aucun serveur SMTP externe n'est requis.
|
||||||
|
|
||||||
|
|
||||||
|
#### [`models.py`](../src/logwatcher/models.py) — LogEntry
|
||||||
|
|
||||||
Dataclass immuable (par convention) représentant une ligne de log parsée.
|
Dataclass immuable (par convention) représentant une ligne de log parsée.
|
||||||
|
|
||||||
@@ -65,41 +133,32 @@ Champs principaux :
|
|||||||
|
|
||||||
LogEntry est créée uniquement par le parser. Le classifier l'enrichit
|
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.
|
(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
|
#### [`parser.py`](../src/logwatcher/parser.py) — lecture et parsing
|
||||||
|
|
||||||
* N2_PATTERNS : dictionnaire des motifs d'erreurs pertinents pour le N2, fournis par les techniciens.
|
* Patterns réutilisables : `SERVER_IP_PATTERN`, `MDC_SERVER_NAME_PATTERN`, `DATE_TIME_PATTERN`, `STORE_NAME_PATTERN`, `ERROR_MESSAGE_PATTERN`.
|
||||||
* classify_log_entries() : sépare les entrées en deux listes (relevant, irrelevant) et remplit entry.error_name pour les entrées pertinentes.
|
* `LOG_PATTERN` : expression régulière compilée du format d'une ligne LAME MDC.
|
||||||
|
* `parse_line()` : parse une ligne, retourne `LogEntry | None`.
|
||||||
|
* `parse_lines()` : parse un itérable de lignes, ignore les lignes non conformes.
|
||||||
|
* `parse_file()` : lit un fichier (encodage windows-1252) et délègue à `parse_lines()`.
|
||||||
|
|
||||||
#### reporter.py — rapports
|
|
||||||
|
#### [`reporter.py`](../src/logwatcher/reporter.py) — rapports
|
||||||
|
|
||||||
* Templates texte (`string.Template`) : `BASE_TEMPLATE`, `N2_SUPPORT_TEMPLATE`, `OTHER_TEMPLATE`, `ERROR_TEMPLATE`.
|
* Templates texte (`string.Template`) : `BASE_TEMPLATE`, `N2_SUPPORT_TEMPLATE`, `OTHER_TEMPLATE`, `ERROR_TEMPLATE`.
|
||||||
* `build_reports()` : construit les trois rapports (`dict n2, other, all`).
|
* `build_reports()` : construit les trois rapports (`dict n2, other, all`).
|
||||||
* `write_log_report()` : écrit les rapports sur disque en Windows-1252.
|
* `write_log_report()` : prend un SourceType (`FILE` ou `MAIL`) pour adapter le libellé de l'en-tête ("Fichier(s) lu(s)" ou "Mail(s) lu(s)"). Les fichiers générés s'appellent `all.log`, `n2.log` et `other.log`.
|
||||||
* `_get_period()` : calcule les bornes min/max des error_time.
|
* `_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
|
## Dépendances
|
||||||
|
|
||||||
| Package | Rôle |
|
| Package | Rôle |
|
||||||
|---------|------|
|
|---------|------|
|
||||||
| `typer` | Interface en ligne de commande |
|
| `exchangelib` | Accès EWS à la boîte mail et envoi des rapports |
|
||||||
|
| `mypy` | Typage statique (dev) |
|
||||||
| `pytest` | Tests (dev) |
|
| `pytest` | Tests (dev) |
|
||||||
| `pytest-cov` | Couverture (dev) |
|
| `pytest-cov` | Couverture (dev) |
|
||||||
| `ruff` | Linting/formatage (dev) |
|
| `ruff` | Linting/formatage (dev) |
|
||||||
| `mypy` | Typage statique (dev) |
|
| `typer` | Interface en ligne de commande |
|
||||||
+100
-16
@@ -7,14 +7,34 @@ Logwatcher analyse les comptes-rendus de suivi d'imports NOSYMAG (fichiers
|
|||||||
pour le support N2. Il génère trois rapports : un pour les erreurs N2, un
|
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.
|
pour les erreurs hors périmètre N2, et un rapport complet.
|
||||||
|
|
||||||
|
Deux modes d'entrée sont disponibles : l'analyse de fichiers locaux, et
|
||||||
|
l'analyse des compte-rendus reçus par correspondance dans une boîte Exchange.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Entrées
|
## Entrées
|
||||||
|
|
||||||
|
### Analyser les compte-rendus reçus par mail
|
||||||
|
```bash
|
||||||
|
logwatcher from-mails --output-dir chemin/vers/sortie
|
||||||
|
```
|
||||||
|
|
||||||
|
Les mails sont lus dans le dossier `Logs` de la boîte (les compte-rendus y sont
|
||||||
|
déplacés par une règle automatique). Les logs sont extraits du corps du message,
|
||||||
|
ou de la pièce jointe `CR_*` lorsque le corps indique que le compte-rendu dépasse
|
||||||
|
100 lignes.
|
||||||
|
|
||||||
|
Après traitement, les mails sont déplacés dans le sous-dossier
|
||||||
|
`Logs/Analyzed` et ne seront pas relus. Le dossier `Analyzed` est créé automatiquement
|
||||||
|
au premier passage s'il n'existe pas. Les trois rapports sont écrits sur disque.
|
||||||
|
Le rapport `n2.log` est envoyé automatiquement aux destinataires définis dans
|
||||||
|
`N2_REPORT_RECIPIENTS` (variable du fichier `.env`). Une copie du mail envoyé est
|
||||||
|
conservée dans le dossier `Logs/Sent`.
|
||||||
|
|
||||||
### Analyser un fichier de logs
|
### Analyser un fichier de logs
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
logwatcher --input-files chemin/vers/CR_fichier.txt
|
logwatcher from-files --input-files chemin/vers/CR_fichier.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Analyser plusieurs fichiers de logs
|
### Analyser plusieurs fichiers de logs
|
||||||
@@ -22,21 +42,22 @@ L'option `--input-files` accepte plusieurs fichiers. Répéter l'option pour
|
|||||||
chaque fichier :
|
chaque fichier :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
logwatcher --input-files fichier1.txt --input-files fichier2.txt
|
logwatcher from-files --input-files fichier1.txt --input-files fichier2.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Analyser tous les logs d'un répertoire
|
### Analyser tous les logs d'un répertoire
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
logwatcher --input-dir chemin/vers/dossier
|
logwatcher from-files --input-dir chemin/vers/dossier
|
||||||
```
|
```
|
||||||
|
|
||||||
L'option `--input-dir` analyse tous les fichiers `.log`, `.txt` ou sans
|
L'option `--input-dir` analyse tous les fichiers `.log`, `.txt` ou sans
|
||||||
extension contenus dans le répertoire. Les autres fichiers sont ignorés.
|
extension contenus dans le répertoire. Les autres fichiers sont ignorés.
|
||||||
|
|
||||||
> ### Règle d'exclusivité
|
> ### Règle d'exclusivité
|
||||||
> `--input-files` et `--input-dir` sont mutuellement exclusifs. Fournir
|
> `--input-files` et `--input-dir` sont mutuellement exclusifs et ne concernent
|
||||||
les deux options lève une erreur et le traitement est interrompu. Ne fournir
|
que la commande from-files. Il n'y a pas de règle équivalente pour from-mails.
|
||||||
|
Fournir les deux options lève une erreur et le traitement est interrompu. Ne fournir
|
||||||
aucune des deux lève également une erreur.
|
aucune des deux lève également une erreur.
|
||||||
|
|
||||||
## Sorties
|
## Sorties
|
||||||
@@ -47,7 +68,7 @@ Par défaut, les rapports sont écrits dans le répertoire `output/` du dossier
|
|||||||
courant. Un autre emplacement peut être fourni avec `--output-dir` :
|
courant. Un autre emplacement peut être fourni avec `--output-dir` :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
logwatcher --input-dir chemin/vers/dossier --output-dir chemin/vers/sortie
|
logwatcher from-files --input-dir chemin/vers/dossier --output-dir chemin/vers/sortie
|
||||||
```
|
```
|
||||||
|
|
||||||
### Les rapports générés
|
### Les rapports générés
|
||||||
@@ -85,19 +106,56 @@ Nombre d'erreurs : 2
|
|||||||
Raison: Erreur : 1099_2622609667_DESADV : / 00098/000 ... ne correspond
|
Raison: Erreur : 1099_2622609667_DESADV : / 00098/000 ... ne correspond
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Pour la commande `from-mails`, la ligne "Fichier(s) lu(s)" devient "Mail(s) lu(s)". Le reste de l'en-tête est identique. Ce libellé est déterminé par le type de source (fichiers ou mails).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
- `--output-dir` : Répertoire de sortie des rapports. Par défaut : `output/`.
|
### Options communes à toutes les commandes
|
||||||
|
- `--verbose, -v` affiche les logs DEBUG sur la sortie standard. Sans cette option, seuls les avertissements et erreurs sont affichés.
|
||||||
|
Le fichier de log contient toujours le niveau DEBUG.
|
||||||
|
|
||||||
|
- `--version` affiche la version du package et quitte.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
logwatcher --input-dir logs/ --output-dir rapports/2026-09-01/
|
logwatcher version: 1.0.1
|
||||||
```
|
```
|
||||||
|
|
||||||
- `--version` : Affiche la version du package et quitte.
|
### Options de `from-files` et `from-mails`
|
||||||
|
- `--output-dir` répertoire de sortie des rapports. Par défaut : `output/`.
|
||||||
|
|
||||||
|
Exemple :
|
||||||
```bash
|
```bash
|
||||||
logwatcher --version
|
logwatcher from-files --input-dir chemin/vers/dossier --output-dir chemin/vers/sortie
|
||||||
# logwatcher version: 0.0.1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Options de `from-files` uniquement
|
||||||
|
- `--input-files` fichiers de logs à analyser.
|
||||||
|
- `--input-dir` répertoire contenant les fichiers de logs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Automatisation sous Windows
|
||||||
|
|
||||||
|
Le flux mail est destiné à être déclenché automatiquement par le Planificateur de tâches Windows, après réception des compte-rendus du créneau.
|
||||||
|
|
||||||
|
- Réglages de la tâche Déclencheur : tous les jours à 07h20, répété toutes les 4 heures pendant 1 jour
|
||||||
|
|
||||||
|
- Action : ...\.venv\Scripts\logwatcher.exe
|
||||||
|
|
||||||
|
- Arguments : from-mails --output-dir ...\results
|
||||||
|
|
||||||
|
- Commencer dans : répertoire du projet
|
||||||
|
|
||||||
|
- Sécurité : "N'exécuter que si un utilisateur a ouvert une session"
|
||||||
|
|
||||||
|
Le mode "uniquement si un utilisateur a ouvert une session" est nécessaire lorsque le compte d'exécution ne dispose pas du droit "Ouvrir une session en tant que tâche" (SeBatchLogonRight).
|
||||||
|
|
||||||
|
Sur une machine de production, ce droit doit être accordé à un compte de service, afin que la tâche puisse s'exécuter sans session ouverte. Conséquence de ce mode : la tâche ne se déclenche pas si aucune session n'est ouverte. Les créneaux de 07h20 à 19h20 tombent dans la journée de travail ; le créneau de 03h20 ne se déclenchera pas.
|
||||||
|
|
||||||
|
|
||||||
## Cas particuliers
|
## Cas particuliers
|
||||||
|
|
||||||
### Fichier vide
|
### Fichier vide
|
||||||
@@ -118,17 +176,34 @@ en-têtes de répertoire, lignes corrompues) sont ignorées silencieusement
|
|||||||
et ne produisent pas d'entrée dans les rapports.
|
et ne produisent pas d'entrée dans les rapports.
|
||||||
|
|
||||||
|
|
||||||
### Encodage des fichiers
|
### Mail en retard
|
||||||
|
|
||||||
|
Si un compte-rendu arrive après le passage du Planificateur, il reste dans le dossier `Logs` et sera traité au créneau suivant. Le rapport indique alors le nombre de mails effectivement lus, sans signaler l'absence.
|
||||||
|
|
||||||
|
### Mails déjà traités
|
||||||
|
|
||||||
|
Les mails traités sont déplacés dans `Logs/Analyzed` et ne sont pas relus. Le dossier `Analyzed` est créé automatiquement au premier passage s'il n'existe pas.
|
||||||
|
|
||||||
|
### Mail sans logs exploitables
|
||||||
|
|
||||||
|
Un mail qui ne contient ni logs dans le corps ni pièce jointe `CR_*` est ignoré, avec un avertissement dans le log applicatif. Il en va de même si le corps annonce une pièce jointe mais qu'aucune n'est trouvée. Les autres mails du dossier sont traités normalement.
|
||||||
|
|
||||||
|
### Échec de l'envoi du rapport
|
||||||
|
|
||||||
|
L'envoi du mail a lieu avant le déplacement des mails vers Analyzed. Si l'envoi échoue, les mails restent dans Logs : le rapport sera régénéré et renvoyé au prochain passage. Un rapport peut donc être envoyé deux fois en cas d'échec après envoi, mais jamais perdu.
|
||||||
|
|
||||||
|
## Encodage des fichiers
|
||||||
|
|
||||||
Les fichiers d'entrée sont lus en Windows-1252 (encodage natif des logs
|
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.
|
LAME MDC). Les rapports générés sont également écrits en Windows-1252.
|
||||||
|
Le corps des mails et le contenu des pièces jointes `CR_*` sont également lus en windows-1252.
|
||||||
|
|
||||||
## Exemple complet
|
## Exemple complet
|
||||||
|
|
||||||
Analyser tous les logs d'un répertoire et écrire les rapports dans un
|
- Analyser tous les logs d'un répertoire et écrire les rapports dans un
|
||||||
dossier dédié :
|
dossier dédié :
|
||||||
```bash
|
```bash
|
||||||
logwatcher --input-dir logs/2026-09-01/ --output-dir rapports/2026-09-01/
|
logwatcher from-files --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/` :
|
Après exécution, les fichiers suivants sont créés dans `rapports/2026-09-01/` :
|
||||||
@@ -138,5 +213,14 @@ n2.log # erreurs pertinentes pour le N2
|
|||||||
other.log # erreurs hors périmètre N2
|
other.log # erreurs hors périmètre N2
|
||||||
```
|
```
|
||||||
|
|
||||||
Le rapport `n2.log` peut ensuite être transmis aux techniciens N2, et
|
La commande n'envoie rien ; le rapport `n2.log` est transmis manuellement.
|
||||||
`other.log` aux équipes concernées par les autres erreurs.
|
|
||||||
|
|
||||||
|
- Depuis la boîte mail :
|
||||||
|
```bash
|
||||||
|
logwatcher from-mails --output-dir rapports/2026-09-01/
|
||||||
|
```
|
||||||
|
|
||||||
|
Les trois rapports sont créés dans `rapports/2026-09-01/`. Le rapport `n2.log` est envoyé aux destinataires définis dans `N2_REPORT_RECIPIENTS`, et une copie du mail est conservée dans `Logs/Sent`. Les mails traités sont déplacés dans `Logs/Analyzed`.
|
||||||
|
|
||||||
|
le rapport `n2.log` est transmis automatiquement aux techniciens N2.
|
||||||
+1
-1
@@ -12,7 +12,7 @@ dependencies = [
|
|||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"ruff>=0.16.7",
|
"ruff>=0.16.8",
|
||||||
"mypy>=2.3.1",
|
"mypy>=2.3.1",
|
||||||
"pytest>=9.1.1",
|
"pytest>=9.1.1",
|
||||||
"pytest-cov>=7.1.0",
|
"pytest-cov>=7.1.0",
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ def from_mails(
|
|||||||
logs = extract_logs_from_mails(messages)
|
logs = extract_logs_from_mails(messages)
|
||||||
log_entries = parse_lines(logs)
|
log_entries = parse_lines(logs)
|
||||||
|
|
||||||
nb_files = len(messages)
|
nb_mails = len(messages)
|
||||||
relevant, irrelevant = classify_log_entries(log_entries)
|
relevant, irrelevant = classify_log_entries(log_entries)
|
||||||
today = datetime.now(FRENCH_TIMEZONE).strftime(DATETIME_FORMAT)
|
today = datetime.now(FRENCH_TIMEZONE).strftime(DATETIME_FORMAT)
|
||||||
start_date, end_date = get_period(relevant + irrelevant)
|
start_date, end_date = get_period(relevant + irrelevant)
|
||||||
@@ -107,7 +107,7 @@ def from_mails(
|
|||||||
SourceType.MAIL,
|
SourceType.MAIL,
|
||||||
relevant,
|
relevant,
|
||||||
irrelevant,
|
irrelevant,
|
||||||
nb_files,
|
nb_mails,
|
||||||
today,
|
today,
|
||||||
start_date,
|
start_date,
|
||||||
end_date,
|
end_date,
|
||||||
@@ -116,7 +116,7 @@ def from_mails(
|
|||||||
|
|
||||||
# send mail with n2.log as attachment
|
# send mail with n2.log as attachment
|
||||||
mail_body = build_mail_summary(
|
mail_body = build_mail_summary(
|
||||||
nb_files=nb_files,
|
nb_mails=nb_mails,
|
||||||
nb_errors_total=len(relevant + irrelevant),
|
nb_errors_total=len(relevant + irrelevant),
|
||||||
nb_errors_n2=len(relevant),
|
nb_errors_n2=len(relevant),
|
||||||
today=today,
|
today=today,
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ ERROR_TEMPLATE = Template("""
|
|||||||
MAIL_TEMPLATE = Template("""
|
MAIL_TEMPLATE = Template("""
|
||||||
Rapport analyse de log - $generation_date
|
Rapport analyse de log - $generation_date
|
||||||
|
|
||||||
Nb de fichiers analysés\t : $nb_files
|
Nb de mail(s) analysé(s)\t : $nb_mail
|
||||||
Erreurs N2/Erreurs totales\t : $n2_errors/$total_errors
|
Erreurs N2/Erreurs totales\t : $n2_errors/$total_errors
|
||||||
Période : $start_date -> $end_date
|
Période : $start_date -> $end_date
|
||||||
|
|
||||||
La liste des erreurs se trouvent en pièce jointe `n2.log`.
|
La liste des erreurs se trouve en pièce jointe `n2.log`.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ def build_reports(
|
|||||||
|
|
||||||
# build templates
|
# build templates
|
||||||
def build_mail_summary(
|
def build_mail_summary(
|
||||||
nb_files: int,
|
nb_mails: int,
|
||||||
nb_errors_total: int,
|
nb_errors_total: int,
|
||||||
nb_errors_n2: int,
|
nb_errors_n2: int,
|
||||||
today: str,
|
today: str,
|
||||||
@@ -199,7 +199,7 @@ def build_mail_summary(
|
|||||||
"""Build a short plain-text summary of a logwatcher run.
|
"""Build a short plain-text summary of a logwatcher run.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nb_files: Number of log files parsed.
|
nb_mails: Number of log files parsed.
|
||||||
nb_errors_total: Total number of errors across all reports.
|
nb_errors_total: Total number of errors across all reports.
|
||||||
nb_errors_n2: Number of N2-relevant errors.
|
nb_errors_n2: Number of N2-relevant errors.
|
||||||
today: # TODO
|
today: # TODO
|
||||||
@@ -213,7 +213,7 @@ def build_mail_summary(
|
|||||||
logger.info("generating mail body...")
|
logger.info("generating mail body...")
|
||||||
return MAIL_TEMPLATE.substitute(
|
return MAIL_TEMPLATE.substitute(
|
||||||
generation_date=today,
|
generation_date=today,
|
||||||
nb_files=nb_files,
|
nb_mail=nb_mails,
|
||||||
n2_errors=nb_errors_n2,
|
n2_errors=nb_errors_n2,
|
||||||
total_errors=nb_errors_total,
|
total_errors=nb_errors_total,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
|
|||||||
@@ -488,7 +488,7 @@ dev = [
|
|||||||
{ name = "mypy", specifier = ">=2.3.1" },
|
{ name = "mypy", specifier = ">=2.3.1" },
|
||||||
{ name = "pytest", specifier = ">=9.1.1" },
|
{ name = "pytest", specifier = ">=9.1.1" },
|
||||||
{ name = "pytest-cov", specifier = ">=7.1.0" },
|
{ name = "pytest-cov", specifier = ">=7.1.0" },
|
||||||
{ name = "ruff", specifier = ">=0.16.7" },
|
{ name = "ruff", specifier = ">=0.16.8" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -800,27 +800,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.16.7"
|
version = "0.16.8"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/82/bb/5a449b9162e49b139d72f61672bd3ac1d790221f796d3304e2241fff4c58/ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6", size = 4924184, upload-time = "2026-09-10T18:04:06.336Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/b2/c80aeeb7f9e469c0d63a85d2f1ab6e1ebfbe10ea7a8d2438b7e09e3ff09e/ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9", size = 10048917, upload-time = "2026-09-10T18:03:30.28Z" },
|
{ url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/96/20bb7bcae008004df52afcb7ac83432d4a467f2c17b672fe46d26be231c5/ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929", size = 10242929, upload-time = "2026-09-10T18:03:32.706Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/b2/f184b0d5abec02db69cfd7e49b688ae0237554528ca777136c613bf36bee/ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728", size = 9847245, upload-time = "2026-09-10T18:03:34.509Z" },
|
{ url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/2d/db1633a641866ed801e34cc6b60ef236c5e16f9b2124ab1d49cc24a5fe4f/ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0", size = 9961780, upload-time = "2026-09-10T18:03:36.618Z" },
|
{ url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/98/edea21e1a3e38dbbc3bf6bb068b863b3b06184cf8533a4c7dbbe208a89d5/ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0", size = 9866337, upload-time = "2026-09-10T18:03:38.805Z" },
|
{ url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/11/a15e60d4c87b214646f116ca9d204475bf993ee1047459bc9a360fd4d6d1/ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9", size = 10562512, upload-time = "2026-09-10T18:03:40.71Z" },
|
{ url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/42/eaff4c9b6d0c7cdf56df313a17e89ae854f5bbc0b0c8f9cce19be0ab7a8f/ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1", size = 11302938, upload-time = "2026-09-10T18:03:42.607Z" },
|
{ url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/43/c75aa59a4ec181fe2ec06cab30e198c1c6d107229a9f008ae3a7c16cabd8/ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9", size = 10840857, upload-time = "2026-09-10T18:03:44.604Z" },
|
{ url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/33/81f3da371942ea031105ba679d8d6e28ec1660ccd690a45f42d381161356/ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55", size = 10370001, upload-time = "2026-09-10T18:03:46.706Z" },
|
{ url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/0b/6345fb4dbf6dd0ed1cfe5d18391dc9c3f59cc81622a7b0a65b84b3e730ba/ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f", size = 10548735, upload-time = "2026-09-10T18:03:48.658Z" },
|
{ url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/4d/c5576adf511f92a328e5569dda190ecdd430da51f1a649f3a4a2fd73e21e/ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656", size = 10108496, upload-time = "2026-09-10T18:03:50.563Z" },
|
{ url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/8c/667d83c16199a17a56adc6b0bd4c3beb5b767a2babcd16a56f76f9be7fd6/ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750", size = 9860136, upload-time = "2026-09-10T18:03:52.621Z" },
|
{ url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/75/78d401106731999a1dd20cc5a6961e37e1eb9397a3b589f73f3a5ce146a3/ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5", size = 10286290, upload-time = "2026-09-10T18:03:55.207Z" },
|
{ url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/49/56f9c3a8b755df93a0ad318b2147bf4ef5dae9a7e5ec61c460109c67957f/ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f", size = 10745048, upload-time = "2026-09-10T18:03:57.299Z" },
|
{ url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/ea/7f9b938a63ece4bec677ad7f9f7fa02df3383db1949ed93a382441c09a87/ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7", size = 10059082, upload-time = "2026-09-10T18:03:59.632Z" },
|
{ url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/11/480a6973a927aa653e1cead6a6416008640e03a99d05b34c0434b8c6c366/ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6", size = 10593368, upload-time = "2026-09-10T18:04:01.999Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/4b/51327018d056f0dad2c2238f26d1fb0f53707a9d91b75dea6d1b3039f136/ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b", size = 10412401, upload-time = "2026-09-10T18:04:04.098Z" },
|
{ url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user