# backend/services/france_travail_offer_service.py import httpx import logging from datetime import datetime, timedelta from typing import List, Optional, Dict, Any, Union from core.config import settings from schemas.france_travail import FranceTravailSearchResponse, OffreDetail, Offre, TypeContrat logger = logging.getLogger(__name__) class FranceTravailOfferService: def __init__(self): self.client_id = settings.FRANCE_TRAVAIL_CLIENT_ID self.client_secret = settings.FRANCE_TRAVAIL_CLIENT_SECRET self.token_url = settings.FRANCE_TRAVAIL_TOKEN_URL self.api_base_url = settings.FRANCE_TRAVAIL_API_BASE_URL self.api_scope = settings.FRANCE_TRAVAIL_API_SCOPE self.access_token = None self.token_expires_at = None async def _get_access_token(self): if self.access_token and self.token_expires_at and datetime.now() < self.token_expires_at: logger.info("Réutilisation du token France Travail existant.") return self.access_token logger.info("Obtention d'un nouveau token d'accès France Travail...") headers = { "Content-Type": "application/x-www-form-urlencoded" } data = { "grant_type": "client_credentials", "client_id": self.client_id, "client_secret": self.client_secret, "scope": self.api_scope } async with httpx.AsyncClient() as client: try: response = await client.post(self.token_url, headers=headers, data=data) response.raise_for_status() token_data = response.json() self.access_token = token_data["access_token"] expires_in = token_data.get("expires_in", 1500) self.token_expires_at = datetime.now() + timedelta(seconds=expires_in - 60) logger.info("Token France Travail obtenu avec succès.") return self.access_token except httpx.HTTPStatusError as e: logger.error(f"Erreur HTTP lors de l'obtention du token France Travail: {e.response.status_code} - {e.response.text}") raise RuntimeError(f"Échec de l'obtention du token France Travail: {e.response.text}") except Exception as e: logger.error(f"Erreur inattendue lors de l'obtention du token France Travail: {e}") raise RuntimeError(f"Échec inattendu lors de l'obtention du token France Travail: {e}") async def get_insee_code_for_commune(self, commune_name: str) -> Optional[str]: """ Récupère le code INSEE d'une commune à partir de son nom. Recherche une correspondance exacte du libellé, ou un code spécifique pour Paris. """ token = await self._get_access_token() headers = { "Accept": "application/json", "Authorization": f"Bearer {token}" } params = { "q": commune_name } async with httpx.AsyncClient() as client: try: response = await client.get( f"{self.api_base_url}/v2/referentiel/communes", headers=headers, params=params ) response.raise_for_status() communes_data = response.json() found_code = None normalized_input_name = commune_name.upper().strip() if communes_data and isinstance(communes_data, list): for commune_info in communes_data: if commune_info and "code" in commune_info and "libelle" in commune_info: normalized_libelle = commune_info["libelle"].upper().strip() # Priorité 1: Recherche spécifique pour "PARIS" avec son code INSEE connu if normalized_input_name == "PARIS" and commune_info["code"] == "75056": found_code = commune_info["code"] break # Priorité 2: Correspondance exacte du libellé elif normalized_libelle == normalized_input_name: found_code = commune_info["code"] break # Priorité 3: Si c'est Paris, mais le libellé renvoyé n'est pas "PARIS" exactement, # mais le code est le bon, on le prend quand même. # Ceci peut arriver si l'API renvoie "Paris 01" par exemple. elif normalized_input_name == "PARIS" and commune_info["code"] in ["75056", "75101", "75102", "75103", "75104", "75105", "75106", "75107", "75108", "75109", "75110", "75111", "75112", "75113", "75114", "75115", "75116", "75117", "75118", "75119", "75120"]: # Note: Les codes 75101 à 75120 sont pour les arrondissements, mais l'API # France Travail utilise souvent le 75056 pour "Paris" globalement. # Cette condition est plus une sécurité, mais 75056 est la cible principale. if commune_info["code"] == "75056": # On préfère le code global de Paris found_code = commune_info["code"] break elif found_code is None: # Si on n'a pas encore trouvé 75056, on prend un arrondissement found_code = commune_info["code"] # Conserver le code d'arrondissement si c'est le seul "Paris" trouvé # Note: La logique ici est à affiner selon si vous voulez les arrondissements ou seulement le code global. # Pour la plupart des cas, "75056" est suffisant. if found_code: logger.info(f"Code INSEE pour '{commune_name}' trouvé : {found_code}") return found_code logger.warning(f"Aucun code INSEE exact trouvé pour la commune '{commune_name}' parmi les résultats de l'API. Vérifiez l'orthographe.") return None except httpx.HTTPStatusError as e: logger.error(f"Erreur HTTP lors de la récupération du code INSEE pour '{commune_name}': {e.response.status_code} - {e.response.text}") return None except Exception as e: logger.error(f"Erreur inattendue lors de la récupération du code INSEE pour '{commune_name}': {e}") return None async def search_offers(self, motsCles: Optional[str] = None, typeContrat: Optional[str] = None, codePostal: Optional[str] = None, commune: Optional[str] = None, distance: Optional[int] = None, alternance: Optional[bool] = None, offresManagerees: Optional[bool] = None, range: str = "0-14") -> FranceTravailSearchResponse: token = await self._get_access_token() headers = { "Accept": "application/json", "Authorization": f"Bearer {token}" } params = { "range": range, } if motsCles: params["motsCles"] = motsCles if typeContrat: params["typeContrat"] = typeContrat if alternance is not None: params["alternance"] = str(alternance).lower() if offresManagerees is not None: params["offresManagerees"] = str(offresManagerees).lower() if codePostal: params["codePostal"] = codePostal if distance is not None: params["distance"] = distance else: params["distance"] = 10 elif commune: params["commune"] = commune if distance is not None: params["distance"] = distance else: params["distance"] = 10 logger.info(f"Paramètres de recherche France Travail: {params}") async with httpx.AsyncClient() as client: try: response = await client.get(f"{self.api_base_url}/v2/offres/search", headers=headers, params=params) response.raise_for_status() return FranceTravailSearchResponse(**response.json()) except httpx.HTTPStatusError as e: logger.error(f"Erreur HTTP lors de la recherche d'offres France Travail: {e.response.status_code} - {e.response.text}") raise RuntimeError(f"Échec de la recherche d'offres France Travail: {e.response.text}") except Exception as e: logger.error(f"Erreur inattendue lors de la recherche d'offres France Travail: {e}") raise RuntimeError(f"Échec inattendu lors de la recherche d'offres France Travail: {e}") async def get_offer_details(self, offer_id: str) -> OffreDetail: token = await self._get_access_token() headers = { "Accept": "application/json", "Authorization": f"Bearer {token}" } async with httpx.AsyncClient() as client: try: response = await client.get(f"{self.api_base_url}/v2/offres/{offer_id}", headers=headers) response.raise_for_status() return OffreDetail(**response.json()) except httpx.HTTPStatusError as e: logger.error(f"Erreur HTTP lors de la récupération des détails de l'offre {offer_id}: {e.response.status_code} - {e.response.text}") raise RuntimeError(f"Échec de la récupération des détails de l'offre {offer_id}: {e.response.text}") except Exception as e: logger.error(f"Erreur inattendue lors de la récupération des détails de l'offre {offer_id}: {e}") raise RuntimeError(f"Échec inattendu lors de la récupération des détails de l'offre {offer_id}: {e}") france_travail_offer_service = FranceTravailOfferService()