Skip to content

Commit

Permalink
Introduce new carrier format based on pydantic models.
Browse files Browse the repository at this point in the history
Implement mondialrelay get_label as an example.
  • Loading branch information
paradoxxxzero committed Sep 11, 2024
1 parent 22adbb0 commit 28c5dad
Show file tree
Hide file tree
Showing 10 changed files with 560 additions and 0 deletions.
1 change: 1 addition & 0 deletions roulier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from . import transport
from . import carrier_action
from . import carriers
from . import carriersv2
import logging

__all__ = [roulier]
Expand Down
1 change: 1 addition & 0 deletions roulier/carriersv2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .mondialrelay import transporter
86 changes: 86 additions & 0 deletions roulier/carriersv2/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# 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 InvalidApiInput


log = logging.getLogger(__name__)


class MetaTransporter(type):
"""
Metaclass for Transporter classes.
Used to register transporter actions in the roulier factory.
"""

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

name = getattr(transporter, "__key__", transporter.__name__.lower())

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

return transporter


class Transporter(metaclass=MetaTransporter):
"""
Base class for pydantic transporters.
"""

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 transporter 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: TransporterLabelInput) -> TransporterLabelOutput:
return TransporterLabelOutput.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:
raise InvalidApiInput(f"Invalid input data {data!r}\n\n{e!s}") from e

rv = f(self, input)

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

return rv

# Mark the function as an action for the metaclass
wrapper.__action__ = True
return wrapper
23 changes: 23 additions & 0 deletions roulier/carriersv2/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 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 typing import ClassVar

REMOVED = ClassVar[None] # Hack to remove a field from inherited class


def prefix(data, prefix):
return {f"{prefix}{k}": v for k, v in data.items()}


def suffix(data, suffix):
return {f"{k}{suffix}": v for k, v in data.items()}


def clean_empty(data):
return {k: v for k, v in data.items() if v is not None and v != ""}


def none_as_empty(data):
return {k: v if v is not None else "" for k, v in data.items()}
Empty file.
149 changes: 149 additions & 0 deletions roulier/carriersv2/mondialrelay/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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",
]
Loading

0 comments on commit 28c5dad

Please sign in to comment.