Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce new carrier format based on pydantic models. #176

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# specific on the project
*credential.py
/roulier/tests/credentials.py
*label*.pdf # files downloaded when executed some tests

# Byte-compiled / optimized / DLL files
Expand Down
2 changes: 0 additions & 2 deletions roulier.py

This file was deleted.

99 changes: 99 additions & 0 deletions roulier/carrier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Florian Mounier <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from functools import wraps
import logging
import typing
from .roulier import factory
from .exception import CarrierError, InvalidApiInput


log = logging.getLogger(__name__)


class MetaCarrier(type):
"""
Metaclass for Carrier classes.

Used to register carrier actions in the roulier factory.
"""

def __new__(cls, name, bases, dct):
carrier = super().__new__(cls, name, bases, dct)

if not hasattr(carrier, "__key__"):
carrier.__key__ = carrier.__name__.lower()

name = getattr(carrier, "__key__")

for key, value in dct.items():
if getattr(value, "__action__", False):
log.debug(f"Registering {key} for {name}")
factory.register_builder(name, key, carrier)

return carrier


class Carrier(metaclass=MetaCarrier):
"""
Base class for pydantic carriers.
"""

def __init__(self, carrier_type, action, **kwargs):
"""This is unused, but required by the factory."""
self.carrier_type = carrier_type
self.action = action


def action(f):
"""
Decorator for carrier actions. Use it to register an action in the
factory and to validate input and output data.

The decorated method must have an `input` argument decorated with a type hint
and a return type hint.

Example:
```python
@action
def get_label(self, input: CarrierLabelInput) -> CarrierLabelOutput:
return CarrierLabelOutput.from_response(
self.fetch(input.to_request())
)
```
"""

@wraps(f)
def wrapper(self, carrier_type, action, data):
hints = typing.get_type_hints(f)
if "input" not in hints:
raise ValueError(f"Missing input argument or type hint for {f}")
if "return" not in hints:
raise ValueError(f"Missing return type hint for {f}")

try:
input = hints["input"](**data)
except Exception as e:
if "auth" in data:
if "login" in data["auth"]:
data["auth"]["login"] = "xxx"
if "password" in data["auth"]:
data["auth"]["password"] = "xxx"
raise InvalidApiInput(f"Invalid input data {data!r}\n\n{e!s}") from e

try:
rv = f(self, input)
except CarrierError as e:
raise e
except Exception as e:
raise CarrierError(None, f"Action failed {data!r}\n\n{e!s}") from e

if isinstance(rv, hints["return"]):
return rv.model_dump()

return rv

# Mark the function as an action for the metaclass
wrapper.__action__ = True
return wrapper
1 change: 1 addition & 0 deletions roulier/carriers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
from . import dpd_fr_soap
from . import geodis_fr
from . import mondialrelay
from . import mondialrelay_fr
1 change: 1 addition & 0 deletions roulier/carriers/mondialrelay_fr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import carrier
48 changes: 48 additions & 0 deletions roulier/carriers/mondialrelay_fr/carrier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Florian Mounier <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import zeep

from ...carrier import Carrier, action
from ...exception import CarrierError
from .schema import (
MondialRelayLabelInput,
MondialRelayLabelOutput,
MondialRelayPickupSiteInput,
MondialRelayPickupSiteOutput,
)
from .constants import STATUSES


class MondialRelay(Carrier):
__key__ = "mondialrelay_fr"

__url__ = "https://api.mondialrelay.com/Web_Services.asmx?WSDL"
__ref__ = "https://www.mondialrelay.fr/media/122867/solution-web-service-v57.pdf"
__ns_prefix__ = "http://www.mondialrelay.fr/webservice/"

@property
def client(self):
client = zeep.Client(wsdl=self.__url__)
client.set_ns_prefix(None, self.__ns_prefix__)
return client.service

def raise_for_status(self, result):
if "STAT" not in result:
raise CarrierError(result, "No status returned")
if result["STAT"] != "0":
raise CarrierError(result, STATUSES[int(result["STAT"])])

@action
def get_label(self, input: MondialRelayLabelInput) -> MondialRelayLabelOutput:
result = self.client.WSI2_CreationEtiquette(**input.soap())
self.raise_for_status(result)
return MondialRelayLabelOutput.from_soap(result)

@action
def find_pickup_site(
self, input: MondialRelayPickupSiteInput
) -> MondialRelayPickupSiteOutput:
result = self.client.WSI4_PointRelais_Recherche(**input.soap())
self.raise_for_status(result)
return MondialRelayPickupSiteOutput.from_soap(result)
162 changes: 162 additions & 0 deletions roulier/carriers/mondialrelay_fr/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
STATUSES = {
0: "Opération effectuée avec succès",
1: "Enseigne invalide",
2: "Numéro d'enseigne vide ou inexistant",
3: "Numéro de compte enseigne invalide",
4: "",
5: "Numéro de dossier enseigne invalide",
6: "",
7: "Numéro de client enseigne invalide (champ NCLIENT)",
8: "Mot de passe ou hachage invalide",
9: "Ville non reconnu ou non unique",
10: "Type de collecte invalide",
11: "Numéro de Relais de Collecte invalide",
12: "Pays de Relais de collecte invalide",
13: "Type de livraison invalide",
14: "Numéro de Relais de livraison invalide",
15: "Pays de Relais de livraison invalide",
16: "",
17: "",
18: "",
19: "",
20: "Poids du colis invalide",
21: "Taille (Longueur + Hauteur) du colis invalide",
22: "Taille du Colis invalide",
23: "",
24: "Numéro d'expédition ou de suivi invalide",
25: "",
26: "Temps de montage invalide",
27: "Mode de collecte ou de livraison invalide",
28: "Mode de collecte invalide",
29: "Mode de livraison invalide",
30: "Adresse (L1) invalide",
31: "Adresse (L2) invalide",
32: "",
33: "Adresse (L3) invalide",
34: "Adresse (L4) invalide",
35: "Ville invalide",
36: "Code postal invalide",
37: "Pays invalide",
38: "Numéro de téléphone invalide",
39: "Adresse e-mail invalide",
40: "Paramètres manquants",
41: "",
42: "Montant CRT invalide",
43: "Devise CRT invalide",
44: "Valeur du colis invalide",
45: "Devise de la valeur du colis invalide",
46: "Plage de numéro d'expédition épuisée",
47: "Nombre de colis invalide",
48: "Multi-Colis Relais Interdit",
49: "Action invalide",
50: "",
51: "",
52: "",
53: "",
54: "",
55: "",
56: "",
57: "",
58: "",
59: "",
60: "Champ texte libre invalide (Ce code erreur n'est pas invalidant)",
61: "Top avisage invalide",
62: "Instruction de livraison invalide",
63: "Assurance invalide",
64: "Temps de montage invalide",
65: "Top rendez-vous invalide",
66: "Top reprise invalide",
67: "Latitude invalide",
68: "Longitude invalide",
69: "Code Enseigne invalide",
70: "Numéro de Point Relais invalide",
71: "Nature de point de vente non valide",
72: "",
73: "",
74: "Langue invalide",
75: "",
76: "",
77: "",
78: "Pays de Collecte invalide",
79: "Pays de Livraison invalide",
80: "Code tracing : Colis enregistré",
81: "Code tracing : Colis en traitement chez Mondial Relay",
82: "Code tracing : Colis livré",
83: "Code tracing : Anomalie",
84: "(Réservé Code Tracing)",
85: "(Réservé Code Tracing)",
86: "(Réservé Code Tracing)",
87: "(Réservé Code Tracing)",
88: "(Réservé Code Tracing)",
89: "(Réservé Code Tracing)",
90: "",
91: "",
92: "Le code pays du destinataire et le code pays du Point Relais doivent être identiques ou solde insuffisant (comptes prépayés).",
93: "Aucun élément retourné par le plan de tri. Si vous effectuez une collecte ou une livraison en Point Relais, vérifiez que les Point Relais sont bien disponibles. Si vous effectuez une livraison à domicile, il est probable que le code postal que vous avez indiqué n'existe pas.",
94: "Colis Inexistant",
95: "Compte Enseigne non activé",
96: "Type d'enseigne incorrect en Base",
97: "Clé de sécurité invalide",
98: "Erreur générique (Paramètres invalides)",
99: "Erreur générique du service",
}

SORTED_KEYS = [
"Enseigne",
"ModeCol",
"ModeLiv",
"NDossier",
"NClient",
"Expe_Langage",
"Expe_Ad1",
"Expe_Ad2",
"Expe_Ad3",
"Expe_Ad4",
"Expe_Ville",
"Expe_CP",
"Expe_Pays",
"Expe_Tel1",
"Expe_Tel2",
"Expe_Mail",
"Dest_Langage",
"Dest_Ad1",
"Dest_Ad2",
"Dest_Ad3",
"Dest_Ad4",
"Dest_Ville",
"Dest_CP",
"Dest_Pays",
"Dest_Tel1",
"Dest_Tel2",
"Dest_Mail",
"Poids",
"Longueur",
"Taille",
"NbColis",
"CRT_Valeur",
"CRT_Devise",
"Exp_Valeur",
"Exp_Devise",
"COL_Rel_Pays",
"COL_Rel",
"LIV_Rel_Pays",
"LIV_Rel",
"TAvisage",
"TReprise",
"Montage",
"Assurance",
"Instructions",
"Pays",
"NumPointRelais",
"Ville",
"CP",
"Latitude",
"Longitude",
"Taille",
"Poids",
"Action",
"DelaiEnvoi",
"RayonRecherche",
"TypeActivite",
"NombreResultats",
]
Loading
Loading