pj-utils 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: pj-utils
3
+ Version: 0.1.0
4
+ Summary: Mes outils Python réutilisables
5
+ Author: Philippe Jeremy
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: sqlalchemy
8
+ Requires-Dist: pymysql
9
+ Requires-Dist: psycopg2-binary
10
+ Requires-Dist: pyodbc
@@ -0,0 +1,158 @@
1
+ # Utilitaires Python
2
+
3
+ <p align="center">
4
+ <b>Une boîte à outils simple et puissante pour automatiser ton quotidien</b><br>
5
+ File management • Logging avancé • Envoi d'emails
6
+ </p>
7
+
8
+ <p align="center">
9
+ <img src="https://img.shields.io/badge/python-3.8+-blue.svg">
10
+ <img src="https://img.shields.io/badge/status-active-success.svg">
11
+ </p>
12
+
13
+ ---
14
+
15
+ ## ✨ Aperçu
16
+
17
+ Ce projet regroupe plusieurs classes utilitaires pour simplifier le développement :
18
+
19
+ * 📁 Gestion intelligente des fichiers
20
+ * 📝 Logging avancé et lisible
21
+ * 📧 Envoi d’emails automatisé
22
+
23
+ ---
24
+
25
+ ## Modules
26
+
27
+ | Module | Description |
28
+ | -------------- | ---------------------------------------- |
29
+ | 📂 FileManager | Gestion avancée des fichiers et dossiers |
30
+ | 📝 ProLogger | Logger personnalisable avec couleurs |
31
+ | 📧 EmailSender | Envoi d’emails avec pièces jointes |
32
+
33
+ ---
34
+
35
+ ## Instalation
36
+
37
+ pip install git+https://github.com/philippeJeremy/Utilitaires_package
38
+
39
+ ---
40
+
41
+ ## FileManager
42
+
43
+ > Simplifie toutes les opérations sur fichiers
44
+
45
+ ### Features
46
+
47
+ * Création automatique de dossiers
48
+ * Renommage intelligent
49
+ * Copie / déplacement
50
+ * Gestion des conflits (overwrite ou auto rename)
51
+
52
+ ### Exemple
53
+
54
+ ```python
55
+ from utilis import FileManager
56
+
57
+ fm = FileManager(overwrite=False, auto_rename=True)
58
+
59
+ fm.ensure_dir("output")
60
+
61
+ new_file = fm.rename("test.txt", "renamed.txt")
62
+
63
+ fm.transfer(new_file, "output/", mode="copy")
64
+ ```
65
+
66
+ ---
67
+
68
+ ## ProLogger
69
+
70
+ > Un logger propre, lisible et configurable
71
+
72
+ ### Features
73
+
74
+ * Console + fichiers
75
+ * Logs colorés
76
+ * Multi-niveaux (info, warning, error)
77
+ * Facile à intégrer
78
+
79
+ ### Exemple
80
+
81
+ ```python
82
+ from services import ProLogger
83
+
84
+ logger = ProLogger(
85
+ name="my_app",
86
+ log_dir="logs",
87
+ console=True,
88
+ colored=True
89
+ ).get()
90
+
91
+ logger.info("App started")
92
+ logger.warning("Watch out")
93
+ logger.error("Boom")
94
+ ```
95
+
96
+ ---
97
+
98
+ ## EmailSender
99
+
100
+ > Envoi d’emails simple et rapide
101
+
102
+ ### Features
103
+
104
+ * Support Gmail
105
+ * CC / BCC
106
+ * Pièces jointes 📎
107
+ * Connexion sécurisée
108
+
109
+ ### Exemple
110
+
111
+ ```python
112
+ from services import EmailSender
113
+
114
+ sender = EmailSender(
115
+ provider="gmail",
116
+ email="tonemail@gmail.com",
117
+ password="mot_de_passe_application"
118
+ )
119
+
120
+ sender.connect()
121
+
122
+ sender.send_email(
123
+ to=["a@gmail.com"],
124
+ cc=["b@gmail.com"],
125
+ bcc=["c@gmail.com"],
126
+ subject="Test CC/BCC",
127
+ content="Hello",
128
+ attachments=["doc.pdf"]
129
+ )
130
+
131
+ sender.close()
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Sécurité
137
+
138
+ ⚠️ Important :
139
+
140
+ * Ne jamais utiliser ton mot de passe principal
141
+ * Utilise un **mot de passe d’application**
142
+ * Active le 2FA (authentification à deux facteurs)
143
+
144
+ ---
145
+
146
+
147
+ ## Support
148
+
149
+ Si ce projet t’aide :
150
+
151
+ Laisse une ⭐ sur GitHub
152
+ Partage le projet
153
+
154
+ ---
155
+
156
+ <p align="center">
157
+ Fait avec ❤️ en Python
158
+ </p>
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: pj-utils
3
+ Version: 0.1.0
4
+ Summary: Mes outils Python réutilisables
5
+ Author: Philippe Jeremy
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: sqlalchemy
8
+ Requires-Dist: pymysql
9
+ Requires-Dist: psycopg2-binary
10
+ Requires-Dist: pyodbc
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ pj_utils.egg-info/PKG-INFO
4
+ pj_utils.egg-info/SOURCES.txt
5
+ pj_utils.egg-info/dependency_links.txt
6
+ pj_utils.egg-info/requires.txt
7
+ pj_utils.egg-info/top_level.txt
8
+ utilitaires_package/__init__.py
9
+ utilitaires_package/services/__init__.py
10
+ utilitaires_package/services/config.py
11
+ utilitaires_package/services/email.py
12
+ utilitaires_package/services/formatters.py
13
+ utilitaires_package/services/logger.py
14
+ utilitaires_package/services/sql.py
15
+ utilitaires_package/utils/__init__.py
16
+ utilitaires_package/utils/exceptions.py
17
+ utilitaires_package/utils/file.py
18
+ utilitaires_package/utils/types.py
@@ -0,0 +1,4 @@
1
+ sqlalchemy
2
+ pymysql
3
+ psycopg2-binary
4
+ pyodbc
@@ -0,0 +1 @@
1
+ utilitaires_package
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pj-utils"
7
+ version = "0.1.0"
8
+ description = "Mes outils Python réutilisables"
9
+ authors = [{name = "Philippe Jeremy"}]
10
+ requires-python = ">=3.10"
11
+
12
+ dependencies = [
13
+ "sqlalchemy",
14
+ "pymysql",
15
+ "psycopg2-binary",
16
+ "pyodbc"
17
+ ]
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["."]
21
+ include = ["utilitaires_package*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from utilitaires_package.utils import FileManager
2
+ from utilitaires_package.services import ProLogger
3
+ from utilitaires_package.services import EmailSender
4
+ from utilitaires_package.services import Database
@@ -0,0 +1,3 @@
1
+ from .logger import ProLogger
2
+ from .email import EmailSender
3
+ from .sql import Database
@@ -0,0 +1,2 @@
1
+ DEFAULT_LOG_FORMAT = "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
2
+ DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
@@ -0,0 +1,76 @@
1
+ import os
2
+ import smtplib
3
+ from email.message import EmailMessage
4
+
5
+
6
+ class EmailSender:
7
+ SMTP_CONFIG = {
8
+ "gmail": ("smtp.gmail.com", 587),
9
+ "outlook": ("smtp.office365.com", 587),
10
+ "yahoo": ("smtp.mail.yahoo.com", 587),
11
+ }
12
+
13
+ def __init__(self, provider, email, password):
14
+ self.smtp_server, self.smtp_port = self.SMTP_CONFIG[provider]
15
+ self.email_user = email
16
+ self.email_password = password
17
+ self.server = None
18
+
19
+ def connect(self):
20
+ self.server = smtplib.SMTP(self.smtp_server, self.smtp_port)
21
+ self.server.starttls()
22
+ self.server.login(self.email_user, self.email_password)
23
+
24
+ def send_email(self, to, subject, content, attachments=None, cc=None, bcc=None):
25
+ if self.server is None:
26
+ raise Exception("❌ Pas connecté au serveur SMTP")
27
+
28
+ msg = EmailMessage()
29
+ msg["From"] = self.email_user
30
+ msg["To"] = ", ".join(to) if isinstance(to, list) else to
31
+ msg["Subject"] = subject
32
+
33
+ if cc:
34
+ msg["Cc"] = ", ".join(cc) if isinstance(cc, list) else cc
35
+
36
+ msg.set_content(content)
37
+
38
+ # Pièces jointes
39
+ if attachments:
40
+ for file_path in attachments:
41
+ with open(file_path, "rb") as f:
42
+ file_data = f.read()
43
+ file_name = os.path.basename(file_path)
44
+
45
+ msg.add_attachment(
46
+ file_data,
47
+ maintype="application",
48
+ subtype="octet-stream",
49
+ filename=file_name
50
+ )
51
+
52
+ # IMPORTANT : inclure tous les destinataires pour l'envoi réel
53
+ recipients = []
54
+
55
+ if isinstance(to, list):
56
+ recipients.extend(to)
57
+ else:
58
+ recipients.append(to)
59
+
60
+ if cc:
61
+ if isinstance(cc, list):
62
+ recipients.extend(cc)
63
+ else:
64
+ recipients.append(cc)
65
+
66
+ if bcc:
67
+ if isinstance(bcc, list):
68
+ recipients.extend(bcc)
69
+ else:
70
+ recipients.append(bcc)
71
+
72
+ self.server.send_message(msg, to_addrs=recipients)
73
+
74
+ def close(self):
75
+ if self.server:
76
+ self.server.quit()
@@ -0,0 +1,17 @@
1
+ import logging
2
+
3
+
4
+ class ColoredFormatter(logging.Formatter):
5
+ COLORS = {
6
+ logging.DEBUG: "\033[90m", # gris
7
+ logging.INFO: "\033[92m", # vert
8
+ logging.WARNING: "\033[93m", # jaune
9
+ logging.ERROR: "\033[91m", # rouge
10
+ logging.CRITICAL: "\033[95m", # magenta
11
+ }
12
+ RESET = "\033[0m"
13
+
14
+ def format(self, record):
15
+ message = super().format(record)
16
+ color = self.COLORS.get(record.levelno, self.RESET)
17
+ return f"{color}{message}{self.RESET}"
@@ -0,0 +1,63 @@
1
+ import logging
2
+ from logging.handlers import TimedRotatingFileHandler
3
+ from pathlib import Path
4
+ from .formatters import ColoredFormatter
5
+ from .config import DEFAULT_LOG_FORMAT, DEFAULT_DATE_FORMAT
6
+
7
+
8
+ class ProLogger:
9
+ def __init__(self, name: str = "prologger", log_dir: str = "logs", level=logging.DEBUG, console: bool = True,
10
+ file_logging: bool = True, rotation_days: int = 30, colored: bool = True):
11
+ self.name = name
12
+ self.log_dir = Path(log_dir)
13
+ self.level = level
14
+ self.console = console
15
+ self.file_logging = file_logging
16
+ self.rotation_days = rotation_days
17
+ self.colored = colored
18
+
19
+ self.logger = logging.getLogger(self.name)
20
+ self.logger.setLevel(self.level)
21
+
22
+ self.setup()
23
+
24
+ def setup(self):
25
+ if self.logger.handlers:
26
+ return
27
+
28
+ formatter = logging.Formatter(
29
+ DEFAULT_LOG_FORMAT,
30
+ DEFAULT_DATE_FORMAT
31
+ )
32
+
33
+ if self.file_logging:
34
+ self.log_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ file_handler = TimedRotatingFileHandler(
37
+ self.log_dir / f"{self.name}.log",
38
+ when="midnight",
39
+ interval=1,
40
+ backupCount=self.rotation_days,
41
+ encoding="utf-8"
42
+ )
43
+
44
+ file_handler.setFormatter(formatter)
45
+ self.logger.addHandler(file_handler)
46
+
47
+ if self.console:
48
+ console_handler = logging.StreamHandler()
49
+
50
+ if self.colored:
51
+ console_handler.setFormatter(
52
+ ColoredFormatter(
53
+ DEFAULT_LOG_FORMAT,
54
+ DEFAULT_DATE_FORMAT
55
+ )
56
+ )
57
+ else:
58
+ console_handler.setFormatter(formatter)
59
+
60
+ self.logger.addHandler(console_handler)
61
+
62
+ def get(self):
63
+ return self.logger
@@ -0,0 +1,46 @@
1
+ from sqlalchemy import create_engine, text
2
+
3
+
4
+ class Database:
5
+ def __init__(self, db_type, host: str = "localhost", port=None, user=None, password=None, database=None, driver=None):
6
+ self.db_type = db_type.lower()
7
+ self.host = host
8
+ self.port = port
9
+ self.user = user
10
+ self.password = password
11
+ self.database = database
12
+ self.driver = driver
13
+
14
+ self.engine = create_engine(self._build_url())
15
+
16
+ def _build_url(self):
17
+ drivers = {
18
+ "sqlite": "",
19
+ "postgres": "postgresql",
20
+ "mysql": "mysql+pymysql",
21
+ "sqlserver": "mssql+pyodbc",
22
+ }
23
+
24
+ db = drivers.get(self.db_type)
25
+ if not db:
26
+ raise ValueError(f"Type de DB non supporté: {self.db_type}")
27
+
28
+ if self.db_type == "sqlite":
29
+ return f"sqlite:///{self.database}"
30
+
31
+ if self.driver:
32
+ db = f"{self.db_type}+{self.driver}"
33
+
34
+ return f"{db}://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}"
35
+
36
+ def execute(self, query, params=None):
37
+ with self.engine.connect() as conn:
38
+ result = conn.execute(text(query), params or {})
39
+ conn.commit()
40
+ return result
41
+
42
+ def fetch_all(self, query, params=None):
43
+ return self.execute(query, params).fetchall()
44
+
45
+ def fetch_one(self, query, params=None):
46
+ return self.execute(query, params).fetchone()
@@ -0,0 +1 @@
1
+ from .file import FileManager
@@ -0,0 +1,14 @@
1
+ class FileUtilsError(Exception):
2
+ """Base exception"""
3
+
4
+
5
+ class FileNotFoundError(FileUtilsError):
6
+ pass
7
+
8
+
9
+ class InvalidModeError(FileUtilsError):
10
+ pass
11
+
12
+
13
+ class FileAlreadyExistsError(FileUtilsError):
14
+ pass
@@ -0,0 +1,73 @@
1
+ from pathlib import Path
2
+ from typing import Union
3
+ import shutil
4
+ import logging
5
+ from .exceptions import FileAlreadyExistsError, InvalidModeError
6
+ from .types import PathLike
7
+
8
+
9
+ class FileManager:
10
+ def __init__(self, overwrite: bool = False, auto_rename: bool = True, logger: logging.Logger | None = None,):
11
+ self.overwrite = overwrite
12
+ self.auto_renames = auto_rename
13
+ self.logger = logger or logging.getLogger(__name__)
14
+
15
+ def _resolve_destination(self, destination: Path) -> Path:
16
+ if not destination.exists():
17
+ return destination
18
+
19
+ if self.overwrite:
20
+ return destination
21
+
22
+ if not self.auto_rename:
23
+ raise FileAlreadyExistsError(f"Fichier existe déjà : {destination}")
24
+
25
+ counter = 1
26
+ stem = destination.stem
27
+ suffix = destination.suffix
28
+
29
+ while True:
30
+ new_name = f"{stem}_{counter}{suffix}"
31
+ new_path = destination.with_name(new_name)
32
+
33
+ if not new_path.exists():
34
+ return new_path
35
+
36
+ counter += 1
37
+
38
+ def rename(self, file_path: PathLike, new_name: str) -> Path:
39
+ source = Path(file_path)
40
+
41
+ if not source.is_file():
42
+ raise FileNotFoundError(f"Fichier introuvable : {source}")
43
+
44
+ destination = self._resolve_destination(source.with_name(new_name))
45
+
46
+ source.rename(destination)
47
+ self.logger.info(f"Renamed: {source} -> {destination}")
48
+
49
+ return destination
50
+
51
+ def transfer(self, source_path: PathLike, destination_path: PathLike, mode: str = 'copy') -> Path:
52
+
53
+ source = Path(source_path)
54
+ destination = Path(destination_path)
55
+
56
+ if not source.is_file():
57
+ raise FileNotFoundError(f"Source introuvable : {source}")
58
+
59
+ if destination.exists() and destination.is_dir():
60
+ destination = destination / source.name
61
+
62
+ destination = self._resolve_destination(destination)
63
+
64
+ if mode == "move":
65
+ shutil.move(str(source), str(destination))
66
+ self.logger.info(f"Moved: {source} -> {destination}")
67
+ elif mode == "copy":
68
+ shutil.copy2(source, destination)
69
+ self.logger.info(f"Copied: {source} -> {destination}")
70
+ else:
71
+ raise InvalidModeError(mode)
72
+
73
+ return destination
@@ -0,0 +1,4 @@
1
+ from pathlib import Path
2
+ from typing import Union
3
+
4
+ PathLike = Union[str, Path]