Skip to content

Commit 5cd35ed

Browse files
committed
Renamed...
1 parent 02e9eb1 commit 5cd35ed

File tree

9 files changed

+280
-208
lines changed

9 files changed

+280
-208
lines changed

pyproject.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "python-threadsafe-logger"
7-
version = "1.0.0"
8-
description = "Une librairie Python fournissant des loggers métier thread-safe avec backend SQLite ou JSON."
7+
version = "2.1.0"
8+
description = "Une librairie Python fournissant des loggers métier thread-safe avec des backends asynchrones."
9+
readme = "README.md"
910
requires-python = ">=3.8"
1011
dependencies = [
1112
"python-dotenv>=1.0.0",
13+
# Installation des backends directement depuis GitHub
14+
"async-sqlite-queue @ git+https://github.com/venantvr-pubsub/Python.SQLite.Async.git",
15+
"python-jsonl-queue @ git+https://github.com/venantvr-pubsub/Python.JSONL.Async.git"
1216
]
1317

14-
# Rend la commande 'make install-dev' et 'pip install -e ".[dev]"' fonctionnelle.
1518
[project.optional-dependencies]
1619
dev = [
1720
"pytest>=7.0",
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
Metadata-Version: 2.4
2+
Name: python-threadsafe-logger
3+
Version: 2.1.0
4+
Summary: Une librairie Python fournissant des loggers métier thread-safe avec des backends asynchrones.
5+
Requires-Python: >=3.8
6+
Description-Content-Type: text/markdown
7+
Requires-Dist: python-dotenv>=1.0.0
8+
Requires-Dist: async-sqlite-queue@ git+https://github.com/venantvr-pubsub/Python.SQLite.Async.git
9+
Requires-Dist: python-jsonl-queue@ git+https://github.com/venantvr-pubsub/Python.JSONL.Async.git
10+
Provides-Extra: dev
11+
Requires-Dist: pytest>=7.0; extra == "dev"
12+
13+
# Python Threadsafe Logger
14+
15+
![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)
16+
![Python Version](https://img.shields.io/badge/python-3.8%2B-brightgreen.svg)
17+
![Status](https://img.shields.io/badge/status-stable-green.svg)
18+
19+
Une librairie Python simple et robuste pour enregistrer des événements métier de manière asynchrone dans des bases de données locales (SQLite ou JSON). Conçue pour être
20+
non-bloquante, thread-safe et facile à intégrer.
21+
22+
## ✨ Fonctionnalités Principales
23+
24+
* **Deux Backends de Stockage :** Choisissez entre **SQLite** pour des logs structurés ou **JSON** (via TinyDB) pour une flexibilité maximale.
25+
* **Non-bloquant & Thread-Safe :** L'écriture des logs est gérée par un thread dédié en arrière-plan, garantissant que votre application principale n'est jamais ralentie.
26+
* **Auto-configurable :** Activez et configurez les loggers simplement via des variables d'environnement dans un fichier `.env`.
27+
* **Gestion de Cycle de Vie Automatique :** Grâce à la prise en charge du gestionnaire de contexte (`with`), l'arrêt propre des loggers est garanti, sans perte de
28+
données.
29+
* **API Simple et Intuitive :** Une seule méthode `.log()` à apprendre.
30+
* **Feedback Visuel :** Les logs sont affichés en temps réel et en couleur dans la console pour un débogage facile.
31+
32+
## ⚙️ Installation et Configuration
33+
34+
L'installation est simplifiée grâce au `Makefile` fourni.
35+
36+
1. **Clonez le dépôt :**
37+
```sh
38+
git clone <votre-url-de-depot>
39+
cd <nom-du-dossier>
40+
```
41+
42+
2. **Configurez votre environnement :**
43+
Copiez le fichier d'exemple et modifiez-le pour activer les loggers de votre choix.
44+
```sh
45+
cp .env.example .env
46+
```
47+
Modifiez le fichier `.env` :
48+
```ini
49+
# .env
50+
51+
# Activer le logger SQLite
52+
SQLITE_BUSINESS_LOGGER_ENABLED=true
53+
SQLITE_BUSINESS_LOGGER_DB_FILE=./logs/business_events.db
54+
55+
# Activer le logger JSON
56+
JSON_BUSINESS_LOGGER_ENABLED=true
57+
JSON_BUSINESS_LOGGER_DB_FILE=./logs/business_events.jsonl
58+
```
59+
60+
3. **Installez l'environnement :**
61+
Cette commande va créer un environnement virtuel (`.venv`) et installer toutes les dépendances nécessaires en mode éditable.
62+
```sh
63+
make install
64+
```
65+
66+
## 🚀 Démarrage Rapide
67+
68+
L'utilisation des loggers est conçue pour être la plus simple possible grâce au gestionnaire de contexte `with`, qui s'occupe automatiquement du `shutdown()`.
69+
70+
```python
71+
# examples/main.py
72+
import time
73+
from dotenv import load_dotenv
74+
75+
# Charger la configuration depuis .env
76+
load_dotenv()
77+
78+
# Importer les loggers depuis la librairie
79+
from threadsafe_logger import sqlite_business_logger, json_business_logger
80+
81+
82+
def run_my_application():
83+
"""Simule l'exécution de l'application principale."""
84+
print("Application en cours d'exécution...")
85+
for i in range(5):
86+
# Enregistrer un événement dans la base de données SQLite
87+
sqlite_business_logger.log(
88+
"ORDER_PROCESSED",
89+
{"order_id": f"ORD-00{i}", "amount": 100 + i}
90+
)
91+
# Enregistrer un événement dans le fichier JSON
92+
json_business_logger.log(
93+
"USER_ACTIVITY",
94+
{"user_id": "user123", "action": "view_page", "page": f"/product/{i}"}
95+
)
96+
time.sleep(1)
97+
print("Les opérations sont terminées.")
98+
99+
100+
if __name__ == "__main__":
101+
# Le bloc 'with' garantit que les deux loggers seront arrêtés proprement
102+
# à la fin, même en cas d'erreur.
103+
with sqlite_business_logger, json_business_logger:
104+
run_my_application()
105+
106+
print("Application terminée avec succès.")
107+
```
108+
109+
## 🧰 Commandes utiles (Makefile)
110+
111+
Utilisez `make` pour automatiser les tâches courantes.
112+
113+
* `make` ou `make help` : Affiche toutes les commandes disponibles.
114+
* `make install` : Installe l'environnement de développement et les dépendances.
115+
* `make run` : Lance le script d'exemple `examples/main.py`.
116+
* `make clean` : Supprime l'environnement virtuel et tous les fichiers temporaires.
117+
118+
## 📚 Composants
119+
120+
### `sqlite_business_logger`
121+
122+
* **Backend :** SQLite
123+
* **Cas d'usage :** Idéal pour des événements structurés, l'audit, et lorsque vous avez besoin de requêter les logs avec la puissance du SQL.
124+
125+
### `json_business_logger`
126+
127+
* **Backend :** Fichier JSON Lines (`.jsonl`)
128+
* **Cas d'usage :** Parfait pour des données avec une structure variable, le prototypage rapide, et des logs facilement lisibles et traitables par des scripts (une
129+
ligne = un objet JSON). Cette approche est plus robuste et performante pour l'écriture de logs que la gestion d'un unique gros fichier JSON.
130+
131+
## 📜 Licence
132+
133+
Ce projet est sous licence MIT.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
README.md
2+
pyproject.toml
3+
src/python_threadsafe_logger.egg-info/PKG-INFO
4+
src/python_threadsafe_logger.egg-info/SOURCES.txt
5+
src/python_threadsafe_logger.egg-info/dependency_links.txt
6+
src/python_threadsafe_logger.egg-info/requires.txt
7+
src/python_threadsafe_logger.egg-info/top_level.txt
8+
src/threadsafe_logger/__init__.py
9+
src/threadsafe_logger/base_logger.py
10+
src/threadsafe_logger/json_business_logger.py
11+
src/threadsafe_logger/sqlite_business_logger.py
12+
tests/test_loggers.py
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
python-dotenv>=1.0.0
2+
async-sqlite-queue@ git+https://github.com/venantvr-pubsub/Python.SQLite.Async.git
3+
python-jsonl-queue@ git+https://github.com/venantvr-pubsub/Python.JSONL.Async.git
4+
5+
[dev]
6+
pytest>=7.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
threadsafe_logger
Lines changed: 54 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,163 +1,110 @@
1-
import datetime
21
import logging
32
import os
4-
import queue
53
import threading
64
from abc import ABC, abstractmethod
75
from typing import Optional, Dict, Any
86

9-
# Configuration du logging de base pour voir les erreurs et avertissements de la librairie
10-
logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s')
11-
7+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
128

139
class BaseBusinessLogger(ABC):
14-
"""
15-
Classe de base abstraite pour les loggers métier.
16-
Gère toute la logique commune : singleton, initialisation paresseuse,
17-
file d'attente, thread de travail, et gestion de contexte 'with'.
18-
"""
19-
_instance = None
10+
_instances = {}
2011
_lock = threading.Lock()
21-
2212
_GREEN = "\033[92m"
2313
_RESET = "\033[0m"
2414

2515
def __new__(cls, *args, **kwargs):
26-
if not cls._instance:
27-
with cls._lock:
28-
if not cls._instance:
29-
cls._instance = super().__new__(cls)
30-
return cls._instance
16+
with cls._lock:
17+
if cls not in cls._instances:
18+
cls._instances[cls] = super().__new__(cls)
19+
return cls._instances[cls]
3120

3221
def __init__(self):
33-
if not hasattr(self, '_initialized_flag'):
34-
self._initialized_flag = False
35-
self.is_enabled = False
36-
self._init_lock = threading.Lock()
22+
if hasattr(self, '_initialized') and self._initialized:
23+
return
24+
self._initialized = False
25+
self.is_enabled = False
26+
self._init_lock = threading.Lock()
27+
self.backend = None
3728

3829
@property
3930
@abstractmethod
4031
def logger_name(self) -> str:
41-
"""Nom du logger pour les messages console."""
4232
pass
4333

4434
@property
4535
@abstractmethod
4636
def enabled_env_var(self) -> str:
47-
"""Clé de la variable d'environnement pour activer le logger."""
4837
pass
4938

5039
@property
5140
@abstractmethod
5241
def db_file_env_var(self) -> str:
53-
"""Clé de la variable d'environnement pour le fichier de BDD."""
54-
pass
55-
56-
@abstractmethod
57-
def _setup_backend(self, db_file: str) -> bool:
58-
"""Prépare le backend de stockage (crée le fichier, la table, etc.). Retourne True si succès."""
5942
pass
6043

44+
@staticmethod
6145
@abstractmethod
62-
def _initialize_backend_for_worker(self):
63-
"""Initialise les ressources (ex: connexion BDD) pour le thread de travail."""
46+
def _create_backend(file_path: str):
6447
pass
6548

6649
@abstractmethod
67-
def _write_log_to_backend(self, log_item: Any):
68-
"""Écrit un seul item de log dans le backend."""
50+
def _on_backend_ready(self):
6951
pass
7052

7153
@abstractmethod
72-
def _shutdown_backend(self):
73-
"""Ferme proprement les connexions au backend (ex: db.close())."""
54+
def log(self, event_type: str, details: Optional[Dict[str, Any]] = None):
7455
pass
7556

7657
def _lazy_initialize(self):
7758
with self._init_lock:
78-
if self._initialized_flag:
79-
return
80-
59+
if self._initialized: return
8160
enabled = os.getenv(self.enabled_env_var, "false").lower() in ("true", "1", "yes")
8261
db_file = os.getenv(self.db_file_env_var)
83-
8462
if enabled and db_file:
85-
if self._setup_backend(db_file):
63+
self.backend = self.__class__._create_backend(db_file)
64+
self.backend.start()
65+
if self.backend.wait_for_ready():
8666
self.is_enabled = True
87-
# Utilisation d'une file de taille limitée pour éviter une surconsommation de mémoire
88-
self.log_queue = queue.Queue(maxsize=1000)
89-
self.worker_thread = threading.Thread(target=self._process_queue, daemon=True)
90-
self.worker_thread.start()
91-
print(f"✅ {self.logger_name} auto-configuré. Logs dans '{db_file}'.")
92-
93-
self._initialized_flag = True
94-
95-
def _process_queue(self):
96-
# Initialisation spécifique au thread (ex: connexion BDD)
97-
self._initialize_backend_for_worker()
98-
99-
while True:
100-
try:
101-
log_item = self.log_queue.get()
102-
if log_item is None: break
103-
self._write_log_to_backend(log_item)
104-
self.log_queue.task_done()
105-
except Exception as e:
106-
# Utiliser le logging standard pour les erreurs internes
107-
logging.error(f"Erreur dans le worker {self.logger_name} : {e}")
67+
self._on_backend_ready()
68+
print(f"✅ {self.logger_name} configured. Logs will be in '{db_file}'.")
69+
else:
70+
logging.error(f"The backend for {self.logger_name} failed to start.")
71+
self._initialized = True
10872

109-
def log(self, event_type: str, details: Optional[Dict[str, Any]] = None):
110-
if not self._initialized_flag: self._lazy_initialize()
111-
if not self.is_enabled: return
112-
113-
timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
114-
details_str = f"- {details}" if details else ""
115-
console_output = f"[{self.logger_name}] {event_type} {details_str}"
116-
print(f"{self._GREEN}{console_output}{self._RESET}")
117-
118-
log_data = {
119-
'timestamp': timestamp,
120-
'event_type': event_type,
121-
'details': details
122-
}
123-
124-
try:
125-
# Ne pas bloquer l'application si la file est pleine
126-
self.log_queue.put(log_data, block=False)
127-
except queue.Full:
128-
logging.warning(f"File d'attente du logger '{self.logger_name}' pleine. Le log a été ignoré.")
129-
130-
def shutdown(self, wait=True):
131-
if not self._initialized_flag: self._lazy_initialize()
132-
if not self.is_enabled or not hasattr(self, 'log_queue'): return
133-
if wait: self.log_queue.join()
134-
self.log_queue.put(None)
135-
if hasattr(self, 'worker_thread'): self.worker_thread.join(timeout=5)
136-
self._shutdown_backend()
137-
print(f"✅ {self.logger_name} arrêté proprement.")
73+
def _ensure_initialized(self):
74+
if not self._initialized: self._lazy_initialize()
75+
76+
def shutdown(self):
77+
self._ensure_initialized()
78+
if self.is_enabled and self.backend:
79+
self.backend.stop()
80+
print(f"✅ {self.logger_name} shut down cleanly.")
13881

13982
def __enter__(self):
140-
self._lazy_initialize()
83+
self._ensure_initialized()
14184
return self
14285

143-
def __exit__(self, exc_type, exc_value, traceback):
86+
def __exit__(self, exc_type, exc_val, exc_tb):
14487
self.shutdown()
14588

89+
# =================================================================
90+
# MÉTHODE CORRIGÉE / AJOUTÉE
91+
# =================================================================
14692
def _reset_for_testing(self):
147-
"""Réinitialise l'état interne du logger. POUR LES TESTS UNIQUEMENT."""
148-
# S'assure que le thread est bien arrêté s'il est en cours
149-
if hasattr(self, 'worker_thread') and self.worker_thread.is_alive():
150-
# CORRIGÉ : On attend la fin du traitement de la file.
151-
self.shutdown(wait=True)
152-
153-
# Réinitialise les flags d'état
154-
self._initialized_flag = False
93+
"""
94+
Réinitialise l'état interne du logger pour les tests.
95+
Cette méthode arrête le backend, réinitialise les flags et supprime
96+
l'instance singleton pour garantir l'isolation des tests.
97+
"""
98+
# 1. Arrête le backend s'il est actif
99+
if self.is_enabled and self.backend:
100+
self.shutdown()
101+
102+
# 2. Réinitialise les attributs d'état de l'instance
103+
self._initialized = False
155104
self.is_enabled = False
105+
self.backend = None
156106

157-
# Vide la queue au cas où un test précédent aurait échoué en laissant des items
158-
if hasattr(self, 'log_queue'):
159-
while not self.log_queue.empty():
160-
try:
161-
self.log_queue.get_nowait()
162-
except queue.Empty:
163-
continue
107+
# 3. Supprime l'instance du cache singleton de la classe
108+
with self.__class__._lock:
109+
if self.__class__ in self.__class__._instances:
110+
del self.__class__._instances[self.__class__]

0 commit comments

Comments
 (0)