backend
This commit is contained in:
commit
d7666f7b2c
44 changed files with 2246 additions and 0 deletions
0
backend/routers/__init__.py
Normal file
0
backend/routers/__init__.py
Normal file
213
backend/routers/ai.py
Normal file
213
backend/routers/ai.py
Normal file
|
@ -0,0 +1,213 @@
|
|||
# backend/routers/ai.py (Mise à jour avec extraction de texte)
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from services.ai_service import ai_service
|
||||
from core.security import get_current_user
|
||||
from models.user import User
|
||||
from typing import Optional
|
||||
|
||||
# NOUVELLE IMPORTATION pour le service France Travail
|
||||
from services.france_travail_offer_service import france_travail_offer_service
|
||||
|
||||
# NOUVELLES IMPORTATIONS pour les documents et la base de données
|
||||
from crud import document as crud_document
|
||||
from models.document import Document
|
||||
from core.database import get_db
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# NOUVELLES IMPORTATIONS pour l'extraction de texte
|
||||
import os
|
||||
import pypdf # Pour les fichiers PDF
|
||||
import docx # Pour les fichiers DOCX (pip install python-docx)
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Modèle de requête pour l'analyse d'offre
|
||||
class AnalyzeRequest(BaseModel):
|
||||
cv_id: Optional[int] = Field(None, description="ID du CV de l'utilisateur déjà stocké. Si fourni, cv_text sera ignoré.")
|
||||
cv_text: Optional[str] = Field(None, description="Texte brut du CV à analyser. Utilisé si cv_id n'est pas fourni (ex: pour analyse anonyme).")
|
||||
|
||||
job_offer_text: Optional[str] = Field(None, description="Le texte complet de l'offre d'emploi à analyser (si pas d'offer_id).")
|
||||
france_travail_offer_id: Optional[str] = Field(None, description="L'ID de l'offre France Travail à analyser (si pas de job_offer_text).")
|
||||
|
||||
@model_validator(mode='after')
|
||||
def check_inputs_provided(self) -> 'AnalyzeRequest':
|
||||
if not (self.cv_id or self.cv_text):
|
||||
raise ValueError("Veuillez fournir un 'cv_id' ou un 'cv_text'.")
|
||||
|
||||
if not (self.job_offer_text or self.france_travail_offer_id):
|
||||
raise ValueError("Au moins 'job_offer_text' ou 'france_travail_offer_id' doit être fourni pour l'offre d'emploi.")
|
||||
return self
|
||||
|
||||
# Fonction utilitaire pour extraire le texte d'un fichier
|
||||
def extract_text_from_file(filepath: str) -> str:
|
||||
file_extension = os.path.splitext(filepath)[1].lower()
|
||||
text_content = ""
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
raise FileNotFoundError(f"Le fichier n'existe pas : {filepath}")
|
||||
|
||||
if file_extension == ".pdf":
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
reader = pypdf.PdfReader(f)
|
||||
for page in reader.pages:
|
||||
text_content += page.extract_text() or ""
|
||||
if not text_content.strip(): # Vérifie si le texte extrait est vide ou ne contient que des espaces
|
||||
logger.warning(f"Le fichier PDF {filepath} a été lu mais aucun texte significatif n'a été extrait.")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'extraction du texte du PDF {filepath}: {e}")
|
||||
raise ValueError(f"Impossible d'extraire le texte du fichier PDF. Erreur: {e}")
|
||||
elif file_extension == ".docx":
|
||||
try:
|
||||
document = docx.Document(filepath)
|
||||
for paragraph in document.paragraphs:
|
||||
text_content += paragraph.text + "\n"
|
||||
if not text_content.strip():
|
||||
logger.warning(f"Le fichier DOCX {filepath} a été lu mais aucun texte significatif n'a été extrait.")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'extraction du texte du DOCX {filepath}: {e}")
|
||||
raise ValueError(f"Impossible d'extraire le texte du fichier DOCX. Erreur: {e}")
|
||||
else: # Tente de lire comme un fichier texte
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
text_content = f.read()
|
||||
except UnicodeDecodeError:
|
||||
# Si UTF-8 échoue, tente latin-1
|
||||
try:
|
||||
with open(filepath, 'r', encoding='latin-1') as f:
|
||||
text_content = f.read()
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la lecture du fichier texte {filepath} avec UTF-8 et Latin-1: {e}")
|
||||
raise ValueError(f"Impossible de lire le fichier texte (problème d'encodage). Erreur: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur inattendue lors de la lecture du fichier texte {filepath}: {e}")
|
||||
raise ValueError(f"Impossible de lire le fichier texte. Erreur: {e}")
|
||||
|
||||
return text_content
|
||||
|
||||
|
||||
@router.post("/analyze-job-offer-and-cv", summary="Analyse la pertinence d'un CV pour une offre d'emploi", response_model=dict)
|
||||
async def analyze_job_offer_and_cv_route(
|
||||
request: AnalyzeRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Analyse la pertinence d'un CV par rapport à une offre d'emploi en utilisant l'IA.
|
||||
Prend en entrée soit les textes bruts, soit les IDs des documents.
|
||||
"""
|
||||
cv_text_to_analyze: Optional[str] = request.cv_text
|
||||
|
||||
if request.cv_id:
|
||||
cv_document: Optional[Document] = crud_document.get_document_by_id(db, request.cv_id, current_user.id)
|
||||
|
||||
if not cv_document:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="CV non trouvé ou non accessible par cet utilisateur.")
|
||||
|
||||
try:
|
||||
# Utilise la nouvelle fonction d'extraction de texte
|
||||
cv_text_to_analyze = extract_text_from_file(cv_document.filepath)
|
||||
if not cv_text_to_analyze.strip(): # Vérifier après extraction si le contenu est vide
|
||||
raise ValueError("Le fichier CV est vide ou l'extraction de texte a échoué.")
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"Fichier CV introuvable: {e}")
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Fichier CV introuvable sur le serveur: {e}")
|
||||
except ValueError as e:
|
||||
logger.error(f"Erreur lors de l'extraction/lecture du CV {cv_document.filepath}: {e}")
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Erreur lors de la lecture ou de l'extraction du CV: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur inattendue lors du traitement du CV {cv_document.filepath}: {e}")
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Erreur interne lors du traitement du CV: {e}")
|
||||
|
||||
# Le reste du code pour l'offre d'emploi reste inchangé
|
||||
job_offer_text_to_analyze: Optional[str] = request.job_offer_text
|
||||
if request.france_travail_offer_id:
|
||||
try:
|
||||
offer_details = await france_travail_offer_service.get_offer_details(request.france_travail_offer_id)
|
||||
job_offer_text_to_analyze = offer_details.description
|
||||
if not job_offer_text_to_analyze:
|
||||
raise ValueError("La description de l'offre France Travail est vide.")
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors de la récupération de l'offre France Travail: {e}"
|
||||
)
|
||||
|
||||
if not job_offer_text_to_analyze:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Impossible d'obtenir le texte de l'offre d'emploi pour l'analyse."
|
||||
)
|
||||
|
||||
if not cv_text_to_analyze:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Le texte du CV n'a pas pu être obtenu.")
|
||||
|
||||
try:
|
||||
analysis_result = await ai_service.analyze_job_offer_and_cv(
|
||||
job_offer_text=job_offer_text_to_analyze,
|
||||
cv_text=cv_text_to_analyze
|
||||
)
|
||||
return analysis_result
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
# L'endpoint /score-offer-anonymous
|
||||
@router.post("/score-offer-anonymous", summary="Analyse la pertinence d'un CV pour une offre d'emploi (anonyme)", response_model=dict)
|
||||
async def score_offer_anonymous(
|
||||
request: AnalyzeRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Analyse la pertinence d'un CV par rapport à une offre d'emploi sans nécessiter d'authentification.
|
||||
Prend uniquement le texte de l'offre d'emploi.
|
||||
"""
|
||||
if not request.job_offer_text and not request.france_travail_offer_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Au moins 'job_offer_text' ou 'france_travail_offer_id' doit être fourni pour l'offre d'emploi."
|
||||
)
|
||||
|
||||
if request.cv_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Le 'cv_id' n'est pas autorisé pour les analyses anonymes."
|
||||
)
|
||||
|
||||
job_offer_text_to_analyze: Optional[str] = request.job_offer_text
|
||||
if request.france_travail_offer_id:
|
||||
try:
|
||||
offer_details = await france_travail_offer_service.get_offer_details(request.france_travail_offer_id)
|
||||
job_offer_text_to_analyze = offer_details.description
|
||||
if not job_offer_text_to_analyze:
|
||||
raise ValueError("La description de l'offre France Travail est vide.")
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors de la récupération de l'offre France Travail: {e}"
|
||||
)
|
||||
|
||||
if not job_offer_text_to_analyze:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Impossible d'obtenir le texte de l'offre d'emploi pour l'analyse."
|
||||
)
|
||||
|
||||
if not request.cv_text:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="'cv_text' est requis pour l'analyse anonyme si le CV n'est pas stocké."
|
||||
)
|
||||
|
||||
try:
|
||||
analysis_result = await ai_service.analyze_job_offer_and_cv(
|
||||
job_offer_text=job_offer_text_to_analyze,
|
||||
cv_text=request.cv_text
|
||||
)
|
||||
return analysis_result
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
45
backend/routers/auth.py
Normal file
45
backend/routers/auth.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from datetime import timedelta
|
||||
|
||||
# Importations ABSOLUES
|
||||
from core.database import get_db
|
||||
from core.security import verify_password, create_access_token
|
||||
from crud import user as crud_user # Était déjà correcte pour "crud", mais assure la cohérence
|
||||
from schemas import user as schemas_user
|
||||
from core.config import settings
|
||||
from core.hashing import verify_password
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/auth",
|
||||
tags=["Authentication"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
|
||||
@router.post("/register", response_model=schemas_user.UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
def register_user(user: schemas_user.UserCreate, db: Session = Depends(get_db)):
|
||||
db_user = crud_user.get_user_by_email(db, email=user.email)
|
||||
if db_user:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email déjà enregistré.")
|
||||
|
||||
new_user = crud_user.create_user(db=db, user=user)
|
||||
return new_user
|
||||
|
||||
@router.post("/login", response_model=dict)
|
||||
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||
user = crud_user.get_user_by_email(db, email=form_data.username)
|
||||
if not user or not verify_password(form_data.password, user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Identifiants incorrects",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.email}, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
119
backend/routers/document.py
Normal file
119
backend/routers/document.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
import os
|
||||
import uuid # Pour générer des noms de fichiers uniques
|
||||
|
||||
from core.database import get_db
|
||||
from core.security import create_access_token # Non utilisé directement ici mais potentiellement dans d'autres routers
|
||||
from core.config import settings # Pour accéder au chemin d'upload
|
||||
from crud import document as crud_document
|
||||
from crud import user as crud_user # Pour récupérer l'utilisateur courant
|
||||
from schemas import document as schemas_document
|
||||
from schemas import user as schemas_user # Pour le modèle UserInDBBase ou UserResponse
|
||||
from dependencies import get_current_user # Pour la protection des routes
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/documents",
|
||||
tags=["Documents"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
@router.post("/upload-cv", response_model=schemas_document.DocumentResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def upload_cv(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas_user.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Permet à un utilisateur authentifié d'uploader un CV.
|
||||
Le fichier est stocké sur le serveur et ses métadonnées sont enregistrées en base de données.
|
||||
"""
|
||||
if not file.filename.lower().endswith(('.pdf', '.doc', '.docx')):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Seuls les fichiers PDF, DOC, DOCX sont autorisés."
|
||||
)
|
||||
|
||||
# Créer un nom de fichier unique pour éviter les collisions et les problèmes de sécurité
|
||||
unique_filename = f"{uuid.uuid4()}_{file.filename}"
|
||||
file_path = os.path.join(settings.UPLOADS_DIR, unique_filename)
|
||||
|
||||
# S'assurer que le répertoire d'uploads existe
|
||||
os.makedirs(settings.UPLOADS_DIR, exist_ok=True)
|
||||
|
||||
try:
|
||||
with open(file_path, "wb") as buffer:
|
||||
# Écrit le fichier par morceaux pour les gros fichiers
|
||||
while content := await file.read(1024 * 1024): # Lire par blocs de 1MB
|
||||
buffer.write(content)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors de l'enregistrement du fichier: {e}"
|
||||
)
|
||||
finally:
|
||||
await file.close()
|
||||
|
||||
# Enregistrer les métadonnées du document dans la base de données
|
||||
document_data = schemas_document.DocumentCreate(filename=file.filename)
|
||||
db_document = crud_document.create_document(db, document_data, file_path, current_user.id)
|
||||
|
||||
return db_document
|
||||
|
||||
@router.get("/", response_model=list[schemas_document.DocumentResponse])
|
||||
def get_user_documents(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas_user.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Récupère tous les documents uploadés par l'utilisateur authentifié.
|
||||
"""
|
||||
documents = crud_document.get_documents_by_owner(db, current_user.id)
|
||||
return documents
|
||||
|
||||
@router.get("/{document_id}", response_model=schemas_document.DocumentResponse)
|
||||
def get_document_details(
|
||||
document_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas_user.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Récupère les détails d'un document spécifique de l'utilisateur authentifié.
|
||||
"""
|
||||
document = crud_document.get_document_by_id(db, document_id)
|
||||
if not document:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document non trouvé.")
|
||||
if document.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Vous n'avez pas accès à ce document.")
|
||||
return document
|
||||
|
||||
@router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_document(
|
||||
document_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas_user.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Supprime un document spécifique de l'utilisateur authentifié,
|
||||
à la fois de la base de données et du système de fichiers.
|
||||
"""
|
||||
db_document = crud_document.get_document_by_id(db, document_id)
|
||||
if not db_document:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document non trouvé.")
|
||||
if db_document.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Vous n'avez pas la permission de supprimer ce document.")
|
||||
|
||||
# Supprimer le fichier du système de fichiers
|
||||
if os.path.exists(db_document.filepath):
|
||||
try:
|
||||
os.remove(db_document.filepath)
|
||||
except OSError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors de la suppression du fichier sur le serveur: {e}"
|
||||
)
|
||||
|
||||
# Supprimer l'entrée de la base de données
|
||||
crud_document.delete_document(db, document_id)
|
||||
return {"message": "Document supprimé avec succès."}
|
97
backend/routers/france_travail_offers.py
Normal file
97
backend/routers/france_travail_offers.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
# backend/routers/france_travail_offers.py
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
|
||||
from services.france_travail_offer_service import france_travail_offer_service
|
||||
from core.security import get_current_user
|
||||
from models.user import User
|
||||
from schemas.france_travail import FranceTravailSearchResponse, OffreDetail, Offre
|
||||
|
||||
import logging
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("/search", response_model=FranceTravailSearchResponse)
|
||||
async def search_france_travail_offers(
|
||||
motsCles: Optional[str] = Query(None, description="Mots-clés de recherche (ex: 'développeur full stack')"),
|
||||
commune_nom_ou_code: Optional[str] = Query(None, alias="commune", description="Nom, code postal ou code INSEE de la commune"),
|
||||
distance: Optional[int] = Query(10, description="Distance maximale en km autour de la commune"),
|
||||
page: int = Query(0, description="Numéro de la page de résultats (commence à 0)"),
|
||||
limit: int = Query(15, description="Nombre d'offres par page (max 100 pour l'API France Travail)"), # Max 100 est une limite courante pour une seule requête à l'API FT
|
||||
contrat: Optional[str] = Query(None, description="Type de contrat (ex: 'CDI', 'CDD', 'MIS')"),
|
||||
experience: Optional[str] = Query(None, description="Niveau d'expérience (ex: '1' pour débutant, '2' pour 1-3 ans, '3' pour >3 ans)"),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Recherche des offres d'emploi via l'API France Travail.
|
||||
Convertit le nom de ville en code INSEE si nécessaire et gère la pagination.
|
||||
Nécessite une authentification.
|
||||
"""
|
||||
if limit > 100: # La limite de l'API France Travail pour 'range' est souvent 150 ou 100 items par requête.
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="La limite de résultats par page ne peut pas dépasser 100 pour une seule requête API."
|
||||
)
|
||||
|
||||
commune_param_for_api = None
|
||||
|
||||
if commune_nom_ou_code:
|
||||
if commune_nom_ou_code.isdigit() and len(commune_nom_ou_code) == 5:
|
||||
commune_param_for_api = commune_nom_ou_code
|
||||
logger.info(f"Recherche par code postal: {commune_nom_ou_code}")
|
||||
else:
|
||||
logger.info(f"Tentative de récupération du code INSEE pour la ville: {commune_nom_ou_code}")
|
||||
insee_code = await france_travail_offer_service.get_insee_code_for_commune(commune_nom_ou_code)
|
||||
if not insee_code:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Code INSEE non trouvé pour la ville '{commune_nom_ou_code}'. Veuillez vérifier l'orthographe ou utiliser un code postal."
|
||||
)
|
||||
commune_param_for_api = insee_code
|
||||
logger.info(f"Code INSEE '{insee_code}' trouvé pour '{commune_nom_ou_code}'.")
|
||||
|
||||
if (commune_param_for_api is not None) and (distance is None):
|
||||
distance = 10
|
||||
|
||||
# Calcul du paramètre 'range' pour l'API France Travail
|
||||
start_index = page * limit
|
||||
end_index = start_index + limit - 1
|
||||
api_range_param = f"{start_index}-{end_index}"
|
||||
logger.info(f"Paramètre 'range' calculé pour l'API France Travail: {api_range_param}")
|
||||
|
||||
try:
|
||||
response = await france_travail_offer_service.search_offers(
|
||||
motsCles=motsCles,
|
||||
commune=commune_param_for_api,
|
||||
distance=distance,
|
||||
range=api_range_param, # On passe le 'range' calculé
|
||||
typeContrat=contrat,
|
||||
# experience=experience # Vérifiez si ce paramètre est géré par l'API France Travail ou doit être mappé
|
||||
)
|
||||
return response
|
||||
except RuntimeError as e:
|
||||
logger.error(f"Erreur lors de la recherche d'offres France Travail: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Impossible de récupérer les offres de France Travail: {e}"
|
||||
)
|
||||
|
||||
@router.get("/{offer_id}", response_model=OffreDetail)
|
||||
async def get_france_travail_offer_details(
|
||||
offer_id: str,
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Récupère les détails d'une offre d'emploi spécifique de l'API France Travail par son ID.
|
||||
Nécessite une authentification.
|
||||
"""
|
||||
try:
|
||||
details = await france_travail_offer_service.get_offer_details(offer_id)
|
||||
return details
|
||||
except RuntimeError as e:
|
||||
logger.error(f"Erreur lors de la récupération des détails de l'offre {offer_id} de France Travail: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Impossible de récupérer les détails de l'offre: {e}"
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue