first commit

This commit is contained in:
mathieu 2025-07-14 17:56:57 +02:00
commit a233e18c0b
48 changed files with 55300 additions and 0 deletions

View file

@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Bash(python test:*)",
"Bash(python:*)",
"Bash(grep:*)",
"Bash(for:*)",
"Bash(do sed -i 's/from constantes import/from atmo_data_wrapper import/g' \"$file\")",
"Bash(done)",
"Bash(do sed -i 's/from atmo_data_client import/from atmo_data_wrapper import/g' \"$file\")",
"Bash(do sed -i 's/from atmo_data_models import/from atmo_data_wrapper import/g' \"$file\")"
],
"deny": []
}
}

27
.gitignore vendored Normal file
View file

@ -0,0 +1,27 @@
# Fichiers de credentials
credentials.json
# Données de test
test_files/
data/
archives/
# Fichiers temporaires Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.env
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Atmo France
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

29
MANIFEST.in Normal file
View file

@ -0,0 +1,29 @@
include docs/README.md
include docs/QUICKSTART.md
include docs/DOCUMENTATION_DEMOS.md
include docs/JOURNAL.md
include docs/notice_Atmo_Data_1eravril2025.md
include LICENSE
include credentials.json.example
include swagger.json
recursive-include atmo_data_wrapper *.py
recursive-include examples *.py
recursive-include demos *.py
recursive-include tests *.py
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
recursive-exclude * *.so
recursive-exclude * .DS_Store
exclude .gitignore
exclude credentials.json
exclude *.pyc
exclude .git*
exclude *.log
exclude .pytest_cache
exclude .mypy_cache
exclude build
exclude dist
exclude *.egg-info

View file

@ -0,0 +1,109 @@
"""
Atmo Data Wrapper - Wrapper Python pour l'API Atmo Data
=====================================================
Ce package fournit un wrapper Python pour l'API Atmo Data (https://admindata.atmo-france.org),
permettant d'accéder facilement aux données de qualité de l'air et de pollution des
Associations agréées de surveillance de la qualité de l'air (AASQA) françaises.
Modules principaux:
- client: Client principal pour l'API
- models: Classes pour les données typées
- constants: Constantes et configurations
Usage:
from atmo_data_wrapper import AtmoDataClient
client = AtmoDataClient()
client.auto_login()
indices = client.get_indices_atmo()
"""
__version__ = "1.0.0"
__author__ = "Atmo Data Wrapper Team"
__email__ = "contact@atmo-france.org"
__description__ = "Wrapper Python pour l'API Atmo Data"
# Import des classes principales pour faciliter l'usage
from .core.client import AtmoDataClient
from .core.models import (
AtmoDataBase,
IndiceAtmo,
EpisodePollution,
EmissionData,
IndicePollen,
AtmoDataCollection,
Coordinates
)
from .core.constants import (
AASQA_CODES,
INDICES_ATMO,
INDICES_POLLENS,
CODE_COLOR_QUALIF,
CODE_POLLUANT,
CODE_TAXON,
SECTEURS_EMISSIONS,
POLLUANTS,
CODE_POLLUANT_EPISODES,
TAXON_MAPPING,
ATMO_LICENCE_COURTE,
ATMO_LICENCE_LONGUE,
ATMO_LICENCE_COMPLETE
)
from .core.utils import (
get_aasqa_by_department,
get_aasqa_info,
get_aasqa_website,
list_departments_by_aasqa,
search_aasqa_by_name,
get_departments_count,
validate_department_coverage,
get_aasqa_statistics,
get_atmo_licence,
print_atmo_licence
)
from .core.exceptions import AtmoDataException
__all__ = [
# Client principal
'AtmoDataClient',
# Classes de données
'AtmoDataBase',
'IndiceAtmo',
'EpisodePollution',
'EmissionData',
'IndicePollen',
'AtmoDataCollection',
'Coordinates',
# Constantes
'AASQA_CODES',
'INDICES_ATMO',
'INDICES_POLLENS',
'CODE_COLOR_QUALIF',
'CODE_POLLUANT',
'CODE_TAXON',
'SECTEURS_EMISSIONS',
'POLLUANTS',
'CODE_POLLUANT_EPISODES',
'TAXON_MAPPING',
'ATMO_LICENCE_COURTE',
'ATMO_LICENCE_LONGUE',
'ATMO_LICENCE_COMPLETE',
# Fonctions utilitaires AASQA
'get_aasqa_by_department',
'get_aasqa_info',
'get_aasqa_website',
'list_departments_by_aasqa',
'search_aasqa_by_name',
'get_departments_count',
'validate_department_coverage',
'get_aasqa_statistics',
'get_atmo_licence',
'print_atmo_licence',
# Exceptions
'AtmoDataException'
]

View file

@ -0,0 +1,77 @@
"""
Core module for Atmo Data Wrapper
"""
from .client import AtmoDataClient
from .models import (
AtmoDataBase,
IndiceAtmo,
EpisodePollution,
EmissionData,
IndicePollen,
AtmoDataCollection,
Coordinates
)
from .constants import (
AASQA_CODES,
INDICES_ATMO,
INDICES_POLLENS,
CODE_COLOR_QUALIF,
CODE_POLLUANT,
CODE_TAXON,
SECTEURS_EMISSIONS,
POLLUANTS,
CODE_POLLUANT_EPISODES,
TAXON_MAPPING,
ATMO_LICENCE_COURTE,
ATMO_LICENCE_LONGUE,
ATMO_LICENCE_COMPLETE
)
from .utils import (
get_aasqa_by_department,
get_aasqa_info,
get_aasqa_website,
list_departments_by_aasqa,
search_aasqa_by_name,
get_departments_count,
validate_department_coverage,
get_aasqa_statistics,
get_atmo_licence,
print_atmo_licence
)
from .exceptions import AtmoDataException
__all__ = [
'AtmoDataClient',
'AtmoDataBase',
'IndiceAtmo',
'EpisodePollution',
'EmissionData',
'IndicePollen',
'AtmoDataCollection',
'Coordinates',
'AASQA_CODES',
'INDICES_ATMO',
'INDICES_POLLENS',
'CODE_COLOR_QUALIF',
'CODE_POLLUANT',
'CODE_TAXON',
'SECTEURS_EMISSIONS',
'POLLUANTS',
'CODE_POLLUANT_EPISODES',
'TAXON_MAPPING',
'ATMO_LICENCE_COURTE',
'ATMO_LICENCE_LONGUE',
'ATMO_LICENCE_COMPLETE',
'get_aasqa_by_department',
'get_aasqa_info',
'get_aasqa_website',
'list_departments_by_aasqa',
'search_aasqa_by_name',
'get_departments_count',
'validate_department_coverage',
'get_aasqa_statistics',
'get_atmo_licence',
'print_atmo_licence',
'AtmoDataException'
]

View file

@ -0,0 +1,598 @@
"""
Wrapper Python pour l'API Atmo Data
API d'accès aux données de qualité de l'air des AASQA françaises
"""
import requests
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List, Union
import json
import re
import csv
import os
from pathlib import Path
from .constants import (
FORMATS_VALIDES, AASQA_CODES, POLLUANTS, SECTEURS_EMISSIONS,
INDICES_ATMO, INDICES_POLLENS, TYPES_EPISODES, ECHEANCES_VALIDES,
ECHELLES_VALIDES, DEFAULT_API_URL, DEFAULT_TIMEOUT, TOKEN_VALIDITY_HOURS,
SAVE_FORMATS, FILE_EXTENSIONS
)
from .models import AtmoDataCollection
from .exceptions import AtmoDataException
class AtmoDataClient:
"""Client pour l'API Atmo Data"""
def __init__(self, base_url: Optional[str] = None, credentials_file: Optional[str] = None):
self.credentials_file = credentials_file or "credentials.json"
self._credentials = None
# Déterminer l'URL de base : paramètre > credentials > défaut
if base_url:
self.base_url = base_url.rstrip('/')
else:
try:
credentials = self._load_credentials()
self.base_url = credentials.get('api_url', DEFAULT_API_URL).rstrip('/')
except:
self.base_url = DEFAULT_API_URL.rstrip('/')
self.session = requests.Session()
self.session.timeout = DEFAULT_TIMEOUT
self.token = None
def _validate_format(self, format_value: str) -> None:
"""Valide le format de sortie supportés par l'API"""
if format_value not in FORMATS_VALIDES:
raise ValueError(f"Format invalide: {format_value}. Formats valides: {FORMATS_VALIDES}")
def _validate_aasqa(self, aasqa: str) -> None:
"""Valide le code AASQA"""
if aasqa not in AASQA_CODES:
raise ValueError(f"Code AASQA invalide: {aasqa}. Codes valides: {list(AASQA_CODES.keys())}")
def _validate_polluant(self, polluant: str) -> None:
"""Valide le polluant"""
if polluant not in POLLUANTS:
raise ValueError(f"Polluant invalide: {polluant}. Polluants valides: {POLLUANTS}")
def _validate_date(self, date: str) -> None:
"""Valide le format de date (YYYY-MM-DD)"""
if not re.match(r'^\d{4}-\d{2}-\d{2}$', date):
raise ValueError(f"Format de date invalide: {date}. Format attendu: YYYY-MM-DD")
try:
datetime.strptime(date, '%Y-%m-%d')
except ValueError:
raise ValueError(f"Date invalide: {date}")
def _validate_code_qualificatif_atmo(self, code: str) -> None:
"""Valide le code qualificatif ATMO"""
if code not in INDICES_ATMO:
raise ValueError(f"Code qualificatif ATMO invalide: {code}. Codes valides: {list(INDICES_ATMO.keys())}")
def _validate_code_qualificatif_pollen(self, code: str) -> None:
"""Valide le code qualificatif pollen"""
if code not in INDICES_POLLENS:
raise ValueError(f"Code qualificatif pollen invalide: {code}. Codes valides: {list(INDICES_POLLENS.keys())}")
def _validate_type_episode(self, type_episode: str) -> None:
"""Valide le type d'épisode de pollution"""
if type_episode not in TYPES_EPISODES:
raise ValueError(f"Type d'épisode invalide: {type_episode}. Types valides: {TYPES_EPISODES}")
def _validate_echeance(self, echeance: str) -> None:
"""Valide l'échéance"""
if echeance not in ECHEANCES_VALIDES:
raise ValueError(f"Échéance invalide: {echeance}. Échéances valides: {ECHEANCES_VALIDES}")
def _validate_echelle(self, echelle: str) -> None:
"""Valide l'échelle"""
if echelle not in ECHELLES_VALIDES:
raise ValueError(f"Échelle invalide: {echelle}. Échelles valides: {ECHELLES_VALIDES}")
def _validate_secteur(self, secteur: str) -> None:
"""Valide le secteur d'émission"""
if secteur not in SECTEURS_EMISSIONS:
raise ValueError(f"Secteur invalide: {secteur}. Secteurs valides: {list(SECTEURS_EMISSIONS.keys())}")
def _validate_bounding_box(self, bounding_box: str) -> None:
"""Valide le format de la bounding box"""
parts = bounding_box.split()
if len(parts) != 4:
raise ValueError(f"Format de bounding box invalide: {bounding_box}. Format attendu: 'xmin ymin xmax ymax'")
try:
coords = [float(part) for part in parts]
if coords[0] >= coords[2] or coords[1] >= coords[3]:
raise ValueError("Bounding box invalide: xmin doit être < xmax et ymin doit être < ymax")
except ValueError as e:
if "invalid literal for float()" in str(e):
raise ValueError(f"Coordonnées de bounding box invalides: {bounding_box}")
raise
def _load_credentials(self) -> Dict[str, str]:
"""Charge les credentials depuis le fichier JSON"""
if self._credentials is not None:
return self._credentials
try:
with open(self.credentials_file, 'r', encoding='utf-8') as f:
self._credentials = json.load(f)
# Valider les champs requis
required_fields = ['username', 'password']
missing_fields = [field for field in required_fields if field not in self._credentials]
if missing_fields:
raise ValueError(f"Champs manquants dans {self.credentials_file}: {missing_fields}")
# Mettre à jour l'URL de base si spécifiée dans les credentials
if 'api_url' in self._credentials:
self.base_url = self._credentials['api_url'].rstrip('/')
return self._credentials
except FileNotFoundError:
raise AtmoDataException(
f"Fichier de credentials '{self.credentials_file}' non trouvé. "
f"Créez-le à partir de '{self.credentials_file}.example' ou utilisez login() avec username/password."
)
except json.JSONDecodeError as e:
raise AtmoDataException(f"Erreur de format JSON dans {self.credentials_file}: {e}")
def login(self, username: Optional[str] = None, password: Optional[str] = None) -> bool:
"""
Connexion à l'API et récupération du token JWT
Args:
username: Nom d'utilisateur (optionnel si fichier credentials.json existe)
password: Mot de passe (optionnel si fichier credentials.json existe)
Returns:
bool: True si la connexion réussit
"""
# Si pas de username/password fournis, charger depuis le fichier
if username is None or password is None:
credentials = self._load_credentials()
username = username or credentials['username']
password = password or credentials['password']
url = f"{self.base_url}/api/login"
data = {
"username": username,
"password": password
}
try:
response = self.session.post(url, json=data)
response.raise_for_status()
result = response.json()
if 'token' in result:
self.token = result['token']
self.session.headers.update({
'Authorization': f'Bearer {self.token}'
})
return True
return False
except requests.exceptions.RequestException as e:
raise AtmoDataException(f"Erreur de connexion: {e}")
def auto_login(self) -> bool:
"""
Connexion automatique en utilisant le fichier de credentials
Returns:
bool: True si la connexion réussit
"""
return self.login()
def _make_request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Effectue une requête à l'API
Args:
endpoint: Endpoint de l'API
params: Paramètres de la requête
Returns:
Dict: Réponse de l'API
"""
if not self.token:
raise AtmoDataException("Token non disponible. Veuillez vous connecter avec login()")
url = f"{self.base_url}{endpoint}"
try:
response = self.session.get(url, params=params)
response.raise_for_status()
content_type = response.headers.get('content-type', '')
if 'application/json' in content_type:
return response.json()
else:
return {'data': response.text}
except requests.exceptions.RequestException as e:
raise AtmoDataException(f"Erreur lors de la requête: {e}")
def get_indices_atmo(self,
format: str = "geojson",
date: Optional[str] = None,
date_historique: Optional[str] = None,
code_zone: Optional[str] = None,
aasqa: Optional[str] = None,
code_qualificatif: Optional[str] = None,
bounding_box: Optional[str] = None,
bounding_box_srs: Optional[str] = None) -> Union[Dict[str, Any], AtmoDataCollection]:
"""
Récupération des indices ATMO
Args:
format: Format de sortie ('geojson' ou 'csv')
date: Date au format YYYY-MM-DD
date_historique: Date de début pour une période
code_zone: Code INSEE (commune/EPCI)
aasqa: Code AASQA (01-94)
code_qualificatif: Valeur d'indice (0-7)
bounding_box: Bounding box "xmin ymin xmax ymax"
bounding_box_srs: EPSG de la bounding box
Returns:
Union[Dict, AtmoDataCollection]: Données des indices ATMO (Dict pour CSV, AtmoDataCollection pour GeoJSON)
"""
# Validation et construction des paramètres
self._validate_format(format)
params = {'format': format}
if date:
self._validate_date(date)
params['date'] = date
if date_historique:
self._validate_date(date_historique)
params['date_historique'] = date_historique
if code_zone:
params['code_zone'] = code_zone
if aasqa:
self._validate_aasqa(aasqa)
params['aasqa'] = aasqa
if code_qualificatif:
self._validate_code_qualificatif_atmo(code_qualificatif)
params['code_qualificatif'] = code_qualificatif
if bounding_box:
self._validate_bounding_box(bounding_box)
params['bounding_box'] = bounding_box
if bounding_box_srs:
params['bounding_box_srs'] = bounding_box_srs
result = self._make_request('/api/v2/data/indices/atmo', params)
# Retourner un objet typé pour GeoJSON, dict brut pour CSV
if format == 'geojson' and 'type' in result:
return AtmoDataCollection(result, 'indices')
return result
def get_episodes_3jours(self,
format: str = "geojson",
aasqa: Optional[str] = None,
code_zone: Optional[str] = None,
polluant: Optional[str] = None,
type_episode: Optional[str] = None,
echeance: Optional[str] = None,
bounding_box: Optional[str] = None,
bounding_box_srs: Optional[str] = None) -> Union[Dict[str, Any], AtmoDataCollection]:
"""
Récupération des épisodes de pollution sur 3 jours (J-1, J0, J+1)
Args:
format: Format de sortie ('geojson' ou 'csv')
aasqa: Code AASQA (01-94)
code_zone: Code INSEE (département ou bassin d'air)
polluant: Polluant ('NO2', 'SO2', 'PM10', 'PM2.5', 'O3')
type_episode: Type d'épisode
echeance: Échéance ('-1', '0', '1')
bounding_box: Bounding box "xmin ymin xmax ymax"
bounding_box_srs: EPSG de la bounding box
Returns:
Union[Dict, AtmoDataCollection]: Données des épisodes (Dict pour CSV, AtmoDataCollection pour GeoJSON)
"""
# Validation et construction des paramètres
self._validate_format(format)
params = {'format': format}
if aasqa:
self._validate_aasqa(aasqa)
params['aasqa'] = aasqa
if code_zone:
params['code_zone'] = code_zone
if polluant:
self._validate_polluant(polluant)
params['polluant'] = polluant
if type_episode:
self._validate_type_episode(type_episode)
params['type_episode'] = type_episode
if echeance:
self._validate_echeance(echeance)
params['echeance'] = echeance
if bounding_box:
self._validate_bounding_box(bounding_box)
params['bounding_box'] = bounding_box
if bounding_box_srs:
params['bounding_box_srs'] = bounding_box_srs
result = self._make_request('/api/v2/data/episodes/3jours', params)
# Retourner un objet typé pour GeoJSON, dict brut pour CSV
if format == 'geojson' and 'type' in result:
return AtmoDataCollection(result, 'episodes')
return result
def get_episodes_historique(self,
date: str,
format: str = "geojson",
aasqa: Optional[str] = None,
code_zone: Optional[str] = None,
polluant: Optional[str] = None,
type_episode: Optional[str] = None,
date_historique: Optional[str] = None,
bounding_box: Optional[str] = None,
bounding_box_srs: Optional[str] = None) -> Union[Dict[str, Any], AtmoDataCollection]:
"""
Récupération des épisodes de pollution historiques
Args:
date: Date de référence (obligatoire) au format YYYY-MM-DD
format: Format de sortie ('geojson' ou 'csv')
aasqa: Code AASQA (01-94)
code_zone: Code INSEE (département ou bassin d'air)
polluant: Polluant ('NO2', 'SO2', 'PM10', 'PM2.5', 'O3')
type_episode: Type d'épisode
date_historique: Date de début pour une période
bounding_box: Bounding box "xmin ymin xmax ymax"
bounding_box_srs: EPSG de la bounding box
Returns:
Union[Dict, AtmoDataCollection]: Données des épisodes historiques (Dict pour CSV, AtmoDataCollection pour GeoJSON)
"""
# Validation et construction des paramètres
self._validate_date(date)
self._validate_format(format)
params = {'date': date, 'format': format}
if aasqa:
self._validate_aasqa(aasqa)
params['aasqa'] = aasqa
if code_zone:
params['code_zone'] = code_zone
if polluant:
self._validate_polluant(polluant)
params['polluant'] = polluant
if type_episode:
self._validate_type_episode(type_episode)
params['type_episode'] = type_episode
if date_historique:
self._validate_date(date_historique)
params['date_historique'] = date_historique
if bounding_box:
self._validate_bounding_box(bounding_box)
params['bounding_box'] = bounding_box
if bounding_box_srs:
params['bounding_box_srs'] = bounding_box_srs
result = self._make_request('/api/v2/data/episodes/historique', params)
# Retourner un objet typé pour GeoJSON, dict brut pour CSV
if format == 'geojson' and 'type' in result:
return AtmoDataCollection(result, 'episodes')
return result
def get_emissions(self,
format: str = "geojson",
echelle: str = "region",
code_zone: Optional[str] = None,
aasqa: Optional[str] = None,
secteur: Optional[str] = None,
bounding_box: Optional[str] = None,
bounding_box_srs: Optional[str] = None) -> Union[Dict[str, Any], AtmoDataCollection]:
"""
Récupération des données d'inventaires des émissions
Args:
format: Format de sortie ('geojson' ou 'csv')
echelle: Échelle d'agrégation ('region' ou 'epci')
code_zone: Code zone (EPCI uniquement)
aasqa: Code AASQA (01-94)
secteur: Code secteur (5, 6, 7, 34, 219)
bounding_box: Bounding box "xmin ymin xmax ymax"
bounding_box_srs: EPSG de la bounding box
Returns:
Union[Dict, AtmoDataCollection]: Données des émissions (Dict pour CSV, AtmoDataCollection pour GeoJSON)
"""
# Validation et construction des paramètres
self._validate_format(format)
self._validate_echelle(echelle)
params = {'format': format, 'echelle': echelle}
if code_zone:
params['code_zone'] = code_zone
if aasqa:
self._validate_aasqa(aasqa)
params['aasqa'] = aasqa
if secteur:
self._validate_secteur(secteur)
params['secteur'] = secteur
if bounding_box:
self._validate_bounding_box(bounding_box)
params['bounding_box'] = bounding_box
if bounding_box_srs:
params['bounding_box_srs'] = bounding_box_srs
result = self._make_request('/api/v2/data/inventaires/emissions', params)
# Retourner un objet typé pour GeoJSON, dict brut pour CSV
if format == 'geojson' and 'type' in result:
return AtmoDataCollection(result, 'emissions')
return result
def get_indices_pollens(self,
format: str = "geojson",
date: Optional[str] = None,
date_historique: Optional[str] = None,
code_zone: Optional[str] = None,
aasqa: Optional[str] = None,
code_qualificatif: Optional[str] = None,
alerte: Optional[bool] = None,
with_geom: bool = False,
bounding_box: Optional[str] = None,
bounding_box_srs: Optional[str] = None) -> Union[Dict[str, Any], AtmoDataCollection]:
"""
Récupération des indices pollen
Args:
format: Format de sortie ('geojson' ou 'csv')
date: Date au format YYYY-MM-DD
date_historique: Date de début pour une période
code_zone: Code INSEE (commune)
aasqa: Code AASQA (01-94)
code_qualificatif: Valeur d'indice pollinique (0-6)
alerte: Filtre par statut d'alerte
with_geom: Inclure les géométries
bounding_box: Bounding box "xmin ymin xmax ymax"
bounding_box_srs: EPSG de la bounding box
Returns:
Union[Dict, AtmoDataCollection]: Données des indices pollen (Dict pour CSV, AtmoDataCollection pour GeoJSON)
"""
# Validation et construction des paramètres
self._validate_format(format)
params = {'format': format, 'with_geom': with_geom}
if date:
self._validate_date(date)
params['date'] = date
if date_historique:
self._validate_date(date_historique)
params['date_historique'] = date_historique
if code_zone:
params['code_zone'] = code_zone
if aasqa:
self._validate_aasqa(aasqa)
params['aasqa'] = aasqa
if code_qualificatif:
self._validate_code_qualificatif_pollen(code_qualificatif)
params['code_qualificatif'] = code_qualificatif
if alerte is not None:
params['alerte'] = alerte
if bounding_box:
self._validate_bounding_box(bounding_box)
params['bounding_box'] = bounding_box
if bounding_box_srs:
params['bounding_box_srs'] = bounding_box_srs
result = self._make_request('/api/v2/data/indices/pollens', params)
# Retourner un objet typé pour GeoJSON, dict brut pour CSV
if format == 'geojson' and 'type' in result:
return AtmoDataCollection(result, 'pollens')
return result
def _validate_save_format(self, file_format: str) -> None:
"""Valide le format de sauvegarde"""
if file_format not in SAVE_FORMATS:
raise ValueError(f"Format de sauvegarde invalide: {file_format}. Formats supportés: {SAVE_FORMATS}")
def _extract_features_to_csv_data(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Extrait les features GeoJSON pour conversion en CSV"""
csv_data = []
if 'features' in data:
for feature in data['features']:
row = {}
# Ajouter les propriétés
if 'properties' in feature:
row.update(feature['properties'])
# Ajouter les coordonnées si présentes
if 'geometry' in feature and feature['geometry'] and 'coordinates' in feature['geometry']:
coords = feature['geometry']['coordinates']
if isinstance(coords, list) and len(coords) >= 2:
row['longitude'] = coords[0]
row['latitude'] = coords[1]
csv_data.append(row)
return csv_data
def save_to_file(self, data: Dict[str, Any], filename: str, file_format: str = 'json') -> str:
"""
Sauvegarde les données de l'API dans un fichier
Args:
data: Données retournées par l'API
filename: Nom du fichier (sans extension)
file_format: Format de sauvegarde ('json', 'csv', 'geojson')
Returns:
str: Chemin complet du fichier créé
Raises:
ValueError: Si le format n'est pas supporté
IOError: Si erreur lors de l'écriture du fichier
"""
self._validate_save_format(file_format)
# Ajouter l'extension appropriée
if not filename.endswith(FILE_EXTENSIONS[file_format]):
filename += FILE_EXTENSIONS[file_format]
# Créer le répertoire parent si nécessaire
filepath = Path(filename)
filepath.parent.mkdir(parents=True, exist_ok=True)
try:
if file_format == 'json':
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
elif file_format == 'geojson':
# Vérifier que c'est bien du GeoJSON
if 'type' not in data or data['type'] != 'FeatureCollection':
raise ValueError("Les données ne sont pas au format GeoJSON")
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
elif file_format == 'csv':
csv_data = self._extract_features_to_csv_data(data)
if not csv_data:
raise ValueError("Aucune donnée à convertir en CSV")
# Obtenir tous les champs possibles
fieldnames = set()
for row in csv_data:
fieldnames.update(row.keys())
fieldnames = sorted(fieldnames)
with open(filepath, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(csv_data)
return str(filepath.absolute())
except ValueError:
# Re-lancer les erreurs de validation sans les enrober
raise
except Exception as e:
raise IOError(f"Erreur lors de la sauvegarde: {e}")
# AtmoDataException is now imported from .exceptions

View file

@ -0,0 +1,297 @@
'''
Constantes pour le wrapper API Atmo Data
'''
# Formats de sortie supportés par l'API
FORMATS_VALIDES = ['geojson', 'csv']
# Codes AASQA (Associations Agréées de Surveillance de la Qualité de l'Air)
# Source : https://www.atmo-france.org/article/laasqa-de-votre-region
# Structure enrichie : région, organisme, départements, site web
AASQA_CODES = {
'01': {
'region': 'Guadeloupe',
'organisme': 'Gwad\'Air',
'departements': ['971'],
'site_web': 'https://www.gwadair.gp',
'description': 'Guadeloupe | Gwad\'Air'
},
'02': {
'region': 'Martinique',
'organisme': 'Madininair',
'departements': ['972'],
'site_web': 'https://www.madininair.fr',
'description': 'Martinique | Madininair'
},
'03': {
'region': 'Guyane',
'organisme': 'Atmo Guyane',
'departements': ['973'],
'site_web': 'https://www.atmo-guyane.org',
'description': 'Guyane | Atmo Guyane'
},
'04': {
'region': 'La Réunion',
'organisme': 'Atmo Réunion',
'departements': ['974'],
'site_web': 'https://www.atmo-reunion.net',
'description': 'La Réunion | Atmo Réunion'
},
'06': {
'region': 'Mayotte',
'organisme': 'Hawa Mayotte',
'departements': ['976'],
'site_web': 'https://www.hawa-mayotte.org',
'description': 'Mayotte | Hawa Mayotte'
},
'11': {
'region': 'Île-de-France',
'organisme': 'Airparif',
'departements': ['75', '77', '78', '91', '92', '93', '94', '95'],
'site_web': 'https://www.airparif.asso.fr',
'description': 'Île-de-France | Airparif'
},
'24': {
'region': 'Centre-Val de Loire',
'organisme': 'Lig\'Air',
'departements': ['18', '28', '36', '37', '41', '45'],
'site_web': 'https://www.ligair.fr',
'description': 'Centre-Val de Loire | Lig\'Air'
},
'27': {
'region': 'Bourgogne-Franche-Comté',
'organisme': 'Atmo Bourgogne-Franche-Comté',
'departements': ['21', '25', '39', '58', '70', '71', '89', '90'],
'site_web': 'https://www.atmo-bfc.org',
'description': 'Bourgogne-Franche-Comté | Atmo Bourgogne-Franche-Comté'
},
'28': {
'region': 'Normandie',
'organisme': 'Atmo Normandie',
'departements': ['14', '27', '50', '61', '76'],
'site_web': 'https://www.atmo-normandie.fr',
'description': 'Normandie | Atmo Normandie'
},
'32': {
'region': 'Hauts-de-France',
'organisme': 'Atmo Hauts-de-France',
'departements': ['02', '59', '60', '62', '80'],
'site_web': 'https://www.atmo-hdf.fr',
'description': 'Hauts-de-France | Atmo Hauts-de-France'
},
'44': {
'region': 'Grand Est',
'organisme': 'ATMO Grand-Est',
'departements': ['08', '10', '51', '52', '54', '55', '57', '67', '68', '88'],
'site_web': 'https://www.atmo-grandest.eu',
'description': 'Grand Est | ATMO Grand-Est'
},
'52': {
'region': 'Pays de la Loire',
'organisme': 'Air Pays de la Loire',
'departements': ['44', '49', '53', '72', '85'],
'site_web': 'https://www.airpl.org',
'description': 'Pays de la Loire | Air Pays de la Loire'
},
'53': {
'region': 'Bretagne',
'organisme': 'Air Breizh',
'departements': ['22', '29', '35', '56'],
'site_web': 'https://www.airbreizh.asso.fr',
'description': 'Bretagne | Air Breizh'
},
'75': {
'region': 'Nouvelle-Aquitaine',
'organisme': 'Atmo Nouvelle-Aquitaine',
'departements': ['16', '17', '19', '23', '24', '33', '40', '47', '64', '79', '86', '87'],
'site_web': 'https://www.atmo-nouvelleaquitaine.org',
'description': 'Nouvelle-Aquitaine | Atmo Nouvelle-Aquitaine'
},
'76': {
'region': 'Occitanie',
'organisme': 'Atmo Occitanie',
'departements': ['09', '11', '12', '30', '31', '32', '34', '46', '48', '65', '66', '81', '82'],
'site_web': 'https://www.atmo-occitanie.org',
'description': 'Occitanie | Atmo Occitanie'
},
'84': {
'region': 'Auvergne-Rhône-Alpes',
'organisme': 'Atmo Auvergne-Rhône-Alpes',
'departements': ['01', '03', '07', '15', '26', '38', '42', '43', '63', '69', '73', '74'],
'site_web': 'https://www.atmo-auvergnerhonealpes.fr',
'description': 'Auvergne-Rhône-Alpes | Atmo Auvergne-Rhône-Alpes'
},
'93': {
'region': 'Provence-Alpes-Côte d\'Azur',
'organisme': 'AtmoSud',
'departements': ['04', '05', '06', '13', '83', '84'],
'site_web': 'https://www.atmosud.org',
'description': 'Provence-Alpes-Côte d\'Azur | AtmoSud'
},
'94': {
'region': 'Corse',
'organisme': 'Qualitair',
'departements': ['2A', '2B'],
'site_web': 'https://www.qualitaircorse.org',
'description': 'Corse | Qualitair'
}
}
# Secteurs d'émission
SECTEURS_EMISSIONS = {
'5': 'Agriculture',
'6': 'Transport routier',
'7': 'Autres transports',
'34': 'Résidentiel - Tertiaire',
'219': 'Industrie, branche énergie, déchets'
}
# Indices de qualité de l'air ATMO
# Source : https://www.atmo-france.org/article/lindice-atmo
# Source : Notice technique et dinformation des données open data sur la qualité de lair disponibles sur Atmo Data - Version 1er avril 2025
INDICES_ATMO = {
0 : 'Absent',
1 : 'Bon',
2 : 'Moyen',
3 : 'Dégradé',
4 : 'Mauvais',
5 : 'Très mauvais',
6 : 'Extrêmement mauvais',
7 : 'Événement'
}
# Indices polliniques
# Source : https://www.atmo-france.org/article/indice-pollen
# Source : Notice technique et dinformation des données open data sur la qualité de lair disponibles sur Atmo Data - Version 1er avril 2025
INDICES_POLLENS = {
0 : 'Indisponible',
1 : 'Très faible',
2 : 'Faible',
3 : 'Modéré',
4 : 'Élevé',
5 : 'Très élevé',
6 : 'Extrêmement élevé'
}
# Couleur des qualificatifs des indices polluants et pollens :
# Source : https://www.atmo-france.org/article/lindice-atmo
# Source : https://www.atmo-france.org/article/indice-pollen
# Source : Notice technique et dinformation des données open data sur la qualité de lair disponibles sur Atmo Data - Version 1er avril 2025
# code_[polluant|pollen] | code couleur (R,V,B) | code couleur #hexa | emojis ronds | emojis carrées
CODE_COLOR_QUALIF = {
0 : [(221,221,221), '#DDDDDD', "", ""],
1 : [(80,240,230), '#50F0E6', "🔵", "🟦"],
2 : [(80,204,170), '#50CCAA', "🟢", "🟩"],
3 : [(240,230,65), '#F0E641', "🟡", "🟨"],
4 : [(255,80,80), '#FF5050', "🔴", "🟥"],
5 : [(150,0,50), '#960032', "🟤", "🟫"],
6 : [(135,33,129), '#872181', "🟣", "🟪"],
7 : [(136,136,136), '#888888', "", ""]
}
"""
Correspondances émojis/couleurs parfaites
- Niveau 0 : Gris clair #DDDDDD
- Niveau 1 : 🔵🟦 Bleu clair #50F0E6
- Niveau 2 : 🟢🟩 Vert #50CCAA
- Niveau 3 : 🟡🟨 Jaune #F0E641
- Niveau 4 : 🔴🟥 Rouge #FF5050
- Niveau 5 : 🟤🟫 Marron #960032
- Niveau 6 : 🟣🟪 Violet #872181
- Niveau 7 : Noir #888888
"""
# Types d'épisodes de pollution
TYPES_EPISODES = [
'PAS DE DEPASSEMENT',
'INFORMATION ET RECOMMANDATION',
'ALERTE SUR PERSISTANCE',
'ALERTE'
]
# Échéances pour les épisodes de pollution (J-1, J0, J+1)
ECHEANCES_VALIDES = ['-1', '0', '1']
# Échelles d'agrégation pour les données d'émissions
ECHELLES_VALIDES = ['region', 'epci']
# URL par défaut de l'API
DEFAULT_API_URL = 'https://admindata.atmo-france.org'
# Timeout par défaut pour les requêtes (en secondes)
DEFAULT_TIMEOUT = 30
# Durée de validité du token JWT (en heures)
TOKEN_VALIDITY_HOURS = 24
# Formats de fichiers supportés pour la sauvegarde
SAVE_FORMATS = ['json', 'csv', 'geojson']
# Extensions de fichiers
FILE_EXTENSIONS = {
'json': '.json',
'csv': '.csv',
'geojson': '.geojson'
}
# Polluants réglementés pris en compte dans l'indice ATMO :
# Source : Notice technique et dinformation des données open data sur la qualité de lair disponibles sur Atmo Data - Version 1er avril 2025
POLLUANTS = ['NO2', 'SO2', 'PM10', 'PM2.5', 'O3']
CODE_POLLUANT = {
'NO2' : 'dioxyde dazote' ,
'SO2' : 'dioxyde de soufre',
'PM10' : 'particules fines inférieures à 10 micromètres',
'PM2.5' : 'particules fines inférieures à 2.5 micromètres',
'O3' : 'ozone'
}
# Mapping des codes polluants pour les épisodes de pollution
CODE_POLLUANT_EPISODES = {
'1': 'NO2',
'2': 'SO2',
'3': 'O3',
'5': 'PM10',
'6': 'PM2.5'
}
# Taxons (espèces) pris en compte dans l'indice pollen :
# Source : Notice technique et dinformation des données open data sur la qualité de lair disponibles sur Atmo Data - Version 1er avril 2025
CODE_TAXON = {
'ambr' : 'Ambroisie',
'arm' : 'Armoise',
'aul' : 'Aulne',
'boul' : 'Bouleau',
'gram' : 'Graminées',
'oliv' : 'Olivier',
}
# Mapping des variantes de noms de taxons vers les codes standards
TAXON_MAPPING = {
'aulne': 'aul',
'bouleau': 'boul',
'olivier': 'oliv',
'graminées': 'gram',
'graminee': 'gram', # Variante sans accent
'armoise': 'arm',
'artemisi': 'arm', # Variante latine de armoise
'ambroisie': 'ambr'
}
# Mentions légales et licence Atmo France / AASQA
# Source : Notice technique et dinformation des données open data sur la qualité de lair disponibles sur Atmo Data - Version 1er avril 2025
ATMO_LICENCE_COURTE = "Atmo France / AASQA"
ATMO_LICENCE_LONGUE = "Atmo France et les Associations agréées de surveillance de la qualité de l'air"
ATMO_LICENCE_COMPLETE = """Données sous licence ODbL (Open Database License)
Source: Atmo France et les Associations agréées de surveillance de la qualité de l'air
URL: https://www.atmo-france.org/
API: https://admindata.atmo-france.org/
Chacun peut bénéficier gratuitement de ces données mises en open data sous licence ODbL,
en indiquant la source "Atmo France et les Associations agréées de surveillance de la qualité de l'air"
ou "Atmo France / AASQA" dans sa version courte."""

View file

@ -0,0 +1,42 @@
"""
Exceptions personnalisées pour Atmo Data Wrapper
"""
class AtmoDataException(Exception):
"""Exception de base pour les erreurs liées à l'API Atmo Data"""
def __init__(self, message: str, status_code: int = None, response_data: dict = None):
super().__init__(message)
self.status_code = status_code
self.response_data = response_data
def __str__(self) -> str:
if self.status_code:
return f"AtmoDataException ({self.status_code}): {super().__str__()}"
return f"AtmoDataException: {super().__str__()}"
class AuthenticationError(AtmoDataException):
"""Erreur d'authentification"""
pass
class ValidationError(AtmoDataException):
"""Erreur de validation des paramètres"""
pass
class APIError(AtmoDataException):
"""Erreur de l'API Atmo Data"""
pass
class NetworkError(AtmoDataException):
"""Erreur de réseau"""
pass
class DataError(AtmoDataException):
"""Erreur dans le traitement des données"""
pass

View file

@ -0,0 +1,631 @@
"""
Classes pour les différents types de données de l'API Atmo Data
"""
from typing import List, Dict, Any, Optional, Tuple
from datetime import datetime
import statistics
from dataclasses import dataclass
from .constants import (
AASQA_CODES, INDICES_ATMO, INDICES_POLLENS, CODE_COLOR_QUALIF,
CODE_POLLUANT, CODE_TAXON, SECTEURS_EMISSIONS, CODE_POLLUANT_EPISODES, TAXON_MAPPING
)
@dataclass
class Coordinates:
"""Classe pour représenter les coordonnées géographiques"""
longitude: float
latitude: float
def __str__(self) -> str:
return f"({self.latitude:.4f}, {self.longitude:.4f})"
def distance_to(self, other: 'Coordinates') -> float:
"""Calcule la distance approximative entre deux points (en km)"""
import math
lat1, lon1 = math.radians(self.latitude), math.radians(self.longitude)
lat2, lon2 = math.radians(other.latitude), math.radians(other.longitude)
dlat = lat2 - lat1
dlon = lon2 - lon1
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
c = 2 * math.asin(math.sqrt(a))
r = 6371 # Rayon de la Terre en km
return r * c
class AtmoDataBase:
"""Classe de base pour tous les types de données Atmo"""
def __init__(self, feature: Dict[str, Any]):
self.raw_data = feature
self.properties = feature.get('properties', {})
self.geometry = feature.get('geometry', {})
# Propriétés communes
self.aasqa = self.properties.get('aasqa', '')
self.source = self.properties.get('source', '')
self.date_maj = self.properties.get('date_maj', '')
self.lib_zone = self.properties.get('lib_zone', '')
self.coordinates = self._extract_coordinates()
def _extract_coordinates(self) -> Optional[Coordinates]:
"""Extrait les coordonnées du feature"""
if self.geometry and self.geometry.get('type') == 'Point':
coords = self.geometry.get('coordinates', [])
if len(coords) >= 2:
return Coordinates(coords[0], coords[1])
elif 'x_wgs84' in self.properties and 'y_wgs84' in self.properties:
return Coordinates(
self.properties['x_wgs84'],
self.properties['y_wgs84']
)
return None
def get_aasqa_name(self) -> str:
"""Retourne le nom de l'AASQA"""
aasqa_info = AASQA_CODES.get(self.aasqa)
if aasqa_info:
return aasqa_info['description']
return f"AASQA {self.aasqa}"
def get_aasqa_website(self) -> str:
"""Retourne le site web de l'AASQA"""
aasqa_info = AASQA_CODES.get(self.aasqa)
if aasqa_info:
return aasqa_info['site_web']
return None
def get_aasqa_region(self) -> str:
"""Retourne la région de l'AASQA"""
aasqa_info = AASQA_CODES.get(self.aasqa)
if aasqa_info:
return aasqa_info['region']
return None
def get_aasqa_organisme(self) -> str:
"""Retourne l'organisme de l'AASQA"""
aasqa_info = AASQA_CODES.get(self.aasqa)
if aasqa_info:
return aasqa_info['organisme']
return None
def get_source(self) -> str:
"""Retourne le nom publique de l'AASQA, texte"""
return self.source
def has_coordinates(self) -> bool:
"""Vérifie si l'objet a des coordonnées"""
return self.coordinates is not None
def get_emoji_by_level(self, level: int, style: str = "round") -> str:
"""Récupère l'émoji correspondant au niveau de qualificatif
Args:
level: Niveau de qualificatif (0-7)
style: Style d'émoji - "round" pour ronds (🟢), "square" pour carrés (🟩)
Returns:
str: Émoji correspondant au niveau et style demandé
"""
color_info = CODE_COLOR_QUALIF.get(level, CODE_COLOR_QUALIF.get(0, [None, None, "", ""]))
if style == "square" and len(color_info) > 3:
# Utiliser l'émoji carré (4ème élément)
return color_info[3]
elif len(color_info) > 2:
# Utiliser l'émoji rond (3ème élément)
return color_info[2]
else:
# Fallback
return ""
def get_color_by_level(self, level: int) -> Tuple[str, List[int]]:
"""Récupère la couleur (hex, rgb) correspondant au niveau de qualificatif"""
color_info = CODE_COLOR_QUALIF.get(level)
if color_info and len(color_info) >= 2:
return color_info[1], color_info[0] # hex, rgb
return "#DDDDDD", [221, 221, 221] # Couleur par défaut
class IndiceAtmo(AtmoDataBase):
"""Classe pour les indices de qualité de l'air ATMO"""
def __init__(self, feature: Dict[str, Any]):
super().__init__(feature)
# Propriétés spécifiques aux indices ATMO
self.code_qual = self.properties.get('code_qual', 0)
self.lib_qual = self.properties.get('lib_qual', '')
self.coul_qual = self.properties.get('coul_qual', '')
self.code_zone = self.properties.get('code_zone', '')
self.lib_zone = self.properties.get('lib_zone', '')
self.date_dif = self.properties.get('date_dif', '')
self.date_ech = self.properties.get('date_ech', '')
# Champs supplémentaires selon la notice officielle
self.type_zone = self.properties.get('type_zone', '') # 'commune' ou 'EPCI'
self.x_reg = self.properties.get('x_reg', 0.0) # Coordonnées réglementaires
self.y_reg = self.properties.get('y_reg', 0.0)
self.epsg_reg = self.properties.get('epsg_reg', '') # Système de projection réglementaire
# Concentrations facultatives (selon notice)
self.conc_no2 = self.properties.get('conc_no2', 0) # Concentration en μg/m³
self.conc_so2 = self.properties.get('conc_so2', 0)
self.conc_o3 = self.properties.get('conc_o3', 0)
self.conc_pm10 = self.properties.get('conc_pm10', 0)
self.conc_pm25 = self.properties.get('conc_pm25', 0)
# Codes par polluant
self.code_no2 = self.properties.get('code_no2', 0)
self.code_so2 = self.properties.get('code_so2', 0)
self.code_o3 = self.properties.get('code_o3', 0)
self.code_pm10 = self.properties.get('code_pm10', 0)
self.code_pm25 = self.properties.get('code_pm25', 0)
def get_qualificatif(self) -> str:
"""Retourne le qualificatif textuel de l'indice"""
return INDICES_ATMO.get(self.code_qual, "Inconnu")
def get_color(self) -> Tuple[str, List[int]]:
"""Retourne la couleur (hex, rgb) associée à l'indice"""
return self.get_color_by_level(self.code_qual)
def get_emoji(self, style: str = "round") -> str:
"""Retourne l'émoji correspondant à l'indice ATMO
Args:
style: Style d'émoji - "round" pour ronds (🟢), "square" pour carrés (🟩)
"""
return self.get_emoji_by_level(self.code_qual, style)
def is_good_quality(self) -> bool:
"""Vérifie si la qualité de l'air est bonne (indice 1-2)"""
return self.code_qual in [1, 2]
def is_poor_quality(self) -> bool:
"""Vérifie si la qualité de l'air est mauvaise (indice 4+)"""
return self.code_qual >= 4
def get_worst_pollutant(self) -> Tuple[str, int]:
"""Retourne le polluant avec le plus mauvais indice"""
pollutants = {
'NO2': self.code_no2,
'SO2': self.code_so2,
'O3': self.code_o3,
'PM10': self.code_pm10,
'PM2.5': self.code_pm25
}
worst = max(pollutants.items(), key=lambda x: x[1])
return worst[0], worst[1]
def get_pollutants_summary(self) -> Dict[str, Dict[str, Any]]:
"""Retourne un résumé de tous les polluants"""
pollutants = {
'NO2': self.code_no2,
'SO2': self.code_so2,
'O3': self.code_o3,
'PM10': self.code_pm10,
'PM2.5': self.code_pm25
}
summary = {}
for polluant, code in pollutants.items():
summary[polluant] = {
'code': code,
'qualificatif': INDICES_ATMO.get(code, "Inconnu"),
'description': CODE_POLLUANT.get(polluant, polluant)
}
return summary
def get_concentrations(self) -> Dict[str, int]:
"""Retourne les concentrations de tous les polluants en μg/m³ (selon notice officielle)"""
return {
'NO2': self.conc_no2,
'SO2': self.conc_so2,
'O3': self.conc_o3,
'PM10': self.conc_pm10,
'PM2.5': self.conc_pm25
}
def is_commune_level(self) -> bool:
"""Vérifie si l'indice est calculé au niveau commune (selon notice officielle)"""
return self.type_zone.lower() == 'commune'
def is_epci_level(self) -> bool:
"""Vérifie si l'indice est calculé au niveau EPCI (selon notice officielle)"""
return self.type_zone.lower() == 'epci'
def get_responsible_pollutants(self) -> List[str]:
"""Retourne le(s) polluant(s) responsable(s) de l'indice global (selon règle n°4 de la notice)"""
pollutants = {
'NO2': self.code_no2,
'SO2': self.code_so2,
'O3': self.code_o3,
'PM10': self.code_pm10,
'PM2.5': self.code_pm25
}
# L'indice global correspond au qualificatif le plus dégradé
max_code = max(pollutants.values())
responsible = [polluant for polluant, code in pollutants.items() if code == max_code]
return responsible
def __str__(self) -> str:
return f"Indice ATMO {self.lib_zone}: {self.get_qualificatif()} ({self.code_qual})"
class EpisodePollution(AtmoDataBase):
"""Classe pour les épisodes de pollution"""
def __init__(self, feature: Dict[str, Any]):
super().__init__(feature)
# Propriétés spécifiques aux épisodes
self.code_pol = self.properties.get('code_pol', '')
self.lib_pol = self.properties.get('lib_pol', '')
self.code_zone = self.properties.get('code_zone', '')
self.lib_zone = self.properties.get('lib_zone', '')
self.date_dif = self.properties.get('date_dif', '')
self.date_ech = self.properties.get('date_ech', '')
self.etat = self.properties.get('etat', '')
def get_polluant_code(self) -> str:
"""Retourne le code du polluant principal"""
return CODE_POLLUANT_EPISODES.get(self.code_pol, self.code_pol)
def is_alert_active(self) -> bool:
"""Vérifie si une alerte est active"""
return self.etat not in ['PAS DE DEPASSEMENT', '']
def get_alert_level(self) -> str:
"""Retourne le niveau d'alerte"""
if 'INFORMATION' in self.etat:
return 'Information'
elif 'ALERTE' in self.etat:
return 'Alerte'
else:
return 'Aucune'
def is_geometry_complex(self) -> bool:
"""Vérifie si la géométrie est complexe (MultiPolygon)"""
return self.geometry.get('type') == 'MultiPolygon'
def __str__(self) -> str:
return f"Épisode {self.lib_pol} - {self.lib_zone}: {self.etat}"
class EmissionData(AtmoDataBase):
"""Classe pour les données d'émissions"""
def __init__(self, feature: Dict[str, Any]):
super().__init__(feature)
# Propriétés spécifiques aux émissions
self.code = self.properties.get('code', '')
self.name = self.properties.get('name', '')
self.population = self.properties.get('population', 0)
self.superficie = self.properties.get('superficie', 0)
# Émissions par polluant
self.nox = self.properties.get('nox', 0)
self.pm10 = self.properties.get('pm10', 0)
self.pm25 = self.properties.get('pm25', 0)
self.ges = self.properties.get('ges', 0) # Gaz à effet de serre
# Secteur
self.code_pcaet = self.properties.get('code_pcaet', '')
def get_emission_density(self, pollutant: str) -> float:
"""Calcule la densité d'émission par km² pour un polluant"""
emission_value = getattr(self, pollutant.lower(), 0)
if self.superficie > 0:
return emission_value / self.superficie
return 0
def get_emission_per_capita(self, pollutant: str) -> float:
"""Calcule l'émission par habitant pour un polluant"""
emission_value = getattr(self, pollutant.lower(), 0)
if self.population > 0:
return emission_value / self.population
return 0
def get_total_emissions(self) -> Dict[str, float]:
"""Retourne toutes les émissions"""
return {
'NOx': self.nox,
'PM10': self.pm10,
'PM2.5': self.pm25,
'GES': self.ges
}
def get_secteur_name(self) -> str:
"""Retourne le nom du secteur d'émission"""
return SECTEURS_EMISSIONS.get(self.code_pcaet, f"Secteur {self.code_pcaet}")
def __str__(self) -> str:
return f"Émissions {self.name}: NOx={self.nox:.1f}, PM10={self.pm10:.1f}"
class IndicePollen(AtmoDataBase):
"""Classe pour les indices pollen"""
def __init__(self, feature: Dict[str, Any]):
super().__init__(feature)
# Propriétés spécifiques aux pollens
# Notice : Déclenchement ou non de lalerte pollen (à partir de code_qual = 4) -> Boolean
self.alerte = self.properties.get('alerte', False)
# Codes par taxon (espèce)
# Notice : Classe du sous-indice, entier de 1 à 6, 0 si absent -> Int
self.code_ambr = self.properties.get('code_ambr', 0) # Ambroisie
self.code_arm = self.properties.get('code_arm', 0) # Armoise
self.code_aul = self.properties.get('code_aul', 0) # Aulne
self.code_boul = self.properties.get('code_boul', 0) # Bouleau
self.code_gram = self.properties.get('code_gram', 0) # Graminées
self.code_oliv = self.properties.get('code_oliv', 0) # Olivier
# Concentrations par taxon (grains/m³)
# Notice : Concentration de pollens du taxons (en grains/m³), à la commune -> Float
self.conc_ambr = self.properties.get('conc_ambr', 0.0) # Ambroisie
self.conc_arm = self.properties.get('conc_arm', 0.0) # Armoise
self.conc_aul = self.properties.get('conc_aul', 0.0) # Aulne
self.conc_boul = self.properties.get('conc_boul', 0.0) # Bouleau
self.conc_gram = self.properties.get('conc_gram', 0.0) # Graminées
self.conc_oliv = self.properties.get('conc_oliv', 0.0) # Olivier
# Taxon(s) responsable(s) de l'indice
# Notice : Taxon(s) responsable(s) de l'indice (aulne, bouleau, olivier, graminées, armoise, ambroisie) -> String
self.pollen_resp = self.properties.get('pollen_resp', '')
def is_alert_active(self) -> bool:
"""Vérifie si une alerte pollen est active"""
return self.alerte
def get_highest_pollen(self) -> Tuple[str, int]:
"""Retourne le pollen avec l'indice le plus élevé"""
pollens = {
'ambr': self.code_ambr,
'arm': self.code_arm,
'aul': self.code_aul,
'boul': self.code_boul,
'gram': self.code_gram,
'oliv': self.code_oliv
}
highest = max(pollens.items(), key=lambda x: x[1])
return highest[0], highest[1]
def get_pollens_summary(self, emoji_style: str = "round") -> Dict[str, Dict[str, Any]]:
"""Retourne un résumé de tous les pollens
Args:
emoji_style: Style d'émoji - "round" pour ronds (🟢), "square" pour carrés (🟩)
"""
pollens = {
'ambr': {'code': self.code_ambr, 'conc': self.conc_ambr},
'arm': {'code': self.code_arm, 'conc': self.conc_arm},
'aul': {'code': self.code_aul, 'conc': self.conc_aul},
'boul': {'code': self.code_boul, 'conc': self.conc_boul},
'gram': {'code': self.code_gram, 'conc': self.conc_gram},
'oliv': {'code': self.code_oliv, 'conc': self.conc_oliv}
}
summary = {}
for code_taxon, data in pollens.items():
indice = data['code']
concentration = data['conc']
summary[code_taxon] = {
'code': indice,
'concentration': concentration,
'qualificatif': INDICES_POLLENS.get(int(indice), "Inconnu"),
'espece': CODE_TAXON.get(code_taxon, code_taxon),
'couleur': self.get_color_by_level(int(indice)),
'emoji': self.get_emoji_by_level(int(indice), emoji_style),
'emoji_round': self.get_emoji_by_level(int(indice), "round"),
'emoji_square': self.get_emoji_by_level(int(indice), "square")
}
return summary
def get_concentrations(self) -> Dict[str, float]:
"""Retourne les concentrations de tous les pollens en grains/m³"""
return {
'ambr': self.conc_ambr,
'arm': self.conc_arm,
'aul': self.conc_aul,
'boul': self.conc_boul,
'gram': self.conc_gram,
'oliv': self.conc_oliv
}
def get_highest_concentration(self) -> Tuple[str, float]:
"""Retourne le pollen avec la plus haute concentration"""
concentrations = self.get_concentrations()
highest = max(concentrations.items(), key=lambda x: x[1])
return highest[0], highest[1]
def get_dangerous_pollens(self) -> List[str]:
"""Retourne la liste des pollens à indice élevé (4+)"""
pollens = {
'ambr': self.code_ambr,
'arm': self.code_arm,
'aul': self.code_aul,
'boul': self.code_boul,
'gram': self.code_gram,
'oliv': self.code_oliv
}
dangerous = []
for code_taxon, indice in pollens.items():
if indice >= 4:
dangerous.append(CODE_TAXON.get(code_taxon, code_taxon))
return dangerous
def get_responsible_pollens(self) -> List[str]:
"""Retourne le ou les taxons responsables de l'indice selon l'API"""
if not self.pollen_resp:
return []
# Le champ peut contenir plusieurs taxons séparés par des virgules ou des espaces
# Exemples: "graminées,armoise", "GRAMINEE", "ARTEMISI GRAMINEE"
pollen_resp_clean = self.pollen_resp.lower().strip()
# Séparer par virgules ou espaces
taxons_raw = []
if ',' in pollen_resp_clean:
taxons_raw = [t.strip() for t in pollen_resp_clean.split(',')]
else:
# Essayer de détecter plusieurs mots (séparés par des espaces)
words = pollen_resp_clean.split()
if len(words) > 1:
taxons_raw = words
else:
taxons_raw = [pollen_resp_clean]
taxons_formatted = []
for taxon_raw in taxons_raw:
if not taxon_raw: # Skip empty strings
continue
# Chercher d'abord dans le mapping
taxon_code = TAXON_MAPPING.get(taxon_raw)
if taxon_code:
# Utiliser le nom complet du CODE_TAXON
taxon_name = CODE_TAXON.get(taxon_code, taxon_raw.title())
if taxon_name not in taxons_formatted: # Éviter les doublons
taxons_formatted.append(taxon_name)
else:
# Si pas trouvé, formater simplement la première lettre en majuscule
formatted_name = taxon_raw.title()
if formatted_name not in taxons_formatted: # Éviter les doublons
taxons_formatted.append(formatted_name)
return taxons_formatted
def __str__(self) -> str:
highest_pollen, highest_code = self.get_highest_pollen()
espece = CODE_TAXON.get(highest_pollen, highest_pollen)
qualif = INDICES_POLLENS.get(highest_code, "Inconnu")
return f"Pollen - Plus élevé: {espece} ({qualif})"
class AtmoDataCollection:
"""Classe pour gérer une collection de données Atmo"""
def __init__(self, data: Dict[str, Any], data_type: str):
self.raw_data = data
self.data_type = data_type
self.features = data.get('features', [])
# Créer les objets typés
self.items = self._create_typed_objects()
def _create_typed_objects(self) -> List[AtmoDataBase]:
"""Crée les objets typés selon le type de données"""
objects = []
for feature in self.features:
if self.data_type == 'indices':
objects.append(IndiceAtmo(feature))
elif self.data_type == 'episodes':
objects.append(EpisodePollution(feature))
elif self.data_type == 'emissions':
objects.append(EmissionData(feature))
elif self.data_type == 'pollens':
objects.append(IndicePollen(feature))
else:
objects.append(AtmoDataBase(feature))
return objects
def __len__(self) -> int:
return len(self.items)
def __iter__(self):
return iter(self.items)
def __getitem__(self, index):
return self.items[index]
def filter_by_aasqa(self, aasqa_code: str) -> 'AtmoDataCollection':
"""Filtre par code AASQA"""
filtered_features = [
item.raw_data for item in self.items
if item.aasqa == aasqa_code
]
filtered_data = {
'type': self.raw_data.get('type'),
'features': filtered_features
}
return AtmoDataCollection(filtered_data, self.data_type)
def filter_by_coordinates(self, center: Coordinates, radius_km: float) -> 'AtmoDataCollection':
"""Filtre par proximité géographique"""
filtered_items = []
for item in self.items:
if item.has_coordinates():
distance = item.coordinates.distance_to(center)
if distance <= radius_km:
filtered_items.append(item.raw_data)
filtered_data = {
'type': self.raw_data.get('type'),
'features': filtered_items
}
return AtmoDataCollection(filtered_data, self.data_type)
def get_statistics(self) -> Dict[str, Any]:
"""Retourne des statistiques sur la collection"""
stats = {
'total_count': len(self.items),
'aasqa_count': len(set(item.aasqa for item in self.items)),
'data_type': self.data_type
}
# Statistiques spécifiques par type
if self.data_type == 'indices' and self.items:
indices_codes = [item.code_qual for item in self.items if hasattr(item, 'code_qual')]
if indices_codes:
stats['quality_stats'] = {
'moyenne': statistics.mean(indices_codes),
'min': min(indices_codes),
'max': max(indices_codes),
'bon_pourcentage': (sum(1 for x in indices_codes if x <= 2) / len(indices_codes)) * 100
}
elif self.data_type == 'episodes' and self.items:
alerts_count = sum(1 for item in self.items if hasattr(item, 'is_alert_active') and item.is_alert_active())
stats['alerts_active'] = alerts_count
stats['alerts_percentage'] = (alerts_count / len(self.items)) * 100
elif self.data_type == 'pollens' and self.items:
alerts_count = sum(1 for item in self.items if hasattr(item, 'alerte') and item.alerte)
stats['pollen_alerts'] = alerts_count
return stats
def to_summary(self) -> str:
"""Retourne un résumé textuel de la collection"""
stats = self.get_statistics()
summary = f"Collection {self.data_type.title()}: {stats['total_count']} éléments"
if self.data_type == 'indices' and 'quality_stats' in stats:
qs = stats['quality_stats']
summary += f" - Qualité moyenne: {qs['moyenne']:.1f}, Bonne qualité: {qs['bon_pourcentage']:.1f}%"
elif self.data_type == 'episodes' and 'alerts_active' in stats:
summary += f" - Alertes actives: {stats['alerts_active']} ({stats['alerts_percentage']:.1f}%)"
elif self.data_type == 'pollens' and 'pollen_alerts' in stats:
summary += f" - Alertes pollen: {stats['pollen_alerts']}"
return summary

View file

@ -0,0 +1,218 @@
"""
Fonctions utilitaires pour le wrapper Atmo Data
"""
from typing import Optional, List, Dict, Any
from .constants import AASQA_CODES, ATMO_LICENCE_COURTE, ATMO_LICENCE_LONGUE, ATMO_LICENCE_COMPLETE
def get_aasqa_by_department(department: str) -> Optional[str]:
"""
Trouve le code AASQA correspondant à un département
Args:
department: Code département (ex: '54', '75', '2A')
Returns:
str: Code AASQA ou None si non trouvé
"""
for aasqa_code, aasqa_data in AASQA_CODES.items():
if department in aasqa_data['departements']:
return aasqa_code
return None
def get_aasqa_info(aasqa_code: str) -> Optional[Dict[str, Any]]:
"""
Récupère les informations complètes d'une AASQA
Args:
aasqa_code: Code AASQA (ex: '44')
Returns:
dict: Informations complètes ou None si non trouvé
"""
return AASQA_CODES.get(aasqa_code)
def get_aasqa_website(aasqa_code: str) -> Optional[str]:
"""
Récupère le site web d'une AASQA
Args:
aasqa_code: Code AASQA (ex: '44')
Returns:
str: URL du site web ou None si non trouvé
"""
aasqa_info = AASQA_CODES.get(aasqa_code)
return aasqa_info['site_web'] if aasqa_info else None
def list_departments_by_aasqa(aasqa_code: str) -> List[str]:
"""
Liste les départements couverts par une AASQA
Args:
aasqa_code: Code AASQA (ex: '44')
Returns:
list: Liste des codes département
"""
aasqa_info = AASQA_CODES.get(aasqa_code)
return aasqa_info['departements'] if aasqa_info else []
def search_aasqa_by_name(search_term: str) -> List[Dict[str, Any]]:
"""
Recherche des AASQA par nom d'organisme ou de région
Args:
search_term: Terme de recherche (case insensitive)
Returns:
list: Liste des AASQA correspondantes avec leurs informations
"""
results = []
search_lower = search_term.lower()
for aasqa_code, aasqa_data in AASQA_CODES.items():
if (search_lower in aasqa_data['organisme'].lower() or
search_lower in aasqa_data['region'].lower()):
results.append({
'code': aasqa_code,
**aasqa_data
})
return results
def get_departments_count() -> Dict[str, int]:
"""
Retourne le nombre de départements par AASQA
Returns:
dict: Code AASQA -> nombre de départements
"""
return {
aasqa_code: len(aasqa_data['departements'])
for aasqa_code, aasqa_data in AASQA_CODES.items()
}
def validate_department_coverage() -> Dict[str, Any]:
"""
Valide la couverture départementale et détecte les anomalies
Returns:
dict: Rapport de validation avec statistiques et anomalies
"""
all_departments = []
for aasqa_data in AASQA_CODES.values():
all_departments.extend(aasqa_data['departements'])
unique_departments = set(all_departments)
duplicates = []
seen = set()
for dept in all_departments:
if dept in seen:
duplicates.append(dept)
seen.add(dept)
return {
'total_departments': len(all_departments),
'unique_departments': len(unique_departments),
'duplicates': list(set(duplicates)),
'has_duplicates': len(duplicates) > 0,
'coverage_complete': len(unique_departments) == len(all_departments)
}
def get_aasqa_statistics() -> Dict[str, Any]:
"""
Génère des statistiques sur les AASQA
Returns:
dict: Statistiques détaillées
"""
dept_counts = get_departments_count()
validation = validate_department_coverage()
max_coverage = max(dept_counts.values())
min_coverage = min(dept_counts.values())
max_aasqa = [code for code, count in dept_counts.items() if count == max_coverage]
min_aasqa = [code for code, count in dept_counts.items() if count == min_coverage]
return {
'total_aasqa': len(AASQA_CODES),
'total_departments_covered': validation['total_departments'],
'unique_departments': validation['unique_departments'],
'max_coverage': {
'count': max_coverage,
'aasqa_codes': max_aasqa,
'aasqa_names': [AASQA_CODES[code]['organisme'] for code in max_aasqa]
},
'min_coverage': {
'count': min_coverage,
'aasqa_codes': min_aasqa,
'aasqa_names': [AASQA_CODES[code]['organisme'] for code in min_aasqa]
},
'average_coverage': sum(dept_counts.values()) / len(dept_counts),
'has_anomalies': validation['has_duplicates']
}
def get_atmo_licence(format: str = "courte") -> str:
"""
Retourne la mention légale Atmo France selon le format demandé
Args:
format: Format de la licence ("courte", "longue", "complete")
Returns:
str: Mention légale correspondante
Examples:
>>> print(get_atmo_licence("courte"))
Atmo France / AASQA
>>> print(get_atmo_licence("longue"))
Atmo France et les Associations agréées de surveillance de la qualité de l'air
>>> print(get_atmo_licence("complete"))
Données sous licence ODbL (Open Database License)
Source: Atmo France et les Associations agréées de surveillance de la qualité de l'air
...
"""
format_lower = format.lower()
if format_lower == "courte":
return ATMO_LICENCE_COURTE
elif format_lower == "longue":
return ATMO_LICENCE_LONGUE
elif format_lower == "complete":
return ATMO_LICENCE_COMPLETE
else:
# Format par défaut si non reconnu
return ATMO_LICENCE_COURTE
def print_atmo_licence(format: str = "courte") -> None:
"""
Affiche la mention légale Atmo France selon le format demandé
Args:
format: Format de la licence ("courte", "longue", "complete")
Examples:
>>> print_atmo_licence("courte")
Atmo France / AASQA
>>> print_atmo_licence("complete")
Données sous licence ODbL (Open Database License)
Source: Atmo France et les Associations agréées de surveillance de la qualité de l'air
...
"""
print(get_atmo_licence(format))

5
credentials.json.example Normal file
View file

@ -0,0 +1,5 @@
{
"username": "votre_nom_utilisateur",
"password": "votre_mot_de_passe",
"api_url": "https://admindata.atmo-france.org"
}

3
demos/__init__.py Normal file
View file

@ -0,0 +1,3 @@
"""
Demos package for Atmo Data Wrapper
"""

View file

@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""
Script de démonstration de toutes les fonctions du datamodel IndiceAtmo
Utilise les données réelles de l'API pour une ville française (aujourd'hui)
Idéal pour documentation et exemples
"""
from atmo_data_wrapper import AtmoDataClient, AtmoDataException
from atmo_data_wrapper import CODE_POLLUANT, INDICES_ATMO
from datetime import datetime
import sys
def demo_atmo_functions():
"""Démonstration complète des fonctions IndiceAtmo avec données réelles"""
# Configuration
AASQA_ILE_DE_FRANCE = "11" # Île-de-France pour avoir des données
today = datetime.now().strftime('%Y-%m-%d')
print("🌬️ DÉMONSTRATION DES FONCTIONS INDICEATMO")
print("=" * 55)
print(f"📍 Région: Île-de-France (AASQA: {AASQA_ILE_DE_FRANCE})")
print(f"🗓️ Date: {today}")
print(f"🏛️ Basé sur la notice officielle du 1er avril 2025")
print()
try:
# Connexion et récupération des données
print("🔐 Connexion à l'API...")
client = AtmoDataClient()
success = client.auto_login()
if not success:
print("❌ Échec de la connexion à l'API")
return False
print("✅ Connecté avec succès !")
# Récupération des données ATMO
print(f"🌬️ Récupération des indices ATMO pour {today}...")
indices = client.get_indices_atmo(
format="geojson",
date=today,
aasqa=AASQA_ILE_DE_FRANCE
)
if not indices or len(indices) == 0:
print("❌ Aucune donnée ATMO trouvée")
return False
print(f"{len(indices)} station(s) trouvée(s)")
# Utiliser la première station pour la démo
atmo = indices[0]
print(f"📍 Station sélectionnée: {atmo.lib_zone}")
print()
# === DÉMONSTRATION DES PROPRIÉTÉS DE BASE ===
print("📊 === PROPRIÉTÉS DE BASE (héritées d'AtmoDataBase) ===")
print()
print("🏛️ INFORMATIONS GÉNÉRALES:")
print(f" • AASQA: {atmo.aasqa}")
print(f" • Zone: {atmo.lib_zone}")
print(f" • Source: {atmo.source}")
print(f" • Type de zone: {atmo.type_zone}")
print(f" • Code zone: {atmo.code_zone}")
print()
print("📅 DONNÉES TEMPORELLES:")
print(f" • Date échéance: {atmo.date_ech}")
print(f" • Date diffusion: {atmo.date_dif}")
print()
print("🗺️ COORDONNÉES:")
if atmo.has_coordinates():
print(f" • Coordonnées disponibles: Oui")
if hasattr(atmo.coordinates, 'latitude'):
print(f" • Latitude: {atmo.coordinates.latitude:.6f}")
print(f" • Longitude: {atmo.coordinates.longitude:.6f}")
else:
print(" • Pas de coordonnées WGS84 disponibles")
print(f" • Coordonnées réglementaires: ({atmo.x_reg:.1f}, {atmo.y_reg:.1f})")
print(f" • Système de projection: EPSG:{atmo.epsg_reg}")
print()
print("🎨 FONCTIONS CENTRALISÉES:")
for level in [0, 1, 2, 3, 4, 5, 6, 7]:
emoji_round = atmo.get_emoji_by_level(level, "round")
emoji_square = atmo.get_emoji_by_level(level, "square")
color_hex, color_rgb = atmo.get_color_by_level(level)
qualif = INDICES_ATMO.get(level, "Inconnu")
print(f" • Niveau {level} ({qualif}): {emoji_round}{emoji_square} {color_hex} {color_rgb}")
print()
# === DÉMONSTRATION DES PROPRIÉTÉS SPÉCIFIQUES ATMO ===
print("🌬️ === PROPRIÉTÉS SPÉCIFIQUES ATMO ===")
print()
print("📈 INDICE GLOBAL:")
print(f" • Code qualificatif: {atmo.code_qual}")
print(f" • Libellé qualificatif: {atmo.lib_qual}")
print(f" • Couleur qualificatif: {atmo.coul_qual}")
print()
print("📊 CODES PAR POLLUANT:")
print(f" • Dioxyde d'azote (NO2): {atmo.code_no2}")
print(f" • Dioxyde de soufre (SO2): {atmo.code_so2}")
print(f" • Ozone (O3): {atmo.code_o3}")
print(f" • Particules PM10: {atmo.code_pm10}")
print(f" • Particules PM2.5: {atmo.code_pm25}")
print()
print("🔬 CONCENTRATIONS (μg/m³) - FACULTATIVES:")
print(f" • NO2: {atmo.conc_no2}")
print(f" • SO2: {atmo.conc_so2}")
print(f" • O3: {atmo.conc_o3}")
print(f" • PM10: {atmo.conc_pm10}")
print(f" • PM2.5: {atmo.conc_pm25}")
print()
# === DÉMONSTRATION DES MÉTHODES HELPER ===
print("🛠️ === MÉTHODES HELPER ===")
print()
# 1. get_qualificatif()
print("📋 1. QUALIFICATIF TEXTUEL:")
qualificatif = atmo.get_qualificatif()
print(f" • get_qualificatif(): '{qualificatif}'")
print(f" → Qualificatif de l'indice global")
print()
# 2. get_color()
print("🎨 2. COULEUR ASSOCIÉE:")
color_hex, color_rgb = atmo.get_color()
print(f" • get_color(): ('{color_hex}', {color_rgb})")
print(f" → Couleur hexadécimale et RGB de l'indice")
print()
# 3. get_emoji()
print("😀 3. ÉMOJIS:")
emoji_round = atmo.get_emoji("round")
emoji_square = atmo.get_emoji("square")
print(f" • get_emoji('round'): '{emoji_round}'")
print(f" • get_emoji('square'): '{emoji_square}'")
print(f" → Émojis rond et carré selon le niveau")
print()
# 4. Tests de qualité
print("✅ 4. TESTS DE QUALITÉ:")
is_good = atmo.is_good_quality()
is_poor = atmo.is_poor_quality()
print(f" • is_good_quality(): {is_good}")
print(f" → Qualité bonne (niveaux 1-2): {'Oui' if is_good else 'Non'}")
print(f" • is_poor_quality(): {is_poor}")
print(f" → Qualité dégradée (niveaux 4+): {'Oui' if is_poor else 'Non'}")
print()
# 5. get_worst_pollutant()
print("🔍 5. POLLUANT LE PLUS PROBLÉMATIQUE:")
worst_pollutant, worst_code = atmo.get_worst_pollutant()
worst_description = CODE_POLLUANT.get(worst_pollutant, worst_pollutant)
print(f" • get_worst_pollutant(): ('{worst_pollutant}', {worst_code})")
print(f" → Polluant: {worst_description}")
print(f" → Niveau: {worst_code} ({INDICES_ATMO.get(worst_code, 'Inconnu')})")
print()
# 6. get_pollutants_summary()
print("📋 6. RÉSUMÉ COMPLET DES POLLUANTS:")
summary = atmo.get_pollutants_summary()
print(f" • get_pollutants_summary():")
print(" → Structure complète par polluant:")
for polluant, info in summary.items():
if info['code'] > 0: # Afficher seulement les polluants avec des données
print(f" - {polluant} ({info['description']}):")
print(f" * Code: {info['code']}")
print(f" * Qualificatif: {info['qualificatif']}")
print()
# 7. get_concentrations() - NOUVELLE MÉTHODE
print("🔬 7. CONCENTRATIONS SELON NOTICE OFFICIELLE:")
concentrations = atmo.get_concentrations()
print(f" • get_concentrations(): {concentrations}")
print(" → Concentrations en μg/m³:")
for polluant, conc in concentrations.items():
description = CODE_POLLUANT.get(polluant, polluant)
print(f" - {description}: {conc} μg/m³")
print()
# 8. Nouvelles méthodes conformité notice
print("📋 8. MÉTHODES CONFORMITÉ NOTICE OFFICIELLE:")
is_commune = atmo.is_commune_level()
is_epci = atmo.is_epci_level()
responsible = atmo.get_responsible_pollutants()
print(f" • is_commune_level(): {is_commune}")
print(f" → Calculé au niveau commune: {'Oui' if is_commune else 'Non'}")
print(f" • is_epci_level(): {is_epci}")
print(f" → Calculé au niveau EPCI: {'Oui' if is_epci else 'Non'}")
print(f" • get_responsible_pollutants(): {responsible}")
if responsible:
resp_descriptions = [CODE_POLLUANT.get(p, p) for p in responsible]
print(f" → Polluants responsables: {', '.join(resp_descriptions)}")
print()
# === DÉMONSTRATION DES MÉTHODES STRING ===
print("📝 === REPRÉSENTATION STRING ===")
print()
print("🔤 MÉTHODE __str__():")
print(f" • str(atmo): '{str(atmo)}'")
print()
# === EXEMPLES D'UTILISATION PRATIQUE ===
print("💡 === EXEMPLES D'UTILISATION PRATIQUE ===")
print()
print("🎯 ANALYSE RAPIDE:")
print(f" • Qualité globale: {qualificatif} (niveau {atmo.code_qual})")
print(f" • Polluant problématique: {worst_description} (niveau {worst_code})")
print(f" • Couleur d'affichage: {color_hex} {emoji_round}")
if is_poor:
print(" • ⚠️ QUALITÉ DÉGRADÉE - Précautions recommandées")
elif is_good:
print(" • ✅ BONNE QUALITÉ DE L'AIR")
else:
print(" • 🔶 QUALITÉ MOYENNE")
print()
print("📈 ANALYSE PAR POLLUANT:")
# Analyse des polluants selon leurs niveaux
pollutants_analysis = {
'NO2': (atmo.code_no2, "Dioxyde d'azote"),
'SO2': (atmo.code_so2, "Dioxyde de soufre"),
'O3': (atmo.code_o3, "Ozone"),
'PM10': (atmo.code_pm10, "Particules PM10"),
'PM2.5': (atmo.code_pm25, "Particules PM2.5")
}
for code, (level, description) in pollutants_analysis.items():
if level > 0:
emoji = atmo.get_emoji_by_level(level)
qualif = INDICES_ATMO.get(level, "Inconnu")
print(f"{description}: {emoji} {qualif} (niveau {level})")
print()
print("🔍 FILTRAGE ET CLASSIFICATION:")
# Classification selon les seuils
critical_pollutants = [p for p, (l, _) in pollutants_analysis.items() if l >= 4]
moderate_pollutants = [p for p, (l, _) in pollutants_analysis.items() if 2 <= l <= 3]
good_pollutants = [p for p, (l, _) in pollutants_analysis.items() if l == 1]
if critical_pollutants:
print(f" • Polluants critiques (≥4): {', '.join(critical_pollutants)}")
if moderate_pollutants:
print(f" • Polluants modérés (2-3): {', '.join(moderate_pollutants)}")
if good_pollutants:
print(f" • Polluants bons (1): {', '.join(good_pollutants)}")
if not critical_pollutants and not moderate_pollutants:
print(" • ✅ Tous les polluants à des niveaux acceptables")
print()
# === INFORMATIONS TECHNIQUES ===
print("🔧 === INFORMATIONS TECHNIQUES ===")
print()
print("📦 STRUCTURE DE DONNÉES:")
print(f" • Type d'objet: {type(atmo).__name__}")
print(f" • Classe parente: {type(atmo).__bases__[0].__name__}")
print(f" • Propriétés disponibles: {len(atmo.properties)} champs")
print(f" • Géométrie: {'Oui' if atmo.has_coordinates() else 'Non'}")
print(f" • Zone géographique: {atmo.type_zone}")
print()
print("🎨 MÉTHODES HÉRITÉES:")
inherited_methods = [
'get_emoji_by_level()', 'get_color_by_level()', 'has_coordinates()',
'get_source()', 'get_aasqa_name()'
]
print(f" • Méthodes de AtmoDataBase: {', '.join(inherited_methods)}")
print()
specific_methods = [
'get_qualificatif()', 'get_color()', 'get_emoji()', 'is_good_quality()',
'is_poor_quality()', 'get_worst_pollutant()', 'get_pollutants_summary()',
'get_concentrations()', 'is_commune_level()', 'is_epci_level()', 'get_responsible_pollutants()'
]
print(f" • Méthodes spécifiques IndiceAtmo: {', '.join(specific_methods)}")
print()
print("🎨 NOUVEAUTÉS ÉMOJIS:")
print(" • get_emoji_by_level(level, style) - style='round'|'square'")
print(" • get_emoji(style) - avec choix de style d'émoji")
print(" • Support des émojis ronds (🟢) et carrés (🟩)")
print()
print("📋 CONFORMITÉ NOTICE OFFICIELLE (1er avril 2025):")
print(" • Tous les champs obligatoires et facultatifs supportés")
print(" • Propriétés conformes aux spécifications pages 12-14")
print(" • Méthodes basées sur les règles officielles de calcul")
print(" • Codes couleur conformes au tableau page 6")
print(" • Support des coordonnées réglementaires (Lambert 93)")
print(" • Concentrations facultatives selon format officiel")
print()
print("✅ === DÉMONSTRATION TERMINÉE ===")
print()
print("📚 Ce script illustre toutes les fonctionnalités de la classe IndiceAtmo")
print("🔧 Utilisez ces exemples pour votre documentation et vos développements")
print("📋 Conforme à l'arrêté du 10 juillet 2020 et à la notice du 1er avril 2025")
print()
return True
except AtmoDataException as e:
print(f"❌ Erreur API: {e}")
return False
except Exception as e:
print(f"❌ Erreur inattendue: {e}")
import traceback
traceback.print_exc()
return False
def main():
"""Point d'entrée principal"""
print("🌬️ Démonstration des fonctions IndiceAtmo")
print("=" * 55)
print()
success = demo_atmo_functions()
if not success:
print("\n❌ La démonstration s'est terminée avec des erreurs")
sys.exit(1)
else:
print("🎉 Démonstration terminée avec succès !")
sys.exit(0)
if __name__ == "__main__":
main()

165
demos/demo_complete.py Normal file
View file

@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""
Démonstration complète du wrapper Atmo Data API
avec connexion réelle et toutes les fonctionnalités
"""
from atmo_data_wrapper import AtmoDataClient, AtmoDataException
from datetime import datetime, timedelta
import os
def main():
"""Démonstration complète de toutes les fonctionnalités"""
print("🌍 === DÉMONSTRATION ATMO DATA API ===\n")
try:
# 1. Connexion automatique
print("1. 🔐 Connexion à l'API...")
client = AtmoDataClient()
success = client.auto_login()
if not success:
print("❌ Échec de connexion")
return
print(f"✅ Connecté à {client.base_url}")
print(f" Token: {client.token[:20]}...")
print()
# 2. Indices ATMO avec objets typés
print("2. 🏭 Récupération des indices de qualité de l'air...")
today = datetime.now().strftime('%Y-%m-%d')
indices = client.get_indices_atmo(date=today, aasqa='11') # Île-de-France
print(f"{len(indices)} indices récupérés")
print(f" Résumé: {indices.to_summary()}")
# Examiner quelques indices
print("\n 📊 Exemples d'indices:")
for i, indice in enumerate(indices[:3]):
print(f"{indice.lib_zone}: {indice.get_qualificatif()}")
if indice.is_poor_quality():
worst_pol, code = indice.get_worst_pollutant()
print(f" ⚠️ Attention: {worst_pol} élevé ({code})")
print()
# 3. Épisodes de pollution
print("3. 🚨 Vérification des épisodes de pollution...")
episodes = client.get_episodes_3jours(aasqa='11')
print(f"{len(episodes)} épisodes trouvés")
alerts_actives = [ep for ep in episodes if ep.is_alert_active()]
if alerts_actives:
print(f" ⚠️ {len(alerts_actives)} alertes actives:")
for ep in alerts_actives[:3]:
print(f"{ep.lib_zone}: {ep.get_alert_level()} - {ep.lib_pol}")
else:
print(" ✅ Aucune alerte active")
print()
# 4. Données d'émissions
print("4. 🏭 Analyse des émissions...")
emissions = client.get_emissions(aasqa='11', echelle='region')
print(f"{len(emissions)} territoires analysés")
if len(emissions) > 0:
em = emissions[0]
print(f" 📍 {em.name}:")
print(f" • Population: {em.population:,.0f} habitants")
total_em = em.get_total_emissions()
print(f" • Émissions NOx: {total_em['NOx']:,.1f} t/an")
print(f" • Émissions PM10: {total_em['PM10']:,.1f} t/an")
# Calculs par habitant
nox_per_capita = em.get_emission_per_capita('nox') * 1000 # kg/hab
print(f" • NOx par habitant: {nox_per_capita:.2f} kg/hab/an")
print()
# 5. Indices pollen
print("5. 🌸 Vérification des indices pollen...")
try:
pollens = client.get_indices_pollens(aasqa='11')
print(f"{len(pollens)} stations pollen")
alerts_pollen = [p for p in pollens if p.is_alert_active()]
if alerts_pollen:
print(f" ⚠️ {len(alerts_pollen)} alertes pollen actives")
for pol in alerts_pollen[:3]:
dangerous = pol.get_dangerous_pollens()
if dangerous:
print(f" • Pollens à risque: {', '.join(dangerous)}")
else:
print(" ✅ Pas d'alerte pollen majeure")
except Exception as e:
print(f" ⚠️ Indices pollen indisponibles: {e}")
print()
# 6. Sauvegarde des données
print("6. 💾 Sauvegarde des données...")
try:
# Créer le dossier de sauvegarde
save_dir = f"export_{today}"
# Sauvegarder en différents formats
json_file = client.save_to_file(indices.raw_data, f"{save_dir}/indices_idf", "json")
csv_file = client.save_to_file(indices.raw_data, f"{save_dir}/indices_idf", "csv")
print(f"✅ Données sauvegardées:")
print(f" • JSON: {json_file} ({os.path.getsize(json_file):,} bytes)")
print(f" • CSV: {csv_file} ({os.path.getsize(csv_file):,} bytes)")
except Exception as e:
print(f" ❌ Erreur sauvegarde: {e}")
print()
# 7. Analyse statistique
print("7. 📈 Analyse statistique...")
stats = indices.get_statistics()
qs = stats['quality_stats']
print(f"✅ Statistiques de qualité de l'air:")
print(f" • Qualité moyenne: {qs['moyenne']:.1f}/7")
print(f" • Bonne qualité: {qs['bon_pourcentage']:.1f}% des zones")
print(f" • Meilleur indice: {qs['min']}")
print(f" • Pire indice: {qs['max']}")
# Classification des zones
bonnes = [i for i in indices if i.is_good_quality()]
mauvaises = [i for i in indices if i.is_poor_quality()]
print(f" • Zones de bonne qualité: {len(bonnes)}")
print(f" • Zones de qualité dégradée: {len(mauvaises)}")
if mauvaises:
print(" ⚠️ Zones à surveiller:")
for zone in mauvaises[:5]:
print(f" - {zone.lib_zone}: {zone.get_qualificatif()}")
print()
# 8. Recommandations
print("8. 💡 Recommandations...")
if qs['moyenne'] <= 2:
print("✅ Qualité de l'air globalement bonne en Île-de-France")
elif qs['moyenne'] <= 3:
print("⚠️ Qualité de l'air modérée - Surveillance recommandée")
else:
print("🚨 Qualité de l'air dégradée - Précautions recommandées")
if alerts_actives:
print("🚨 Épisodes de pollution en cours - Limitez les activités extérieures")
print("\n🎉 === DÉMONSTRATION TERMINÉE ===")
print(f"📊 Données analysées: {len(indices)} indices, {len(episodes)} épisodes, {len(emissions)} territoires")
print("💾 Fichiers exportés dans le dossier export_*")
except AtmoDataException as e:
print(f"❌ Erreur API: {e}")
except Exception as e:
print(f"❌ Erreur inattendue: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,472 @@
#!/usr/bin/env python3
"""
Script de démonstration des fonctionnalités de la classe EmissionData
================================================================
Ce script illustre l'utilisation de toutes les méthodes disponibles
dans la classe EmissionData pour analyser les données d'émissions.
Fonctionnalités testées:
- Analyse des émissions par polluant
- Calcul de densités d'émission par km²
- Calcul d'émissions par habitant
- Analyse des secteurs d'émission
- Gestion des coordonnées géographiques
- Utilisation des méthodes héritées de la classe de base
"""
import json
from pathlib import Path
from atmo_wrapper import AtmoDataWrapper
from atmo_data_wrapper import EmissionData, AtmoDataCollection, Coordinates
from atmo_data_wrapper import SECTEURS_EMISSIONS
def demo_basic_properties():
"""Démonstration des propriétés de base d'un objet EmissionData"""
print("=" * 60)
print("DÉMONSTRATION DES PROPRIÉTÉS DE BASE")
print("=" * 60)
# Exemple de données d'émission simulées
sample_emission = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [6.1667, 49.1333] # Nancy
},
"properties": {
"aasqa": "90",
"source": "ATMO Grand Est",
"date_maj": "2024-01-15",
"lib_zone": "Nancy Métropole",
"code": "54395",
"name": "Nancy",
"population": 104885,
"superficie": 15.01,
"nox": 125.5,
"pm10": 45.2,
"pm25": 28.7,
"ges": 850.3,
"code_pcaet": "01"
}
}
emission = EmissionData(sample_emission)
print(f"Code zone: {emission.code}")
print(f"Nom: {emission.name}")
print(f"Population: {emission.population:,} habitants")
print(f"Superficie: {emission.superficie} km²")
print(f"AASQA: {emission.get_aasqa_name()}")
print(f"Source: {emission.get_source()}")
print(f"Date de mise à jour: {emission.date_maj}")
print(f"Zone: {emission.lib_zone}")
print(f"Secteur: {emission.get_secteur_name()}")
if emission.has_coordinates():
print(f"Coordonnées: {emission.coordinates}")
else:
print("Pas de coordonnées disponibles")
print(f"Représentation: {emission}")
print()
def demo_emission_analysis():
"""Démonstration de l'analyse des émissions"""
print("=" * 60)
print("ANALYSE DES ÉMISSIONS PAR POLLUANT")
print("=" * 60)
# Exemple avec plusieurs zones d'émission
emission_samples = [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [6.1667, 49.1333]},
"properties": {
"aasqa": "90", "name": "Nancy", "population": 104885, "superficie": 15.01,
"nox": 125.5, "pm10": 45.2, "pm25": 28.7, "ges": 850.3, "code_pcaet": "01"
}
},
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [7.7500, 48.5833]},
"properties": {
"aasqa": "90", "name": "Strasbourg", "population": 280966, "superficie": 78.26,
"nox": 285.7, "pm10": 92.1, "pm25": 58.4, "ges": 1850.9, "code_pcaet": "02"
}
},
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [6.2000, 49.2500]},
"properties": {
"aasqa": "90", "name": "Metz", "population": 116429, "superficie": 41.94,
"nox": 155.8, "pm10": 52.3, "pm25": 35.1, "ges": 950.7, "code_pcaet": "03"
}
}
]
for i, emission_data in enumerate(emission_samples, 1):
emission = EmissionData(emission_data)
print(f"{i}. {emission.name}")
print(f" Population: {emission.population:,} hab, Superficie: {emission.superficie} km²")
# Émissions totales
total_emissions = emission.get_total_emissions()
print(f" Émissions totales:")
for polluant, valeur in total_emissions.items():
print(f" - {polluant}: {valeur:.1f} tonnes/an")
print()
def demo_density_calculations():
"""Démonstration des calculs de densité d'émission"""
print("=" * 60)
print("CALCULS DE DENSITÉ D'ÉMISSION (tonnes/km²)")
print("=" * 60)
emission_data = {
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [2.3522, 48.8566]},
"properties": {
"aasqa": "18", "name": "Paris", "population": 2161000, "superficie": 105.4,
"nox": 1250.5, "pm10": 425.2, "pm25": 280.7, "ges": 8500.3, "code_pcaet": "01"
}
}
emission = EmissionData(emission_data)
print(f"Zone: {emission.name}")
print(f"Superficie: {emission.superficie} km²")
print()
print("Densités d'émission par km²:")
pollutants = ['nox', 'pm10', 'pm25', 'ges']
for pollutant in pollutants:
density = emission.get_emission_density(pollutant)
polluant_name = pollutant.upper().replace('GES', 'GES (CO2 eq)')
print(f" - {polluant_name}: {density:.2f} tonnes/km²")
print()
def demo_per_capita_calculations():
"""Démonstration des calculs d'émission par habitant"""
print("=" * 60)
print("CALCULS D'ÉMISSION PAR HABITANT (tonnes/hab/an)")
print("=" * 60)
emission_data = {
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [3.0573, 50.6292]},
"properties": {
"aasqa": "59", "name": "Lille Métropole", "population": 1182127, "superficie": 611.0,
"nox": 1580.3, "pm10": 520.7, "pm25": 315.8, "ges": 11250.5, "code_pcaet": "02"
}
}
emission = EmissionData(emission_data)
print(f"Zone: {emission.name}")
print(f"Population: {emission.population:,} habitants")
print()
print("Émissions par habitant (en tonnes/habitant/an):")
pollutants = ['nox', 'pm10', 'pm25', 'ges']
for pollutant in pollutants:
per_capita = emission.get_emission_per_capita(pollutant)
polluant_name = pollutant.upper().replace('GES', 'GES (CO2 eq)')
# Convertir en kg/hab/an pour plus de lisibilité
per_capita_kg = per_capita * 1000
print(f" - {polluant_name}: {per_capita:.6f} t/hab/an ({per_capita_kg:.2f} kg/hab/an)")
print()
def demo_secteur_analysis():
"""Démonstration de l'analyse par secteur d'émission"""
print("=" * 60)
print("ANALYSE PAR SECTEUR D'ÉMISSION")
print("=" * 60)
# Simulation de différents secteurs d'émission
secteurs_samples = [
{"code_pcaet": "01", "name": "Transport routier", "nox": 850.5, "pm10": 45.2},
{"code_pcaet": "02", "name": "Industrie manufacturière", "nox": 320.8, "pm10": 85.7},
{"code_pcaet": "03", "name": "Résidentiel", "nox": 125.3, "pm10": 95.4},
{"code_pcaet": "04", "name": "Agriculture", "nox": 85.2, "pm10": 125.8},
{"code_pcaet": "05", "name": "Tertiaire", "nox": 65.7, "pm10": 25.3}
]
print("Émissions par secteur:")
print()
for i, secteur_data in enumerate(secteurs_samples, 1):
emission_data = {
"type": "Feature",
"properties": {
"aasqa": "25", "name": f"Zone {i}", "population": 50000, "superficie": 25.0,
"nox": secteur_data["nox"], "pm10": secteur_data["pm10"], "pm25": 15.0, "ges": 500.0,
"code_pcaet": secteur_data["code_pcaet"]
}
}
emission = EmissionData(emission_data)
print(f"{i}. Secteur: {emission.get_secteur_name()}")
print(f" Code PCAET: {emission.code_pcaet}")
print(f" Émissions NOx: {emission.nox:.1f} t/an")
print(f" Émissions PM10: {emission.pm10:.1f} t/an")
print()
def demo_coordinate_functions():
"""Démonstration des fonctions de coordonnées géographiques"""
print("=" * 60)
print("FONCTIONS DE COORDONNÉES GÉOGRAPHIQUES")
print("=" * 60)
# Créer des émissions avec coordonnées
emission_lille = EmissionData({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [3.0573, 50.6292]},
"properties": {
"aasqa": "59", "name": "Lille", "population": 233897, "superficie": 34.8,
"nox": 285.5, "pm10": 85.2, "pm25": 55.7, "ges": 1250.3, "code_pcaet": "01"
}
})
emission_nancy = EmissionData({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [6.1667, 49.1333]},
"properties": {
"aasqa": "90", "name": "Nancy", "population": 104885, "superficie": 15.01,
"nox": 125.5, "pm10": 45.2, "pm25": 28.7, "ges": 850.3, "code_pcaet": "01"
}
})
print(f"Émission 1: {emission_lille.name}")
print(f" Coordonnées: {emission_lille.coordinates}")
print(f" A des coordonnées: {emission_lille.has_coordinates()}")
print()
print(f"Émission 2: {emission_nancy.name}")
print(f" Coordonnées: {emission_nancy.coordinates}")
print(f" A des coordonnées: {emission_nancy.has_coordinates()}")
print()
# Calcul de distance
if emission_lille.has_coordinates() and emission_nancy.has_coordinates():
distance = emission_lille.coordinates.distance_to(emission_nancy.coordinates)
print(f"Distance entre {emission_lille.name} et {emission_nancy.name}: {distance:.1f} km")
print()
def demo_inherited_methods():
"""Démonstration des méthodes héritées de la classe de base"""
print("=" * 60)
print("MÉTHODES HÉRITÉES DE LA CLASSE DE BASE")
print("=" * 60)
emission_data = {
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [4.8357, 45.7640]},
"properties": {
"aasqa": "84", "name": "Lyon Métropole", "population": 1398892, "superficie": 533.68,
"nox": 1850.7, "pm10": 625.4, "pm25": 385.2, "ges": 13250.8, "code_pcaet": "02",
"source": "ATMO Auvergne-Rhône-Alpes", "date_maj": "2024-01-15"
}
}
emission = EmissionData(emission_data)
print(f"Zone: {emission.name}")
print(f"AASQA: {emission.get_aasqa_name()}")
print(f"Source: {emission.get_source()}")
print()
# Test des fonctions de couleur et emoji (niveau fictif pour démonstration)
print("Fonctions de couleur et emoji (exemple avec niveau 3):")
test_level = 3
couleur_hex, couleur_rgb = emission.get_color_by_level(test_level)
emoji_round = emission.get_emoji_by_level(test_level, "round")
emoji_square = emission.get_emoji_by_level(test_level, "square")
print(f" - Couleur hex: {couleur_hex}")
print(f" - Couleur RGB: {couleur_rgb}")
print(f" - Emoji rond: {emoji_round}")
print(f" - Emoji carré: {emoji_square}")
print()
def demo_comparative_analysis():
"""Démonstration d'une analyse comparative entre plusieurs zones"""
print("=" * 60)
print("ANALYSE COMPARATIVE ENTRE ZONES")
print("=" * 60)
zones_data = [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [2.3522, 48.8566]},
"properties": {
"aasqa": "18", "name": "Paris", "population": 2161000, "superficie": 105.4,
"nox": 1250.5, "pm10": 425.2, "pm25": 280.7, "ges": 8500.3, "code_pcaet": "01"
}
},
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [4.8357, 45.7640]},
"properties": {
"aasqa": "84", "name": "Lyon", "population": 1398892, "superficie": 533.68,
"nox": 1850.7, "pm10": 625.4, "pm25": 385.2, "ges": 13250.8, "code_pcaet": "02"
}
},
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [5.3698, 43.2965]},
"properties": {
"aasqa": "13", "name": "Marseille", "population": 868277, "superficie": 240.62,
"nox": 980.3, "pm10": 385.7, "pm25": 245.8, "ges": 7250.5, "code_pcaet": "01"
}
}
]
emissions = [EmissionData(zone) for zone in zones_data]
print("Comparaison des émissions par habitant (kg/hab/an):")
print("-" * 55)
print(f"{'Zone':<15} {'NOx':<8} {'PM10':<8} {'PM2.5':<8} {'GES':<10}")
print("-" * 55)
for emission in emissions:
nox_per_cap = emission.get_emission_per_capita('nox') * 1000 # Conversion en kg
pm10_per_cap = emission.get_emission_per_capita('pm10') * 1000
pm25_per_cap = emission.get_emission_per_capita('pm25') * 1000
ges_per_cap = emission.get_emission_per_capita('ges') * 1000
print(f"{emission.name:<15} {nox_per_cap:<8.1f} {pm10_per_cap:<8.1f} {pm25_per_cap:<8.1f} {ges_per_cap:<10.1f}")
print()
print("Comparaison des densités d'émission (tonnes/km²):")
print("-" * 55)
print(f"{'Zone':<15} {'NOx':<8} {'PM10':<8} {'PM2.5':<8} {'GES':<10}")
print("-" * 55)
for emission in emissions:
nox_density = emission.get_emission_density('nox')
pm10_density = emission.get_emission_density('pm10')
pm25_density = emission.get_emission_density('pm25')
ges_density = emission.get_emission_density('ges')
print(f"{emission.name:<15} {nox_density:<8.1f} {pm10_density:<8.1f} {pm25_density:<8.1f} {ges_density:<10.1f}")
print()
def demo_edge_cases():
"""Démonstration de la gestion des cas particuliers"""
print("=" * 60)
print("GESTION DES CAS PARTICULIERS")
print("=" * 60)
# Cas 1: Données manquantes
print("1. Zone sans coordonnées:")
emission_no_coords = EmissionData({
"type": "Feature",
"properties": {
"aasqa": "99", "name": "Zone sans coordonnées", "population": 10000, "superficie": 20.0,
"nox": 50.0, "pm10": 20.0, "pm25": 15.0, "ges": 300.0, "code_pcaet": "01"
}
})
print(f" A des coordonnées: {emission_no_coords.has_coordinates()}")
print(f" Coordonnées: {emission_no_coords.coordinates}")
print()
# Cas 2: Population nulle (division par zéro)
print("2. Zone sans population:")
emission_no_pop = EmissionData({
"type": "Feature",
"properties": {
"aasqa": "99", "name": "Zone industrielle", "population": 0, "superficie": 5.0,
"nox": 150.0, "pm10": 25.0, "pm25": 18.0, "ges": 800.0, "code_pcaet": "02"
}
})
nox_per_cap = emission_no_pop.get_emission_per_capita('nox')
print(f" Émission NOx par habitant: {nox_per_cap} (population = 0)")
print()
# Cas 3: Superficie nulle
print("3. Zone sans superficie:")
emission_no_area = EmissionData({
"type": "Feature",
"properties": {
"aasqa": "99", "name": "Point source", "population": 100, "superficie": 0,
"nox": 25.0, "pm10": 8.0, "pm25": 5.0, "ges": 150.0, "code_pcaet": "02"
}
})
nox_density = emission_no_area.get_emission_density('nox')
print(f" Densité NOx: {nox_density} (superficie = 0)")
print()
# Cas 4: Secteur inconnu
print("4. Secteur d'émission non référencé:")
emission_unknown_sector = EmissionData({
"type": "Feature",
"properties": {
"aasqa": "99", "name": "Zone test", "population": 5000, "superficie": 10.0,
"nox": 35.0, "pm10": 12.0, "pm25": 8.0, "ges": 200.0, "code_pcaet": "99"
}
})
print(f" Code secteur: {emission_unknown_sector.code_pcaet}")
print(f" Nom secteur: {emission_unknown_sector.get_secteur_name()}")
print()
def main():
"""Fonction principale de démonstration"""
print("SCRIPT DE DÉMONSTRATION - CLASSE EMISSIONDATA")
print("=" * 60)
print("Ce script teste toutes les fonctionnalités de la classe EmissionData")
print("pour l'analyse des données d'émissions atmosphériques.")
print()
try:
# Exécution de toutes les démonstrations
demo_basic_properties()
demo_emission_analysis()
demo_density_calculations()
demo_per_capita_calculations()
demo_secteur_analysis()
demo_coordinate_functions()
demo_inherited_methods()
demo_comparative_analysis()
demo_edge_cases()
print("=" * 60)
print("RÉCAPITULATIF DES MÉTHODES TESTÉES")
print("=" * 60)
print("Méthodes spécifiques à EmissionData:")
print("✓ get_emission_density(pollutant)")
print("✓ get_emission_per_capita(pollutant)")
print("✓ get_total_emissions()")
print("✓ get_secteur_name()")
print()
print("Méthodes héritées de AtmoDataBase:")
print("✓ get_aasqa_name()")
print("✓ get_source()")
print("✓ has_coordinates()")
print("✓ get_emoji_by_level(level, style)")
print("✓ get_color_by_level(level)")
print()
print("Propriétés testées:")
print("✓ Coordonnées géographiques et distance")
print("✓ Émissions par polluant (nox, pm10, pm25, ges)")
print("✓ Données démographiques (population, superficie)")
print("✓ Secteurs d'émission (code_pcaet)")
print("✓ Gestion des cas particuliers (données manquantes)")
print()
print("✅ TOUTES LES FONCTIONNALITÉS ONT ÉTÉ TESTÉES AVEC SUCCÈS")
except Exception as e:
print(f"❌ ERREUR lors de l'exécution: {e}")
raise
if __name__ == "__main__":
main()

View file

@ -0,0 +1,536 @@
#!/usr/bin/env python3
"""
Script de démonstration des fonctionnalités de la classe EpisodePollution
=====================================================================
Ce script illustre l'utilisation de toutes les méthodes disponibles
dans la classe EpisodePollution pour analyser les épisodes de pollution.
Fonctionnalités testées:
- Analyse des alertes de pollution
- Identification des polluants responsables
- Analyse des niveaux d'alerte (Information/Alerte)
- Gestion des géométries complexes
- Analyse des zones géographiques affectées
- Utilisation des méthodes héritées de la classe de base
"""
import json
from pathlib import Path
from atmo_wrapper import AtmoDataWrapper
from atmo_data_wrapper import EpisodePollution, AtmoDataCollection, Coordinates
from atmo_data_wrapper import CODE_POLLUANT
def demo_basic_properties():
"""Démonstration des propriétés de base d'un objet EpisodePollution"""
print("=" * 60)
print("DÉMONSTRATION DES PROPRIÉTÉS DE BASE")
print("=" * 60)
# Exemple d'épisode de pollution aux particules fines
sample_episode = {
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [[[
[6.0, 49.0], [6.5, 49.0], [6.5, 49.5], [6.0, 49.5], [6.0, 49.0]
]]]
},
"properties": {
"aasqa": "90",
"source": "ATMO Grand Est",
"date_maj": "2024-01-15T10:00:00",
"lib_zone": "Agglomération de Nancy",
"code_pol": "5",
"lib_pol": "PM10",
"code_zone": "54395",
"date_dif": "2024-01-15",
"date_ech": "2024-01-16",
"etat": "PROCEDURE D'INFORMATION ET DE RECOMMANDATION"
}
}
episode = EpisodePollution(sample_episode)
print(f"Zone affectée: {episode.lib_zone}")
print(f"Code polluant: {episode.code_pol}")
print(f"Polluant: {episode.lib_pol}")
print(f"Code polluant normalisé: {episode.get_polluant_code()}")
print(f"État: {episode.etat}")
print(f"AASQA: {episode.get_aasqa_name()}")
print(f"Source: {episode.get_source()}")
print(f"Date de diffusion: {episode.date_dif}")
print(f"Date d'échéance: {episode.date_ech}")
print(f"Date de mise à jour: {episode.date_maj}")
print(f"Géométrie complexe: {episode.is_geometry_complex()}")
if episode.has_coordinates():
print(f"Coordonnées: {episode.coordinates}")
else:
print("Pas de coordonnées ponctuelles (géométrie de zone)")
print(f"Représentation: {episode}")
print()
def demo_alert_analysis():
"""Démonstration de l'analyse des alertes"""
print("=" * 60)
print("ANALYSE DES ALERTES DE POLLUTION")
print("=" * 60)
# Exemples d'épisodes avec différents niveaux d'alerte
episodes_samples = [
{
"properties": {
"aasqa": "18", "lib_zone": "Île-de-France", "code_pol": "5", "lib_pol": "PM10",
"etat": "PROCEDURE D'INFORMATION ET DE RECOMMANDATION", "date_dif": "2024-01-15"
}
},
{
"properties": {
"aasqa": "18", "lib_zone": "Paris", "code_pol": "3", "lib_pol": "O3",
"etat": "PROCEDURE D'ALERTE", "date_dif": "2024-07-20"
}
},
{
"properties": {
"aasqa": "90", "lib_zone": "Strasbourg", "code_pol": "1", "lib_pol": "NO2",
"etat": "PAS DE DEPASSEMENT", "date_dif": "2024-01-15"
}
},
{
"properties": {
"aasqa": "13", "lib_zone": "Marseille", "code_pol": "6", "lib_pol": "PM2.5",
"etat": "PROCEDURE D'INFORMATION", "date_dif": "2024-03-10"
}
},
{
"properties": {
"aasqa": "84", "lib_zone": "Lyon", "code_pol": "3", "lib_pol": "O3",
"etat": "ALERTE NIVEAU 1", "date_dif": "2024-08-15"
}
}
]
print("Analyse des différents types d'alertes:")
print()
for i, episode_data in enumerate(episodes_samples, 1):
episode_data["type"] = "Feature"
episode = EpisodePollution(episode_data)
print(f"{i}. {episode.lib_zone} - {episode.lib_pol}")
print(f" État: {episode.etat}")
print(f" Alerte active: {'' if episode.is_alert_active() else ''}")
print(f" Niveau d'alerte: {episode.get_alert_level()}")
print(f" Code polluant: {episode.get_polluant_code()}")
print(f" Date: {episode.date_dif}")
print()
def demo_pollutant_analysis():
"""Démonstration de l'analyse par polluant"""
print("=" * 60)
print("ANALYSE PAR POLLUANT")
print("=" * 60)
# Exemples avec différents polluants
pollutants_samples = [
{"code_pol": "1", "lib_pol": "NO2", "zone": "Zone urbaine dense"},
{"code_pol": "2", "lib_pol": "SO2", "zone": "Zone industrielle"},
{"code_pol": "3", "lib_pol": "O3", "zone": "Zone péri-urbaine"},
{"code_pol": "5", "lib_pol": "PM10", "zone": "Centre-ville"},
{"code_pol": "6", "lib_pol": "PM2.5", "zone": "Zone trafic"}
]
print("Polluants détectés dans les épisodes:")
print()
for i, poll_data in enumerate(pollutants_samples, 1):
episode_data = {
"type": "Feature",
"properties": {
"aasqa": "99", "lib_zone": poll_data["zone"],
"code_pol": poll_data["code_pol"], "lib_pol": poll_data["lib_pol"],
"etat": "PROCEDURE D'INFORMATION", "date_dif": "2024-01-15"
}
}
episode = EpisodePollution(episode_data)
polluant_description = CODE_POLLUANT.get(episode.get_polluant_code(), "Description non disponible")
print(f"{i}. Polluant: {episode.lib_pol} (Code: {episode.code_pol})")
print(f" Code normalisé: {episode.get_polluant_code()}")
print(f" Zone: {episode.lib_zone}")
print(f" Description: {polluant_description}")
print()
def demo_geometry_analysis():
"""Démonstration de l'analyse des géométries"""
print("=" * 60)
print("ANALYSE DES GÉOMÉTRIES")
print("=" * 60)
# Épisode avec géométrie simple (Point)
episode_point = EpisodePollution({
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.3522, 48.8566]
},
"properties": {
"aasqa": "18", "lib_zone": "Paris Centre", "code_pol": "3", "lib_pol": "O3",
"etat": "PROCEDURE D'INFORMATION", "date_dif": "2024-07-15"
}
})
# Épisode avec géométrie complexe (MultiPolygon)
episode_multipolygon = EpisodePollution({
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[[[2.0, 48.5], [2.5, 48.5], [2.5, 49.0], [2.0, 49.0], [2.0, 48.5]]],
[[[2.8, 48.7], [3.2, 48.7], [3.2, 49.1], [2.8, 49.1], [2.8, 48.7]]]
]
},
"properties": {
"aasqa": "18", "lib_zone": "Île-de-France", "code_pol": "5", "lib_pol": "PM10",
"etat": "PROCEDURE D'ALERTE", "date_dif": "2024-01-20"
}
})
# Épisode sans géométrie
episode_no_geom = EpisodePollution({
"type": "Feature",
"properties": {
"aasqa": "90", "lib_zone": "Grand Est", "code_pol": "1", "lib_pol": "NO2",
"etat": "PAS DE DEPASSEMENT", "date_dif": "2024-01-15"
}
})
episodes = [
("Point", episode_point),
("MultiPolygon", episode_multipolygon),
("Sans géométrie", episode_no_geom)
]
print("Types de géométries dans les épisodes:")
print()
for i, (type_geom, episode) in enumerate(episodes, 1):
print(f"{i}. {type_geom} - {episode.lib_zone}")
print(f" Type géométrie: {episode.geometry.get('type', 'Non défini')}")
print(f" Géométrie complexe: {'' if episode.is_geometry_complex() else ''}")
print(f" A des coordonnées: {'' if episode.has_coordinates() else ''}")
if episode.has_coordinates():
print(f" Coordonnées: {episode.coordinates}")
print(f" État: {episode.etat}")
print()
def demo_temporal_analysis():
"""Démonstration de l'analyse temporelle des épisodes"""
print("=" * 60)
print("ANALYSE TEMPORELLE DES ÉPISODES")
print("=" * 60)
# Simulation d'épisodes à différentes dates
temporal_episodes = [
{
"date_dif": "2024-01-15", "date_ech": "2024-01-16",
"lib_zone": "Lyon", "lib_pol": "PM10", "etat": "PROCEDURE D'INFORMATION",
"aasqa": "84", "code_pol": "5"
},
{
"date_dif": "2024-07-20", "date_ech": "2024-07-21",
"lib_zone": "Marseille", "lib_pol": "O3", "etat": "PROCEDURE D'ALERTE",
"aasqa": "13", "code_pol": "3"
},
{
"date_dif": "2024-12-05", "date_ech": "2024-12-07",
"lib_zone": "Strasbourg", "lib_pol": "NO2", "etat": "PROCEDURE D'INFORMATION",
"aasqa": "90", "code_pol": "1"
}
]
print("Épisodes chronologiques:")
print()
for i, episode_data in enumerate(temporal_episodes, 1):
episode_data["type"] = "Feature"
episode = EpisodePollution(episode_data)
# Calcul de la durée (simplifié)
try:
from datetime import datetime
date_debut = datetime.strptime(episode.date_dif, "%Y-%m-%d")
date_fin = datetime.strptime(episode.date_ech, "%Y-%m-%d")
duree = (date_fin - date_debut).days
except:
duree = "Non calculable"
print(f"{i}. {episode.lib_zone} - {episode.lib_pol}")
print(f" Période: du {episode.date_dif} au {episode.date_ech}")
print(f" Durée: {duree} jour(s)" if duree != "Non calculable" else f" Durée: {duree}")
print(f" État: {episode.etat}")
print(f" Niveau: {episode.get_alert_level()}")
print()
def demo_regional_analysis():
"""Démonstration de l'analyse régionale des épisodes"""
print("=" * 60)
print("ANALYSE RÉGIONALE DES ÉPISODES")
print("=" * 60)
# Simulation d'épisodes dans différentes régions
regional_episodes = [
{"aasqa": "18", "region": "Île-de-France", "lib_zone": "Paris", "lib_pol": "PM10"},
{"aasqa": "84", "region": "Auvergne-Rhône-Alpes", "lib_zone": "Lyon", "lib_pol": "O3"},
{"aasqa": "13", "region": "Provence-Alpes-Côte d'Azur", "lib_zone": "Marseille", "lib_pol": "PM2.5"},
{"aasqa": "90", "region": "Grand Est", "lib_zone": "Strasbourg", "lib_pol": "NO2"},
{"aasqa": "59", "region": "Hauts-de-France", "lib_zone": "Lille", "lib_pol": "O3"}
]
print("Épisodes par région AASQA:")
print()
for i, episode_data in enumerate(regional_episodes, 1):
episode_data.update({
"type": "Feature",
"code_pol": "5", "etat": "PROCEDURE D'INFORMATION", "date_dif": "2024-01-15"
})
episode = EpisodePollution(episode_data)
print(f"{i}. Région: {episode_data['region']}")
print(f" AASQA: {episode.get_aasqa_name()}")
print(f" Zone: {episode.lib_zone}")
print(f" Polluant: {episode.lib_pol}")
print(f" Code AASQA: {episode.aasqa}")
print()
def demo_inherited_methods():
"""Démonstration des méthodes héritées de la classe de base"""
print("=" * 60)
print("MÉTHODES HÉRITÉES DE LA CLASSE DE BASE")
print("=" * 60)
episode_data = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [4.8357, 45.7640]
},
"properties": {
"aasqa": "84",
"source": "ATMO Auvergne-Rhône-Alpes",
"date_maj": "2024-07-15T14:30:00",
"lib_zone": "Lyon Métropole",
"code_pol": "3",
"lib_pol": "O3",
"etat": "PROCEDURE D'ALERTE",
"date_dif": "2024-07-15",
"date_ech": "2024-07-16"
}
}
episode = EpisodePollution(episode_data)
print(f"Zone affectée: {episode.lib_zone}")
print(f"AASQA: {episode.get_aasqa_name()}")
print(f"Source: {episode.get_source()}")
print(f"A des coordonnées: {'' if episode.has_coordinates() else ''}")
if episode.has_coordinates():
print(f"Coordonnées: {episode.coordinates}")
print()
# Test des fonctions de couleur et emoji (niveau fictif pour démonstration)
print("Fonctions de couleur et emoji (exemple avec niveau 4 - Alerte):")
test_level = 4 # Niveau d'alerte
couleur_hex, couleur_rgb = episode.get_color_by_level(test_level)
emoji_round = episode.get_emoji_by_level(test_level, "round")
emoji_square = episode.get_emoji_by_level(test_level, "square")
print(f" - Couleur hex: {couleur_hex}")
print(f" - Couleur RGB: {couleur_rgb}")
print(f" - Emoji rond: {emoji_round}")
print(f" - Emoji carré: {emoji_square}")
print()
def demo_comparative_analysis():
"""Démonstration d'une analyse comparative entre épisodes"""
print("=" * 60)
print("ANALYSE COMPARATIVE DES ÉPISODES")
print("=" * 60)
episodes_data = [
{
"type": "Feature",
"properties": {
"aasqa": "18", "lib_zone": "Paris", "code_pol": "5", "lib_pol": "PM10",
"etat": "PROCEDURE D'INFORMATION ET DE RECOMMANDATION", "date_dif": "2024-01-15"
}
},
{
"type": "Feature",
"properties": {
"aasqa": "84", "lib_zone": "Lyon", "code_pol": "3", "lib_pol": "O3",
"etat": "PROCEDURE D'ALERTE", "date_dif": "2024-07-20"
}
},
{
"type": "Feature",
"properties": {
"aasqa": "13", "lib_zone": "Marseille", "code_pol": "6", "lib_pol": "PM2.5",
"etat": "ALERTE NIVEAU 1", "date_dif": "2024-03-10"
}
},
{
"type": "Feature",
"properties": {
"aasqa": "90", "lib_zone": "Strasbourg", "code_pol": "1", "lib_pol": "NO2",
"etat": "PAS DE DEPASSEMENT", "date_dif": "2024-05-05"
}
}
]
episodes = [EpisodePollution(ep) for ep in episodes_data]
print("Comparaison des épisodes de pollution:")
print("-" * 70)
print(f"{'Zone':<15} {'Polluant':<8} {'Niveau':<12} {'Alerte':<8} {'État'}")
print("-" * 70)
for episode in episodes:
alerte_status = "" if episode.is_alert_active() else ""
niveau = episode.get_alert_level()
print(f"{episode.lib_zone:<15} {episode.lib_pol:<8} {niveau:<12} {alerte_status:<8} {episode.etat}")
print()
# Statistiques
total_episodes = len(episodes)
alertes_actives = sum(1 for ep in episodes if ep.is_alert_active())
print(f"Statistiques:")
print(f" - Total d'épisodes: {total_episodes}")
print(f" - Alertes actives: {alertes_actives}")
print(f" - Pourcentage d'alertes: {(alertes_actives/total_episodes)*100:.1f}%")
# Répartition par polluant
polluants = {}
for episode in episodes:
polluant = episode.get_polluant_code()
polluants[polluant] = polluants.get(polluant, 0) + 1
print(f" - Répartition par polluant:")
for polluant, count in polluants.items():
print(f" * {polluant}: {count} épisode(s)")
print()
def demo_edge_cases():
"""Démonstration de la gestion des cas particuliers"""
print("=" * 60)
print("GESTION DES CAS PARTICULIERS")
print("=" * 60)
# Cas 1: Épisode sans état défini
print("1. Épisode sans état défini:")
episode_no_state = EpisodePollution({
"type": "Feature",
"properties": {
"aasqa": "99", "lib_zone": "Zone test", "code_pol": "5", "lib_pol": "PM10",
"etat": "", "date_dif": "2024-01-15"
}
})
print(f" État: '{episode_no_state.etat}'")
print(f" Alerte active: {'' if episode_no_state.is_alert_active() else ''}")
print(f" Niveau d'alerte: {episode_no_state.get_alert_level()}")
print()
# Cas 2: Code polluant non standard
print("2. Code polluant non référencé:")
episode_unknown_pollutant = EpisodePollution({
"type": "Feature",
"properties": {
"aasqa": "99", "lib_zone": "Zone test", "code_pol": "99", "lib_pol": "Polluant inconnu",
"etat": "PROCEDURE D'INFORMATION", "date_dif": "2024-01-15"
}
})
print(f" Code polluant original: {episode_unknown_pollutant.code_pol}")
print(f" Code polluant normalisé: {episode_unknown_pollutant.get_polluant_code()}")
print(f" Nom polluant: {episode_unknown_pollutant.lib_pol}")
print()
# Cas 3: Géométrie malformée
print("3. Géométrie non standard:")
episode_no_geometry = EpisodePollution({
"type": "Feature",
"geometry": None,
"properties": {
"aasqa": "99", "lib_zone": "Zone sans géométrie", "code_pol": "3", "lib_pol": "O3",
"etat": "PROCEDURE D'ALERTE", "date_dif": "2024-01-15"
}
})
print(f" Géométrie: {episode_no_geometry.geometry}")
print(f" Type géométrie: {episode_no_geometry.geometry.get('type', 'Non défini') if episode_no_geometry.geometry else 'None'}")
print(f" Géométrie complexe: {'' if episode_no_geometry.is_geometry_complex() else ''}")
print(f" A des coordonnées: {'' if episode_no_geometry.has_coordinates() else ''}")
print()
def main():
"""Fonction principale de démonstration"""
print("SCRIPT DE DÉMONSTRATION - CLASSE EPISODEPOLLUTION")
print("=" * 60)
print("Ce script teste toutes les fonctionnalités de la classe EpisodePollution")
print("pour l'analyse des épisodes de pollution atmosphérique.")
print()
try:
# Exécution de toutes les démonstrations
demo_basic_properties()
demo_alert_analysis()
demo_pollutant_analysis()
demo_geometry_analysis()
demo_temporal_analysis()
demo_regional_analysis()
demo_inherited_methods()
demo_comparative_analysis()
demo_edge_cases()
print("=" * 60)
print("RÉCAPITULATIF DES MÉTHODES TESTÉES")
print("=" * 60)
print("Méthodes spécifiques à EpisodePollution:")
print("✓ get_polluant_code()")
print("✓ is_alert_active()")
print("✓ get_alert_level()")
print("✓ is_geometry_complex()")
print()
print("Méthodes héritées de AtmoDataBase:")
print("✓ get_aasqa_name()")
print("✓ get_source()")
print("✓ has_coordinates()")
print("✓ get_emoji_by_level(level, style)")
print("✓ get_color_by_level(level)")
print()
print("Propriétés testées:")
print("✓ Codes et noms des polluants")
print("✓ États et niveaux d'alerte")
print("✓ Zones géographiques affectées")
print("✓ Géométries (Point, MultiPolygon)")
print("✓ Informations temporelles (dates)")
print("✓ Gestion des cas particuliers")
print()
print("✅ TOUTES LES FONCTIONNALITÉS ONT ÉTÉ TESTÉES AVEC SUCCÈS")
except Exception as e:
print(f"❌ ERREUR lors de l'exécution: {e}")
raise
if __name__ == "__main__":
main()

213
demos/demo_licence_atmo.py Normal file
View file

@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""
Démonstration des fonctions de licence Atmo France
=================================================
Ce script montre comment utiliser les fonctions utilitaires pour afficher
la mention légale requise par Atmo France selon leur licence d'utilisation
des données en open data sous licence ODbL.
Conformément aux exigences d'Atmo France:
"Chacun peut donc bénéficier gratuitement de ces données mises en open data
sous licence ODbL, en indiquant la source "Atmo France et les Associations
agréées de surveillance de la qualité de l'air" ou "Atmo France / AASQA"
dans sa version courte."
"""
import sys
import os
# Ajouter le répertoire parent au PYTHONPATH pour importer le package local
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from atmo_data_wrapper import (
get_atmo_licence,
print_atmo_licence,
ATMO_LICENCE_COURTE,
ATMO_LICENCE_LONGUE,
ATMO_LICENCE_COMPLETE
)
def demo_licence_formats():
"""Démonstration des différents formats de licence"""
print("📋 FORMATS DE LICENCE ATMO FRANCE")
print("=" * 50)
print()
print("1⃣ Version courte (recommandée pour citations):")
print(f"{get_atmo_licence('courte')}")
print()
print("2⃣ Version longue (nom officiel complet):")
print(f"{get_atmo_licence('longue')}")
print()
print("3⃣ Version complète (avec détails de licence):")
print(get_atmo_licence('complete'))
print()
def demo_usage_examples():
"""Exemples d'utilisation pratique"""
print("💡 EXEMPLES D'UTILISATION PRATIQUE")
print("=" * 50)
print()
print("1⃣ Dans un script de données:")
print("```python")
print("from atmo_data_wrapper import get_atmo_licence")
print()
print("# Récupérer des données")
print("indices = client.get_indices_atmo()")
print()
print("# Afficher la source")
print('print(f"Source: {get_atmo_licence(\'courte\')}")')
print("```")
print()
print("Résultat:")
print(f"Source: {get_atmo_licence('courte')}")
print()
print("2⃣ Dans un rapport ou documentation:")
print("```python")
print("print_atmo_licence('complete')")
print("```")
print()
print("Résultat:")
print_atmo_licence('complete')
print()
print("3⃣ Accès direct aux constantes:")
print("```python")
print("from atmo_data_wrapper import ATMO_LICENCE_COURTE")
print("print(ATMO_LICENCE_COURTE)")
print("```")
print()
print("Résultat:")
print(ATMO_LICENCE_COURTE)
print()
def demo_integration_examples():
"""Exemples d'intégration dans différents contextes"""
print("🔧 EXEMPLES D'INTÉGRATION")
print("=" * 50)
print()
print("1⃣ Dans un fichier CSV:")
print("# Commentaire en en-tête de fichier CSV")
print(f"# Source: {get_atmo_licence('longue')}")
print("# Licence: ODbL")
print("commune,indice_atmo,date")
print("Nancy,2,2024-01-15")
print("...")
print()
print("2⃣ Dans un graphique matplotlib:")
print("```python")
print("import matplotlib.pyplot as plt")
print("from atmo_data_wrapper import get_atmo_licence")
print()
print("# Créer le graphique")
print("plt.plot(dates, indices)")
print("plt.title('Évolution de la qualité de l\\'air')")
print()
print("# Ajouter la source")
print("plt.figtext(0.02, 0.02, f'Source: {get_atmo_licence(\'courte\')}', fontsize=8)")
print("plt.show()")
print("```")
print()
print("3⃣ Dans une API REST:")
print("```python")
print("from flask import Flask, jsonify")
print("from atmo_data_wrapper import get_atmo_licence")
print()
print("@app.route('/api/air-quality')")
print("def get_air_quality():")
print(" data = get_air_quality_data()")
print(" return jsonify({")
print(" 'data': data,")
print(" 'source': get_atmo_licence('courte'),")
print(" 'licence': 'ODbL'")
print(" })")
print("```")
print()
print("4⃣ Dans un footer HTML:")
print("```html")
print("<footer>")
print(f" <p>Source des données: {get_atmo_licence('longue')}</p>")
print(" <p>Licence: <a href='https://opendatacommons.org/licenses/odbl/'>ODbL</a></p>")
print("</footer>")
print("```")
print()
def demo_licence_compliance():
"""Vérification de conformité avec les exigences"""
print("✅ CONFORMITÉ AVEC LES EXIGENCES ATMO FRANCE")
print("=" * 55)
print()
print("📋 Exigences officielles:")
print('• Indiquer la source "Atmo France et les Associations agréées"')
print('• Ou version courte "Atmo France / AASQA"')
print('• Respecter la licence ODbL')
print()
print("✅ Notre implémentation:")
print(f"• Version courte: '{get_atmo_licence('courte')}'")
print(f"• Version longue: '{get_atmo_licence('longue')}'")
print("• Mention de la licence ODbL incluse")
print("• URLs officielles fournies")
print()
print("🎯 Recommandations d'usage:")
print("• Utiliser la version courte pour les citations courtes")
print("• Utiliser la version longue pour les documents officiels")
print("• Utiliser la version complète pour les mentions légales détaillées")
print("• Toujours mentionner la licence ODbL")
print()
def main():
"""Fonction principale"""
print("DÉMONSTRATION DES FONCTIONS DE LICENCE ATMO FRANCE")
print("=" * 60)
print("Conformité avec les exigences de licence ODbL d'Atmo France")
print()
try:
demo_licence_formats()
demo_usage_examples()
demo_integration_examples()
demo_licence_compliance()
print("=" * 60)
print("✅ DÉMONSTRATION TERMINÉE AVEC SUCCÈS")
print()
print("🔑 Fonctions disponibles:")
print(" • get_atmo_licence(format): Retourne la licence selon le format")
print(" • print_atmo_licence(format): Affiche la licence")
print()
print("📋 Formats supportés:")
print("'courte': Version abrégée")
print("'longue': Version officielle complète")
print("'complete': Avec détails de licence")
print()
print("📁 Constantes disponibles:")
print(" • ATMO_LICENCE_COURTE")
print(" • ATMO_LICENCE_LONGUE")
print(" • ATMO_LICENCE_COMPLETE")
except Exception as e:
print(f"❌ Erreur lors de l'exécution: {e}")
import traceback
print("\nDétails de l'erreur:")
print(traceback.format_exc())
if __name__ == "__main__":
main()

View file

@ -0,0 +1,333 @@
#!/usr/bin/env python3
"""
Script de démonstration de toutes les fonctions du datamodel IndicePollen
Utilise les données réelles de l'API pour Tomblaine (aujourd'hui)
Idéal pour documentation et exemples
"""
from atmo_data_wrapper import AtmoDataClient, AtmoDataException
from atmo_data_wrapper import CODE_TAXON
from datetime import datetime
import sys
def demo_pollen_functions():
"""Démonstration complète des fonctions IndicePollen avec données réelles"""
# Configuration
CODE_INSEE_TOMBLAINE = "54526"
AASQA_GRAND_EST = "44"
today = datetime.now().strftime('%Y-%m-%d')
print("🌸 DÉMONSTRATION DES FONCTIONS INDICEPOLLEN")
print("=" * 55)
print(f"📍 Ville: Tomblaine (INSEE: {CODE_INSEE_TOMBLAINE})")
print(f"🗓️ Date: {today}")
print(f"🏛️ AASQA: {AASQA_GRAND_EST} (Grand Est)")
print()
try:
# Connexion et récupération des données
print("🔐 Connexion à l'API...")
client = AtmoDataClient()
success = client.auto_login()
if not success:
print("❌ Échec de la connexion à l'API")
return False
print("✅ Connecté avec succès !")
# Récupération des données pollen
print(f"🌸 Récupération des indices pollen pour {today}...")
pollens = client.get_indices_pollens(
format="geojson",
date=today,
aasqa=AASQA_GRAND_EST,
code_zone=CODE_INSEE_TOMBLAINE,
with_geom=False
)
if not pollens or len(pollens) == 0:
print("❌ Aucune donnée pollen trouvée")
return False
print(f"{len(pollens)} station(s) trouvée(s)")
# Utiliser la première station pour la démo
pollen = pollens[0]
print(f"📍 Station sélectionnée: {pollen.lib_zone}")
print()
# === DÉMONSTRATION DES PROPRIÉTÉS DE BASE ===
print("📊 === PROPRIÉTÉS DE BASE (héritées d'AtmoDataBase) ===")
print()
print("🏛️ INFORMATIONS GÉNÉRALES:")
print(f" • AASQA: {pollen.aasqa}")
print(f" • Zone: {pollen.lib_zone}")
print(f" • Source: {pollen.source}")
print(f" • Alerte active: {pollen.alerte}")
print()
print("🗺️ COORDONNÉES:")
if pollen.has_coordinates():
coords = pollen.get_coordinates()
print(f" • Latitude: {coords.latitude:.6f}")
print(f" • Longitude: {coords.longitude:.6f}")
else:
print(" • Pas de coordonnées disponibles")
print()
print("🎨 FONCTIONS CENTRALISÉES:")
for level in [0, 1, 2, 3, 4]:
emoji_round = pollen.get_emoji_by_level(level, "round")
emoji_square = pollen.get_emoji_by_level(level, "square")
color_hex, color_rgb = pollen.get_color_by_level(level)
print(f" • Niveau {level}: {emoji_round}{emoji_square} {color_hex} {color_rgb}")
print()
# === DÉMONSTRATION DES PROPRIÉTÉS SPÉCIFIQUES POLLEN ===
print("🌿 === PROPRIÉTÉS SPÉCIFIQUES POLLEN ===")
print()
print("📈 CODES PAR TAXON:")
print(f" • Ambroisie (ambr): {pollen.code_ambr}")
print(f" • Armoise (arm): {pollen.code_arm}")
print(f" • Aulne (aul): {pollen.code_aul}")
print(f" • Bouleau (boul): {pollen.code_boul}")
print(f" • Graminées (gram): {pollen.code_gram}")
print(f" • Olivier (oliv): {pollen.code_oliv}")
print()
print("🔬 CONCENTRATIONS (grains/m³):")
print(f" • Ambroisie: {pollen.conc_ambr:.1f}")
print(f" • Armoise: {pollen.conc_arm:.1f}")
print(f" • Aulne: {pollen.conc_aul:.1f}")
print(f" • Bouleau: {pollen.conc_boul:.1f}")
print(f" • Graminées: {pollen.conc_gram:.1f}")
print(f" • Olivier: {pollen.conc_oliv:.1f}")
print()
print("🎯 TAXONS RESPONSABLES DE L'INDICE:")
print(f" • pollen_resp (raw): '{pollen.pollen_resp}'")
print()
# === DÉMONSTRATION DES MÉTHODES HELPER ===
print("🛠️ === MÉTHODES HELPER ===")
print()
# 1. is_alert_active()
print("🚨 1. DÉTECTION D'ALERTE:")
alert_active = pollen.is_alert_active()
print(f" • is_alert_active(): {alert_active}")
if alert_active:
print(" → Alerte pollen active !")
else:
print(" → Pas d'alerte pollen")
print()
# 2. get_highest_pollen()
print("🏆 2. POLLEN LE PLUS ÉLEVÉ:")
highest_taxon, highest_code = pollen.get_highest_pollen()
highest_name = CODE_TAXON.get(highest_taxon, highest_taxon.title())
print(f" • get_highest_pollen(): ('{highest_taxon}', {highest_code})")
print(f" → Espèce: {highest_name}")
print(f" → Niveau: {highest_code}")
print()
# 3. get_highest_concentration()
print("🔬 3. CONCENTRATION LA PLUS ÉLEVÉE:")
highest_conc_taxon, highest_conc_value = pollen.get_highest_concentration()
highest_conc_name = CODE_TAXON.get(highest_conc_taxon, highest_conc_taxon.title())
print(f" • get_highest_concentration(): ('{highest_conc_taxon}', {highest_conc_value})")
print(f" → Espèce: {highest_conc_name}")
print(f" → Concentration: {highest_conc_value:.1f} grains/m³")
print()
# 4. get_dangerous_pollens()
print("⚠️ 4. POLLENS DANGEREUX (niveau ≥ 4):")
dangerous = pollen.get_dangerous_pollens()
print(f" • get_dangerous_pollens(): {dangerous}")
if dangerous:
dangerous_names = [CODE_TAXON.get(p, p.title()) for p in dangerous]
print(f" → Espèces à risque: {', '.join(dangerous_names)}")
else:
print(" → Aucun pollen à risque élevé")
print()
# 5. get_responsible_pollens() - NOUVELLE MÉTHODE
print("🎯 5. TAXONS RESPONSABLES DE L'INDICE (API):")
responsible = pollen.get_responsible_pollens()
print(f" • get_responsible_pollens(): {responsible}")
if responsible:
print(f" → Espèces responsables selon l'API: {', '.join(responsible)}")
else:
print(" → Aucun taxon responsable spécifié par l'API")
print()
# 6. get_concentrations()
print("📊 6. TOUTES LES CONCENTRATIONS:")
concentrations = pollen.get_concentrations()
print(f" • get_concentrations(): {concentrations}")
print(" → Détail:")
for taxon, conc in concentrations.items():
taxon_name = CODE_TAXON.get(taxon, taxon.title())
print(f" - {taxon_name}: {conc:.1f} grains/m³")
print()
# 7. get_pollens_summary()
print("📋 7. RÉSUMÉ COMPLET:")
summary = pollen.get_pollens_summary()
print(f" • get_pollens_summary():")
print(" → Structure complète par taxon:")
for code_taxon, info in summary.items():
if info['code'] > 0: # Afficher seulement les pollens détectés
print(f" - {code_taxon}:")
print(f" * Code: {info['code']}")
print(f" * Espèce: {info['espece']}")
print(f" * Qualificatif: {info['qualificatif']}")
print(f" * Concentration: {info['concentration']:.1f} gr/m³")
print(f" * Couleur: {info['couleur']}")
print(f" * Émoji rond: {info['emoji_round']}")
print(f" * Émoji carré: {info['emoji_square']}")
print()
# 8. Test des styles d'émojis
print("🎨 8. TEST DES STYLES D'ÉMOJIS:")
summary_round = pollen.get_pollens_summary("round")
summary_square = pollen.get_pollens_summary("square")
print(" • Comparaison des styles par défaut:")
for code_taxon in ['arm', 'gram']: # Tester avec les pollens détectés
if summary_round[code_taxon]['code'] > 0:
round_emoji = summary_round[code_taxon]['emoji']
square_emoji = summary_square[code_taxon]['emoji']
espece = summary_round[code_taxon]['espece']
print(f" - {espece}: Rond={round_emoji} | Carré={square_emoji}")
print()
# === DÉMONSTRATION DES MÉTHODES STRING ===
print("📝 === REPRÉSENTATION STRING ===")
print()
print("🔤 MÉTHODE __str__():")
print(f" • str(pollen): '{str(pollen)}'")
print()
# === EXEMPLES D'UTILISATION PRATIQUE ===
print("💡 === EXEMPLES D'UTILISATION PRATIQUE ===")
print()
print("🎯 ANALYSE RAPIDE:")
print(f" • Niveau global le plus élevé: {highest_name} (niveau {highest_code})")
print(f" • Concentration maximale: {highest_conc_name} ({highest_conc_value:.1f} gr/m³)")
if alert_active:
print(" • ⚠️ ALERTE ACTIVE - Précautions recommandées")
if dangerous:
print(f" • 🚨 POLLENS À RISQUE: {', '.join([CODE_TAXON.get(p, p) for p in dangerous])}")
else:
print(" • ✅ Aucun pollen à risque élevé")
print()
print("📈 DÉTECTION DE TENDANCES:")
detected_pollens = [taxon for taxon, info in summary.items() if info['code'] > 0]
significant_pollens = [taxon for taxon, info in summary.items() if info['code'] >= 2]
print(f" • Pollens détectés: {len(detected_pollens)} espèces")
print(f" • Pollens significatifs (≥2): {len(significant_pollens)} espèces")
if significant_pollens:
sig_names = [CODE_TAXON.get(p, p) for p in significant_pollens]
print(f"{', '.join(sig_names)}")
print()
print("🔍 FILTRAGE AVANCÉ:")
# Exemple de filtrage par concentration
high_conc = {t: c for t, c in concentrations.items() if c > 5.0}
if high_conc:
print(" • Concentrations élevées (>5 gr/m³):")
for taxon, conc in high_conc.items():
taxon_name = CODE_TAXON.get(taxon, taxon.title())
print(f"{taxon_name}: {conc:.1f} gr/m³")
else:
print(" • Aucune concentration élevée (>5 gr/m³)")
print()
# === INFORMATIONS TECHNIQUES ===
print("🔧 === INFORMATIONS TECHNIQUES ===")
print()
print("📦 STRUCTURE DE DONNÉES:")
print(f" • Type d'objet: {type(pollen).__name__}")
print(f" • Classe parente: {type(pollen).__bases__[0].__name__}")
print(f" • Propriétés disponibles: {len(pollen.properties)} champs")
print(f" • Géométrie: {'Oui' if pollen.has_coordinates() else 'Non'}")
print()
print("🎨 MÉTHODES HÉRITÉES:")
inherited_methods = [
'get_emoji_by_level()', 'get_color_by_level()', 'has_coordinates()',
'get_coordinates()', 'get_source()'
]
print(f" • Méthodes de AtmoDataBase: {', '.join(inherited_methods)}")
print()
specific_methods = [
'is_alert_active()', 'get_highest_pollen()', 'get_highest_concentration()',
'get_dangerous_pollens()', 'get_responsible_pollens()', 'get_concentrations()', 'get_pollens_summary()'
]
print(f" • Méthodes spécifiques IndicePollen: {', '.join(specific_methods)}")
print()
print("🎨 NOUVEAUTÉS ÉMOJIS:")
print(" • get_emoji_by_level(level, style) - style='round'|'square'")
print(" • get_emoji(style) - pour IndiceAtmo avec choix de style")
print(" • get_pollens_summary(emoji_style) - résumé avec style d'émoji")
print(" • Chaque résumé inclut emoji_round ET emoji_square")
print()
print("📋 CONFORMITÉ NOTICE OFFICIELLE (1er avril 2025):")
print(" • Tous les champs de la notice officielle sont supportés")
print(" • Classes IndiceAtmo et IndicePollen conformes aux spécifications")
print(" • Nouvelles propriétés pour IndiceAtmo : type_zone, coordonnées réglementaires")
print(" • Concentrations facultatives ajoutées selon la notice")
print(" • Méthodes basées sur les règles officielles de calcul")
print(" • Codes couleur et qualificatifs conformes au tableau page 6")
print()
print("✅ === DÉMONSTRATION TERMINÉE ===")
print()
print("📚 Ce script illustre toutes les fonctionnalités de la classe IndicePollen")
print("🔧 Utilisez ces exemples pour votre documentation et vos développements")
print()
return True
except AtmoDataException as e:
print(f"❌ Erreur API: {e}")
return False
except Exception as e:
print(f"❌ Erreur inattendue: {e}")
import traceback
traceback.print_exc()
return False
def main():
"""Point d'entrée principal"""
print("🌸 Démonstration des fonctions IndicePollen")
print("=" * 55)
print()
success = demo_pollen_functions()
if not success:
print("\n❌ La démonstration s'est terminée avec des erreurs")
sys.exit(1)
else:
print("🎉 Démonstration terminée avec succès !")
sys.exit(0)
if __name__ == "__main__":
main()

645
docs/DOCUMENTATION_DEMOS.md Normal file
View file

@ -0,0 +1,645 @@
# Documentation des Scripts de Démonstration
## Vue d'ensemble
Ce document présente la documentation complète des deux scripts de démonstration créés pour illustrer les fonctionnalités des classes de données du wrapper API Atmo Data. Ces scripts utilisent des données réelles de l'API pour démontrer chaque méthode et propriété disponible.
---
## 📊 Script 1 : `demo_atmo_functions.py`
### Description générale
Script de démonstration complet pour la classe `IndiceAtmo`, illustrant toutes les fonctionnalités liées aux indices de qualité de l'air ATMO.
### Configuration
- **Région testée** : Île-de-France (AASQA 11)
- **Source de données** : API Atmo Data en temps réel
- **Date** : Jour d'exécution du script
- **Format** : GeoJSON
- **Conformité** : Notice officielle du 1er avril 2025
### Sections démontrées
#### 1. Propriétés de base (héritées d'AtmoDataBase)
```python
# Informations générales
atmo.aasqa # Code AASQA
atmo.lib_zone # Nom de la zone
atmo.source # Nom de l'organisme
atmo.type_zone # 'commune' ou 'EPCI'
atmo.code_zone # Code INSEE
# Données temporelles
atmo.date_ech # Date d'échéance
atmo.date_dif # Date de diffusion
# Coordonnées
atmo.has_coordinates() # Disponibilité coordonnées
atmo.coordinates # Objet Coordinates si disponible
atmo.x_reg, atmo.y_reg # Coordonnées réglementaires
atmo.epsg_reg # Système de projection
```
#### 2. Propriétés spécifiques ATMO
```python
# Indice global
atmo.code_qual # Code qualificatif (0-7)
atmo.lib_qual # Libellé ("Bon", "Dégradé"...)
atmo.coul_qual # Couleur hexadécimale
# Codes par polluant
atmo.code_no2 # Niveau NO2
atmo.code_so2 # Niveau SO2
atmo.code_o3 # Niveau O3
atmo.code_pm10 # Niveau PM10
atmo.code_pm25 # Niveau PM2.5
# Concentrations facultatives (μg/m³)
atmo.conc_no2 # Concentration NO2
atmo.conc_so2 # Concentration SO2
atmo.conc_o3 # Concentration O3
atmo.conc_pm10 # Concentration PM10
atmo.conc_pm25 # Concentration PM2.5
```
#### 3. Méthodes helper essentielles
```python
# Qualificatif et apparence
atmo.get_qualificatif() # → "Dégradé"
atmo.get_color() # → ("#F0E641", (240, 230, 65))
atmo.get_emoji("round") # → "🟡"
atmo.get_emoji("square") # → "🟨"
# Tests de qualité
atmo.is_good_quality() # → True si niveaux 1-2
atmo.is_poor_quality() # → True si niveaux 4+
# Analyse des polluants
atmo.get_worst_pollutant() # → ("O3", 3)
atmo.get_pollutants_summary() # → Dict complet par polluant
atmo.get_concentrations() # → Dict concentrations
atmo.get_responsible_pollutants() # → ["O3"] (règle officielle)
# Conformité notice officielle
atmo.is_commune_level() # → True si type_zone='commune'
atmo.is_epci_level() # → True si type_zone='EPCI'
```
#### 4. Fonctions centralisées (base)
```python
# Émojis et couleurs par niveau
atmo.get_emoji_by_level(3, "round") # → "🟡"
atmo.get_emoji_by_level(3, "square") # → "🟨"
atmo.get_color_by_level(3) # → ("#F0E641", (240, 230, 65))
```
### Exemple de sortie
```
📍 Station sélectionnée: Tousson
🎯 ANALYSE RAPIDE:
• Qualité globale: Dégradé (niveau 3)
• Polluant problématique: ozone (niveau 3)
• Couleur d'affichage: #F0E641 🟡
📈 ANALYSE PAR POLLUANT:
• Dioxyde d'azote: 🔵 Bon (niveau 1)
• Ozone: 🟡 Dégradé (niveau 3)
• Particules PM10: 🔵 Bon (niveau 1)
```
### Usage
```bash
python demo_atmo_functions.py
```
---
## 🌸 Script 2 : `demo_pollen_functions.py`
### Description générale
Script de démonstration complet pour la classe `IndicePollen`, illustrant toutes les fonctionnalités liées aux indices pollen.
### Configuration
- **Ville testée** : Tomblaine (INSEE 54526)
- **Région** : Grand Est (AASQA 44)
- **Source de données** : API Atmo Data en temps réel
- **Date** : Jour d'exécution du script
- **Format** : GeoJSON
### Sections démonstrées
#### 1. Propriétés de base (héritées d'AtmoDataBase)
```python
# Informations générales
pollen.aasqa # Code AASQA
pollen.lib_zone # Nom de la zone
pollen.source # Nom de l'organisme
pollen.alerte # Statut d'alerte (True/False)
# Coordonnées et fonctions centralisées
pollen.has_coordinates() # Disponibilité coordonnées
pollen.get_emoji_by_level(2, "round") # → "🟢"
pollen.get_color_by_level(2) # → ("#50CCAA", (80, 204, 170))
```
#### 2. Propriétés spécifiques pollen
```python
# Codes par taxon (espèce)
pollen.code_ambr # Ambroisie (0-6)
pollen.code_arm # Armoise (0-6)
pollen.code_aul # Aulne (0-6)
pollen.code_boul # Bouleau (0-6)
pollen.code_gram # Graminées (0-6)
pollen.code_oliv # Olivier (0-6)
# Concentrations (grains/m³)
pollen.conc_ambr # Concentration ambroisie
pollen.conc_arm # Concentration armoise
pollen.conc_aul # Concentration aulne
pollen.conc_boul # Concentration bouleau
pollen.conc_gram # Concentration graminées
pollen.conc_oliv # Concentration olivier
# Taxons responsables (API)
pollen.pollen_resp # Chaîne brute de l'API
```
#### 3. Méthodes helper spécialisées
```python
# Détection d'alertes
pollen.is_alert_active() # → False
# Analyse des niveaux
pollen.get_highest_pollen() # → ("arm", 2.0)
pollen.get_highest_concentration() # → ("gram", 28.0)
pollen.get_dangerous_pollens() # → [] (niveau ≥ 4)
# Taxons responsables (parsing intelligent)
pollen.get_responsible_pollens() # → ["Armoise", "Graminées"]
# Données quantitatives
pollen.get_concentrations() # → {"gram": 28.0, "arm": 5.9, ...}
# Résumé complet avec styles d'émojis
pollen.get_pollens_summary("round") # Style rond par défaut
pollen.get_pollens_summary("square") # Style carré
```
#### 4. Structure du résumé complet
```python
summary = pollen.get_pollens_summary()
# Retourne pour chaque taxon :
{
'code': 2.0, # Niveau de pollen
'concentration': 5.9, # Grains/m³
'qualificatif': 'Faible', # Texte du niveau
'espece': 'Armoise', # Nom complet
'couleur': ('#50CCAA', (80, 204, 170)), # Hex + RGB
'emoji': '🟢', # Émoji selon style
'emoji_round': '🟢', # Émoji rond
'emoji_square': '🟩' # Émoji carré
}
```
### Exemple de sortie
```
📍 Station sélectionnée: Tomblaine
🎯 5. TAXONS RESPONSABLES DE L'INDICE (API):
• get_responsible_pollens(): ['Armoise', 'Graminées']
→ Espèces responsables selon l'API: Armoise, Graminées
🔬 3. CONCENTRATION LA PLUS ÉLEVÉE:
• get_highest_concentration(): ('gram', 28.0)
→ Espèce: Graminées
→ Concentration: 28.0 grains/m³
🎨 8. TEST DES STYLES D'ÉMOJIS:
• Comparaison des styles par défaut:
- Armoise: Rond=🟢 | Carré=🟩
- Graminées: Rond=🟢 | Carré=🟩
```
### Usage
```bash
python demo_pollen_functions.py
```
---
## 🔧 Fonctionnalités communes
### Système d'émojis unifié
Les deux scripts démontrent le système d'émojis centralisé :
```python
# Styles disponibles
"round" → 🟢 🟡 🔴 🟣 ⚫ # Formes rondes
"square" → 🟩 🟨 🟥 🟪 ⬛ # Formes carrées
# Usage dans les classes
atmo.get_emoji("round") # IndiceAtmo
pollen.get_pollens_summary("square") # IndicePollen avec style
```
### Conformité réglementaire
Les deux scripts valident la conformité avec :
- **Arrêté du 10 juillet 2020** (réglementation ATMO)
- **Notice technique du 1er avril 2025** (spécifications officielles)
- **Structure des données API** (pages 12-14 de la notice)
- **Codes couleur officiels** (tableau page 6 de la notice)
### Gestion des erreurs
```python
try:
# Connexion API
client = AtmoDataClient()
success = client.auto_login()
# Récupération données
data = client.get_indices_atmo(...)
except AtmoDataException as e:
print(f"❌ Erreur API: {e}")
except Exception as e:
print(f"❌ Erreur inattendue: {e}")
```
---
## 📋 Informations techniques
### Prérequis
- **Python 3.7+**
- **Fichier `credentials.json`** configuré
- **Connexion internet** pour l'API
- **Modules** : `atmo_data_client`, `atmo_data_models`, `constantes`
### Données utilisées
- **ATMO** : 1288+ stations Île-de-France
- **Pollen** : Données Tomblaine (Grand Est)
- **Format** : GeoJSON avec objets typés
- **Temps réel** : Date du jour d'exécution
### Structure de sortie
1. **Connexion et récupération** des données
2. **Propriétés de base** (héritées)
3. **Propriétés spécifiques** (ATMO/Pollen)
4. **Méthodes helper** (une par une)
5. **Exemples pratiques** d'utilisation
6. **Informations techniques** et conformité
### Performance
- **Temps d'exécution** : ~3-5 secondes
- **Données récupérées** : 1-1500 objets selon l'endpoint
- **Mémoire** : ~10-50 MB selon le volume
- **Connexions** : 1 session HTTP réutilisée
---
## 💡 Cas d'usage
### Pour les développeurs
- **Apprentissage** : Comprendre l'utilisation de chaque méthode
- **Tests** : Valider le bon fonctionnement des classes
- **Intégration** : Voir des exemples concrets d'usage
- **Débogage** : Identifier les problèmes potentiels
### Pour la documentation
- **Exemples vivants** : Code qui fonctionne avec vraies données
- **Référence complète** : Toutes les méthodes illustrées
- **Validation** : Preuve de conformité réglementaire
- **Formation** : Support pour apprendre l'API
### Pour les tests
- **Validation fonctionnelle** : Chaque méthode testée
- **Tests d'intégration** : API + classes + méthodes
- **Tests de régression** : Vérifier les mises à jour
- **Tests de conformité** : Respect des spécifications
---
## 🚀 Extensions possibles
### Nouvelles fonctionnalités
- **Comparaison multi-dates** : Évolution dans le temps
- **Analyse géographique** : Comparaison multi-zones
- **Export des résultats** : Sauvegarde des démonstrations
- **Mode interactif** : Choix de la zone/date par l'utilisateur
### Optimisations
- **Cache des données** : Éviter les appels répétés
- **Mode offline** : Utilisation de données pré-enregistrées
- **Parallélisation** : Récupération simultanée des données
- **Configuration** : Paramètres externalisés
---
## 📞 Support
### En cas de problème
1. **Vérifier** le fichier `credentials.json`
2. **Tester** la connexion API manuellement
3. **Contrôler** la disponibilité des données (weekend/jours fériés)
4. **Consulter** les logs d'erreurs détaillés
### Maintenance
- **Mise à jour** selon les évolutions de l'API
- **Adaptation** aux nouvelles spécifications réglementaires
- **Extension** pour de nouveaux types de données
- **Optimisation** des performances selon l'usage
---
## 📈 Script 3 : `demo_emission_functions.py`
### Description générale
Script de démonstration complet pour la classe `EmissionData`, illustrant toutes les fonctionnalités liées aux données d'émissions atmosphériques.
### Configuration
- **Zones testées** : Principales métropoles françaises (Nancy, Strasbourg, Metz, Paris, Lyon, Marseille, etc.)
- **Source de données** : Données simulées basées sur des valeurs réalistes
- **Type** : Émissions par polluant et par secteur d'activité
- **Format** : GeoJSON avec coordonnées géographiques
### Sections démonstrées
#### 1. Propriétés de base (héritées d'AtmoDataBase)
```python
# Informations générales
emission.code # Code zone (INSEE)
emission.name # Nom de la zone
emission.aasqa # Code AASQA
emission.source # Nom de l'organisme
emission.date_maj # Date de mise à jour
# Données démographiques
emission.population # Nombre d'habitants
emission.superficie # Superficie en km²
# Coordonnées géographiques
emission.has_coordinates() # Disponibilité coordonnées
emission.coordinates # Objet Coordinates
```
#### 2. Propriétés spécifiques émissions
```python
# Émissions par polluant (tonnes/an)
emission.nox # Oxydes d'azote
emission.pm10 # Particules PM10
emission.pm25 # Particules PM2.5
emission.ges # Gaz à effet de serre (CO2 eq.)
# Secteur d'activité
emission.code_pcaet # Code secteur PCAET
```
#### 3. Méthodes helper essentielles
```python
# Calculs de densité
emission.get_emission_density('nox') # → 8.36 tonnes/km²
emission.get_emission_density('pm10') # → 2.85 tonnes/km²
# Calculs par habitant
emission.get_emission_per_capita('nox') # → 0.001197 t/hab/an
emission.get_emission_per_capita('ges') # → 0.008108 t/hab/an
# Données globales
emission.get_total_emissions() # → Dict complet
emission.get_secteur_name() # → "Transport routier"
```
#### 4. Fonctions centralisées (base)
```python
# Émojis et couleurs (niveau fictif pour visualisation)
emission.get_emoji_by_level(3, "round") # → "🟡"
emission.get_color_by_level(3) # → ("#F0E641", (240, 230, 65))
```
### Exemple de sortie
```
Zone: Nancy
Population: 104,885 habitants
Superficie: 15.01 km²
Émissions totales:
- NOx: 125.5 tonnes/an
- PM10: 45.2 tonnes/an
- PM2.5: 28.7 tonnes/an
- GES: 850.3 tonnes/an
Densités d'émission par km²:
- NOx: 8.36 tonnes/km²
- PM10: 3.01 tonnes/km²
Émissions par habitant:
- NOx: 1.20 kg/hab/an
- GES: 8.11 kg/hab/an
```
### Usage
```bash
python demo_emission_functions.py
```
---
## 🚨 Script 4 : `demo_episode_functions.py`
### Description générale
Script de démonstration complet pour la classe `EpisodePollution`, illustrant toutes les fonctionnalités liées aux épisodes de pollution.
### Configuration
- **Zones testées** : Principales agglomérations françaises
- **Types d'épisodes** : Information, Recommandation, Alerte
- **Polluants** : NO2, SO2, O3, PM10, PM2.5
- **Format** : GeoJSON avec géométries variées (Point, MultiPolygon)
### Sections démonstrées
#### 1. Propriétés de base (héritées d'AtmoDataBase)
```python
# Informations générales
episode.aasqa # Code AASQA
episode.lib_zone # Zone affectée
episode.source # Nom de l'organisme
episode.date_maj # Date de mise à jour
# Informations temporelles
episode.date_dif # Date de diffusion
episode.date_ech # Date d'échéance
# Géométrie
episode.geometry # Géométrie GeoJSON
episode.has_coordinates() # Coordonnées disponibles
```
#### 2. Propriétés spécifiques épisodes
```python
# Polluant concerné
episode.code_pol # Code polluant (1-6)
episode.lib_pol # Nom polluant
episode.code_zone # Code de la zone
episode.etat # État de l'épisode
# États typiques:
# "PAS DE DEPASSEMENT"
# "PROCEDURE D'INFORMATION"
# "PROCEDURE D'INFORMATION ET DE RECOMMANDATION"
# "PROCEDURE D'ALERTE"
# "ALERTE NIVEAU 1"
```
#### 3. Méthodes helper spécialisées
```python
# Analyse des alertes
episode.is_alert_active() # → True/False
episode.get_alert_level() # → "Information"/"Alerte"/"Aucune"
# Analyse des polluants
episode.get_polluant_code() # → "PM10" (normalisé)
# Analyse géométrique
episode.is_geometry_complex() # → True si MultiPolygon
```
#### 4. Mapping des codes polluants
```python
# Codes API → Codes normalisés
'1' → 'NO2' # Dioxyde d'azote
'2' → 'SO2' # Dioxyde de soufre
'3' → 'O3' # Ozone
'5' → 'PM10' # Particules PM10
'6' → 'PM2.5' # Particules PM2.5
```
### Exemple de sortie
```
Zone affectée: Agglomération de Nancy
Polluant: PM10 (Code: 5)
Code polluant normalisé: PM10
État: PROCEDURE D'INFORMATION ET DE RECOMMANDATION
Alerte active: ✓
Niveau d'alerte: Information
Géométrie complexe: ✓
Comparaison des épisodes:
Zone Polluant Niveau Alerte État
Paris PM10 Information ✓ PROCEDURE D'INFORMATION
Lyon O3 Alerte ✓ PROCEDURE D'ALERTE
Marseille PM2.5 Alerte ✓ ALERTE NIVEAU 1
Strasbourg NO2 Aucune ✗ PAS DE DEPASSEMENT
```
### Usage
```bash
python demo_episode_functions.py
```
---
## 🔧 Fonctionnalités communes (scripts 3-4)
### Gestion des données manquantes
Les deux nouveaux scripts démontrent la robustesse face aux cas particuliers :
```python
# Cas gérés automatiquement
- Population = 0 → émission par habitant = 0
- Superficie = 0 → densité = 0
- État vide → alerte = False
- Code inconnu → valeur par défaut
- Géométrie None → coordonnées = None
```
### Calculs avancés
```python
# EmissionData
emission.get_emission_density('nox') # Densité spatiale
emission.get_emission_per_capita('ges') # Impact par habitant
emission.get_total_emissions() # Vue d'ensemble
# EpisodePollution
episode.is_alert_active() # Détection automatique
episode.get_alert_level() # Classification
episode.is_geometry_complex() # Type de zone
```
### Analyses comparatives
```python
# Comparaison inter-zones (EmissionData)
for emission in emissions:
nox_per_cap = emission.get_emission_per_capita('nox') * 1000
print(f"{emission.name}: {nox_per_cap:.1f} kg/hab/an")
# Analyse d'alertes (EpisodePollution)
alertes_actives = sum(1 for ep in episodes if ep.is_alert_active())
print(f"Alertes actives: {alertes_actives}/{len(episodes)}")
```
---
## 📋 Informations techniques mises à jour
### Couverture complète du datamodel
Les 4 scripts couvrent désormais **toutes les classes** du datamodel :
- ✅ **IndiceAtmo** : Qualité de l'air (script 1)
- ✅ **IndicePollen** : Indices pollen (script 2)
- ✅ **EmissionData** : Données d'émissions (script 3)
- ✅ **EpisodePollution** : Épisodes de pollution (script 4)
### Méthodes testées (au total)
**Classes spécialisées** : 25+ méthodes spécifiques
**Classe de base** : 8 méthodes héritées
**Propriétés** : 50+ propriétés documentées
**Cas particuliers** : 15+ scénarios de robustesse
### Types de données
- **Indices temps réel** : Qualité air + Pollen
- **Données statistiques** : Émissions par secteur
- **Alertes dynamiques** : Épisodes de pollution
- **Géométries** : Point, MultiPolygon, coordonnées
### Performance globale
- **Temps d'exécution** : 3-5 secondes par script
- **Données traitées** : 1-1500 objets selon le type
- **Mémoire** : 10-50 MB selon le volume
- **Robustesse** : Gestion complète des erreurs
---
## 💡 Cas d'usage étendus
### Analyse environnementale complète
```python
# Workflow complet avec les 4 classes
indices = get_indices_atmo(region="idf") # Qualité air actuelle
pollens = get_indices_pollen(ville="nancy") # Allergènes
emissions = get_emissions(zone="metropole") # Sources pollution
episodes = get_episodes(region="nationale") # Alertes actives
# Analyse croisée
if any(ep.is_alert_active() for ep in episodes):
# Corréler avec indices et émissions
responsible_sources = analyze_emission_sources(emissions)
current_quality = analyze_air_quality(indices)
```
### Surveillance réglementaire
```python
# Conformité notice officielle (tous scripts)
- Respect codes couleur officiels
- Validation structure données API
- Gestion coordonnées réglementaires
- Calculs selon arrêté du 10 juillet 2020
```
### Applications métier
- **Collectivités** : Surveillance qualité air + Alertes
- **Santé publique** : Pollens + Épisodes + Indices
- **Industrie** : Émissions + Conformité réglementaire
- **Recherche** : Données complètes + Analyses croisées
---
*Documentation complète pour les 4 scripts de démonstration du wrapper API Atmo Data*
*Conforme à la notice officielle du 1er avril 2025*

1433
docs/JOURNAL.md Normal file

File diff suppressed because it is too large Load diff

157
docs/QUICKSTART.md Normal file
View file

@ -0,0 +1,157 @@
# Guide de démarrage rapide
## 1. Configuration initiale
### Installer le package
```bash
# Installation depuis le repository local
pip install -e .
# Ou installation des dépendances seulement
pip install requests>=2.25.0
```
### Configurer vos credentials
```bash
# Option 1: Script automatique
python examples/setup_credentials.py
# Option 2: Manuelle
cp credentials.json.example credentials.json
# Puis éditer credentials.json avec vos identifiants
```
## 2. Test de connexion
```bash
python tests/test_real_connection.py
```
## 3. Premier script
```python
from atmo_data_wrapper import AtmoDataClient
# Connexion automatique
client = AtmoDataClient()
client.auto_login()
# Récupérer les indices ATMO d'Île-de-France
indices = client.get_indices_atmo(aasqa="11")
# Afficher les résultats
print(f"Récupéré {len(indices)} indices")
for indice in indices:
print(f"{indice.lib_zone}: {indice.get_qualificatif()}")
# Afficher la source des données (obligatoire)
from atmo_data_wrapper import get_atmo_licence
print(f"\nSource: {get_atmo_licence('courte')}")
```
## 4. Exemples disponibles
### Scripts d'introduction
- `examples/example_usage.py` - Tous les endpoints avec données brutes
- `examples/example_data_models.py` - Objets typés et méthodes helper
- `examples/example_save_files.py` - Sauvegarde en différents formats
- `examples/example_aasqa_utilities.py` - Fonctions utilitaires AASQA
- `examples/demo_licence_atmo.py` - Mentions légales et licence
### Scripts de démonstration complète
- `demos/demo_atmo_functions.py` - **Toutes les fonctionnalités IndiceAtmo**
- `demos/demo_pollen_functions.py` - **Toutes les fonctionnalités IndicePollen**
- `demos/demo_emission_functions.py` - **Toutes les fonctionnalités EmissionData**
- `demos/demo_episode_functions.py` - **Toutes les fonctionnalités EpisodePollution**
**Recommandé** : Commencer par les scripts de démonstration pour voir toutes les possibilités !
## 5. Structure des données
### Objets typés disponibles
- **IndiceAtmo** : Qualité de l'air avec helpers (couleurs, qualificatifs, émojis)
- **EpisodePollution** : Épisodes avec niveaux d'alerte et géométries
- **EmissionData** : Émissions avec calculs par habitant/km² et secteurs
- **IndicePollen** : Pollens avec détection des risques et taxons responsables
### Méthodes utiles
```python
# Qualité de l'air
indice.get_qualificatif() # "Bon", "Moyen", etc.
indice.get_color() # Couleur hex et RGB
indice.get_emoji("round") # Émojis ronds 🟢
indice.get_emoji("square") # Émojis carrés 🟩
indice.is_poor_quality() # Bool
# Pollens
pollen.get_responsible_pollens() # ["Armoise", "Graminées"]
pollen.get_pollens_summary() # Détail avec émojis
# Émissions et épisodes
emission.get_emission_per_capita('nox') # kg/hab/an
episode.get_alert_level() # "Information"/"Alerte"
# Collections
indices.get_statistics() # Stats globales
indices.filter_by_aasqa("11") # Filtrage par région
indices.filter_by_coordinates(...) # Filtrage géographique
```
## 6. Formats de sortie
- **GeoJSON** (défaut) → Objets typés avec méthodes helper
- **CSV** → Dictionnaires Python bruts
## 7. Sauvegarde
```python
# Sauvegarder les résultats
client.save_to_file(indices, "data/indices", "json")
client.save_to_file(indices, "data/indices", "csv")
client.save_to_file(indices, "data/indices", "geojson")
```
## 8. Codes utiles
### Régions (AASQA)
```python
from atmo_data_wrapper import get_aasqa_by_department, get_aasqa_info
# Trouver l'AASQA d'un département
aasqa = get_aasqa_by_department("54") # Nancy -> "44" (Grand Est)
# Infos complètes
info = get_aasqa_info("44")
print(info['organisme']) # "ATMO Grand-Est"
print(info['site_web']) # URL officielle
```
Codes principaux :
- `11` : Île-de-France (Airparif)
- `44` : Grand Est (ATMO Grand-Est)
- `93` : PACA (AtmoSud)
- `84` : Auvergne-Rhône-Alpes
### Polluants
- `NO2`, `SO2`, `O3`, `PM10`, `PM2.5`
## 9. Dépannage
```bash
# Vérifier la configuration
python tests/test_credentials_system.py
# Test complet
python tests/test_real_connection.py
# Debug d'un endpoint spécifique
python -c "
from atmo_data_wrapper import AtmoDataClient
client = AtmoDataClient()
client.auto_login()
print(client.get_indices_atmo())
"
# Exécution des tests
python -m pytest tests/
```

537
docs/README.md Normal file
View file

@ -0,0 +1,537 @@
# Wrapper Python pour l'API Atmo Data
Ce projet fournit un wrapper Python pour l'API Atmo Data ([https://admindata.atmo-france.org](https://admindata.atmo-france.org)), permettant d'accéder facilement aux données de qualité de l'air et de pollution des Associations agréées de surveillance de la qualité de lair (AASQA) françaises.
Atmo Data est un agrégateur national des données produites par les AASQA en accès libre et sous licence ODbL, géré par Atmo France ([https://www.atmo-france.org](https://www.atmo-france.org)), la Fédération des Associations agréées de surveillance de la qualité de l'air.
Elle met à disposition 6 flux agrégés sur les 4 thèmes suivants :
- [indice ATMO de la qualité de l'air](https://www.atmo-france.org/article/lindice-atmo)
- [les épisodes de pollution](https://www.atmo-france.org/article/les-episodes-de-pollution)
- les émissions à l'échelle de la région et de l'établissement public de coopération intercommunale (EPCI)
- [l'indice pollen](https://www.atmo-france.org/article/indice-pollen)
Chacun peut donc bénéficier gratuitement de ces données mises en open data sous licence ODbL, en indiquant la source "Atmo France et les Associations agréées de surveillance de la qualité de lair" ou "Atmo France / AASQA" dans sa version courte.
La documentation de l'API est disponible en ligne à l'adresse : [https://admindata.atmo-france.org/api/doc/v2](https://admindata.atmo-france.org/api/doc/v2).
La Notice technique et dinformation des données open data sur la qualité de lair disponibles sur Atmo Data (Version 1er avril 2025) est disponible en ligne à l'adresse : [https://www.atmo-france.org/sites/federation/files/medias/documents/2025-04/notice_Atmo_Data_1eravril2025.pdf](https://www.atmo-france.org/sites/federation/files/medias/documents/2025-04/notice_Atmo_Data_1eravril2025.pdf).
## Structure du projet
```
atmo-data-wrapper/
├── atmo_data_wrapper/ # Package principal
│ ├── __init__.py # Exports principaux du package
│ └── core/ # Module central
│ ├── __init__.py # Exports du module core
│ ├── client.py # Client principal pour l'API
│ ├── models.py # Classes pour les données typées
│ ├── constants.py # Constantes et configurations
│ ├── utils.py # Fonctions utilitaires
│ └── exceptions.py # Exceptions personnalisées
├── archives/ # Anciens scripts (historique)
├── examples/ # Scripts d'exemples
│ ├── __init__.py
│ ├── example_usage.py # Exemples d'utilisation des endpoints
│ ├── example_save_files.py # Exemples de sauvegarde
│ ├── example_data_models.py # Exemples avec objets typés
│ ├── example_aasqa_utilities.py # Exemples fonctions utilitaires AASQA
│ ├── example_aasqa_advanced.py # Analyses avancées AASQA
│ ├── example_synthese_nancy.py # Exemple synthèse complète
│ └── setup_credentials.py # Configuration des credentials
├── demos/ # Scripts de démonstration complète
│ ├── __init__.py
│ ├── demo_atmo_functions.py # Démonstration complète IndiceAtmo
│ ├── demo_pollen_functions.py # Démonstration complète IndicePollen
│ ├── demo_emission_functions.py # Démonstration complète EmissionData
│ ├── demo_licence_atmo.py # Démonstration licence Atmo France
│ └── demo_episode_functions.py # Démonstration complète EpisodePollution
├── tests/ # Tests unitaires
│ ├── __init__.py
│ ├── test_validations.py # Tests de validation
│ ├── test_save_functionality.py # Tests de sauvegarde
│ ├── test_typed_client.py # Tests d'intégration modèles
│ ├── test_real_connection.py # Test de connexion réelle à l'API
│ └── test_credentials_system.py # Tests du système de credentials
├── docs/ # Documentation
│ ├── README.md # Documentation principale
│ ├── QUICKSTART.md # Guide de démarrage rapide
│ ├── DOCUMENTATION_DEMOS.md # Documentation des scripts de démonstration
│ ├── JOURNAL.md # Journal de développement
│ ├── notice_Atmo_Data_1eravril2025.md # Notice officielle API
│ └── swagger.json # Spécification API
├── credentials.json.example # Modèle de fichier credentials
├── credentials.json # Vos identifiants (à créer, ignoré par git)
├── setup.py # Configuration du package
├── pyproject.toml # Configuration moderne du package
├── requirements.txt # Dépendances de production
├── requirements-dev.txt # Dépendances de développement
├── MANIFEST.in # Fichiers à inclure dans le package
├── LICENSE # Licence MIT
└── .gitignore # Fichiers à ignorer par git
```
## Installation
### Installation depuis le repository local
```bash
# Cloner ou télécharger le project
git clone https://github.com/atmo-france/atmo-data-wrapper.git
cd atmo-data-wrapper
# Installation en mode développement
pip install -e .
# Ou installation normale
pip install .
```
### Dépendances
Le package nécessite Python 3.7+ et les dépendances suivantes :
```bash
pip install requests>=2.25.0
```
### Installation des dépendances de développement
```bash
pip install -r requirements-dev.txt
```
## Configuration des credentials
### 0. Pré requis : Demander l'autorisation
L'accès aux données requiert l'autorisation d'un d'administrateur de l'application API Atmo Data. Vous pouvez réaliser une demande de création de compte à cette adresse : https://admindata.atmo-france.org/inscription-api.
Pour vous connecter à l'API Atmo Data, vous devez créer un fichier de configuration avec vos identifiants.
### 1. Créer le fichier credentials.json
```bash
# Copier le fichier exemple
cp credentials.json.example credentials.json
# Éditer avec vos identifiants
nano credentials.json
```
### 2. Format du fichier credentials.json
```json
{
"username": "votre_nom_utilisateur",
"password": "votre_mot_de_passe",
"api_url": "https://api.atmo-data.org"
}
```
**Important** :
- Le fichier `credentials.json` est ajouté au `.gitignore` pour éviter de commiter vos identifiants
- Remplacez les valeurs par vos vrais identifiants fournis par Atmo Data
- L'URL de l'API peut être modifiée si nécessaire
## Utilisation
### Authentification
#### Méthode recommandée : Fichier credentials.json
```python
from atmo_data_wrapper import AtmoDataClient, AASQA_CODES, POLLUANTS
# Connexion automatique avec credentials.json
client = AtmoDataClient()
client.auto_login() # Utilise automatiquement credentials.json
```
## Fonctions utilitaires AASQA
Le wrapper fournit des fonctions utilitaires pour faciliter la manipulation des codes AASQA et la recherche d'organismes :
```python
from atmo_data_wrapper import (
get_aasqa_by_department,
get_aasqa_info,
search_aasqa_by_name,
get_departments_count,
validate_department_coverage,
get_aasqa_statistics
)
# Trouver l'AASQA d'un département
aasqa_code = get_aasqa_by_department("54") # Nancy
print(f"Département 54 -> AASQA {aasqa_code}") # "44" (Grand Est)
# Informations complètes d'une AASQA
info = get_aasqa_info("44")
print(f"Organisme: {info['organisme']}") # "ATMO Grand-Est"
print(f"Site web: {info['site_web']}") # URL officielle
print(f"Départements: {info['departements']}") # Liste des départements
# Recherche par nom
resultats = search_aasqa_by_name("Atmo")
for r in resultats:
print(f"{r['organisme']} - {r['region']}")
# Statistiques
stats = get_aasqa_statistics()
print(f"Nombre total d'AASQA: {stats['total_aasqa']}")
print(f"Couverture moyenne: {stats['average_coverage']:.1f} départements/AASQA")
```
## Licence et mentions légales
Conformément aux exigences d'Atmo France, le wrapper fournit des fonctions pour afficher les mentions légales requises :
```python
from atmo_data_wrapper import get_atmo_licence, print_atmo_licence
# Version courte (recommandée)
print(get_atmo_licence("courte"))
# "Atmo France / AASQA"
# Version longue (officielle)
print(get_atmo_licence("longue"))
# "Atmo France et les Associations agréées de surveillance de la qualité de l'air"
# Version complète avec licence ODbL
print_atmo_licence("complete")
# Affiche les détails complets de la licence
# Utilisation dans un script
data = client.get_indices_atmo()
print(f"Source: {get_atmo_licence('courte')}")
```
#### Méthode alternative : Identifiants directs
```python
# Connexion avec identifiants fournis directement
client = AtmoDataClient()
client.login("votre_username", "votre_password")
# Ou mélange des deux (override du fichier)
client.login(username="autre_user") # password vient du fichier
```
### Récupération des indices ATMO
```python
# Indices d'aujourd'hui
indices = client.get_indices_atmo()
# Indices d'une date spécifique
indices = client.get_indices_atmo(date="2024-06-08")
# Indices pour une région spécifique (utilisation des constantes)
indices = client.get_indices_atmo(aasqa="11") # AASQA_CODES['11'] = Île-de-France
```
### Épisodes de pollution
```python
# Épisodes en cours (3 jours)
episodes = client.get_episodes_3jours()
# Épisodes historiques
episodes = client.get_episodes_historique(date="2024-06-08")
# Filtre par polluant (utilisation des constantes)
episodes = client.get_episodes_3jours(polluant=POLLUANTS[2]) # PM10
```
### Données d'émissions
```python
# Émissions par région
emissions = client.get_emissions(echelle="region")
# Émissions par EPCI
emissions = client.get_emissions(echelle="epci", aasqa="11")
```
### Indices pollen
```python
# Indices pollen actuels
pollens = client.get_indices_pollens()
# Avec alertes uniquement
pollens = client.get_indices_pollens(alerte=True)
```
## Objets typés et modèles de données
Le wrapper utilise des objets typés pour faciliter l'exploitation des données. Chaque type de données retourné par l'API est représenté par une classe spécialisée avec des méthodes helper.
### Types de données disponibles
- **IndiceAtmo** : Indices de qualité de l'air ATMO (0-7)
- **EpisodePollution** : Épisodes de pollution atmosphérique
- **EmissionData** : Données d'inventaires des émissions
- **IndicePollen** : Indices polliniques (0-6)
- **AtmoDataCollection** : Collection gérant plusieurs objets
### Utilisation avec objets typés
```python
from atmo_data_wrapper import AtmoDataClient, Coordinates
client = AtmoDataClient()
client.auto_login() # Connexion avec credentials.json
# Les données GeoJSON retournent des objets typés
indices = client.get_indices_atmo(aasqa="11") # Retourne AtmoDataCollection
# Parcourir les données avec méthodes helper
for indice in indices:
print(f"Zone: {indice.lib_zone}")
print(f"Qualité: {indice.get_qualificatif()}")
print(f"Couleur: {indice.get_color()[0]}") # Couleur hex
if indice.is_poor_quality():
worst_pol, code = indice.get_worst_pollutant()
print(f"Attention: {worst_pol} élevé ({code})")
# Statistiques sur la collection
stats = indices.get_statistics()
print(f"Qualité moyenne: {stats['quality_stats']['moyenne']:.1f}")
# Filtrage géographique
paris = Coordinates(2.3522, 48.8566)
nearby = indices.filter_by_coordinates(paris, radius_km=20)
```
### Classes et méthodes disponibles
#### IndiceAtmo
- `get_qualificatif()` : Qualificatif textuel (Bon, Moyen, etc.)
- `get_color()` : Couleur hex et RGB associée
- `is_good_quality()` / `is_poor_quality()` : Tests de qualité
- `get_worst_pollutant()` : Polluant le plus problématique
- `get_pollutants_summary()` : Résumé de tous les polluants
#### EpisodePollution
- `is_alert_active()` : Vérifie si une alerte est en cours
- `get_alert_level()` : Niveau d'alerte (Information/Alerte/Aucune)
- `get_polluant_code()` : Code du polluant principal
#### EmissionData
- `get_emission_density(polluant)` : Densité d'émission par km²
- `get_emission_per_capita(polluant)` : Émission par habitant
- `get_total_emissions()` : Toutes les émissions
- `get_secteur_name()` : Nom du secteur d'émission
#### IndicePollen
- `is_alert_active()` : Alerte pollen active
- `get_highest_pollen()` : Espèce avec l'indice le plus élevé
- `get_dangerous_pollens()` : Liste des pollens à risque élevé
- `get_pollens_summary()` : Détail par espèce avec émojis
- `get_responsible_pollens()` : Taxons responsables selon l'API
#### AtmoDataCollection
- `filter_by_aasqa(code)` : Filtrage par région
- `filter_by_coordinates(center, radius)` : Filtrage géographique
- `get_statistics()` : Statistiques de la collection
- `to_summary()` : Résumé textuel
## API Endpoints
- `get_indices_atmo()` - Indices de qualité de l'air ATMO
- `get_episodes_3jours()` - Épisodes de pollution sur 3 jours
- `get_episodes_historique()` - Épisodes de pollution historiques
- `get_emissions()` - Inventaires des émissions
- `get_indices_pollens()` - Indices pollen
## Codes AASQA
Le wrapper fournit des constantes enrichies avec sites web et départements couverts :
- 01: Guadeloupe (Gwad'Air) - Département 971
- 02: Martinique (Madininair) - Département 972
- 03: Guyane (Atmo Guyane) - Département 973
- 04: La Réunion (Atmo Réunion) - Département 974
- 06: Mayotte (Hawa Mayotte) - Département 976
- 11: Île-de-France (Airparif) - Départements 75, 77, 78, 91, 92, 93, 94, 95
- 24: Centre-Val de Loire (Lig'Air) - Départements 18, 28, 36, 37, 41, 45
- 27: Bourgogne-Franche-Comté (Atmo BFC) - Départements 21, 25, 39, 58, 70, 71, 89, 90
- 28: Normandie (Atmo Normandie) - Départements 14, 27, 50, 61, 76
- 32: Hauts-de-France (Atmo HDF) - Départements 02, 59, 60, 62, 80
- 44: Grand Est (ATMO Grand-Est) - Départements 08, 10, 51, 52, 54, 55, 57, 67, 68, 88
- 52: Pays de la Loire (Air Pays de la Loire) - Départements 44, 49, 53, 72, 85
- 53: Bretagne (Air Breizh) - Départements 22, 29, 35, 56
- 75: Nouvelle-Aquitaine (Atmo NA) - Départements 16, 17, 19, 23, 24, 33, 40, 47, 64, 79, 86, 87
- 76: Occitanie (Atmo Occitanie) - Départements 09, 11, 12, 30, 31, 32, 34, 46, 48, 65, 66, 81, 82
- 84: Auvergne-Rhône-Alpes (Atmo AURA) - Départements 01, 03, 07, 15, 26, 38, 42, 43, 63, 69, 73, 74
- 93: Provence-Alpes-Côte d'Azur (AtmoSud) - Départements 04, 05, 06, 13, 83, 84
- 94: Corse (Qualitair) - Départements 2A, 2B
Accès programmatique :
```python
from atmo_data_wrapper import AASQA_CODES
info = AASQA_CODES['44']
print(f"Site web: {info['site_web']}")
print(f"Départements: {info['departements']}")
```
## Gestion des erreurs
Le wrapper utilise des exceptions personnalisées pour gérer différents types d'erreurs :
```python
from atmo_data_wrapper import AtmoDataClient, AtmoDataException
try:
client = AtmoDataClient()
client.auto_login()
indices = client.get_indices_atmo()
except ValueError as e:
print(f"Erreur de validation: {e}")
except AtmoDataException as e:
print(f"Erreur API: {e}")
except Exception as e:
print(f"Erreur inattendue: {e}")
```
### Types d'exceptions disponibles
- `AtmoDataException` : Exception de base
- `AuthenticationError` : Erreur d'authentification
- `ValidationError` : Erreur de validation des paramètres
- `APIError` : Erreur de l'API Atmo Data
- `NetworkError` : Erreur de réseau
- `DataError` : Erreur dans le traitement des données
## Validation des paramètres
Le wrapper valide automatiquement tous les paramètres avant d'envoyer les requêtes :
- **Formats** : Seuls `geojson` et `csv` sont acceptés
- **Codes AASQA** : Validation contre la liste des codes valides (01-94)
- **Polluants** : Validation contre la liste des polluants supportés
- **Dates** : Validation du format YYYY-MM-DD et de l'existence de la date
- **Indices** : Validation des codes qualificatifs ATMO (0-7) et pollen (0-6)
- **Bounding box** : Validation du format et de la cohérence des coordonnées
Exemple d'erreur de validation :
```python
try:
client.get_indices_atmo(format="xml") # Format invalide
except ValueError as e:
print(e) # "Format invalide: xml. Formats valides: ['geojson', 'csv']"
```
## Sauvegarde des données
Le wrapper permet de sauvegarder facilement les résultats de l'API dans différents formats :
```python
# Récupérer des données
indices = client.get_indices_atmo(aasqa="11")
# Sauvegarder en JSON
json_file = client.save_to_file(indices, "data/indices_atmo", "json")
# Sauvegarder en CSV
csv_file = client.save_to_file(indices, "data/indices_atmo", "csv")
# Sauvegarder en GeoJSON
geojson_file = client.save_to_file(indices, "data/indices_atmo", "geojson")
```
### Formats supportés pour la sauvegarde :
- `json` - Format JSON standard
- `csv` - Format CSV avec extraction automatique des propriétés
- `geojson` - Format GeoJSON (données géographiques)
### Fonctionnalités :
- **Création automatique de répertoires** : Les dossiers parents sont créés si nécessaire
- **Extensions automatiques** : Les extensions (.json, .csv, .geojson) sont ajoutées automatiquement
- **Extraction CSV intelligente** : Conversion automatique des données GeoJSON vers CSV avec coordonnées
- **Validation des formats** : Vérification de la compatibilité des données avec le format choisi
## Formats de sortie API
L'API supporte deux formats :
- `geojson` (par défaut)
- `csv`
## Tests et Exemples
### Scripts d'exemples
- `example_usage.py` - Exemples d'utilisation des endpoints (données brutes)
- `example_save_files.py` - Exemples de sauvegarde de données
- `example_data_models.py` - Exemples avec objets typés et méthodes helper
- `example_aasqa_utilities.py` - Exemples des fonctions utilitaires AASQA
- `example_aasqa_advanced.py` - Analyses avancées et statistiques AASQA
- `demo_licence_atmo.py` - Démonstration des fonctions de licence
- `example_synthese_tomblaine.py` - Exemple de synthèse complète (Atmo + Pollen)
### Scripts de démonstration complète
- `demo_atmo_functions.py` - **Démonstration complète de la classe IndiceAtmo**
- `demo_pollen_functions.py` - **Démonstration complète de la classe IndicePollen**
- `demo_emission_functions.py` - **Démonstration complète de la classe EmissionData**
- `demo_episode_functions.py` - **Démonstration complète de la classe EpisodePollution**
Ces scripts illustrent **toutes les fonctionnalités** de chaque classe avec des données réelles de l'API. Voir `DOCUMENTATION_DEMOS.md` pour une documentation détaillée.
### Scripts de test
- `test_validations.py` - Tests de validation des paramètres
- `test_save_functionality.py` - Tests de sauvegarde de fichiers
- `test_typed_client.py` - Tests d'intégration des modèles typés
- `test_real_connection.py` - Test de connexion réelle à l'API
### Test de votre configuration
```bash
# Vérifier la configuration et tester la connexion
python tests/test_real_connection.py
```
Ce script vérifie :
- La présence et validité du fichier `credentials.json`
- La connexion à l'API
- Le bon fonctionnement des différents endpoints
- Les objets typés et leurs méthodes
### Exécution des scripts
```bash
# Scripts d'exemples
python examples/example_usage.py
python examples/example_data_models.py
python examples/example_aasqa_utilities.py
python examples/demo_licence_atmo.py
# Scripts de démonstration
python demos/demo_atmo_functions.py
python demos/demo_pollen_functions.py
# Tests
python -m pytest tests/
```
## Nouvelles fonctionnalités
### Architecture améliorée
- **Séparation des constantes et utilitaires** : Les constantes sont dans `constants.py`, les fonctions utilitaires dans `utils.py`
- **Constantes enrichies** : Les codes AASQA incluent maintenant sites web et départements
- **Mapping des codes** : Constantes séparées pour les épisodes de pollution et les taxons de pollens
### Fonctions utilitaires AASQA
- Recherche par département ou nom d'organisme
- Statistiques de couverture territoriale
- Validation de l'intégrité des données
- Analyses comparatives entre organismes
### Conformité légale
- Fonctions dédiées pour les mentions légales Atmo France
- Respect de la licence ODbL
- Intégration facilitée dans rapports et applications
### Organisation du projet
- Dossier `archives/` pour l'historique des anciens scripts
- Structure claire et maintenable
- Documentation complète et à jour

View file

@ -0,0 +1,379 @@
# Notice Technique - Indice ATMO
**Guide de calcul en application de l'arrêté du 10 juillet 2020**
---
## Informations générales
- **Version** : 14 décembre 2020
- **Organisme** : Atmo France
- **Contact** : contact@atmo-france.org
- **Site web** : http://www.atmo-france.org
- **Adresse** : 7 rue Crillon
- **Téléphone** : 09 72 62 73 95
---
## Table des matières
1. [Préambule](#préambule)
2. [Historique mise en place](#1-historique--mise-en-place)
3. [Définitions](#2-définitions)
4. [Objectifs de l'indice ATMO](#3-objectifs-de-lindice-atmo)
5. [Règles de calcul de l'indice ATMO](#4-règles-de-calcul-de-lindice-atmo)
6. [Limites de l'indice](#5-limites-de-lindice)
7. [Annexes](#annexes)
---
## Préambule
L'indice ATMO est un indicateur journalier qualificatif de la qualité de l'air. Il se décline en six qualificatifs définis selon différentes classes pour cinq polluants : « bon », « moyen », « dégradé », « mauvais », « très mauvais », « extrêmement mauvais ».
Il intègre les polluants réglementés que l'on rencontre au quotidien :
- Les particules en suspension (PM₁₀ et PM₂.₅)
- Le dioxyde d'azote (NO₂)
- L'ozone (O₃)
- Le dioxyde de soufre (SO₂)
### Caractéristiques principales
- **Calcul obligatoire** sur l'ensemble du territoire français (France métropolitaine et outre-mer)
- **Périmètre géographique** ne pouvant dépasser celui de l'établissement public de coopération intercommunale (EPCI)
- **Zone plus fine** à l'échelle de la commune possible
- **Représentation simplifiée** de la qualité de l'air quotidienne en situation « de fond »
- **Calcul par les AASQA** à partir de données de modélisation, météorologiques, d'inventaire des émissions et d'observation
### Base réglementaire
Les modalités de calcul de cet indice sont précisées dans l'arrêté du 10 juillet 2020 relatif à l'indice de la qualité de l'air ambiant.
---
## 1) Historique mise en place
### Création et évolution
L'indice ATMO réglementaire a été créé en **1994** à l'initiative du ministère chargé de l'Environnement et de plusieurs Associations agréées de surveillance de la qualité de l'air (AASQA).
### Base légale
- **Article R 221-5** du code de l'environnement : prévoit la mise à disposition de l'indice de la qualité de l'air
- **Arrêté du 10 janvier 2000** : rend obligatoire le calcul de l'indice Atmo sur tout le territoire national
- **Arrêté du 22 juillet 2004** : remplace l'arrêté de 2000, modifié par l'arrêté du 21 décembre 2011
- **Arrêté du 10 juillet 2020** : abroge l'arrêté du 22 juillet 2004 et constitue la base actuelle
### Processus de révision
La révision de l'indice ATMO a été alimentée par une réflexion menée au sein d'un groupe de travail mandaté par le ministère en charge de l'Environnement en février 2018, piloté par Atmo France, et rassemblant :
- L'ensemble des AASQA
- Le Bureau de la qualité de l'air (BQA)
- Le Service de la donnée et des études statistiques (SDES) de la DGEC
### Alignement européen
Ce nouvel indice ATMO a été conçu comme une déclinaison de l'indice horaire de l'Agence européenne pour l'environnement, avec :
- Un mode de calcul différent pour fournir un indice journalier
- Une information sur l'ensemble du territoire (pas uniquement à la station)
- Une méthode d'agrégation spatiale grâce à la modélisation
---
## 2) Définitions
### Termes techniques
**Modèle méso-échelle** : Modèle fonctionnant sur un domaine de plusieurs centaines de kilomètres et dont la résolution de l'ordre de quelques kilomètres permet d'évaluer la qualité de l'air en situation de fond.
**Modèle fine échelle** : Modèle caractérisé par une résolution de l'ordre de quelques dizaines de mètres permettant l'évaluation de la qualité de l'air en proximité des sources (ex: trafic routier).
### Échéances temporelles
Les modèles de dispersion atmosphérique effectuent des calculs pour plusieurs échéances :
- **j-1** : la veille
- **j0** : le jour-même
- **j+1** : le lendemain
- **j+2** : le surlendemain
### Types de données
**Donnée assimilée** : Résulte de la combinaison des résultats de simulation et des observations pour limiter l'incertitude.
**Prévision statistique** : Révision issue de la correction statistique des simulations pour corriger les biais systématiques identifiés. Ce post-traitement utilise notamment les dernières mesures disponibles.
---
## 3) Objectifs de l'indice ATMO
Les objectifs de cet indice sont les suivants :
1. **Outil de communication** permettant de fournir une information synthétique sous une forme simple (couleur + qualificatif)
2. **Facilement compréhensible** par le grand public
3. **Qualifier la qualité de l'air** en situation de fond (pollution à laquelle personne n'échappe)
4. **Prévision** pour les journées J et J+1 a minima
5. **Couverture complète** de la zone de compétence de l'AASQA pour chacune de ses communes ou EPCI
---
## 4) Règles de calcul de l'indice ATMO
L'indice est calculé chaque jour. Il caractérise l'état de la qualité de l'air à l'échelle spatiale que l'AASQA aura jugée représentative. L'indice ATMO est diffusé soit à la commune, soit à l'EPCI, en fonction de ce que chaque AASQA considère pertinent sur son territoire.
### 4.1) Règle n°1 : Polluants concernés
Cinq polluants sont utilisés pour construire cet indice :
- **PM₂.₅** : Particules fines < 2.5 microns
- **PM₁₀** : Particules fines < 10 microns
- **O₃** : Ozone
- **NO₂** : Dioxyde d'azote
- **SO₂** : Dioxyde de soufre
### 4.2) Règle n°2 : Période prise en compte
- L'indice ATMO caractérise un état global de la qualité de l'air prévu sur **une journée**
- Calculé pour **24h** avec heure de départ à **0h TU**
- Diffusion quotidienne au plus tard à **12h (heure locale)** avec tolérance à 14h en cas de difficulté
### 4.3) Règle n°3 : Échelles des sous-indices
Un qualificatif est attribué à chaque polluant selon sa concentration. L'indice ATMO de la zone correspond au **qualificatif le plus dégradé**.
#### Tableau des seuils et qualificatifs
| Qualificatif | PM₂.₅ (μg/m³) | PM₁₀ (μg/m³) | NO₂ (μg/m³) | O₃ (μg/m³) | SO₂ (μg/m³) |
|--------------|---------------|--------------|-------------|------------|-------------|
| **Bon** | 0-10 | 0-20 | 0-40 | 0-50 | 0-100 |
| **Moyen** | 11-20 | 21-40 | 41-90 | 51-100 | 101-200 |
| **Dégradé** | 21-25 | 41-50 | 91-120 | 101-130 | 201-350 |
| **Mauvais** | 26-50 | 51-100 | 121-230 | 131-240 | 351-500 |
| **Très mauvais** | 51-75 | 101-150 | 231-340 | 241-380 | 501-750 |
| **Extrêmement mauvais** | >75 | >150 | >340 | >380 | >750 |
#### Codes couleur des qualificatifs
| Qualificatif | R | G | B | Hexadécimal |
|--------------|---|---|---|-------------|
| **Bon** | 80 | 240 | 230 | #50F0E6 |
| **Moyen** | 80 | 204 | 170 | #50CCAA |
| **Dégradé** | 240 | 230 | 65 | #F0E641 |
| **Mauvais** | 255 | 80 | 80 | #FF5050 |
| **Très mauvais** | 150 | 0 | 50 | #960032 |
| **Extrêmement mauvais** | 135 | 33 | 129 | #872181 |
#### Codes supplémentaires
| Qualificatif | R | G | B | Hexadécimal | Usage |
|--------------|---|---|---|-------------|-------|
| **Indisponible** | 221 | 221 | 221 | #DDDDDD | Absence de données |
| **Événement** | 136 | 136 | 136 | #888888 | Incident/accident |
### 4.4) Règle n°4 : Calcul de l'indice ATMO
#### Méthodologie préférentielle
Le calcul est réalisé **préférentiellement par modélisation** pour permettre une couverture complète du territoire pour J0 et J+1. À défaut, la mesure de fond peut être utilisée si elle est représentative.
#### Modèles méso-échelles (recommandés)
**Étapes de calcul** :
1. **En chaque point du modèle** : détermination du qualificatif et du/des polluant(s) responsable(s)
2. **Sélection des points** intersectant chaque commune
3. **Sélection du qualificatif le plus dégradé** parmi ceux calculés sur la commune
**Pour les communes sans point modèle** :
- Utiliser la valeur du point modèle le plus proche
- Sur-échantillonner le maillage sans interpolation
- Toute autre méthode garantissant la représentativité de fond
#### Calcul à l'EPCI
L'indice EPCI correspond à **l'indice communal le plus élevé** de l'EPCI.
### 4.5) Règle n°5 : Représentativité de la zone géographique
La représentativité spatiale de référence est donnée par les **stations de mesure de fond**. Selon la réglementation, les points de prélèvement sont représentatifs de plusieurs kilomètres carrés.
L'assimilation des données de ces stations et les prévisions statistiques associées doivent garantir la représentativité des résultats des modèles méso-échelles.
### 4.6) Règle n°6 : EPCI répartie entre deux régions
Les AASQA concernées par des EPCI à cheval entre deux régions se concertent pour une **communication cohérente**.
### 4.7) Règle n°7 : Calcul pour la veille
En cas de calcul d'un indice pour la veille (J-1), les données doivent s'appuyer sur des **données modélisées assimilées** (données brutes post-traitées pour intégrer les observations aux stations).
### 4.8) Règle n°8 : Diffusion et historique
#### Obligations de diffusion
Chaque AASQA doit publier les indices ATMO quotidiennement :
- **Flux WMS ou CSV** sur sa plateforme open data
- **Site internet** de l'AASQA
- **Informations obligatoires** : qualificatif, couleur, polluant(s) majoritaire(s)
#### Historique
En cas de calcul d'indice pour la veille, un **historique d'un an** de données en plus de l'année en cours est mis à disposition.
### 4.9) Règle n°9 : Événements spécifiques
En cas d'incident ou d'accident engendrant des émissions atmosphériques spécifiques rendant l'indice non représentatif, l'AASQA :
- **Informe** que l'indice n'est pas représentatif
- **Intègre** cette information dans le flux open data avec couleur et qualificatif spécifique (code "événement")
### 4.10) Règle n°10 : Cas particulier du SO₂
Lorsque l'AASQA mesure des niveaux importants de SO₂, notamment en cas d'incident industriel, elle peut :
- **Appliquer la règle n°9** (code événement)
- **Expertiser la situation** avec tous les outils disponibles et faire évoluer le sous-indice SO₂ en conséquence
---
## 5) Limites de l'indice
### Objectif et portée
L'indice ATMO permet de fournir une **information synthétique** sur la qualité globale de l'air de fond d'un territoire, dans l'état actuel des connaissances et de la réglementation.
### Ce que l'indice ne fait PAS
- **Ne caractérise pas** la qualité de l'air à proximité immédiate de sources fixes ou mobiles de pollution
- **Ne remplace pas** l'information spécifique lors des dépassements de seuils réglementaires
- **N'est pas adapté** pour les études épidémiologiques ou statistiques (utiliser les concentrations d'origine)
- **Ne rend pas compte** de la pollution cumulée sur de longues périodes (basé sur les seuils d'effet à court terme)
---
## Annexes
### Description du jeu de données - Indice ATMO 2021
#### Métadonnées générales
- **Jeu de données** : Indice Atmo de la qualité de l'air
- **Version** : 2021
- **Thème Inspire** : Zone de pollution potentielle
- **Résolution temporelle** : Jour
- **Résolution spatiale** : Commune ou EPCI
- **Fréquence de mise à jour** : Quotidienne à 14H locales
- **Profondeur des données** : Année N-1 complète + année en cours jusqu'à J+1 (voire J+2)
- **Licence** : ODbL 1.0 - Open Database Licence
#### Structure des flux de données
##### Flux WFS (obligatoire)
- **Type** : 1 seul WFS, 1 seule couche
- **Granularité** : Par commune ou EPCI sur la région
- **Polluants** : Tous dans la même couche
- **Profondeur** : N-1 et N jusqu'à J+1
- **Nom recommandé** : `ind_<région>` (ex: `ind_bretagne`)
##### Flux CSV (de secours)
- **Type** : 1 seul CSV, 1 seule table
- **Usage** : Obligatoire seulement si problème technique pour le WFS
- **Structure** : Identique au WFS
#### Champs obligatoires
| Nom du champ | Type | Description | Exemple |
|--------------|------|-------------|---------|
| **date_ech** | Date ISO 8601 | Date de valeur de l'indice (TU) | 2021-12-31T00:00:00Z |
| **code_qual** | Int | Classe de l'indice (1-6, 0 si absent, 7 si événement) | 1 |
| **lib_qual** | Varchar | Qualificatif textuel | Bon |
| **coul_qual** | Varchar | Couleur hexadécimale | #50F0E6 |
| **date_dif** | Date ISO 8601 | Date de diffusion (TU) | 2021-12-31T00:00:00Z |
| **source** | Varchar | Nom public de l'AASQA | Atmo Auvergne-Rhône-Alpes |
| **type_zone** | Varchar | Type de zone [commune, EPCI] | commune |
| **code_zone** | Varchar | Code commune INSEE ou code EPCI INSEE | 59350 |
| **lib_zone** | Varchar | Libellé commune ou EPCI | Marseille |
#### Codes par polluant
| Nom du champ | Type | Description | Exemple |
|--------------|------|-------------|---------|
| **code_no2** | Int | Classe sous-indice NO₂ (1-6, 0 si absent, 7 si événement) | 1 |
| **code_so2** | Int | Classe sous-indice SO₂ (1-6, 0 si absent, 7 si événement) | 2 |
| **code_o3** | Int | Classe sous-indice O₃ (1-6, 0 si absent, 7 si événement) | 3 |
| **code_pm10** | Int | Classe sous-indice PM₁₀ (1-6, 0 si absent, 7 si événement) | 4 |
| **code_pm25** | Int | Classe sous-indice PM₂.₅ (1-6, 0 si absent, 7 si événement) | 5 |
#### Coordonnées
| Nom du champ | Type | Description | Exemple |
|--------------|------|-------------|---------|
| **x_wgs84** | Float | Coordonnées en WGS84 EPSG:4326 | 3.0 |
| **y_wgs84** | Float | Coordonnées en WGS84 EPSG:4326 | 50.0 |
| **x_reg** | Float | Coordonnées réglementaires | 760889 |
| **y_reg** | Float | Coordonnées réglementaires | 6999650 |
| **epsg_reg** | Varchar | Système de projection réglementaire | 2154 |
| **geom** | Géométrie | Géométrie ponctuelle WKB | - |
#### Champs facultatifs - Concentrations
| Nom du champ | Type | Description |
|--------------|------|-------------|
| **conc_no2** | Int | Concentration de NO₂ en μg/m³ |
| **conc_so2** | Int | Concentration de SO₂ en μg/m³ |
| **conc_o3** | Int | Concentration d'O₃ en μg/m³ |
| **conc_pm10** | Int | Concentration de PM₁₀ en μg/m³ |
| **conc_pm25** | Int | Concentration de PM₂.₅ en μg/m³ |
#### Clé primaire
`date_ech` + `code_zone`
#### Exemple de gestion des versions
```
Je publie l'indice du 07-08-2020 en J+1 :
date_ech = 07-08-2020, date_diff = 06-08-2020
Je publie l'indice du 07-08-2020 en J+0 :
date_ech = 07-08-2020, date_diff = 07-08-2020
(remplace les valeurs précédentes)
Je publie l'indice du 07-08-2020 en J-1 (observé) :
date_ech = 07-08-2020, date_diff = 08-08-2020
(remplace les valeurs précédentes)
```
### Proposition méthodologique pour la représentativité
Pour analyser et garantir la représentativité spatiale des indices, il est proposé de comparer sur une année complète de données J-1 assimilées :
#### Méthodologie 1 (règle officielle)
**Indice du point modèle maximum** : Sélection de l'indice le plus dégradé parmi l'ensemble des points du modèle intersectant la commune.
#### Méthodologie 2 (validation)
**Comptage des points** : L'indice ayant le plus grand nombre de points du modèle intersectant la commune devient l'indice de la commune.
#### Méthodologie 3 (validation)
**Superficie pondérée** : Calcul par indice de la superficie cumulée intersectée entre les mailles du modèle et la commune. L'indice présentant la superficie la plus grande devient l'indice de la commune.
L'obtention de **résultats proches** entre ces méthodologies permet de valider l'échelle de diffusion retenue.
---
## Rédacteurs et validation
### Rédacteurs
- Yann Channac Mongredien (AtmoSud)
- Carole Flambard (Lig'Air)
- Delphine Guillaume (Atmo France)
- Jérôme Le Paih (ATMO Grand Est)
- Nathalie Pujol-Söhne (Atmo Hauts-de-France)
- Jérôme Rangognio (Lig'Air)
- Marine Tondelier (Atmo France)
- Romain Vidaud (Atmo Auvergne-Rhône-Alpes)
- Abderrazak Yahyaoui (Lig'Air)
### Relecteurs
- Cédric Messier, Julien Rude et Pascale Vizy (Bureau de la qualité de l'air / Ministère de la Transition écologique)
- Laurence Rouil (Ineris)
### Validation
**Validé en Comité de pilotage de surveillance le 15 décembre 2020**
---
*Document officiel - Atmo France - Version du 14 décembre 2020*

Binary file not shown.

1
docs/swagger.json Normal file

File diff suppressed because one or more lines are too long

3
examples/__init__.py Normal file
View file

@ -0,0 +1,3 @@
"""
Examples package for Atmo Data Wrapper
"""

View file

@ -0,0 +1,276 @@
#!/usr/bin/env python3
"""
Démonstration des fonctions utilitaires AASQA avancées
===================================================
Ce script illustre l'utilisation des nouvelles fonctions utilitaires
avancées pour analyser et rechercher dans les données AASQA.
Fonctionnalités:
- Recherche par nom d'organisme ou région
- Statistiques détaillées sur la couverture
- Validation de l'intégrité des données
- Analyses comparatives
"""
import sys
import os
# Ajouter le répertoire parent au PYTHONPATH pour importer le package local
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from atmo_data_wrapper import (
search_aasqa_by_name,
get_departments_count,
validate_department_coverage,
get_aasqa_statistics,
get_aasqa_info
)
def demo_search_functionality():
"""Démonstration de la recherche par nom"""
print("🔍 RECHERCHE PAR NOM D'ORGANISME OU RÉGION")
print("=" * 55)
search_terms = ['Atmo', 'Air', 'France', 'Sud', 'Grand']
for term in search_terms:
results = search_aasqa_by_name(term)
print(f"🔎 Recherche '{term}'{len(results)} résultat(s)")
for result in results[:3]: # Limiter à 3 résultats pour la démo
print(f"{result['organisme']} ({result['region']})")
if len(results) > 3:
print(f" ... et {len(results) - 3} autre(s)")
print()
def demo_department_statistics():
"""Démonstration des statistiques départementales"""
print("📊 STATISTIQUES DÉPARTEMENTALES")
print("=" * 55)
dept_counts = get_departments_count()
# Tri par nombre de départements (décroissant)
sorted_counts = sorted(dept_counts.items(), key=lambda x: x[1], reverse=True)
print("Classement par nombre de départements couverts:")
print()
for i, (aasqa_code, count) in enumerate(sorted_counts, 1):
aasqa_info = get_aasqa_info(aasqa_code)
organisme = aasqa_info['organisme']
region = aasqa_info['region']
if i <= 5: # Top 5
print(f"{i:2d}. {organisme:<30} : {count:2d} département(s) ({region})")
elif i == 6:
print(" ...")
elif i >= len(sorted_counts) - 2: # Bottom 3
print(f"{i:2d}. {organisme:<30} : {count:2d} département(s) ({region})")
print()
def demo_validation_report():
"""Démonstration du rapport de validation"""
print("✅ RAPPORT DE VALIDATION")
print("=" * 55)
validation = validate_department_coverage()
print("Intégrité des données AASQA:")
print()
print(f"📊 Total d'entrées départements: {validation['total_departments']}")
print(f"📊 Départements uniques: {validation['unique_departments']}")
print(f"📊 Couverture complète: {'✅ Oui' if validation['coverage_complete'] else '❌ Non'}")
print(f"📊 Doublons détectés: {'❌ Oui' if validation['has_duplicates'] else '✅ Non'}")
if validation['duplicates']:
print(f"⚠️ Départements en doublon: {', '.join(validation['duplicates'])}")
print()
def demo_comprehensive_statistics():
"""Démonstration des statistiques complètes"""
print("📈 STATISTIQUES COMPLÈTES")
print("=" * 55)
stats = get_aasqa_statistics()
print("Vue d'ensemble:")
print(f" • Nombre total d'AASQA: {stats['total_aasqa']}")
print(f" • Départements couverts: {stats['total_departments_covered']}")
print(f" • Départements uniques: {stats['unique_departments']}")
print(f" • Couverture moyenne: {stats['average_coverage']:.1f} départements/AASQA")
print()
print("Couverture maximale:")
max_info = stats['max_coverage']
print(f"{max_info['count']} départements")
print(f" • Organisme(s): {', '.join(max_info['aasqa_names'])}")
print()
print("Couverture minimale:")
min_info = stats['min_coverage']
print(f"{min_info['count']} département(s)")
print(f" • Organisme(s): {', '.join(min_info['aasqa_names'])}")
print()
print(f"Anomalies: {'❌ Détectées' if stats['has_anomalies'] else '✅ Aucune'}")
print()
def demo_practical_analysis():
"""Démonstration d'analyses pratiques"""
print("🎯 ANALYSES PRATIQUES")
print("=" * 55)
# Analyse 1: Organismes DOM-TOM
print("1⃣ Analyse DOM-TOM:")
dom_tom_codes = ['971', '972', '973', '974', '976']
from atmo_data_wrapper import get_aasqa_by_department
for code in dom_tom_codes:
aasqa_code = get_aasqa_by_department(code)
if aasqa_code:
aasqa_info = get_aasqa_info(aasqa_code)
print(f"{code}: {aasqa_info['region']}{aasqa_info['organisme']}")
print()
# Analyse 2: Régions métropolitaines les plus étendues
print("2⃣ Grandes régions métropolitaines:")
dept_counts = get_departments_count()
metro_counts = {}
for aasqa_code, count in dept_counts.items():
aasqa_info = get_aasqa_info(aasqa_code)
# Exclure DOM-TOM (codes postaux 97x)
if not any(dept.startswith('97') for dept in aasqa_info['departements']):
metro_counts[aasqa_code] = count
top_metro = sorted(metro_counts.items(), key=lambda x: x[1], reverse=True)[:3]
for i, (aasqa_code, count) in enumerate(top_metro, 1):
aasqa_info = get_aasqa_info(aasqa_code)
print(f" {i}. {aasqa_info['region']}: {count} départements ({aasqa_info['organisme']})")
print()
# Analyse 3: Organismes avec URL atmo vs autres
print("3⃣ Analyse des noms d'organismes:")
from atmo_data_wrapper import AASQA_CODES
atmo_orgs = []
other_orgs = []
for aasqa_code, aasqa_data in AASQA_CODES.items():
if 'atmo' in aasqa_data['organisme'].lower():
atmo_orgs.append(aasqa_data['organisme'])
else:
other_orgs.append(aasqa_data['organisme'])
print(f" • Organismes 'Atmo': {len(atmo_orgs)} ({len(atmo_orgs)/len(AASQA_CODES)*100:.0f}%)")
print(f" • Autres noms: {len(other_orgs)} ({len(other_orgs)/len(AASQA_CODES)*100:.0f}%)")
print(f" • Noms alternatifs: {', '.join(other_orgs[:3])}{'...' if len(other_orgs) > 3 else ''}")
print()
def demo_data_quality_checks():
"""Démonstration des vérifications de qualité"""
print("🔬 VÉRIFICATIONS DE QUALITÉ DES DONNÉES")
print("=" * 55)
from atmo_data_wrapper import AASQA_CODES
print("1⃣ Vérification des URLs:")
urls_https = 0
urls_http = 0
for aasqa_data in AASQA_CODES.values():
url = aasqa_data['site_web']
if url.startswith('https://'):
urls_https += 1
elif url.startswith('http://'):
urls_http += 1
print(f" • URLs HTTPS: {urls_https}/{len(AASQA_CODES)} ({urls_https/len(AASQA_CODES)*100:.0f}%)")
if urls_http > 0:
print(f" ⚠️ URLs HTTP: {urls_http}")
else:
print(f" ✅ Toutes les URLs sont sécurisées (HTTPS)")
print()
print("2⃣ Vérification des codes départementaux:")
valid_formats = 0
invalid_formats = []
for aasqa_code, aasqa_data in AASQA_CODES.items():
for dept in aasqa_data['departements']:
# Vérifier le format (2 chiffres, 3 chiffres pour DOM-TOM, ou 2A/2B pour Corse)
if (dept.isdigit() and len(dept) in [2, 3]) or dept in ['2A', '2B']:
valid_formats += 1
else:
invalid_formats.append(dept)
print(f" • Codes valides: {valid_formats}")
if invalid_formats:
print(f" ⚠️ Codes invalides: {invalid_formats}")
else:
print(f" ✅ Tous les codes départementaux sont valides")
print()
print("3⃣ Cohérence des descriptions:")
consistent = 0
inconsistent = []
for aasqa_code, aasqa_data in AASQA_CODES.items():
expected_desc = f"{aasqa_data['region']} | {aasqa_data['organisme']}"
if aasqa_data['description'] == expected_desc:
consistent += 1
else:
inconsistent.append(aasqa_code)
print(f" • Descriptions cohérentes: {consistent}/{len(AASQA_CODES)}")
if inconsistent:
print(f" ⚠️ Incohérences: {inconsistent}")
else:
print(f" ✅ Toutes les descriptions sont cohérentes")
def main():
"""Fonction principale"""
print("DÉMONSTRATION DES FONCTIONS UTILITAIRES AASQA AVANCÉES")
print("=" * 65)
print("Nouvelles analyses et vérifications de qualité des données")
print()
try:
demo_search_functionality()
demo_department_statistics()
demo_validation_report()
demo_comprehensive_statistics()
demo_practical_analysis()
demo_data_quality_checks()
print("=" * 65)
print("✅ TOUTES LES ANALYSES TERMINÉES AVEC SUCCÈS")
print()
print("🎯 Fonctions utilitaires démontrées:")
print(" • search_aasqa_by_name(): Recherche textuelle")
print(" • get_departments_count(): Comptage par AASQA")
print(" • validate_department_coverage(): Validation intégrité")
print(" • get_aasqa_statistics(): Statistiques complètes")
print(" • Analyses qualité et cohérence des données")
print()
print("📁 Fichiers concernés:")
print(" • constants.py: Données pures (sites web, départements)")
print(" • utils.py: Fonctions utilitaires (nouvelle architecture)")
except Exception as e:
print(f"❌ Erreur lors de l'exécution: {e}")
import traceback
print("\nDétails de l'erreur:")
print(traceback.format_exc())
if __name__ == "__main__":
main()

View file

@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""
Démonstration des nouvelles fonctionnalités AASQA
==============================================
Ce script illustre l'utilisation des nouvelles données enrichies
des AASQA : sites web, départements, et fonctions utilitaires.
Fonctionnalités démontrées:
- Recherche AASQA par département
- Informations complètes des organismes
- Sites web et contacts
- Nouvelles méthodes des classes de données
"""
import sys
import os
# Ajouter le répertoire parent au PYTHONPATH pour importer le package local
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from atmo_data_wrapper import (
AtmoDataClient,
AASQA_CODES,
get_aasqa_by_department,
get_aasqa_info,
get_aasqa_website,
list_departments_by_aasqa
)
def demo_department_search():
"""Démonstration de la recherche par département"""
print("🔍 RECHERCHE D'AASQA PAR DÉPARTEMENT")
print("=" * 50)
departments_test = ['54', '75', '13', '2A', '974', '99']
for dept in departments_test:
aasqa_code = get_aasqa_by_department(dept)
if aasqa_code:
aasqa_info = get_aasqa_info(aasqa_code)
print(f"📍 Département {dept}: {aasqa_info['organisme']} ({aasqa_info['region']})")
else:
print(f"❌ Département {dept}: Non trouvé")
print()
def demo_aasqa_details():
"""Démonstration des informations détaillées AASQA"""
print("📋 INFORMATIONS DÉTAILLÉES DES AASQA")
print("=" * 50)
# Quelques AASQA représentatives
aasqa_samples = ['44', '11', '93', '01', '94']
for aasqa_code in aasqa_samples:
info = get_aasqa_info(aasqa_code)
if info:
print(f"🏢 {info['organisme']}")
print(f" 📍 Région: {info['region']}")
print(f" 🌐 Site web: {info['site_web']}")
print(f" 📋 Départements: {', '.join(info['departements'])}")
print(f" 📊 Nombre de départements: {len(info['departements'])}")
print()
def demo_website_access():
"""Démonstration de l'accès aux sites web"""
print("🌐 SITES WEB DES AASQA")
print("=" * 50)
all_aasqa = list(AASQA_CODES.keys())
print("Liste des sites web par région:")
print()
for aasqa_code in all_aasqa:
website = get_aasqa_website(aasqa_code)
aasqa_info = get_aasqa_info(aasqa_code)
if website and aasqa_info:
print(f"{aasqa_info['region']:<25}{website}")
print()
def demo_department_coverage():
"""Démonstration de la couverture départementale"""
print("🗺️ COUVERTURE DÉPARTEMENTALE")
print("=" * 50)
# Statistiques sur la couverture
total_departments = 0
largest_coverage = 0
largest_aasqa = ""
print("Couverture par AASQA:")
print()
for aasqa_code in AASQA_CODES.keys():
departments = list_departments_by_aasqa(aasqa_code)
aasqa_info = get_aasqa_info(aasqa_code)
total_departments += len(departments)
if len(departments) > largest_coverage:
largest_coverage = len(departments)
largest_aasqa = aasqa_info['organisme']
print(f"📊 {aasqa_info['organisme']:<30} : {len(departments):2d} département(s)")
print()
print(f"📈 Statistiques:")
print(f" • Total départements couverts: {total_departments}")
print(f" • Plus grande couverture: {largest_aasqa} ({largest_coverage} départements)")
print()
def demo_enhanced_data_models():
"""Démonstration des nouvelles méthodes dans les modèles de données"""
print("🎯 NOUVELLES MÉTHODES DES MODÈLES")
print("=" * 50)
try:
# Connexion et récupération de données
client = AtmoDataClient()
success = client.auto_login()
if not success:
print("❌ Échec de la connexion API")
return
print("✅ Connexion API réussie")
print()
# Récupération d'indices ATMO pour test
print("📥 Récupération de données de test...")
atmo_data = client.get_indices_atmo(aasqa="44", format="geojson") # Grand Est
if atmo_data and len(atmo_data) > 0:
indice = atmo_data[0]
print(f"🎯 Données exemple: {indice.lib_zone}")
print()
print("🆕 Nouvelles méthodes AASQA:")
print(f" • get_aasqa_name(): {indice.get_aasqa_name()}")
print(f" • get_aasqa_website(): {indice.get_aasqa_website()}")
print(f" • get_aasqa_region(): {indice.get_aasqa_region()}")
print(f" • get_aasqa_organisme(): {indice.get_aasqa_organisme()}")
print()
# Trouver l'AASQA depuis un département fictif
dept_example = "54" # Meurthe-et-Moselle
aasqa_found = get_aasqa_by_department(dept_example)
print(f"🔍 Recherche par département {dept_example}: AASQA {aasqa_found}")
if aasqa_found:
deps = list_departments_by_aasqa(aasqa_found)
print(f" • Départements couverts: {', '.join(deps)}")
else:
print("❌ Aucune donnée récupérée")
except Exception as e:
print(f"❌ Erreur: {e}")
print()
def demo_practical_usage():
"""Démonstration d'usage pratique"""
print("💡 EXEMPLES D'USAGE PRATIQUE")
print("=" * 50)
print("1⃣ Trouver l'organisme responsable d'un département:")
print()
# Exemples pratiques
examples = [
("Nancy (54)", "54"),
("Paris (75)", "75"),
("Marseille (13)", "13"),
("Ajaccio (2A)", "2A"),
("Saint-Denis (974)", "974")
]
for ville, dept in examples:
aasqa_code = get_aasqa_by_department(dept)
if aasqa_code:
info = get_aasqa_info(aasqa_code)
print(f" {ville:<15}{info['organisme']}")
print(f" {'':>15} Site: {info['site_web']}")
else:
print(f" {ville:<15} → Non trouvé")
print()
print("2⃣ Vérification de cohérence:")
print()
# Vérification que tous les départements français sont couverts
all_covered_depts = []
for aasqa_code in AASQA_CODES.keys():
all_covered_depts.extend(list_departments_by_aasqa(aasqa_code))
print(f" • Départements uniques couverts: {len(set(all_covered_depts))}")
print(f" • Total d'entrées départements: {len(all_covered_depts)}")
# Recherche de doublons
seen = set()
duplicates = set()
for dept in all_covered_depts:
if dept in seen:
duplicates.add(dept)
seen.add(dept)
if duplicates:
print(f" ⚠️ Départements en doublon: {duplicates}")
else:
print(f" ✅ Aucun doublon détecté")
print()
def main():
"""Fonction principale"""
print("DÉMONSTRATION DES FONCTIONNALITÉS AASQA ENRICHIES")
print("=" * 60)
print("Nouvelles données : sites web, départements, fonctions utilitaires")
print()
try:
demo_department_search()
demo_aasqa_details()
demo_website_access()
demo_department_coverage()
demo_enhanced_data_models()
demo_practical_usage()
print("=" * 60)
print("✅ TOUTES LES DÉMONSTRATIONS TERMINÉES AVEC SUCCÈS")
print()
print("📋 Nouvelles fonctionnalités disponibles:")
print(" • Structure AASQA enrichie (sites web, départements)")
print(" • Fonctions utilitaires de recherche")
print(" • Nouvelles méthodes dans les modèles de données")
print(" • Usage simplifié par département")
except Exception as e:
print(f"❌ Erreur lors de l'exécution: {e}")
import traceback
print("\nDétails de l'erreur:")
print(traceback.format_exc())
if __name__ == "__main__":
main()

View file

@ -0,0 +1,367 @@
#!/usr/bin/env python3
"""
Exemples d'utilisation des nouveaux modèles de données typés
"""
from atmo_data_wrapper import AtmoDataClient
from atmo_data_wrapper import Coordinates
def example_indices_atmo():
"""Exemple d'utilisation des indices ATMO avec objets typés"""
print("=== Exemple Indices ATMO avec objets typés ===\n")
client = AtmoDataClient()
# Connexion automatique avec credentials.json
# Décommentez les lignes suivantes pour une vraie connexion API :
# try:
# client.auto_login()
# indices = client.get_indices_atmo(aasqa="11") # Données réelles
# except Exception as e:
# print(f"Erreur de connexion: {e}")
# print("Utilisation de données de test...")
# # Fallback sur données de test...
# Récupération des indices ATMO - maintenant retourne AtmoDataCollection
try:
# indices = client.get_indices_atmo(aasqa="11") # Île-de-France
# Pour la démonstration, créons des données de test
test_data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.3522, 48.8566]
},
"properties": {
"aasqa": "11",
"code_qual": 3,
"lib_qual": "Moyen",
"coul_qual": "#FFFF00",
"lib_zone": "Paris Centre",
"date_dif": "2024-07-07",
"code_no2": 2,
"code_so2": 1,
"code_o3": 3,
"code_pm10": 2,
"code_pm25": 3
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.4522, 48.9566]
},
"properties": {
"aasqa": "11",
"code_qual": 5,
"lib_qual": "Mauvais",
"coul_qual": "#FF0000",
"lib_zone": "Banlieue Nord",
"date_dif": "2024-07-07",
"code_no2": 4,
"code_so2": 2,
"code_o3": 5,
"code_pm10": 4,
"code_pm25": 5
}
}
]
}
# Créer une collection typée
from atmo_data_wrapper import AtmoDataCollection
indices = AtmoDataCollection(test_data, 'indices')
print(f"Collection: {len(indices)} indices récupérés")
print(f"Résumé: {indices.to_summary()}\n")
# Parcourir les indices avec les objets typés
for i, indice in enumerate(indices):
print(f"Indice {i+1}:")
print(f" Zone: {indice.lib_zone}")
print(f" AASQA: {indice.get_aasqa_name()}")
print(f" Qualité: {indice.get_qualificatif()} (code: {indice.code_qual})")
# Utiliser les méthodes helper
hex_color, rgb_color = indice.get_color()
print(f" Couleur: {hex_color} (RGB: {rgb_color})")
print(f" Bonne qualité: {'Oui' if indice.is_good_quality() else 'Non'}")
print(f" Mauvaise qualité: {'Oui' if indice.is_poor_quality() else 'Non'}")
# Polluant le plus problématique
worst_pol, worst_code = indice.get_worst_pollutant()
print(f" Pire polluant: {worst_pol} (code: {worst_code})")
# Coordonnées
if indice.has_coordinates():
print(f" Coordonnées: {indice.coordinates}")
print()
# Statistiques sur la collection
stats = indices.get_statistics()
print("Statistiques de la collection:")
for key, value in stats.items():
print(f" {key}: {value}")
print()
# Filtrage par qualité
print("=== Filtrage et analyse ===")
mauvaise_qualite = [indice for indice in indices if indice.is_poor_quality()]
print(f"Zones avec mauvaise qualité: {len(mauvaise_qualite)}")
for indice in mauvaise_qualite:
print(f" - {indice.lib_zone}: {indice.get_qualificatif()}")
except Exception as e:
print(f"Erreur: {e}")
def example_episodes_pollution():
"""Exemple d'utilisation des épisodes de pollution avec objets typés"""
print("\n=== Exemple Épisodes de Pollution avec objets typés ===\n")
# Données de test pour épisodes
test_data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [[[2.0, 48.0], [3.0, 48.0], [3.0, 49.0], [2.0, 49.0], [2.0, 48.0]]]
},
"properties": {
"aasqa": "11",
"code_pol": "5",
"lib_pol": "PM10",
"lib_zone": "Île-de-France",
"date_dif": "2024-07-07",
"etat": "INFORMATION ET RECOMMANDATIONS"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [[[1.0, 47.0], [2.0, 47.0], [2.0, 48.0], [1.0, 48.0], [1.0, 47.0]]]
},
"properties": {
"aasqa": "24",
"code_pol": "3",
"lib_pol": "Ozone",
"lib_zone": "Centre-Val de Loire",
"date_dif": "2024-07-07",
"etat": "ALERTE"
}
}
]
}
from atmo_data_wrapper import AtmoDataCollection
episodes = AtmoDataCollection(test_data, 'episodes')
print(f"Collection: {len(episodes)} épisodes récupérés")
print(f"Résumé: {episodes.to_summary()}\n")
for i, episode in enumerate(episodes):
print(f"Épisode {i+1}:")
print(f" Zone: {episode.lib_zone}")
print(f" Polluant: {episode.lib_pol} (code: {episode.get_polluant_code()})")
print(f" État: {episode.etat}")
print(f" Alerte active: {'Oui' if episode.is_alert_active() else 'Non'}")
print(f" Niveau d'alerte: {episode.get_alert_level()}")
print(f" Géométrie complexe: {'Oui' if episode.is_geometry_complex() else 'Non'}")
print()
def example_emissions_data():
"""Exemple d'utilisation des données d'émissions avec objets typés"""
print("=== Exemple Données d'Émissions avec objets typés ===\n")
# Données de test pour émissions
test_data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.3522, 48.8566]
},
"properties": {
"aasqa": "11",
"code": "75056",
"name": "Paris",
"population": 2165423,
"superficie": 105.4,
"nox": 15420.5,
"pm10": 890.2,
"pm25": 623.8,
"ges": 12345678.9,
"code_pcaet": "7"
}
}
]
}
from atmo_data_wrapper import AtmoDataCollection
emissions = AtmoDataCollection(test_data, 'emissions')
print(f"Collection: {len(emissions)} données d'émissions récupérées\n")
for emission in emissions:
print(f"Territoire: {emission.name}")
print(f" Population: {emission.population:,} habitants")
print(f" Superficie: {emission.superficie} km²")
print(f" Secteur: {emission.get_secteur_name()}")
print()
# Émissions totales
total_emissions = emission.get_total_emissions()
print(" Émissions totales:")
for polluant, valeur in total_emissions.items():
print(f" {polluant}: {valeur:,.1f} t/an")
print()
# Densités et per capita
print(" Densités d'émission (t/km²):")
for polluant in ['nox', 'pm10', 'pm25']:
density = emission.get_emission_density(polluant)
print(f" {polluant.upper()}: {density:.2f}")
print(" Émissions par habitant (kg/hab):")
for polluant in ['nox', 'pm10', 'pm25']:
per_capita = emission.get_emission_per_capita(polluant) * 1000 # Convert to kg
print(f" {polluant.upper()}: {per_capita:.2f}")
print()
def example_pollen_indices():
"""Exemple d'utilisation des indices pollen avec objets typés"""
print("=== Exemple Indices Pollen avec objets typés ===\n")
# Données de test pour pollens
test_data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.3522, 48.8566]
},
"properties": {
"aasqa": "11",
"alerte": True,
"code_ambr": 5,
"code_arm": 2,
"code_aul": 1,
"code_boul": 4,
"code_gram": 3,
"code_oliv": 1
}
}
]
}
from atmo_data_wrapper import AtmoDataCollection
pollens = AtmoDataCollection(test_data, 'pollens')
print(f"Collection: {len(pollens)} indices pollen récupérés\n")
for pollen in pollens:
print(f"Station pollen:")
print(f" Alerte active: {'Oui' if pollen.is_alert_active() else 'Non'}")
# Pollen le plus élevé
highest_pollen, highest_code = pollen.get_highest_pollen()
print(f" Plus haut niveau: {highest_pollen} (code: {highest_code})")
# Résumé de tous les pollens
summary = pollen.get_pollens_summary()
print(" Détail par espèce:")
for code, info in summary.items():
print(f" {info['espece']}: {info['qualificatif']} (code: {info['code']})")
# Pollens dangereux
dangerous = pollen.get_dangerous_pollens()
if dangerous:
print(f" Pollens à risque élevé: {', '.join(dangerous)}")
else:
print(" Aucun pollen à risque élevé")
print()
def example_geographic_filtering():
"""Exemple de filtrage géographique"""
print("=== Exemple Filtrage Géographique ===\n")
# Créer des données avec différentes localisations
test_data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.3522, 48.8566] # Paris
},
"properties": {
"aasqa": "11",
"code_qual": 3,
"lib_zone": "Paris Centre"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.2945, 48.8584] # Proche de Paris
},
"properties": {
"aasqa": "11",
"code_qual": 2,
"lib_zone": "Boulogne"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [5.3698, 43.2965] # Marseille (loin)
},
"properties": {
"aasqa": "93",
"code_qual": 4,
"lib_zone": "Marseille"
}
}
]
}
from atmo_data_wrapper import AtmoDataCollection, Coordinates
indices = AtmoDataCollection(test_data, 'indices')
print(f"Collection initiale: {len(indices)} éléments")
# Définir un point central (Paris)
paris_center = Coordinates(2.3522, 48.8566)
# Filtrer dans un rayon de 10 km autour de Paris
nearby = indices.filter_by_coordinates(paris_center, 10.0)
print(f"Dans un rayon de 10km de Paris: {len(nearby)} éléments")
for item in nearby:
if item.has_coordinates():
distance = item.coordinates.distance_to(paris_center)
print(f" {item.lib_zone}: {distance:.2f} km de Paris")
print()
if __name__ == "__main__":
example_indices_atmo()
example_episodes_pollution()
example_emissions_data()
example_pollen_indices()
example_geographic_filtering()

View file

@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""
Exemples d'utilisation de la fonctionnalité de sauvegarde du wrapper AtmoDataClient
"""
from atmo_data_wrapper import AtmoDataClient, AtmoDataException
from atmo_data_wrapper import AASQA_CODES, POLLUANTS
from datetime import datetime, timedelta
import os
def main():
"""Exemples de sauvegarde de données"""
print("=== Exemples de sauvegarde de données API Atmo ===\n")
# Initialisation du client
client = AtmoDataClient()
# Note: Pour ces exemples, nous utilisons des données factices
# car nous n'avons pas d'authentification réelle
# Données d'exemple au format GeoJSON (structure typique de l'API)
sample_geojson_data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.3522, 48.8566] # Paris
},
"properties": {
"code_zone": "75056",
"nom_zone": "Paris",
"aasqa": "11",
"nom_aasqa": "Airparif",
"date": "2024-07-07",
"code_qualificatif": "2",
"qualificatif": "Moyen",
"polluant_principal": "PM10",
"valeur": 45.2
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.2945, 48.8584] # Boulogne
},
"properties": {
"code_zone": "92012",
"nom_zone": "Boulogne-Billancourt",
"aasqa": "11",
"nom_aasqa": "Airparif",
"date": "2024-07-07",
"code_qualificatif": "3",
"qualificatif": "Dégradé",
"polluant_principal": "NO2",
"valeur": 52.8
}
}
]
}
# Exemple 1: Sauvegarde en JSON
print("1. Sauvegarde en format JSON...")
try:
json_file = client.save_to_file(
data=sample_geojson_data,
filename="data/indices_atmo",
file_format="json"
)
print(f"✅ Fichier JSON sauvegardé: {json_file}")
except Exception as e:
print(f"❌ Erreur JSON: {e}")
# Exemple 2: Sauvegarde en GeoJSON
print("\n2. Sauvegarde en format GeoJSON...")
try:
geojson_file = client.save_to_file(
data=sample_geojson_data,
filename="data/indices_atmo_geo",
file_format="geojson"
)
print(f"✅ Fichier GeoJSON sauvegardé: {geojson_file}")
except Exception as e:
print(f"❌ Erreur GeoJSON: {e}")
# Exemple 3: Sauvegarde en CSV
print("\n3. Sauvegarde en format CSV...")
try:
csv_file = client.save_to_file(
data=sample_geojson_data,
filename="data/indices_atmo",
file_format="csv"
)
print(f"✅ Fichier CSV sauvegardé: {csv_file}")
except Exception as e:
print(f"❌ Erreur CSV: {e}")
# Exemple 4: Sauvegarde avec chemin complet
print("\n4. Sauvegarde avec chemin personnalisé...")
try:
custom_file = client.save_to_file(
data=sample_geojson_data,
filename="exports/qualite_air/paris_2024",
file_format="json"
)
print(f"✅ Fichier personnalisé sauvegardé: {custom_file}")
except Exception as e:
print(f"❌ Erreur sauvegarde personnalisée: {e}")
# Exemple 5: Gestion d'erreurs - format invalide
print("\n5. Test de validation - format invalide...")
try:
client.save_to_file(
data=sample_geojson_data,
filename="test",
file_format="xml" # Format non supporté
)
print("❌ Validation échouée - format invalide accepté")
except ValueError as e:
print(f"✅ Validation réussie: {e}")
# Exemple 6: Workflow complet avec récupération de données
print("\n6. Exemple de workflow complet (simulation)...")
try:
# Simulation d'un appel API réel
print(" - Récupération des indices ATMO (simulé)...")
# indices = client.get_indices_atmo(aasqa="11", date="2024-07-07")
# Utilisation des données d'exemple
print(" - Sauvegarde des données...")
workflow_file = client.save_to_file(
data=sample_geojson_data,
filename=f"exports/daily/indices_atmo_{datetime.now().strftime('%Y%m%d')}",
file_format="csv"
)
print(f"✅ Workflow terminé: {workflow_file}")
# Affichage des informations sur le fichier
if os.path.exists(workflow_file):
size = os.path.getsize(workflow_file)
print(f" - Taille du fichier: {size} bytes")
print(f" - Nombre de lignes: {len(sample_geojson_data['features']) + 1}") # +1 pour l'en-tête
except Exception as e:
print(f"❌ Erreur workflow: {e}")
# Exemple 7: Données sans géométrie (pour test CSV)
print("\n7. Test CSV sans coordonnées...")
sample_data_no_geom = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": None,
"properties": {
"code_zone": "75056",
"nom_zone": "Paris",
"date": "2024-07-07",
"indice_atmo": 3,
"qualificatif": "Dégradé"
}
}
]
}
try:
no_geom_file = client.save_to_file(
data=sample_data_no_geom,
filename="data/indices_sans_coordonnees",
file_format="csv"
)
print(f"✅ CSV sans géométrie sauvegardé: {no_geom_file}")
except Exception as e:
print(f"❌ Erreur CSV sans géométrie: {e}")
print("\n=== Exemples terminés ===")
print("\nFichiers créés dans:")
print("- ./data/")
print("- ./exports/")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,324 @@
#!/usr/bin/env python3
"""
Vue synthétique combinée : Qualité de l'air et Pollen pour Nancy
================================================================
Ce script combine les données d'indices ATMO (qualité de l'air) et
d'indices Pollen pour la ville de Nancy (54395) afin de fournir
une vue synthétique de la situation environnementale.
Fonctionnalités:
- Récupération simultanée des indices ATMO et Pollen
- Analyse croisée qualité air / risque pollinique
- Vue synthétique avec recommandations
- Affichage avec émojis et couleurs
- Détection des risques combinés
"""
import sys
import os
from datetime import datetime
# Ajouter le répertoire parent au PYTHONPATH pour importer le package local
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from atmo_data_wrapper import AtmoDataClient, AASQA_CODES
def get_risk_level_combined(atmo_code: int, pollen_max: int, pollen_alert: bool) -> dict:
"""Détermine le niveau de risque combiné air + pollen"""
# Conversion des codes en niveaux de risque (0-4)
atmo_risk = min(atmo_code, 4) if atmo_code > 0 else 0
pollen_risk = min(pollen_max, 4) if pollen_max > 0 else 0
# Bonus si alerte pollen active
if pollen_alert:
pollen_risk = min(pollen_risk + 1, 4)
# Calcul du risque combiné (moyenne pondérée)
combined_risk = int((atmo_risk * 0.6 + pollen_risk * 0.4))
risk_levels = {
0: {"level": "Très faible", "emoji": "🟢", "color": "#50F0E6", "advice": "Conditions excellentes"},
1: {"level": "Faible", "emoji": "🟡", "color": "#50CCAA", "advice": "Conditions favorables"},
2: {"level": "Modéré", "emoji": "🟠", "color": "#F0E641", "advice": "Prudence pour personnes sensibles"},
3: {"level": "Élevé", "emoji": "🔴", "color": "#FF5050", "advice": "Éviter activités extérieures prolongées"},
4: {"level": "Très élevé", "emoji": "🟣", "color": "#A020F0", "advice": "Rester à l'intérieur si possible"}
}
result = risk_levels.get(combined_risk, risk_levels[2])
result["risk_score"] = combined_risk
result["atmo_contribution"] = atmo_risk
result["pollen_contribution"] = pollen_risk
return result
def format_pollutants_summary(atmo_indice) -> str:
"""Formate un résumé des polluants"""
summary = atmo_indice.get_pollutants_summary()
parts = []
for polluant, data in summary.items():
if data['code'] > 2: # Seulement les polluants problématiques
parts.append(f"{polluant}({data['code']})")
return ", ".join(parts) if parts else "Aucun polluant problématique"
def format_pollens_summary(pollen_indice) -> str:
"""Formate un résumé des pollens"""
summary = pollen_indice.get_pollens_summary()
parts = []
for code_taxon, data in summary.items():
if data['code'] >= 3: # Pollens avec indice moyen à élevé
espece = data['espece']
niveau = data['code']
emoji = data['emoji']
parts.append(f"{espece} {emoji}({niveau})")
return ", ".join(parts) if parts else "Tous pollens à niveau faible"
def get_detailed_recommendations(atmo_indice, pollen_indice, combined_risk: dict) -> list:
"""Génère des recommandations détaillées"""
recommendations = []
# Recommandations basées sur la qualité de l'air
if atmo_indice.is_poor_quality():
worst_pol, code = atmo_indice.get_worst_pollutant()
recommendations.append(f"🌬️ Qualité air dégradée ({worst_pol}: niveau {code})")
if code >= 4:
recommendations.append(" → Éviter le sport en extérieur")
recommendations.append(" → Aérer pendant les pics de trafic")
# Recommandations basées sur les pollens
if pollen_indice.is_alert_active():
recommendations.append("🌸 Alerte pollen active")
recommendations.append(" → Éviter sorties tôt le matin")
recommendations.append(" → Garder fenêtres fermées")
dangerous_pollens = pollen_indice.get_dangerous_pollens()
if dangerous_pollens:
pollens_str = ", ".join(dangerous_pollens)
recommendations.append(f"🤧 Pollens à risque élevé: {pollens_str}")
recommendations.append(" → Prévoir antihistaminiques si nécessaire")
# Recommandations combinées
if combined_risk["risk_score"] >= 3:
recommendations.append("⚠️ Risque combiné élevé")
recommendations.append(" → Limiter activités extérieures")
recommendations.append(" → Privilégier intérieur climatisé/filtré")
# Recommandations positives
if combined_risk["risk_score"] <= 1:
recommendations.append("✅ Conditions favorables")
recommendations.append(" → Profitez des activités extérieures")
recommendations.append(" → Idéal pour aérer le logement")
return recommendations
def display_synthesis_header():
"""Affiche l'en-tête de la synthèse"""
print("=" * 70)
print("🌍 SYNTHÈSE ENVIRONNEMENTALE - Nancy")
print("=" * 70)
print(f"📅 Date: {datetime.now().strftime('%d/%m/%Y à %H:%M')}")
print(f"📍 Ville: Nancy (54395) - Région Grand Est")
print()
def display_section_header(title: str, emoji: str = "📊"):
"""Affiche un en-tête de section"""
print(f"{emoji} {title}")
print("-" * 50)
def main():
"""Fonction principale"""
display_synthesis_header()
try:
# Connexion au client
print("🔗 Connexion à l'API Atmo Data...")
client = AtmoDataClient()
success = client.auto_login()
if not success:
print("❌ Échec de la connexion")
return
print("✅ Connexion réussie")
print()
# === RÉCUPÉRATION DES DONNÉES ===
print("📥 Récupération des données...")
# Données ATMO spécifiques à Nancy (code INSEE 54395)
atmo_data = client.get_indices_atmo(
code_zone="54395", # Code INSEE de Nancy
format="geojson"
)
# Données Pollen pour Nancy
pollen_data = client.get_indices_pollens(
code_zone="54395",
format="geojson"
)
if not atmo_data or len(atmo_data) == 0:
print("❌ Aucune donnée ATMO trouvée pour Nancy (54395)")
print(" Essai avec les données régionales...")
# Fallback: récupérer les données régionales du Grand Est
atmo_data = client.get_indices_atmo(aasqa="44", format="geojson")
if not atmo_data or len(atmo_data) == 0:
print("❌ Aucune donnée ATMO trouvée pour la région Grand Est")
return
else:
print(f"✅ Données régionales récupérées ({len(atmo_data)} stations)")
if not pollen_data or len(pollen_data) == 0:
print("❌ Aucune donnée Pollen trouvée pour Nancy")
return
# Sélection des données pertinentes
# Pour ATMO: prendre la première donnée (spécifique à Nancy si disponible)
atmo_indice = atmo_data[0]
# Pour Pollen: prendre la première donnée
pollen_indice = pollen_data[0]
print(f"✅ Données récupérées - ATMO: {atmo_indice.lib_zone}, Pollen: {pollen_indice.lib_zone}")
print()
# === ANALYSE QUALITÉ DE L'AIR ===
display_section_header("QUALITÉ DE L'AIR", "🌬️")
print(f"Zone: {atmo_indice.lib_zone}")
print(f"Indice global: {atmo_indice.get_qualificatif()} {atmo_indice.get_emoji()} (niveau {atmo_indice.code_qual})")
hex_color, rgb_color = atmo_indice.get_color()
print(f"Couleur: {hex_color}")
# Polluants problématiques
worst_pol, worst_code = atmo_indice.get_worst_pollutant()
print(f"Polluant principal: {worst_pol} (niveau {worst_code})")
# Résumé des polluants
pollutants_summary = format_pollutants_summary(atmo_indice)
print(f"Polluants élevés: {pollutants_summary}")
# Coordonnées et données réglementaires
if atmo_indice.has_coordinates():
print(f"Coordonnées: {atmo_indice.coordinates}")
print(f"Type de zone: {atmo_indice.type_zone or 'Non spécifié'}")
print(f"Source: {atmo_indice.get_source()}")
print()
# === ANALYSE POLLEN ===
display_section_header("INDICES POLLEN", "🌸")
print(f"Zone: {pollen_indice.lib_zone}")
print(f"Alerte active: {'✅ OUI' if pollen_indice.is_alert_active() else '❌ Non'}")
# Pollen le plus élevé
highest_pollen, highest_code = pollen_indice.get_highest_pollen()
if highest_code > 0:
highest_name = pollen_indice.get_pollens_summary()[highest_pollen]['espece']
highest_emoji = pollen_indice.get_pollens_summary()[highest_pollen]['emoji']
print(f"Pollen dominant: {highest_name} {highest_emoji} (niveau {highest_code})")
else:
print("Pollen dominant: Aucun pollen détecté")
# Taxons responsables selon l'API
responsible_pollens = pollen_indice.get_responsible_pollens()
if responsible_pollens:
print(f"Taxons responsables: {', '.join(responsible_pollens)}")
# Résumé des pollens élevés
pollens_summary = format_pollens_summary(pollen_indice)
print(f"Pollens à surveiller: {pollens_summary}")
# Concentration la plus élevée
highest_conc_taxon, highest_conc_value = pollen_indice.get_highest_concentration()
if highest_conc_value > 0:
conc_name = pollen_indice.get_pollens_summary()[highest_conc_taxon]['espece']
print(f"Concentration max: {conc_name} ({highest_conc_value:.1f} grains/m³)")
print(f"Source: {pollen_indice.get_source()}")
print()
# === ANALYSE COMBINÉE ===
display_section_header("RISQUE COMBINÉ", "⚖️")
# Calcul du risque combiné
combined_risk = get_risk_level_combined(
atmo_indice.code_qual,
highest_code,
pollen_indice.is_alert_active()
)
print(f"Niveau de risque: {combined_risk['level']} {combined_risk['emoji']}")
print(f"Score global: {combined_risk['risk_score']}/4")
print(f" • Contribution air: {combined_risk['atmo_contribution']}/4")
print(f" • Contribution pollen: {combined_risk['pollen_contribution']}/4")
print(f"Conseil général: {combined_risk['advice']}")
print()
# === RECOMMANDATIONS ===
display_section_header("RECOMMANDATIONS", "💡")
recommendations = get_detailed_recommendations(atmo_indice, pollen_indice, combined_risk)
if recommendations:
for i, rec in enumerate(recommendations, 1):
print(f"{rec}")
else:
print("✅ Aucune recommandation particulière")
print(" Conditions normales pour toutes activités")
print()
# === RÉSUMÉ TECHNIQUE ===
display_section_header("INFORMATIONS TECHNIQUES", "🔬")
print("Qualité de l'air:")
print(f" • Date échéance: {atmo_indice.date_ech}")
print(f" • Date diffusion: {atmo_indice.date_dif}")
print(f" • AASQA: {atmo_indice.get_aasqa_name()}")
# Concentrations si disponibles
concentrations = atmo_indice.get_concentrations()
significant_conc = {k: v for k, v in concentrations.items() if v > 0}
if significant_conc:
print(" • Concentrations (μg/m³):")
for polluant, conc in significant_conc.items():
print(f" - {polluant}: {conc}")
print()
print("Pollen:")
all_concentrations = pollen_indice.get_concentrations()
significant_pollen = {k: v for k, v in all_concentrations.items() if v > 0}
if significant_pollen:
print(" • Concentrations (grains/m³):")
for taxon, conc in significant_pollen.items():
taxon_name = pollen_indice.get_pollens_summary()[taxon]['espece']
print(f" - {taxon_name}: {conc:.1f}")
print()
# === FOOTER ===
print("=" * 70)
print("🌱 Données fournies par Atmo France et les AASQA")
print("📊 Synthèse générée par Atmo Data Wrapper")
print("=" * 70)
except Exception as e:
print(f"❌ Erreur lors de l'exécution: {e}")
import traceback
print("\nDétails de l'erreur:")
print(traceback.format_exc())
sys.exit(1)
if __name__ == "__main__":
main()

166
examples/example_usage.py Normal file
View file

@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""
Exemples d'utilisation du wrapper AtmoDataClient
"""
from atmo_data_wrapper import AtmoDataClient, AtmoDataException
from atmo_data_wrapper import AASQA_CODES, POLLUANTS
from datetime import datetime, timedelta
def main():
# Initialisation du client
client = AtmoDataClient()
# Authentification automatique avec credentials.json
try:
success = client.auto_login()
if not success:
print("Échec de l'authentification")
return
print("Connexion réussie avec credentials.json !")
except AtmoDataException as e:
print(f"Erreur d'authentification: {e}")
print("💡 Assurez-vous d'avoir créé le fichier credentials.json")
print(" Utilisez credentials.json.example comme modèle")
return
# Exemple 1: Récupération des indices ATMO d'aujourd'hui
print("\n=== Indices ATMO d'aujourd'hui ===")
try:
today = datetime.now().strftime("%Y-%m-%d")
indices = client.get_indices_atmo(
date=today,
aasqa="11", # Spécifier une région pour éviter erreur serveur
format="geojson"
)
print(f"Nombre d'indices récupérés: {len(indices)}")
print(f"Résumé: {indices.to_summary()}")
# Exemples avec objets typés
if len(indices) > 0:
first_indice = indices[0]
print(f"Premier indice - Zone: {first_indice.lib_zone}")
print(f"Qualité: {first_indice.get_qualificatif()}")
except AtmoDataException as e:
print(f"Erreur: {e}")
# Exemple 2: Récupération des épisodes de pollution en cours
print("\n=== Épisodes de pollution en cours ===")
try:
episodes = client.get_episodes_3jours(
format="geojson",
aasqa="11", # Île-de-France
polluant=POLLUANTS[2] # PM10
)
print(f"Nombre d'épisodes: {len(episodes)}")
# Analyser les alertes
alerts_actives = [ep for ep in episodes if ep.is_alert_active()]
print(f"Alertes actives: {len(alerts_actives)}")
if alerts_actives:
for alert in alerts_actives[:3]: # Max 3 exemples
print(f" - {alert.lib_zone}: {alert.get_alert_level()}")
except AtmoDataException as e:
print(f"Erreur: {e}")
# Exemple 3: Données d'émissions pour l'Île-de-France
print(f"\n=== Émissions {AASQA_CODES['11']} ===")
try:
emissions = client.get_emissions(
aasqa="11", # Île-de-France
echelle="region",
format="geojson"
)
print(f"Données d'émissions récupérées: {len(emissions)}")
# Analyser les émissions
if len(emissions) > 0:
em = emissions[0]
print(f"Territoire: {em.name}")
print(f"Population: {em.population:,.0f} habitants")
total_em = em.get_total_emissions()
print(f"Émissions NOx: {total_em['NOx']:,.1f} t/an")
except AtmoDataException as e:
print(f"Erreur: {e}")
# Exemple 4: Indices pollen avec alerte
print("\n=== Indices pollen avec alerte ===")
try:
pollens = client.get_indices_pollens(
format="geojson",
aasqa="11", # Île-de-France
alerte=True,
with_geom=True
)
print(f"Nombre d'alertes pollen: {len(pollens)}")
# Analyser les pollens dangereux
if len(pollens) > 0:
dangerous_pollens = []
for pollen in pollens[:5]: # Max 5 exemples
dangerous = pollen.get_dangerous_pollens()
if dangerous:
dangerous_pollens.extend(dangerous)
if dangerous_pollens:
print(f"Pollens à risque élevé: {', '.join(set(dangerous_pollens))}")
except AtmoDataException as e:
print(f"Erreur: {e}")
# Exemple 5: Recherche dans une zone géographique (bounding box)
print("\n=== Recherche dans une zone géographique ===")
try:
# Bounding box approximative de Paris
bbox = "2.2 48.8 2.4 48.9"
indices_paris = client.get_indices_atmo(
bounding_box=bbox,
date=today,
format="geojson"
)
print(f"Indices dans la zone Paris: {len(indices_paris)}")
# Analyser la qualité
if len(indices_paris) > 0:
stats = indices_paris.get_statistics()
qs = stats['quality_stats']
print(f"Qualité moyenne: {qs['moyenne']:.1f}/7")
print(f"Bonne qualité: {qs['bon_pourcentage']:.1f}%")
except AtmoDataException as e:
print(f"Erreur: {e}")
# Exemple 6: Données historiques
print("\n=== Données historiques ===")
try:
# Episodes d'il y a 30 jours
date_historique = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
episodes_historiques = client.get_episodes_historique(
date=today,
date_historique=date_historique,
aasqa="11", # Île-de-France
format="geojson"
)
print(f"Épisodes sur 30 jours: {len(episodes_historiques)}")
# Analyser l'évolution
if len(episodes_historiques) > 0:
alerts_historiques = [ep for ep in episodes_historiques if ep.is_alert_active()]
print(f"Alertes historiques: {len(alerts_historiques)}")
# Compter par polluant
polluants_hist = {}
for ep in episodes_historiques:
pol = ep.lib_pol
polluants_hist[pol] = polluants_hist.get(pol, 0) + 1
print("Répartition par polluant:")
for pol, count in polluants_hist.items():
print(f" {pol}: {count} épisodes")
except AtmoDataException as e:
print(f"Erreur: {e}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""
Script d'aide à la configuration des credentials
"""
import json
import os
import shutil
from getpass import getpass
def setup_credentials():
"""Guide l'utilisateur pour configurer ses credentials"""
print("=== Configuration des credentials Atmo Data ===\n")
credentials_file = "credentials.json"
example_file = "credentials.json.example"
# Vérifier si le fichier existe déjà
if os.path.exists(credentials_file):
response = input(f"Le fichier {credentials_file} existe déjà. Le remplacer ? (y/n): ")
if response.lower() != 'y':
print("Configuration annulée.")
return
# Vérifier la présence du fichier exemple
if not os.path.exists(example_file):
print(f"❌ Fichier exemple {example_file} manquant.")
print("Création du fichier exemple...")
example_content = {
"username": "votre_nom_utilisateur",
"password": "votre_mot_de_passe",
"api_url": "https://api.atmo-data.org"
}
with open(example_file, 'w', encoding='utf-8') as f:
json.dump(example_content, f, indent=2, ensure_ascii=False)
print(f"✅ Fichier {example_file} créé.")
print("\nSaisie des credentials :")
print("(Laissez vide pour utiliser la valeur par défaut)")
# Charger les valeurs par défaut depuis l'exemple
try:
with open(example_file, 'r', encoding='utf-8') as f:
defaults = json.load(f)
except:
defaults = {}
# Saisie interactive
username = input(f"Nom d'utilisateur [{defaults.get('username', '')}]: ").strip()
if not username:
username = defaults.get('username', '')
password = getpass(f"Mot de passe [{defaults.get('password', '')}]: ").strip()
if not password:
password = defaults.get('password', '')
api_url = input(f"URL de l'API [{defaults.get('api_url', 'https://api.atmo-data.org')}]: ").strip()
if not api_url:
api_url = defaults.get('api_url', 'https://api.atmo-data.org')
# Vérifier que les champs obligatoires sont remplis
if not username or not password:
print("❌ Le nom d'utilisateur et le mot de passe sont obligatoires.")
return False
# Créer le fichier credentials
credentials = {
"username": username,
"password": password,
"api_url": api_url
}
try:
with open(credentials_file, 'w', encoding='utf-8') as f:
json.dump(credentials, f, indent=2, ensure_ascii=False)
print(f"\n✅ Fichier {credentials_file} créé avec succès !")
print("\nPour tester votre configuration :")
print(" python test_real_connection.py")
# Vérifier que le fichier est dans .gitignore
gitignore_file = ".gitignore"
if os.path.exists(gitignore_file):
with open(gitignore_file, 'r') as f:
content = f.read()
if credentials_file not in content:
print(f"\n⚠️ Attention: {credentials_file} n'est pas dans .gitignore")
add_to_gitignore = input("Ajouter à .gitignore ? (y/n): ")
if add_to_gitignore.lower() == 'y':
with open(gitignore_file, 'a') as f:
f.write(f"\n# Fichiers de credentials\n{credentials_file}\n")
print("✅ Ajouté à .gitignore")
return True
except Exception as e:
print(f"❌ Erreur lors de la création du fichier: {e}")
return False
def test_configuration():
"""Test rapide de la configuration"""
print("\n=== Test de la configuration ===")
try:
from atmo_data_wrapper import AtmoDataClient
client = AtmoDataClient()
credentials = client._load_credentials()
print("✅ Fichier credentials.json lu avec succès")
print(f" Nom d'utilisateur: {credentials['username']}")
print(f" URL API: {credentials.get('api_url', 'URL par défaut')}")
# Test de connexion (simulation)
print("\nPour tester la connexion réelle à l'API, exécutez :")
print(" python test_real_connection.py")
except Exception as e:
print(f"❌ Erreur lors du test: {e}")
if __name__ == "__main__":
success = setup_credentials()
if success:
test_configuration()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
aasqa,code_qualificatif,code_zone,date,latitude,longitude,nom_aasqa,nom_zone,polluant_principal,qualificatif,valeur
11,2,75056,2024-07-07,48.8566,2.3522,Airparif,Paris,PM10,Moyen,45.2
11,3,92012,2024-07-07,48.8584,2.2945,Airparif,Boulogne-Billancourt,NO2,Dégradé,52.8
1 aasqa code_qualificatif code_zone date latitude longitude nom_aasqa nom_zone polluant_principal qualificatif valeur
2 11 2 75056 2024-07-07 48.8566 2.3522 Airparif Paris PM10 Moyen 45.2
3 11 3 92012 2024-07-07 48.8584 2.2945 Airparif Boulogne-Billancourt NO2 Dégradé 52.8

View file

@ -0,0 +1,47 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
2.3522,
48.8566
]
},
"properties": {
"code_zone": "75056",
"nom_zone": "Paris",
"aasqa": "11",
"nom_aasqa": "Airparif",
"date": "2024-07-07",
"code_qualificatif": "2",
"qualificatif": "Moyen",
"polluant_principal": "PM10",
"valeur": 45.2
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
2.2945,
48.8584
]
},
"properties": {
"code_zone": "92012",
"nom_zone": "Boulogne-Billancourt",
"aasqa": "11",
"nom_aasqa": "Airparif",
"date": "2024-07-07",
"code_qualificatif": "3",
"qualificatif": "Dégradé",
"polluant_principal": "NO2",
"valeur": 52.8
}
}
]
}

144
pyproject.toml Normal file
View file

@ -0,0 +1,144 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "atmo-data-wrapper"
dynamic = ["version"]
description = "Wrapper Python pour l'API Atmo Data"
readme = "docs/README.md"
license = {file = "LICENSE"}
authors = [
{name = "Atmo Data Wrapper Team", email = "contact@atmo-france.org"}
]
maintainers = [
{name = "Atmo Data Wrapper Team", email = "contact@atmo-france.org"}
]
keywords = [
"atmo",
"air quality",
"pollution",
"environment",
"france",
"aasqa",
"api",
"wrapper",
"qualité air",
"pollen",
"emissions"
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Topic :: Scientific/Engineering :: Atmospheric Science",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: OS Independent",
"Natural Language :: French",
]
requires-python = ">=3.7"
dependencies = [
"requests>=2.25.0",
]
[project.optional-dependencies]
dev = [
"pytest>=6.0",
"pytest-cov",
"black",
"flake8",
"mypy",
]
docs = [
"sphinx",
"sphinx-rtd-theme",
]
[project.urls]
Homepage = "https://github.com/atmo-france/atmo-data-wrapper"
Documentation = "https://github.com/atmo-france/atmo-data-wrapper/blob/main/docs/README.md"
Repository = "https://github.com/atmo-france/atmo-data-wrapper"
Issues = "https://github.com/atmo-france/atmo-data-wrapper/issues"
"API Documentation" = "https://admindata.atmo-france.org/api/doc/v2"
"Atmo France" = "https://www.atmo-france.org"
[project.scripts]
atmo-data-test = "atmo_data_wrapper.core.client:main"
[tool.setuptools]
packages = ["atmo_data_wrapper", "atmo_data_wrapper.core"]
[tool.setuptools.dynamic]
version = {attr = "atmo_data_wrapper.__version__"}
[tool.setuptools.package-data]
atmo_data_wrapper = ["*.json", "core/*.json"]
[tool.black]
line-length = 100
target-version = ['py37', 'py38', 'py39', 'py310', 'py311', 'py312']
include = '\.pyi?$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "--verbose --tb=short"
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests",
]
[tool.mypy]
python_version = "3.7"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_equality = true
[tool.flake8]
max-line-length = 100
extend-ignore = ["E203", "W503"]
exclude = [
".git",
"__pycache__",
"build",
"dist",
".eggs",
"*.egg-info",
".venv",
".mypy_cache",
".pytest_cache"
]

23
requirements-dev.txt Normal file
View file

@ -0,0 +1,23 @@
# Development dependencies
-r requirements.txt
# Testing
pytest>=6.0
pytest-cov
pytest-mock
# Code quality
black
flake8
mypy
isort
# Documentation
sphinx
sphinx-rtd-theme
# Development tools
pre-commit
twine
wheel
setuptools

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
# Production dependencies
requests>=2.25.0

96
setup.py Normal file
View file

@ -0,0 +1,96 @@
"""
Setup configuration for Atmo Data Wrapper package
"""
from setuptools import setup, find_packages
import os
# Read the README file
with open("docs/README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
# Read version from package
def get_version():
"""Extract version from package __init__.py"""
import sys
sys.path.insert(0, os.path.dirname(__file__))
from atmo_data_wrapper import __version__
return __version__
setup(
name="atmo-data-wrapper",
version=get_version(),
author="Atmo Data Wrapper Team",
author_email="contact@atmo-france.org",
description="Wrapper Python pour l'API Atmo Data",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/atmo-france/atmo-data-wrapper",
project_urls={
"Bug Tracker": "https://github.com/atmo-france/atmo-data-wrapper/issues",
"Documentation": "https://github.com/atmo-france/atmo-data-wrapper/blob/main/docs/README.md",
"API Documentation": "https://admindata.atmo-france.org/api/doc/v2",
"Atmo France": "https://www.atmo-france.org"
},
packages=find_packages(),
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Topic :: Scientific/Engineering :: Atmospheric Science",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: OS Independent",
"Natural Language :: French",
],
python_requires=">=3.7",
install_requires=[
"requests>=2.25.0",
],
extras_require={
"dev": [
"pytest>=6.0",
"pytest-cov",
"black",
"flake8",
"mypy",
],
"docs": [
"sphinx",
"sphinx-rtd-theme",
],
},
entry_points={
"console_scripts": [
"atmo-data-test=atmo_data_wrapper.core.client:main",
],
},
include_package_data=True,
package_data={
"atmo_data_wrapper": [
"*.json",
"core/*.json",
],
},
keywords=[
"atmo",
"air quality",
"pollution",
"environment",
"france",
"aasqa",
"api",
"wrapper",
"qualité air",
"pollen",
"emissions"
],
zip_safe=False,
)

3
tests/__init__.py Normal file
View file

@ -0,0 +1,3 @@
"""
Test package for Atmo Data Wrapper
"""

View file

@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
Test du système de credentials sans connexion réelle
"""
from atmo_data_wrapper import AtmoDataClient, AtmoDataException
import json
import os
import tempfile
def test_credentials_loading():
"""Test du chargement des credentials"""
print("=== Test du système de credentials ===\n")
# Test 1: Fichier manquant
print("1. Test fichier credentials manquant...")
client = AtmoDataClient(credentials_file="inexistant.json")
try:
client._load_credentials()
print("❌ Erreur: Exception attendue pour fichier manquant")
except AtmoDataException as e:
if "non trouvé" in str(e):
print("✅ Exception correcte pour fichier manquant")
else:
print(f"❌ Message d'erreur inattendu: {e}")
# Test 2: Fichier JSON invalide
print("\n2. Test fichier JSON invalide...")
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
f.write("{ invalid json }")
invalid_file = f.name
try:
client_invalid = AtmoDataClient(credentials_file=invalid_file)
client_invalid._load_credentials()
print("❌ Erreur: Exception attendue pour JSON invalide")
except AtmoDataException as e:
if "JSON" in str(e):
print("✅ Exception correcte pour JSON invalide")
else:
print(f"❌ Message d'erreur inattendu: {e}")
finally:
os.unlink(invalid_file)
# Test 3: Champs manquants
print("\n3. Test champs manquants...")
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
json.dump({"username": "test"}, f) # Manque password
incomplete_file = f.name
try:
client_incomplete = AtmoDataClient(credentials_file=incomplete_file)
client_incomplete._load_credentials()
print("❌ Erreur: Exception attendue pour champs manquants")
except ValueError as e:
if "manquants" in str(e):
print("✅ Exception correcte pour champs manquants")
else:
print(f"❌ Message d'erreur inattendu: {e}")
finally:
os.unlink(incomplete_file)
# Test 4: Fichier valide
print("\n4. Test fichier credentials valide...")
test_credentials = {
"username": "test_user",
"password": "test_pass",
"api_url": "https://test-api.example.com"
}
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
json.dump(test_credentials, f)
valid_file = f.name
try:
client_valid = AtmoDataClient(credentials_file=valid_file)
credentials = client_valid._load_credentials()
if credentials['username'] == 'test_user' and credentials['password'] == 'test_pass':
print("✅ Credentials chargés correctement")
print(f" Username: {credentials['username']}")
print(f" API URL mise à jour: {client_valid.base_url}")
else:
print("❌ Credentials incorrects")
except Exception as e:
print(f"❌ Erreur inattendue: {e}")
finally:
os.unlink(valid_file)
# Test 5: Login avec credentials
print("\n5. Test méthode login avec credentials...")
# Créer un client avec mock
client_mock = AtmoDataClient(credentials_file=valid_file)
# Mock de la méthode _make_request pour simuler réponse login
def mock_post(url, json=None):
class MockResponse:
def raise_for_status(self):
pass
def json(self):
return {"token": "test_token_123"}
return MockResponse()
# Remplacer temporairement
original_post = client_mock.session.post
client_mock.session.post = mock_post
# Créer un fichier credentials temporaire pour ce test
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
json.dump(test_credentials, f)
temp_cred_file = f.name
try:
client_mock.credentials_file = temp_cred_file
success = client_mock.login() # Sans paramètres, doit utiliser le fichier
if success and client_mock.token == "test_token_123":
print("✅ Login avec credentials automatique réussi")
else:
print("❌ Login avec credentials échoué")
except Exception as e:
print(f"❌ Erreur login: {e}")
finally:
client_mock.session.post = original_post
os.unlink(temp_cred_file)
print("\n=== Tests du système de credentials terminés ===")
def test_auto_login():
"""Test de la méthode auto_login"""
print("\n=== Test auto_login ===\n")
test_credentials = {
"username": "auto_user",
"password": "auto_pass"
}
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
json.dump(test_credentials, f)
cred_file = f.name
try:
client = AtmoDataClient(credentials_file=cred_file)
# Mock de la session
def mock_post(url, json=None):
class MockResponse:
def raise_for_status(self):
pass
def json(self):
return {"token": "auto_token_456"}
return MockResponse()
client.session.post = mock_post
success = client.auto_login()
if success and client.token == "auto_token_456":
print("✅ auto_login() fonctionne correctement")
else:
print("❌ auto_login() a échoué")
except Exception as e:
print(f"❌ Erreur auto_login: {e}")
finally:
os.unlink(cred_file)
if __name__ == "__main__":
test_credentials_loading()
test_auto_login()

View file

@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""
Test de connexion réelle à l'API Atmo Data
Nécessite un fichier credentials.json valide
"""
from atmo_data_wrapper import AtmoDataClient, AtmoDataException
from datetime import datetime
def test_real_api_connection():
"""Test de connexion et requêtes réelles à l'API"""
print("=== Test de connexion réelle à l'API Atmo Data ===\n")
try:
# Initialisation du client
client = AtmoDataClient()
print("Client initialisé")
# Test de connexion automatique
print("Tentative de connexion avec credentials.json...")
success = client.auto_login()
if not success:
print("❌ Échec de l'authentification")
return False
print("✅ Connexion réussie !")
print(f"URL de l'API: {client.base_url}")
# Test 1: Récupération des indices ATMO
print("\n=== Test 1: Indices ATMO ===")
try:
indices = client.get_indices_atmo(format="geojson")
print(f"✅ Indices ATMO récupérés: {len(indices)} éléments")
# Afficher un échantillon
if len(indices) > 0:
first_item = indices[0]
print(f" Premier élément - Zone: {first_item.lib_zone}")
print(f" Qualité: {first_item.get_qualificatif()}")
print(f" AASQA: {first_item.get_aasqa_name()}")
# Statistiques
stats = indices.get_statistics()
print(f" Statistiques: {stats}")
except Exception as e:
print(f"❌ Erreur récupération indices: {e}")
# Test 2: Épisodes de pollution
print("\n=== Test 2: Épisodes de pollution ===")
try:
episodes = client.get_episodes_3jours(format="geojson")
print(f"✅ Épisodes récupérés: {len(episodes)} éléments")
if len(episodes) > 0:
alerts_actives = [ep for ep in episodes if ep.is_alert_active()]
print(f" Alertes actives: {len(alerts_actives)}")
for episode in alerts_actives[:3]: # Max 3 exemples
print(f" - {episode.lib_zone}: {episode.get_alert_level()} ({episode.lib_pol})")
except Exception as e:
print(f"❌ Erreur récupération épisodes: {e}")
# Test 3: Données d'émissions (région)
print("\n=== Test 3: Données d'émissions ===")
try:
emissions = client.get_emissions(
echelle="region",
format="geojson"
)
print(f"✅ Données d'émissions récupérées: {len(emissions)} éléments")
if len(emissions) > 0:
first_emission = emissions[0]
print(f" Premier territoire: {first_emission.name}")
total_em = first_emission.get_total_emissions()
print(f" NOx: {total_em['NOx']:.1f} t/an")
except Exception as e:
print(f"❌ Erreur récupération émissions: {e}")
# Test 4: Indices pollen
print("\n=== Test 4: Indices pollen ===")
try:
pollens = client.get_indices_pollens(format="geojson")
print(f"✅ Indices pollen récupérés: {len(pollens)} éléments")
if len(pollens) > 0:
alerts_pollen = [p for p in pollens if p.is_alert_active()]
print(f" Alertes pollen actives: {len(alerts_pollen)}")
for pollen in alerts_pollen[:3]: # Max 3 exemples
dangerous = pollen.get_dangerous_pollens()
if dangerous:
print(f" - Pollens à risque: {', '.join(dangerous)}")
except Exception as e:
print(f"❌ Erreur récupération pollens: {e}")
# Test 5: Filtrage géographique (Paris)
print("\n=== Test 5: Filtrage géographique ===")
try:
# Bounding box de Paris
bbox = "2.2 48.8 2.4 48.9"
indices_paris = client.get_indices_atmo(
bounding_box=bbox,
format="geojson"
)
print(f"✅ Indices Paris récupérés: {len(indices_paris)} éléments")
# Filtrage par proximité
if len(indices_paris) > 0:
from atmo_data_wrapper import Coordinates
paris_center = Coordinates(2.3522, 48.8566)
nearby = indices_paris.filter_by_coordinates(paris_center, 10.0)
print(f" Dans un rayon de 10km: {len(nearby)} éléments")
except Exception as e:
print(f"❌ Erreur filtrage géographique: {e}")
print("\n✅ Tests terminés avec succès !")
return True
except AtmoDataException as e:
print(f"❌ Erreur API: {e}")
return False
except Exception as e:
print(f"❌ Erreur inattendue: {e}")
return False
def test_credentials_file():
"""Test de présence et validité du fichier credentials"""
print("=== Vérification du fichier credentials ===\n")
import os
import json
credentials_file = "credentials.json"
example_file = "credentials.json.example"
# Vérifier la présence du fichier exemple
if os.path.exists(example_file):
print(f"✅ Fichier exemple trouvé: {example_file}")
else:
print(f"❌ Fichier exemple manquant: {example_file}")
# Vérifier la présence du fichier credentials
if os.path.exists(credentials_file):
print(f"✅ Fichier credentials trouvé: {credentials_file}")
try:
with open(credentials_file, 'r') as f:
creds = json.load(f)
required_fields = ['username', 'password']
missing_fields = [field for field in required_fields if field not in creds]
if missing_fields:
print(f"❌ Champs manquants: {missing_fields}")
else:
print("✅ Structure du fichier credentials valide")
# Masquer les credentials sensibles
safe_creds = {k: "***" if k in ['password'] else v for k, v in creds.items()}
print(f" Contenu: {safe_creds}")
except json.JSONDecodeError as e:
print(f"❌ Erreur de format JSON: {e}")
else:
print(f"❌ Fichier credentials manquant: {credentials_file}")
print(f"💡 Créez le fichier à partir de {example_file}")
print(" 1. Copiez credentials.json.example vers credentials.json")
print(" 2. Remplacez les valeurs par vos vrais identifiants")
if __name__ == "__main__":
# Test du fichier credentials d'abord
test_credentials_file()
print()
# Puis test de connexion réelle si possible
if input("Tester la connexion réelle à l'API ? (y/n): ").lower() == 'y':
test_real_api_connection()
else:
print("Test de connexion ignoré.")

View file

@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""
Tests pour la fonctionnalité de sauvegarde
"""
from atmo_data_wrapper import AtmoDataClient
import os
import json
import csv
from pathlib import Path
def test_save_functionality():
"""Test complet de la fonctionnalité de sauvegarde"""
print("=== Tests de la fonctionnalité de sauvegarde ===\n")
client = AtmoDataClient()
# Données de test
test_data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.3522, 48.8566]
},
"properties": {
"nom": "Test Paris",
"valeur": 42.5,
"date": "2024-07-07"
}
}
]
}
test_dir = "test_files"
# Test 1: Validation du format
print("1. Test de validation du format...")
try:
client._validate_save_format("json")
print("✅ Format JSON valide")
except Exception as e:
print(f"❌ Erreur format JSON: {e}")
try:
client._validate_save_format("xml")
print("❌ Format XML invalide accepté")
except ValueError as e:
print(f"✅ Format XML correctement rejeté: {e}")
# Test 2: Sauvegarde JSON
print("\n2. Test sauvegarde JSON...")
try:
json_file = client.save_to_file(test_data, f"{test_dir}/test", "json")
if os.path.exists(json_file):
# Vérifier que le fichier est du JSON valide
with open(json_file, 'r') as f:
loaded_data = json.load(f)
print("✅ Fichier JSON créé et valide")
else:
print("❌ Fichier JSON non créé")
except Exception as e:
print(f"❌ Erreur JSON: {e}")
# Test 3: Sauvegarde CSV
print("\n3. Test sauvegarde CSV...")
try:
csv_file = client.save_to_file(test_data, f"{test_dir}/test", "csv")
if os.path.exists(csv_file):
# Vérifier le contenu CSV
with open(csv_file, 'r') as f:
reader = csv.reader(f)
rows = list(reader)
if len(rows) >= 2: # Header + au moins une ligne
print("✅ Fichier CSV créé avec en-tête et données")
else:
print("❌ Fichier CSV incomplet")
else:
print("❌ Fichier CSV non créé")
except Exception as e:
print(f"❌ Erreur CSV: {e}")
# Test 4: Sauvegarde GeoJSON
print("\n4. Test sauvegarde GeoJSON...")
try:
geojson_file = client.save_to_file(test_data, f"{test_dir}/test", "geojson")
if os.path.exists(geojson_file):
# Vérifier que c'est du GeoJSON valide
with open(geojson_file, 'r') as f:
loaded_data = json.load(f)
if loaded_data.get('type') == 'FeatureCollection':
print("✅ Fichier GeoJSON créé et valide")
else:
print("❌ Fichier GeoJSON invalide")
else:
print("❌ Fichier GeoJSON non créé")
except Exception as e:
print(f"❌ Erreur GeoJSON: {e}")
# Test 5: Création de répertoires
print("\n5. Test création de répertoires...")
try:
nested_file = client.save_to_file(test_data, f"{test_dir}/nested/deep/test", "json")
if os.path.exists(nested_file):
print("✅ Répertoires imbriqués créés automatiquement")
else:
print("❌ Répertoires imbriqués non créés")
except Exception as e:
print(f"❌ Erreur création répertoires: {e}")
# Test 6: Extension automatique
print("\n6. Test ajout automatique d'extension...")
try:
auto_ext_file = client.save_to_file(test_data, f"{test_dir}/sans_extension", "json")
if auto_ext_file.endswith('.json'):
print("✅ Extension .json ajoutée automatiquement")
else:
print("❌ Extension non ajoutée")
except Exception as e:
print(f"❌ Erreur extension automatique: {e}")
# Test 7: Données sans géométrie
print("\n7. Test données sans géométrie...")
data_no_geom = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": None,
"properties": {
"nom": "Test sans géométrie",
"valeur": 123
}
}
]
}
try:
no_geom_file = client.save_to_file(data_no_geom, f"{test_dir}/no_geom", "csv")
if os.path.exists(no_geom_file):
with open(no_geom_file, 'r') as f:
content = f.read()
if 'nom,valeur' in content and 'Test sans géométrie,123' in content:
print("✅ CSV sans géométrie créé correctement")
else:
print("❌ CSV sans géométrie incorrect")
else:
print("❌ Fichier sans géométrie non créé")
except Exception as e:
print(f"❌ Erreur données sans géométrie: {e}")
# Test 8: Données invalides pour GeoJSON
print("\n8. Test données invalides pour GeoJSON...")
invalid_geojson = {"data": "not a geojson"}
try:
client.save_to_file(invalid_geojson, f"{test_dir}/invalid", "geojson")
print("❌ Données invalides acceptées pour GeoJSON")
except ValueError as e:
print(f"✅ Données invalides correctement rejetées: {e}")
# Nettoyage
print("\n9. Nettoyage des fichiers de test...")
try:
import shutil
if os.path.exists(test_dir):
shutil.rmtree(test_dir)
print("✅ Fichiers de test supprimés")
except Exception as e:
print(f"❌ Erreur nettoyage: {e}")
print("\n=== Tests terminés ===")
if __name__ == "__main__":
test_save_functionality()

107
tests/test_typed_client.py Normal file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Test simple du client avec les nouveaux objets typés
"""
from atmo_data_wrapper import AtmoDataClient
from atmo_data_wrapper import AtmoDataCollection, IndiceAtmo
def test_client_integration():
"""Test que le client retourne bien les bons types d'objets"""
print("=== Test d'intégration client/modèles ===\n")
client = AtmoDataClient()
# Test avec données simulées (sans vraie connexion API)
# Simuler _make_request pour retourner des données de test
def mock_make_request(endpoint, params=None):
return {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.3522, 48.8566]
},
"properties": {
"aasqa": "11",
"code_qual": 3,
"lib_qual": "Dégradé",
"lib_zone": "Test Zone"
}
}
]
}
# Remplacer temporairement la méthode
original_make_request = client._make_request
client._make_request = mock_make_request
client.token = "test_token" # Simuler une connexion
try:
# Test 1: get_indices_atmo avec format geojson -> AtmoDataCollection
print("1. Test get_indices_atmo format geojson...")
result = client.get_indices_atmo(format="geojson")
if isinstance(result, AtmoDataCollection):
print("✅ Retourne bien AtmoDataCollection")
print(f" Type de données: {result.data_type}")
print(f" Nombre d'éléments: {len(result)}")
# Vérifier que les éléments sont du bon type
if len(result) > 0 and isinstance(result[0], IndiceAtmo):
print("✅ Les éléments sont bien des objets IndiceAtmo")
print(f" Zone: {result[0].lib_zone}")
print(f" Qualité: {result[0].get_qualificatif()}")
else:
print("❌ Les éléments ne sont pas des objets IndiceAtmo")
else:
print(f"❌ Retourne {type(result)} au lieu de AtmoDataCollection")
print()
# Test 2: Simuler format CSV -> dict
def mock_csv_request(endpoint, params=None):
return {"data": "csv,data,here"}
client._make_request = mock_csv_request
print("2. Test get_indices_atmo format csv...")
result_csv = client.get_indices_atmo(format="csv")
if isinstance(result_csv, dict):
print("✅ Format CSV retourne bien un dict")
else:
print(f"❌ Format CSV retourne {type(result_csv)} au lieu de dict")
print()
# Test 3: Test différents endpoints
client._make_request = mock_make_request
endpoints_to_test = [
("get_episodes_3jours", "episodes"),
("get_emissions", "emissions"),
("get_indices_pollens", "pollens")
]
print("3. Test autres endpoints...")
for method_name, expected_type in endpoints_to_test:
method = getattr(client, method_name)
result = method(format="geojson")
if isinstance(result, AtmoDataCollection) and result.data_type == expected_type:
print(f"{method_name} retourne bien AtmoDataCollection({expected_type})")
else:
print(f"{method_name} problème de type: {type(result)}, data_type: {getattr(result, 'data_type', 'N/A')}")
print("\n=== Test terminé ===")
finally:
# Restaurer la méthode originale
client._make_request = original_make_request
if __name__ == "__main__":
test_client_integration()

261
tests/test_validations.py Normal file
View file

@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""
Script de test pour valider les validations du wrapper AtmoDataClient
"""
from atmo_data_wrapper import AtmoDataClient
def test_validations():
"""Test des validations des paramètres"""
client = AtmoDataClient()
print("=== Test des validations ===\n")
# Test format invalide
try:
client._validate_format("xml")
print("❌ Format invalide non détecté")
except ValueError as e:
print(f"✅ Format invalide détecté: {e}")
# Test format valide
try:
client._validate_format("geojson")
print("✅ Format valide accepté")
except ValueError as e:
print(f"❌ Format valide rejeté: {e}")
# Test AASQA invalide
try:
client._validate_aasqa("99")
print("❌ Code AASQA invalide non détecté")
except ValueError as e:
print(f"✅ Code AASQA invalide détecté: {e}")
# Test AASQA valide
try:
client._validate_aasqa("11")
print("✅ Code AASQA valide accepté")
except ValueError as e:
print(f"❌ Code AASQA valide rejeté: {e}")
# Test polluant invalide
try:
client._validate_polluant("CO")
print("❌ Polluant invalide non détecté")
except ValueError as e:
print(f"✅ Polluant invalide détecté: {e}")
# Test polluant valide
try:
client._validate_polluant("PM10")
print("✅ Polluant valide accepté")
except ValueError as e:
print(f"❌ Polluant valide rejeté: {e}")
# Test date invalide (format)
try:
client._validate_date("2024/06/08")
print("❌ Format de date invalide non détecté")
except ValueError as e:
print(f"✅ Format de date invalide détecté: {e}")
# Test date invalide (date inexistante)
try:
client._validate_date("2024-02-30")
print("❌ Date inexistante non détectée")
except ValueError as e:
print(f"✅ Date inexistante détectée: {e}")
# Test date valide
try:
client._validate_date("2024-06-08")
print("✅ Date valide acceptée")
except ValueError as e:
print(f"❌ Date valide rejetée: {e}")
# Test code qualificatif ATMO invalide
try:
client._validate_code_qualificatif_atmo("8")
print("❌ Code qualificatif ATMO invalide non détecté")
except ValueError as e:
print(f"✅ Code qualificatif ATMO invalide détecté: {e}")
# Test code qualificatif ATMO valide
try:
client._validate_code_qualificatif_atmo("3")
print("✅ Code qualificatif ATMO valide accepté")
except ValueError as e:
print(f"❌ Code qualificatif ATMO valide rejeté: {e}")
# Test code qualificatif pollen invalide
try:
client._validate_code_qualificatif_pollen("7")
print("❌ Code qualificatif pollen invalide non détecté")
except ValueError as e:
print(f"✅ Code qualificatif pollen invalide détecté: {e}")
# Test code qualificatif pollen valide
try:
client._validate_code_qualificatif_pollen("4")
print("✅ Code qualificatif pollen valide accepté")
except ValueError as e:
print(f"❌ Code qualificatif pollen valide rejeté: {e}")
# Test type épisode invalide
try:
client._validate_type_episode("CRITIQUE")
print("❌ Type épisode invalide non détecté")
except ValueError as e:
print(f"✅ Type épisode invalide détecté: {e}")
# Test type épisode valide
try:
client._validate_type_episode("ALERTE")
print("✅ Type épisode valide accepté")
except ValueError as e:
print(f"❌ Type épisode valide rejeté: {e}")
# Test échéance invalide
try:
client._validate_echeance("2")
print("❌ Échéance invalide non détectée")
except ValueError as e:
print(f"✅ Échéance invalide détectée: {e}")
# Test échéance valide
try:
client._validate_echeance("-1")
print("✅ Échéance valide acceptée")
except ValueError as e:
print(f"❌ Échéance valide rejetée: {e}")
# Test échelle invalide
try:
client._validate_echelle("commune")
print("❌ Échelle invalide non détectée")
except ValueError as e:
print(f"✅ Échelle invalide détectée: {e}")
# Test échelle valide
try:
client._validate_echelle("epci")
print("✅ Échelle valide acceptée")
except ValueError as e:
print(f"❌ Échelle valide rejetée: {e}")
# Test secteur invalide
try:
client._validate_secteur("99")
print("❌ Secteur invalide non détecté")
except ValueError as e:
print(f"✅ Secteur invalide détecté: {e}")
# Test secteur valide
try:
client._validate_secteur("5")
print("✅ Secteur valide accepté")
except ValueError as e:
print(f"❌ Secteur valide rejeté: {e}")
# Test bounding box invalide (format)
try:
client._validate_bounding_box("2.2 48.8 2.4")
print("❌ Format bounding box invalide non détecté")
except ValueError as e:
print(f"✅ Format bounding box invalide détecté: {e}")
# Test bounding box invalide (coordonnées)
try:
client._validate_bounding_box("2.4 48.8 2.2 48.9")
print("❌ Coordonnées bounding box invalides non détectées")
except ValueError as e:
print(f"✅ Coordonnées bounding box invalides détectées: {e}")
# Test bounding box valide
try:
client._validate_bounding_box("2.2 48.8 2.4 48.9")
print("✅ Bounding box valide acceptée")
except ValueError as e:
print(f"❌ Bounding box valide rejetée: {e}")
print("\n=== Test des méthodes avec validations ===\n")
# Test méthode avec paramètres invalides
try:
client.get_indices_atmo(format="xml")
print("❌ Méthode avec format invalide non bloquée")
except ValueError as e:
print(f"✅ Méthode avec format invalide bloquée: {e}")
try:
client.get_episodes_3jours(aasqa="99")
print("❌ Méthode avec AASQA invalide non bloquée")
except ValueError as e:
print(f"✅ Méthode avec AASQA invalide bloquée: {e}")
try:
client.get_episodes_historique(date="2024/06/08")
print("❌ Méthode avec date invalide non bloquée")
except ValueError as e:
print(f"✅ Méthode avec date invalide bloquée: {e}")
try:
client.get_emissions(echelle="commune")
print("❌ Méthode avec échelle invalide non bloquée")
except ValueError as e:
print(f"✅ Méthode avec échelle invalide bloquée: {e}")
try:
client.get_indices_pollens(code_qualificatif="7")
print("❌ Méthode avec code qualificatif pollen invalide non bloquée")
except ValueError as e:
print(f"✅ Méthode avec code qualificatif pollen invalide bloquée: {e}")
print("\n=== Tests terminés ===")
def test_integration_examples():
"""Test d'intégration avec des exemples réalistes"""
print("\n=== Test d'intégration ===\n")
client = AtmoDataClient()
# Test avec paramètres valides (sans authentification)
try:
# Ces appels échoueront sur l'authentification mais les validations passeront
client.get_indices_atmo(
format="geojson",
date="2024-06-08",
aasqa="11",
code_qualificatif="3"
)
print("❌ Appel réussi sans authentification")
except ValueError as e:
print(f"❌ Validation échouée avec paramètres valides: {e}")
except Exception as e:
if "Token non disponible" in str(e):
print("✅ Validations réussies, échec sur l'authentification comme attendu")
else:
print(f"❌ Erreur inattendue: {e}")
try:
client.get_episodes_3jours(
format="csv",
polluant="PM10",
type_episode="ALERTE",
echeance="0"
)
print("❌ Appel réussi sans authentification")
except ValueError as e:
print(f"❌ Validation échouée avec paramètres valides: {e}")
except Exception as e:
if "Token non disponible" in str(e):
print("✅ Validations réussies, échec sur l'authentification comme attendu")
else:
print(f"❌ Erreur inattendue: {e}")
if __name__ == "__main__":
test_validations()
test_integration_examples()