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.
- pj_utils-0.1.0/PKG-INFO +10 -0
- pj_utils-0.1.0/README.md +158 -0
- pj_utils-0.1.0/pj_utils.egg-info/PKG-INFO +10 -0
- pj_utils-0.1.0/pj_utils.egg-info/SOURCES.txt +18 -0
- pj_utils-0.1.0/pj_utils.egg-info/dependency_links.txt +1 -0
- pj_utils-0.1.0/pj_utils.egg-info/requires.txt +4 -0
- pj_utils-0.1.0/pj_utils.egg-info/top_level.txt +1 -0
- pj_utils-0.1.0/pyproject.toml +21 -0
- pj_utils-0.1.0/setup.cfg +4 -0
- pj_utils-0.1.0/utilitaires_package/__init__.py +4 -0
- pj_utils-0.1.0/utilitaires_package/services/__init__.py +3 -0
- pj_utils-0.1.0/utilitaires_package/services/config.py +2 -0
- pj_utils-0.1.0/utilitaires_package/services/email.py +76 -0
- pj_utils-0.1.0/utilitaires_package/services/formatters.py +17 -0
- pj_utils-0.1.0/utilitaires_package/services/logger.py +63 -0
- pj_utils-0.1.0/utilitaires_package/services/sql.py +46 -0
- pj_utils-0.1.0/utilitaires_package/utils/__init__.py +1 -0
- pj_utils-0.1.0/utilitaires_package/utils/exceptions.py +14 -0
- pj_utils-0.1.0/utilitaires_package/utils/file.py +73 -0
- pj_utils-0.1.0/utilitaires_package/utils/types.py +4 -0
pj_utils-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
pj_utils-0.1.0/README.md
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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*"]
|
pj_utils-0.1.0/setup.cfg
ADDED
|
@@ -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,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
|