import json import logging import requests from typing import Dict, Any, Optional class AuthManager: """ Gestionnaire d'authentification pour l'API Odoo. Gère la connexion et les appels RPC à l'API Odoo. """ def __init__(self, url: str, db: str, username: str, api_key: str): """ Initialise le gestionnaire d'authentification. Args: url: URL de l'instance Odoo db: Nom de la base de données Odoo username: Nom d'utilisateur pour la connexion api_key: Clé API ou mot de passe pour l'authentification """ self.url = url.rstrip('/') self.db = db self.username = username self.api_key = api_key self.uid = None self.session = requests.Session() self.session.headers.update({ 'Content-Type': 'application/json', 'Accept': 'application/json' }) self.max_retries = 3 self.timeout = 30 # secondes def login(self) -> bool: """ Se connecte à l'API Odoo en utilisant les identifiants fournis. Returns: True si l'authentification réussie, False sinon """ try: logging.info(f"Tentative de connexion à {self.url} avec l'utilisateur {self.username}") endpoint = '/web/session/authenticate' payload = { "jsonrpc": "2.0", "params": { "db": self.db, "login": self.username, "password": self.api_key } } response = self.session.post( f"{self.url}{endpoint}", data=json.dumps(payload), timeout=self.timeout ) response.raise_for_status() result = response.json() if 'error' in result: error = result['error'] logging.error(f"Erreur d'authentification: {error.get('message', 'Erreur inconnue')}") return False self.uid = result.get('result', {}).get('uid') if not self.uid: logging.error("Erreur: UID non trouvé dans la réponse d'authentification") return False logging.info(f"Authentification réussie. UID: {self.uid}") return True except requests.RequestException as e: logging.error(f"Erreur de connexion à l'API Odoo: {e}") return False except json.JSONDecodeError as e: logging.error(f"Erreur de décodage JSON: {e}") return False except Exception as e: logging.error(f"Erreur inattendue lors de l'authentification: {e}") return False def _rpc_call(self, endpoint: str, params: Dict[str, Any], retry_count: int = 0) -> Any: """ Effectue un appel RPC à l'API Odoo. Args: endpoint: Point de terminaison de l'API params: Paramètres de l'appel retry_count: Nombre de tentatives actuelles (pour les nouvelles tentatives) Returns: Résultat de l'appel RPC ou None en cas d'erreur """ if not self.uid and endpoint != '/web/session/authenticate': logging.warning("Tentative d'appel RPC sans être authentifié. Reconnexion...") if not self.login(): logging.error("Échec de la reconnexion") return None try: payload = { "jsonrpc": "2.0", "params": params } response = self.session.post( f"{self.url}{endpoint}", data=json.dumps(payload), timeout=self.timeout ) response.raise_for_status() result = response.json() if 'error' in result: error = result['error'] error_msg = error.get('message', 'Erreur inconnue') error_data = error.get('data', {}) error_name = error_data.get('name', 'UnknownError') logging.error(f"Erreur RPC: {error_name} - {error_msg}") # Gérer les erreurs d'authentification if "session expired" in error_msg or "Access denied" in error_msg: if retry_count < self.max_retries: logging.info("Session expirée, nouvelle tentative d'authentification...") if self.login(): return self._rpc_call(endpoint, params, retry_count + 1) return None return result.get('result') except requests.RequestException as e: logging.error(f"Erreur de requête RPC: {e}") if retry_count < self.max_retries: logging.info(f"Nouvelle tentative ({retry_count + 1}/{self.max_retries})...") return self._rpc_call(endpoint, params, retry_count + 1) return None except json.JSONDecodeError as e: logging.error(f"Erreur de décodage JSON dans la réponse RPC: {e}") return None except Exception as e: logging.error(f"Erreur inattendue lors de l'appel RPC: {e}") return None def search_read(self, model: str, domain: list, fields: list, **kwargs) -> list: """ Effectue une recherche et lecture sur le modèle spécifié. Args: model: Nom du modèle Odoo domain: Domaine de recherche (filtres) fields: Liste des champs à récupérer **kwargs: Arguments supplémentaires (limit, offset, etc.) Returns: Liste des enregistrements trouvés """ params = { "model": model, "method": "search_read", "args": [domain, fields], "kwargs": kwargs } return self._rpc_call("/web/dataset/call_kw", params) or [] def read(self, model: str, ids: list, fields: list) -> list: """ Lit les enregistrements spécifiés par leurs IDs. Args: model: Nom du modèle Odoo ids: Liste des IDs des enregistrements à lire fields: Liste des champs à récupérer Returns: Liste des enregistrements lus """ if not ids: return [] params = { "model": model, "method": "read", "args": [ids, fields], "kwargs": {} } return self._rpc_call("/web/dataset/call_kw", params) or [] def get_fields(self, model: str) -> Dict[str, Any]: """ Récupère les informations sur les champs d'un modèle. Args: model: Nom du modèle Odoo Returns: Dictionnaire avec les informations sur les champs """ params = { "model": model, "method": "fields_get", "args": [], "kwargs": {} } return self._rpc_call("/web/dataset/call_kw", params) or {}