first commit
This commit is contained in:
commit
a233e18c0b
48 changed files with 55300 additions and 0 deletions
15
.claude/settings.local.json
Normal file
15
.claude/settings.local.json
Normal 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
27
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
29
MANIFEST.in
Normal 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
|
109
atmo_data_wrapper/__init__.py
Normal file
109
atmo_data_wrapper/__init__.py
Normal 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'
|
||||
]
|
77
atmo_data_wrapper/core/__init__.py
Normal file
77
atmo_data_wrapper/core/__init__.py
Normal 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'
|
||||
]
|
598
atmo_data_wrapper/core/client.py
Normal file
598
atmo_data_wrapper/core/client.py
Normal 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
|
297
atmo_data_wrapper/core/constants.py
Normal file
297
atmo_data_wrapper/core/constants.py
Normal 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 d’information des données open data sur la qualité de l’air 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 d’information des données open data sur la qualité de l’air 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 d’information des données open data sur la qualité de l’air 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 d’information des données open data sur la qualité de l’air disponibles sur Atmo Data - Version 1er avril 2025
|
||||
POLLUANTS = ['NO2', 'SO2', 'PM10', 'PM2.5', 'O3']
|
||||
|
||||
CODE_POLLUANT = {
|
||||
'NO2' : 'dioxyde d’azote' ,
|
||||
'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 d’information des données open data sur la qualité de l’air 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 d’information des données open data sur la qualité de l’air 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."""
|
||||
|
42
atmo_data_wrapper/core/exceptions.py
Normal file
42
atmo_data_wrapper/core/exceptions.py
Normal 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
|
631
atmo_data_wrapper/core/models.py
Normal file
631
atmo_data_wrapper/core/models.py
Normal 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 l’alerte 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
|
218
atmo_data_wrapper/core/utils.py
Normal file
218
atmo_data_wrapper/core/utils.py
Normal 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
5
credentials.json.example
Normal 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
3
demos/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Demos package for Atmo Data Wrapper
|
||||
"""
|
343
demos/demo_atmo_functions.py
Normal file
343
demos/demo_atmo_functions.py
Normal 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
165
demos/demo_complete.py
Normal 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()
|
472
demos/demo_emission_functions.py
Normal file
472
demos/demo_emission_functions.py
Normal 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()
|
536
demos/demo_episode_functions.py
Normal file
536
demos/demo_episode_functions.py
Normal 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
213
demos/demo_licence_atmo.py
Normal 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()
|
333
demos/demo_pollen_functions.py
Normal file
333
demos/demo_pollen_functions.py
Normal 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
645
docs/DOCUMENTATION_DEMOS.md
Normal 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
1433
docs/JOURNAL.md
Normal file
File diff suppressed because it is too large
Load diff
157
docs/QUICKSTART.md
Normal file
157
docs/QUICKSTART.md
Normal 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
537
docs/README.md
Normal 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 l’air (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 l’air" 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 d’information des données open data sur la qualité de l’air 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
|
379
docs/notice_Atmo_Data_1eravril2025.md
Normal file
379
docs/notice_Atmo_Data_1eravril2025.md
Normal 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*
|
BIN
docs/notice_Atmo_Data_1eravril2025.pdf
Normal file
BIN
docs/notice_Atmo_Data_1eravril2025.pdf
Normal file
Binary file not shown.
1
docs/swagger.json
Normal file
1
docs/swagger.json
Normal file
File diff suppressed because one or more lines are too long
3
examples/__init__.py
Normal file
3
examples/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Examples package for Atmo Data Wrapper
|
||||
"""
|
276
examples/example_aasqa_advanced.py
Normal file
276
examples/example_aasqa_advanced.py
Normal 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()
|
250
examples/example_aasqa_utilities.py
Normal file
250
examples/example_aasqa_utilities.py
Normal 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()
|
367
examples/example_data_models.py
Normal file
367
examples/example_data_models.py
Normal 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()
|
185
examples/example_save_files.py
Normal file
185
examples/example_save_files.py
Normal 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()
|
324
examples/example_synthese_nancy.py
Normal file
324
examples/example_synthese_nancy.py
Normal 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
166
examples/example_usage.py
Normal 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()
|
126
examples/setup_credentials.py
Normal file
126
examples/setup_credentials.py
Normal 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()
|
1289
export_2025-07-07/indices_idf.csv
Normal file
1289
export_2025-07-07/indices_idf.csv
Normal file
File diff suppressed because it is too large
Load diff
43804
export_2025-07-07/indices_idf.json
Normal file
43804
export_2025-07-07/indices_idf.json
Normal file
File diff suppressed because it is too large
Load diff
3
exports/daily/indices_atmo_20250707.csv
Normal file
3
exports/daily/indices_atmo_20250707.csv
Normal 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
|
|
47
exports/qualite_air/paris_2024.json
Normal file
47
exports/qualite_air/paris_2024.json
Normal 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
144
pyproject.toml
Normal 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
23
requirements-dev.txt
Normal 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
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Production dependencies
|
||||
requests>=2.25.0
|
96
setup.py
Normal file
96
setup.py
Normal 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
3
tests/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Test package for Atmo Data Wrapper
|
||||
"""
|
174
tests/test_credentials_system.py
Normal file
174
tests/test_credentials_system.py
Normal 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()
|
187
tests/test_real_connection.py
Normal file
187
tests/test_real_connection.py
Normal 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é.")
|
177
tests/test_save_functionality.py
Normal file
177
tests/test_save_functionality.py
Normal 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
107
tests/test_typed_client.py
Normal 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
261
tests/test_validations.py
Normal 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()
|
Loading…
Add table
Add a link
Reference in a new issue