Add mondialRelay.py
This commit is contained in:
212
mondialRelay.py
Normal file
212
mondialRelay.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import time
|
||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
import configparser
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import Select, WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from selenium.common.exceptions import TimeoutException
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
|
class MondialRelaySession:
|
||||||
|
SUBMIT_URL = "https://www.mondialrelay.fr/envoi-de-colis/"
|
||||||
|
COOKIE_FILE = "cookies.pkl"
|
||||||
|
CONFIG_FILE = "config.ini"
|
||||||
|
|
||||||
|
def __init__(self, driver=None, login_email=None, login_password=None, config_path=None):
|
||||||
|
# Load credentials from parameters or config file
|
||||||
|
self.login_email, self.login_password = self._load_credentials(
|
||||||
|
login_email, login_password, config_path or self.CONFIG_FILE
|
||||||
|
)
|
||||||
|
|
||||||
|
if driver:
|
||||||
|
self.driver = driver
|
||||||
|
else:
|
||||||
|
# If running as a PyInstaller bundle, drivers may live in _MEIPASS
|
||||||
|
base_path = getattr(sys, '_MEIPASS', os.path.abspath('.'))
|
||||||
|
driver_exe = os.path.join(base_path, 'chromedriver.exe')
|
||||||
|
if os.path.exists(driver_exe):
|
||||||
|
self.driver = webdriver.Chrome(executable_path=driver_exe)
|
||||||
|
else:
|
||||||
|
self.driver = webdriver.Chrome()
|
||||||
|
|
||||||
|
self.wait = WebDriverWait(self.driver, 5)
|
||||||
|
self._prepare_session()
|
||||||
|
|
||||||
|
def _save_cookies(self):
|
||||||
|
with open(self.COOKIE_FILE, "wb") as f:
|
||||||
|
pickle.dump(self.driver.get_cookies(), f)
|
||||||
|
|
||||||
|
def _load_cookies(self):
|
||||||
|
with open(self.COOKIE_FILE, "rb") as f:
|
||||||
|
cookies = pickle.load(f)
|
||||||
|
for c in cookies:
|
||||||
|
# Selenium expects int expiry
|
||||||
|
if isinstance(c.get("expiry"), float):
|
||||||
|
c["expiry"] = int(c["expiry"])
|
||||||
|
self.driver.add_cookie(c)
|
||||||
|
|
||||||
|
def _load_credentials(self, email, password, path):
|
||||||
|
if email and password:
|
||||||
|
return email, password
|
||||||
|
config = configparser.ConfigParser(interpolation=None)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise RuntimeError(f"Config file not found: {path}")
|
||||||
|
config.read(path)
|
||||||
|
try:
|
||||||
|
creds = config['credentials']
|
||||||
|
return creds.get('email'), creds.get('password')
|
||||||
|
except KeyError:
|
||||||
|
raise RuntimeError("'credentials' section missing in config file")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_logged_in(self):
|
||||||
|
try:
|
||||||
|
self.wait.until(EC.visibility_of_element_located((By.ID, "MenuConnecte")))
|
||||||
|
return True
|
||||||
|
except TimeoutException:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _prepare_session(self):
|
||||||
|
# Go to page to set domain context
|
||||||
|
self.driver.get(self.SUBMIT_URL)
|
||||||
|
time.sleep(1)
|
||||||
|
# Try load cookies
|
||||||
|
if os.path.exists(self.COOKIE_FILE):
|
||||||
|
self._load_cookies()
|
||||||
|
self.driver.refresh()
|
||||||
|
# If still not logged, perform inline login
|
||||||
|
if not self._is_logged_in():
|
||||||
|
# open login modal
|
||||||
|
print("Not logged in")
|
||||||
|
self.driver.execute_script("callConnexion();")
|
||||||
|
form = self.wait.until(EC.visibility_of_element_located((By.ID, "LogOn_Form")))
|
||||||
|
form.find_element(By.ID, "LogOn_LoginJson").send_keys(self.login_email)
|
||||||
|
form.find_element(By.ID, "LogOn_MotDePasse").send_keys(self.login_password)
|
||||||
|
form.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
|
||||||
|
# wait for login
|
||||||
|
self.wait.until(EC.visibility_of_element_located((By.ID, "MenuConnecte")))
|
||||||
|
# save cookies
|
||||||
|
self._save_cookies()
|
||||||
|
|
||||||
|
def send_parcel(self, country: str, weight_kg: float = 1.0,
|
||||||
|
email: str = "test@test.com", comment: str = "Coucou"): # -> bool
|
||||||
|
# Navigate back to form page each time
|
||||||
|
self.driver.get(self.SUBMIT_URL)
|
||||||
|
self.wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
||||||
|
# Weight
|
||||||
|
w = self.wait.until(EC.element_to_be_clickable((By.ID, "poids")))
|
||||||
|
w.clear(); w.send_keys(str(weight_kg))
|
||||||
|
# Mode relais
|
||||||
|
lbl_mode = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "label[for='mode-relais']")))
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView(true);", lbl_mode)
|
||||||
|
lbl_mode.click()
|
||||||
|
# Laisser destinataire decider
|
||||||
|
lbl_dest = self.wait.until(EC.element_to_be_clickable((By.ID, "label-email-destinataire")))
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView(true);", lbl_dest)
|
||||||
|
lbl_dest.click()
|
||||||
|
# Email
|
||||||
|
e = self.wait.until(EC.element_to_be_clickable((By.ID, "destinataire-email")))
|
||||||
|
e.clear(); e.send_keys(email)
|
||||||
|
# Country
|
||||||
|
sel = Select(self.wait.until(EC.element_to_be_clickable((By.ID, "CreerEnvoi_Destinataire_Email_Pays"))))
|
||||||
|
sel.select_by_visible_text(country)
|
||||||
|
# Comment
|
||||||
|
c = self.wait.until(EC.element_to_be_clickable((By.ID, "Destinataire_Commentaire")))
|
||||||
|
c.clear(); c.send_keys(comment)
|
||||||
|
# Submit
|
||||||
|
submit_btn = self.wait.until(
|
||||||
|
EC.element_to_be_clickable((By.ID, "ECommerce_CreerEnvoi_Form_Submit"))
|
||||||
|
)
|
||||||
|
submit_btn.click()
|
||||||
|
# Check confirmation overlay
|
||||||
|
try:
|
||||||
|
overlay = self.wait.until(
|
||||||
|
EC.visibility_of_element_located((By.ID, "ECommerce_CreerEnvoi_OverlayConfirm"))
|
||||||
|
)
|
||||||
|
return overlay.is_displayed()
|
||||||
|
except TimeoutException:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def quit(self):
|
||||||
|
self.driver.quit()
|
||||||
|
|
||||||
|
def process_csv(input_csv="MondialRelay.csv",
|
||||||
|
sent_csv="sent.csv",
|
||||||
|
not_sent_csv="not_sent.csv",
|
||||||
|
config_path=None):
|
||||||
|
"""
|
||||||
|
Reads rows from input_csv, sends parcel for valid countries, and writes
|
||||||
|
sent/not_sent outputs with a Status column.
|
||||||
|
"""
|
||||||
|
# Valid destination list
|
||||||
|
valid_countries = {
|
||||||
|
"Allemagne", "Autriche", "Belgique", "Espagne",
|
||||||
|
"France", "Italie", "Luxembourg", "Pays Bas",
|
||||||
|
"Pologne", "Portugal"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize session once
|
||||||
|
session = MondialRelaySession(config_path=config_path)
|
||||||
|
|
||||||
|
with open(input_csv, newline='', encoding='utf-8') as fin, \
|
||||||
|
open(sent_csv, 'w', newline='', encoding='utf-8') as f_sent, \
|
||||||
|
open(not_sent_csv, 'w', newline='', encoding='utf-8') as f_not:
|
||||||
|
|
||||||
|
reader = csv.DictReader(fin)
|
||||||
|
fieldnames = reader.fieldnames + ["Status"]
|
||||||
|
sent_writer = csv.DictWriter(f_sent, fieldnames=fieldnames)
|
||||||
|
not_sent_writer = csv.DictWriter(f_not, fieldnames=fieldnames)
|
||||||
|
|
||||||
|
sent_writer.writeheader()
|
||||||
|
not_sent_writer.writeheader()
|
||||||
|
|
||||||
|
for row in reader:
|
||||||
|
country = row.get("Pays", "").strip()
|
||||||
|
email = row.get("E-mail", row.get("Email", ""))
|
||||||
|
weight = float(row.get("poids", row.get("Poids", 1)))
|
||||||
|
comment = row.get("Commentaire", row.get("commentaire", ""))
|
||||||
|
|
||||||
|
if country in valid_countries:
|
||||||
|
try:
|
||||||
|
ok = session.send_parcel(
|
||||||
|
country=country,
|
||||||
|
weight_kg=weight,
|
||||||
|
email=email,
|
||||||
|
comment=comment
|
||||||
|
)
|
||||||
|
row["Status"] = "sent" if ok else "not sent"
|
||||||
|
if ok:
|
||||||
|
print(f"Email sent to {email}")
|
||||||
|
sent_writer.writerow(row)
|
||||||
|
else:
|
||||||
|
print(f"Email FAILED TO SENT to {email} for country {country}")
|
||||||
|
not_sent_writer.writerow(row)
|
||||||
|
except Exception as e:
|
||||||
|
row["Status"] = f"error: {e}"
|
||||||
|
not_sent_writer.writerow(row)
|
||||||
|
else:
|
||||||
|
row["Status"] = "not sent"
|
||||||
|
not_sent_writer.writerow(row)
|
||||||
|
|
||||||
|
session.quit()
|
||||||
|
print(f"Processing complete. Sent: {sent_csv}, Not sent: {not_sent_csv}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Process Mondial Relay shipments from a CSV file")
|
||||||
|
parser.add_argument('input_csv', help='Path to the input CSV file')
|
||||||
|
parser.add_argument('--sent_csv', default='sent.csv', help='Path for output file of sent parcels')
|
||||||
|
parser.add_argument('--not_sent_csv', default='not_sent.csv', help='Path for output file of not sent parcels')
|
||||||
|
parser.add_argument('--config', default=MondialRelaySession.CONFIG_FILE, help='Path to config.ini for credentials')
|
||||||
|
args = parser.parse_args()
|
||||||
|
process_csv(
|
||||||
|
input_csv=args.input_csv,
|
||||||
|
sent_csv=args.sent_csv,
|
||||||
|
not_sent_csv=args.not_sent_csv,
|
||||||
|
config_path=args.config
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user