- Mise à jour du README.md avec les nouvelles fonctionnalités - Documentation API mise à jour avec les intégrations externes - Guide d'installation avec bootstrap automatisé - Architecture mise à jour avec Celery et intégrations - CHANGELOG détaillé avec toutes les nouvelles fonctionnalités - Nouvelle documentation des fonctionnalités v1.2.0 Nouvelles sections documentées: - Pipeline de traitement asynchrone avec Celery - Intégrations avec APIs externes (Cadastre, Géorisques, BODACC, etc.) - Clients d'intégration (AnythingLLM, Neo4j, OpenSearch) - Configuration d'environnement centralisée - Script bootstrap automatisé - Monitoring et observabilité - Exemples d'utilisation et API
438 lines
16 KiB
Python
438 lines
16 KiB
Python
"""
|
|
Intégrations avec les APIs externes pour la vérification des données
|
|
"""
|
|
import os
|
|
import logging
|
|
import requests
|
|
from typing import Dict, Any, Optional, List
|
|
import json
|
|
from datetime import datetime
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ExternalAPIManager:
|
|
"""Gestionnaire des APIs externes pour la vérification des données"""
|
|
|
|
def __init__(self):
|
|
self.session = requests.Session()
|
|
self.session.headers.update({
|
|
'User-Agent': 'Notariat-Pipeline/1.2.0'
|
|
})
|
|
|
|
# Configuration des URLs des APIs
|
|
self.apis = {
|
|
'cadastre': os.getenv('CADASTRE_API_URL', 'https://apicarto.ign.fr/api/cadastre'),
|
|
'georisques': os.getenv('GEORISQUES_API_URL', 'https://www.georisques.gouv.fr/api'),
|
|
'bodacc': os.getenv('BODACC_API_URL', 'https://bodacc-datadila.opendatasoft.com/api'),
|
|
'infogreffe': os.getenv('INFOGREFFE_API_URL', 'https://entreprise.api.gouv.fr/v2/infogreffe'),
|
|
'rbe': os.getenv('RBE_API_URL', 'https://www.data.gouv.fr/api/1/datasets/registre-des-beneficiaires-effectifs')
|
|
}
|
|
|
|
# Cache pour éviter les appels répétés
|
|
self.cache = {}
|
|
self.cache_ttl = 3600 # 1 heure
|
|
|
|
async def verify_address(self, address: str, postal_code: str = None, city: str = None) -> Dict[str, Any]:
|
|
"""
|
|
Vérification d'une adresse via l'API Cadastre
|
|
|
|
Args:
|
|
address: Adresse à vérifier
|
|
postal_code: Code postal
|
|
city: Ville
|
|
|
|
Returns:
|
|
Résultat de la vérification
|
|
"""
|
|
logger.info(f"🏠 Vérification de l'adresse: {address}")
|
|
|
|
try:
|
|
# Construction de la requête
|
|
params = {
|
|
'q': address,
|
|
'limit': 5
|
|
}
|
|
|
|
if postal_code:
|
|
params['code_postal'] = postal_code
|
|
if city:
|
|
params['commune'] = city
|
|
|
|
# Appel à l'API Cadastre
|
|
response = self.session.get(
|
|
f"{self.apis['cadastre']}/parcelle",
|
|
params=params,
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
|
|
if data.get('features'):
|
|
# Adresse trouvée
|
|
feature = data['features'][0]
|
|
properties = feature.get('properties', {})
|
|
|
|
return {
|
|
'status': 'verified',
|
|
'confidence': 0.95,
|
|
'verified_address': properties.get('adresse', address),
|
|
'cadastral_reference': properties.get('numero', ''),
|
|
'surface': properties.get('contenance', 0),
|
|
'coordinates': feature.get('geometry', {}).get('coordinates', []),
|
|
'source': 'cadastre_api',
|
|
'verified_at': datetime.now().isoformat()
|
|
}
|
|
else:
|
|
# Adresse non trouvée
|
|
return {
|
|
'status': 'not_found',
|
|
'confidence': 0.0,
|
|
'message': 'Adresse non trouvée dans le cadastre',
|
|
'source': 'cadastre_api',
|
|
'verified_at': datetime.now().isoformat()
|
|
}
|
|
else:
|
|
logger.warning(f"Erreur API Cadastre: {response.status_code}")
|
|
return {
|
|
'status': 'error',
|
|
'confidence': 0.0,
|
|
'error': f"Erreur API: {response.status_code}",
|
|
'source': 'cadastre_api'
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la vérification de l'adresse: {e}")
|
|
return {
|
|
'status': 'error',
|
|
'confidence': 0.0,
|
|
'error': str(e),
|
|
'source': 'cadastre_api'
|
|
}
|
|
|
|
async def check_geological_risks(self, address: str, coordinates: List[float] = None) -> Dict[str, Any]:
|
|
"""
|
|
Vérification des risques géologiques via l'API Géorisques
|
|
|
|
Args:
|
|
address: Adresse à vérifier
|
|
coordinates: Coordonnées GPS [longitude, latitude]
|
|
|
|
Returns:
|
|
Résultat de la vérification des risques
|
|
"""
|
|
logger.info(f"🌍 Vérification des risques géologiques: {address}")
|
|
|
|
try:
|
|
# Si pas de coordonnées, essayer de les obtenir via géocodage
|
|
if not coordinates:
|
|
coords_result = await self._geocode_address(address)
|
|
if coords_result.get('coordinates'):
|
|
coordinates = coords_result['coordinates']
|
|
|
|
if not coordinates:
|
|
return {
|
|
'status': 'error',
|
|
'confidence': 0.0,
|
|
'error': 'Coordonnées non disponibles',
|
|
'source': 'georisques_api'
|
|
}
|
|
|
|
# Appel à l'API Géorisques
|
|
params = {
|
|
'lon': coordinates[0],
|
|
'lat': coordinates[1],
|
|
'distance': 1000 # 1km de rayon
|
|
}
|
|
|
|
response = self.session.get(
|
|
f"{self.apis['georisques']}/v1/gaspar/risques",
|
|
params=params,
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
|
|
risks = []
|
|
if data.get('data'):
|
|
for risk in data['data']:
|
|
risks.append({
|
|
'type': risk.get('type_risque', ''),
|
|
'level': risk.get('niveau_risque', ''),
|
|
'description': risk.get('description', ''),
|
|
'distance': risk.get('distance', 0)
|
|
})
|
|
|
|
return {
|
|
'status': 'completed',
|
|
'confidence': 0.90,
|
|
'risks_found': len(risks),
|
|
'risks': risks,
|
|
'coordinates': coordinates,
|
|
'source': 'georisques_api',
|
|
'checked_at': datetime.now().isoformat()
|
|
}
|
|
else:
|
|
logger.warning(f"Erreur API Géorisques: {response.status_code}")
|
|
return {
|
|
'status': 'error',
|
|
'confidence': 0.0,
|
|
'error': f"Erreur API: {response.status_code}",
|
|
'source': 'georisques_api'
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la vérification des risques géologiques: {e}")
|
|
return {
|
|
'status': 'error',
|
|
'confidence': 0.0,
|
|
'error': str(e),
|
|
'source': 'georisques_api'
|
|
}
|
|
|
|
async def verify_company(self, company_name: str, siren: str = None) -> Dict[str, Any]:
|
|
"""
|
|
Vérification d'une entreprise via l'API BODACC
|
|
|
|
Args:
|
|
company_name: Nom de l'entreprise
|
|
siren: Numéro SIREN (optionnel)
|
|
|
|
Returns:
|
|
Résultat de la vérification de l'entreprise
|
|
"""
|
|
logger.info(f"🏢 Vérification de l'entreprise: {company_name}")
|
|
|
|
try:
|
|
# Construction de la requête
|
|
params = {
|
|
'q': company_name,
|
|
'rows': 5
|
|
}
|
|
|
|
if siren:
|
|
params['siren'] = siren
|
|
|
|
# Appel à l'API BODACC
|
|
response = self.session.get(
|
|
f"{self.apis['bodacc']}/records/1.0/search/",
|
|
params=params,
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
|
|
if data.get('records'):
|
|
# Entreprise trouvée
|
|
record = data['records'][0]
|
|
fields = record.get('fields', {})
|
|
|
|
return {
|
|
'status': 'verified',
|
|
'confidence': 0.90,
|
|
'company_name': fields.get('nom_raison_sociale', company_name),
|
|
'siren': fields.get('siren', siren),
|
|
'siret': fields.get('siret', ''),
|
|
'address': fields.get('adresse', ''),
|
|
'postal_code': fields.get('code_postal', ''),
|
|
'city': fields.get('ville', ''),
|
|
'activity': fields.get('activite_principale', ''),
|
|
'legal_form': fields.get('forme_juridique', ''),
|
|
'creation_date': fields.get('date_creation', ''),
|
|
'status': fields.get('etat_administratif', ''),
|
|
'source': 'bodacc_api',
|
|
'verified_at': datetime.now().isoformat()
|
|
}
|
|
else:
|
|
# Entreprise non trouvée
|
|
return {
|
|
'status': 'not_found',
|
|
'confidence': 0.0,
|
|
'message': 'Entreprise non trouvée dans le BODACC',
|
|
'source': 'bodacc_api',
|
|
'verified_at': datetime.now().isoformat()
|
|
}
|
|
else:
|
|
logger.warning(f"Erreur API BODACC: {response.status_code}")
|
|
return {
|
|
'status': 'error',
|
|
'confidence': 0.0,
|
|
'error': f"Erreur API: {response.status_code}",
|
|
'source': 'bodacc_api'
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la vérification de l'entreprise: {e}")
|
|
return {
|
|
'status': 'error',
|
|
'confidence': 0.0,
|
|
'error': str(e),
|
|
'source': 'bodacc_api'
|
|
}
|
|
|
|
async def verify_person(self, first_name: str, last_name: str, birth_date: str = None) -> Dict[str, Any]:
|
|
"""
|
|
Vérification d'une personne (recherche d'informations publiques)
|
|
|
|
Args:
|
|
first_name: Prénom
|
|
last_name: Nom de famille
|
|
birth_date: Date de naissance (format YYYY-MM-DD)
|
|
|
|
Returns:
|
|
Résultat de la vérification de la personne
|
|
"""
|
|
logger.info(f"👤 Vérification de la personne: {first_name} {last_name}")
|
|
|
|
try:
|
|
# Recherche dans le RBE (Registre des Bénéficiaires Effectifs)
|
|
rbe_result = await self._search_rbe(first_name, last_name)
|
|
|
|
# Recherche dans Infogreffe (si entreprise)
|
|
infogreffe_result = await self._search_infogreffe(first_name, last_name)
|
|
|
|
# Compilation des résultats
|
|
results = {
|
|
'status': 'completed',
|
|
'confidence': 0.70,
|
|
'person_name': f"{first_name} {last_name}",
|
|
'birth_date': birth_date,
|
|
'rbe_results': rbe_result,
|
|
'infogreffe_results': infogreffe_result,
|
|
'source': 'multiple_apis',
|
|
'verified_at': datetime.now().isoformat()
|
|
}
|
|
|
|
# Calcul de la confiance globale
|
|
if rbe_result.get('found') or infogreffe_result.get('found'):
|
|
results['confidence'] = 0.85
|
|
|
|
return results
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la vérification de la personne: {e}")
|
|
return {
|
|
'status': 'error',
|
|
'confidence': 0.0,
|
|
'error': str(e),
|
|
'source': 'person_verification'
|
|
}
|
|
|
|
async def _geocode_address(self, address: str) -> Dict[str, Any]:
|
|
"""Géocodage d'une adresse"""
|
|
try:
|
|
# Utilisation de l'API de géocodage de l'IGN
|
|
params = {
|
|
'q': address,
|
|
'limit': 1
|
|
}
|
|
|
|
response = self.session.get(
|
|
f"{self.apis['cadastre']}/geocodage",
|
|
params=params,
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
if data.get('features'):
|
|
feature = data['features'][0]
|
|
coords = feature.get('geometry', {}).get('coordinates', [])
|
|
return {
|
|
'coordinates': coords,
|
|
'formatted_address': feature.get('properties', {}).get('label', address)
|
|
}
|
|
|
|
return {'coordinates': None}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors du géocodage: {e}")
|
|
return {'coordinates': None}
|
|
|
|
async def _search_rbe(self, first_name: str, last_name: str) -> Dict[str, Any]:
|
|
"""Recherche dans le Registre des Bénéficiaires Effectifs"""
|
|
try:
|
|
params = {
|
|
'q': f"{first_name} {last_name}",
|
|
'rows': 5
|
|
}
|
|
|
|
response = self.session.get(
|
|
f"{self.apis['rbe']}/search",
|
|
params=params,
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return {
|
|
'found': len(data.get('results', [])) > 0,
|
|
'count': len(data.get('results', [])),
|
|
'results': data.get('results', [])[:3] # Limite à 3 résultats
|
|
}
|
|
|
|
return {'found': False, 'count': 0, 'results': []}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la recherche RBE: {e}")
|
|
return {'found': False, 'count': 0, 'results': []}
|
|
|
|
async def _search_infogreffe(self, first_name: str, last_name: str) -> Dict[str, Any]:
|
|
"""Recherche dans Infogreffe"""
|
|
try:
|
|
params = {
|
|
'q': f"{first_name} {last_name}",
|
|
'per_page': 5
|
|
}
|
|
|
|
response = self.session.get(
|
|
f"{self.apis['infogreffe']}/search",
|
|
params=params,
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return {
|
|
'found': len(data.get('results', [])) > 0,
|
|
'count': len(data.get('results', [])),
|
|
'results': data.get('results', [])[:3] # Limite à 3 résultats
|
|
}
|
|
|
|
return {'found': False, 'count': 0, 'results': []}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la recherche Infogreffe: {e}")
|
|
return {'found': False, 'count': 0, 'results': []}
|
|
|
|
def get_cache_key(self, api: str, params: Dict[str, Any]) -> str:
|
|
"""Génère une clé de cache pour les paramètres donnés"""
|
|
import hashlib
|
|
key_data = f"{api}:{json.dumps(params, sort_keys=True)}"
|
|
return hashlib.md5(key_data.encode()).hexdigest()
|
|
|
|
def is_cache_valid(self, cache_key: str) -> bool:
|
|
"""Vérifie si le cache est encore valide"""
|
|
if cache_key not in self.cache:
|
|
return False
|
|
|
|
cache_time = self.cache[cache_key].get('timestamp', 0)
|
|
current_time = datetime.now().timestamp()
|
|
|
|
return (current_time - cache_time) < self.cache_ttl
|
|
|
|
def get_from_cache(self, cache_key: str) -> Optional[Dict[str, Any]]:
|
|
"""Récupère une valeur du cache"""
|
|
if self.is_cache_valid(cache_key):
|
|
return self.cache[cache_key].get('data')
|
|
return None
|
|
|
|
def set_cache(self, cache_key: str, data: Dict[str, Any]) -> None:
|
|
"""Met une valeur en cache"""
|
|
self.cache[cache_key] = {
|
|
'data': data,
|
|
'timestamp': datetime.now().timestamp()
|
|
}
|