Code source de pyspc.webservice.lamedo.bdimage

#!/usr/bin/python3
# -*- coding: utf-8 -*-
########################################################################
#
# This file is part of python module <pyspc>.
# Copyright (C) 2013-2021  R. Marty
#   (renaud.marty@developpement-durable.gouv.fr)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program (see COPYING.txt).
# If not, see <http://www.gnu.org/licenses/>.
#
########################################################################
"""Webservice - Projet LAMEDO - BdImage."""
import collections
import itertools
import os.path
import re
import requests

import pyspc.core.exception as _exception
from pyspc.convention.lamedo import (
    BDIMAGE_BANDS, BDIMAGE_FORECASTS, BDIMAGE_PRECISION, BDIMAGE_STATS)


BBox = collections.namedtuple('BBox', ['ul', 'lr', 'asstr'],
                              defaults=[None, None, None])
BBox.__doc__ = """Bounding Box pour BdImage

Attributes
----------
ul : tuple
    Point haut gauche
lr : tuple
    Point bas droite
asstr : str
    Bounding Box sous forme de texte

"""

RE_STATUT = re.compile(r'<statut>0</statut>')


[docs] class BdImage(): """ Structure du client accédant aux données de BdImage. Attributes ---------- proxies : None Dictionnaire des proxys {'protocol': 'proxy'} client : libbdimage.bdiws.Client Client de connexion à BdImage filename : None, str Nom du dernier fichier écrit timeout : int Délai maximal de réception des requêtes. Défault: 60 url : None, str URL de la dernière requête """
[docs] def __init__(self, timeout=60): """ Initialise l'instance du webservice BdImage. Parameters ---------- timeout : int Délai maximal de réception des requêtes. Défault: 60 """ super().__init__() self.filename = None self.url = None self.timeout = timeout self.proxies = None self._client = self._constructor_client( proxies=self.proxies, timeout=timeout) self._webservices = { (True, False, 'pixels'): self._client.getobsstatsbypixels, (True, False, 'zones'): self._client.getobsstatsbyzones, (True, True, 'bbox'): self._client.getobsvaluesbybbox, (True, True, 'pixels'): self._client.getobsvaluesbypixels, (True, True, 'zones'): self._client.getobsvaluesbyzones, (False, False, 'pixels'): self._client.getprevbynetworkstatsbypixels, (False, False, 'zones'): self._client.getprevbynetworkstatsbyzones, (False, True, 'bbox'): self._client.getprevbynetworkvaluesbybbox, (False, True, 'pixels'): self._client.getprevbynetworkvaluesbypixels, (False, True, 'zones'): self._client.getprevbynetworkvaluesbyzones, }
def __str__(self): """Afficher les méta-données de l'instance BdImage.""" text = """ ************************************* ********* WEBSERVICE - BdImage ****** ************************************* * BdIMAGES PROXIES = {proxies} * BdIMAGES URL = {url} * NOM FICHIER = {filename} ************************************* """ return text.format(**vars(self)) @property def _constructor_client(self): """Importer libbdimage.bdiws.Client car lib non publique.""" from libbdimage.bdiws import Client return Client @property def _constructor_specie(self): """Importer libbdimage.bdibase.Specie car lib non publique.""" from libbdimage.bdibase import Specie return Specie @property def client(self): """Client BdImage.""" return self._client @property def webservices(self): """Webservices BdImage.""" return self._webservices
[docs] def check_client(self): """ Contrôler si le client BdImage existe. Raises ------ ValueError Si le client est incorrect """ client = self._constructor_client if not isinstance(self.client, client): raise ValueError("Le Client BdImage n'en est pas un.")
[docs] @classmethod def check_image(cls, image): """ Contrôler s'il s'agit bien d'une image BdImage. Parameters ---------- image : tuple Image BdImage (type, sous-type, bande) Raises ------ ValueError Si l'image est incorrecte See Also -------- BdImage.get_datatypes """ test = image in cls.get_datatypes(asstr=isinstance(image, str)) if not test: raise ValueError("Image mal renseignée")
[docs] def check_precision(self, precision): """ Contrôler si la précision est autorisée. Parameters ---------- precision : str Qualité de la précision Raises ------ ValueError Si la précision est incorrecte """ self.check_client() try: self.client._check_precision(precision=precision) except ValueError as ve: raise ValueError("Précision mal renseignée") from ve
[docs] def check_start(self, image=None, date=None): """ Contrôler si la date respecte le premier instant de disponibilité. Parameters ---------- date : datetime.datetime Date à tester image : tuple Tuple à 3 dimensions de l'identifiant de l'image (type Image, sous type Image, bande). Voir BdImage.get_datatypes() pour avoir la liste des images autorisées Raises ------ ValueError Date est antérieure à la première date disponible de l'image """ _exception.check_dt(date) self.check_image(image) specie = self._constructor_specie(image[0], image[1]) test = date >= specie.firstdate if not test: raise ValueError("Date antérieure à la première date de l'image " f"{image} : {date} / {specie.firstdate}")
[docs] def check_stats(self, stats): """ Contrôler si la stat est autorisée. Parameters ---------- stats : str Longueur du retour des statistiques. Raises ------ ValueError Si la stat est incorrecte """ self.check_client() try: self.client._check_stats(stats=stats) except ValueError as ve: raise ValueError("Statistique mal renseignée") from ve
[docs] def get(self, image=None, tdelta=None, first_dtime=None, last_dtime=None, runtime=None, domains=None, epsg='2154', stats=None, precision=None): """ Récupérer les données de la BdImage. Parameters ---------- image : tuple Tuple à 3 dimensions de l'identifiant de l'image (type Image, sous type Image, bande). tdelta : str, timedelta, None Pas de temps de cumul de l'image first_dtime : str, datetime, None Première date de la collection d'images last_dtime : str, datetime, None Dernière date de la collection d'images runtime : str, datetime, None Instant de production de la prévision. Utilisé uniquement si le type d'image est parmi BDIMAGE_FORECASTS domains : str Chaine de caractères définissant les domaines epsg : str Identifiant de la projection. Défaut: '2154' (Lambert 93) stats : str Longueur du retour des statistiques. precision : str Qualité de la précision. Returns ------- content : dict Dictionnaire des retours XML - clé: (domain, image, first_dtime, last_dtime, runtime, varname) - valeur: contenu de la réponse XML See Also -------- pyspc.convention.lamedo.BDIMAGE_FORECASTS BdImage.get_datatypes BdImage.get_precision BdImage.get_stats BdImage.split_domains """ self.url = None # --------------------------------------------------------------------- # 0- Contrôles : par libbdimage # --------------------------------------------------------------------- # --------------------------------------------------------------------- # 1- Collection d'images # --------------------------------------------------------------------- try: image_coll = _set_collection( image=image, network=runtime, start=first_dtime, stop=last_dtime, depth=tdelta) except ValueError as ve: if 'durée' in ve.args[0]: _exception.Warning( None, "Une erreur survient à cause de la profondeur/durée. Une " "tentative est réalisée en la fixant à None.") image_coll = _set_collection( image=image, network=runtime, start=first_dtime, stop=last_dtime, depth=None) else: image_coll = None # --------------------------------------------------------------------- # 2- Domaines d'extraction # --------------------------------------------------------------------- content = collections.OrderedDict() splitted_domains = split_domains(domains=domains) # --------------------------------------------------------------------- # 3 - Boucle sur les 3 types de domaine # --------------------------------------------------------------------- for domaintype, subdomains in splitted_domains.items(): if subdomains is None: continue ws_content = _request_bdimage( bdimage=self, image_coll=image_coll, band=image[-1], domain=subdomains, domaintype=domaintype, epsg=epsg, stats=stats, precision=precision ) if domaintype == 'zones': key = ("+".join(subdomains), image, first_dtime, last_dtime, runtime, tdelta) elif domaintype == 'pixels': key = "+".join([",".join([str(pp) for pp in p]) for p in subdomains]) key = (key, image, first_dtime, last_dtime, runtime, tdelta) elif domaintype == 'bbox': key = (subdomains.asstr, image, first_dtime, last_dtime, runtime, tdelta) content.setdefault(key, ws_content) # --------------------------------------------------------------------- # 4- Renvoi des contenus XML # --------------------------------------------------------------------- return content
[docs] def retrieve(self, dirname='.', domainname=None, image=None, tdelta=None, first_dtime=None, last_dtime=None, runtime=None, domains=None, epsg='2154', stats=None, precision=None): """ Récupérer les données de la BdImage et les enregistrer dans des xml. Parameters ---------- dirname : str Répertoire local d'archivage des fichiers XML de BdImage domainname : str Nom des domaines géographiques. Si utilisé, le lieu est défini par domainname-domaintype Other Parameters ---------------- image : tuple Tuple à 3 dimensions de l'identifiant de l'image (type Image, sous type Image, bande). tdelta : str, timedelta, None Pas de temps de cumul de l'image first_dtime : str, datetime, None Première date de la collection d'images last_dtime : str, datetime, None Dernière date de la collection d'images runtime : str, datetime, None Instant de production de la prévision. Utilisé uniquement si le type d'image est parmi BDIMAGE_FORECASTS domains : str Chaine de caractères définissant les domaines epsg : str Identifiant de la projection. Défaut: '2154' (Lambert 93) stats : str Longueur du retour des statistiques. precision : str Qualité de la précision. Returns ------- filenames : list Fichiers XML enregistrés See Also -------- pyspc.convention.lamedo.BDIMAGE_FORECASTS BdImage.get """ from libbdimage.bdibase import depth2str # --------------------------------------------------------------------- # 1- Récupération des contenus XML # --------------------------------------------------------------------- content = self.get( image=image, tdelta=tdelta, first_dtime=first_dtime, last_dtime=last_dtime, runtime=runtime, domains=domains, epsg=epsg, stats=stats, precision=precision ) # --------------------------------------------------------------------- # 2- Enregistrement des contenus XML # --------------------------------------------------------------------- filenames = [] for key in content: try: if not RE_STATUT.search(str(content[key])): raise ValueError except (KeyError, ValueError): _exception.Warning( None, f"Contenu XML incorrect pour la clé {key}") continue code = key[0] if isinstance(domainname, str): splitted_domains = split_domains(domains=domains) domaintype = [t for t, s in splitted_domains.items() if s is not None][0] code = f"{domainname}-{domaintype}" if key[-1] is None: depth = '000000' else: depth = depth2str(key[-1]) if key[4] is None: basename = f"{code}_{'-'.join(list(key[1]))}"\ f"_{key[2].strftime('%Y%m%d%H%M')}"\ f"-{key[3].strftime('%Y%m%d%H%M')}_{depth}.xml" else: basename = f"{code}_{'-'.join(list(key[1]))}"\ f"_{key[4].strftime('%Y%m%d%H%M')}"\ f"-{key[2].strftime('%Y%m%d%H%M')}"\ f"-{key[3].strftime('%Y%m%d%H%M')}_{depth}.xml" filename = os.path.join(dirname, basename) try: with open(filename, 'w', encoding='utf-8', newline="\n") as f: f.write(content[key].decode('utf-8')) except FileNotFoundError: _exception.Warning( None, "Répertoire local inexistant ou nom de fichier trop " f"long pour la clé {key}. Veuillez réduire le nombre de " "pixels/zones ou utiliser l'argument domainname.") continue self.filename = filename filenames.append(filename) # --------------------------------------------------------------------- # 3- Renvoi de la liste des fichiers # --------------------------------------------------------------------- return filenames
[docs] @classmethod def get_precision(cls): """Lister des précisions.""" return sorted(BDIMAGE_PRECISION)
[docs] @classmethod def get_stats(cls): """Lister des longueurs du retour des statistiques.""" return sorted(BDIMAGE_STATS)
[docs] @classmethod def get_datatypes(cls, asstr=False): """ Lister des images. Parameters ---------- asstr : bool Renvoyer une liste de str (True) ou de tuples (False) Returns ------- images : list Liste des images : (type, soustype, bande) ou type_soustype_bande """ if asstr: return sorted(itertools.chain.from_iterable( [[f"{i}_{b}" for b in BDIMAGE_BANDS[i]] for i in BDIMAGE_BANDS.keys()]) ) return sorted(itertools.chain.from_iterable( [[(*i.split('_'), b) for b in BDIMAGE_BANDS[i]] for i in BDIMAGE_BANDS.keys()]) )
def split_domains(domains=None, epsg='2154'): """ Définir les zones / pixels / bbox à partir d'une chaine de caractères. Parameters ---------- domains : str Chaine de caractères définissant les domaines epsg : str Identifiant de la projection. Défaut: '2154' (Lambert 93) Returns ------- dict {'zones': list, None, 'pixels': libbdimage.bdipixel.Nppixels, None, 'bbox': Bbox, None} Notes ----- Définition des domaines géographiques - Une zone est libellé ainsi: AB123456 - Une sous-bassin est libellé ainsi: AB123-AB456 - Un pixel est libellé ainsi: 1,1 - Un rectangle est libellé ainsi: Bx1,2,3,4 Pour considérer plusieurs zones ou pixels, il suffit de les "coller" avec le signe '+'. Cette fonctionnalité est ignorée dans le cas de rectangle Les pixels et coordonnées des rectangles sont à renseigner en mètre. Pour le rectangle, il faut renseigner, dans l'ordre - préfixe 'Bx' - coordonnée X du point en haut à gauche - coordonnée Y du point en haut à gauche - coordonnée X du point en bas à droite - coordonnée Y du point en bas à droite """ from libbdimage.bdipixel import Nppixels # --------------------------------------------------------------------- # 0- Contrôles # --------------------------------------------------------------------- _exception.check_str(domains) _exception.raise_valueerror( domains.count('Bx') > 1, "une seule Bounding Box est autorisée" ) # --------------------------------------------------------------------- # 1- Initialisation # --------------------------------------------------------------------- zones = [] pixels = [] bbox = None # --------------------------------------------------------------------- # 2- Découpage des domaines # --------------------------------------------------------------------- for d in domains.split('+'): # Cas BOUNDING BOX if d.startswith('Bx'): b = d[2:].split(',') if len(b) != 4: _exception.Warning( f"La Bounding Box '{d}' est incorrecte. Elle est ignorée") continue bbox = BBox((float(b[0]), float(b[1])), (float(b[2]), float(b[3])), d) # Cas ZONES elif d[0:2].isalpha() and ',' not in d[2:]: zones.append(d) # Cas PIXELS elif d.replace(',', '').replace('.', '').isdigit(): p = [float(x) for x in d.split(',')] if len(p) != 2: _exception.Warning( f"Le pixel '{d}' est incorrect. Il est ignoré") continue pixels.append(tuple(p)) # --------------------------------------------------------------------- # 3- Renvoi des domaines # --------------------------------------------------------------------- return {'zones': zones if zones else None, 'pixels': Nppixels(pixels, epsg=epsg) if pixels else None, 'bbox': bbox} def _request_bdimage(bdimage=None, image_coll=None, band=None, domain=None, domaintype=None, epsg='2154', stats=None, precision=None): """ Requêter BdImage. Parameters ---------- bdimage : BdImage Instance BdImage image_coll : libbdimage.bdimage.Collection, libbdimage.bdimage.NCollection Collection d'images band : str Bande de l'image (3e élément du tuple image) domain : list, libbdimage.bdipixel.Nppixels, Bbox Domaine géographique: liste de zones, Pixels, Bounding Box domaintype : str Type de domaine géographique, parmi ['zones', 'pixels', 'bbox'] epsg : str Identifiant de la projection. Défaut: '2154' (Lambert 93) stats : str Longueur du retour des statistiques. precision : str Qualité de la précision. Returns ------- content : bytes, str Contenu du XML BdImage """ # --------------------------------------------------------------------- # 0- Initialisation # --------------------------------------------------------------------- params = {'bands': band} # --------------------------------------------------------------------- # 1- OBS ou PREV ? # --------------------------------------------------------------------- if image_coll.specie.family in BDIMAGE_FORECASTS: isobs = False params['ncollection'] = image_coll else: isobs = True params['collection'] = image_coll # --------------------------------------------------------------------- # 2- STATS or VALUES ? # --------------------------------------------------------------------- if stats is None and precision is None: isvalues = True else: isvalues = False if stats is not None: params['stats'] = stats if precision is not None: params['precision'] = precision # --------------------------------------------------------------------- # 3- DOMAIN : ZONES, PIXELS or BBOX ? # --------------------------------------------------------------------- params.update(_process_request_domaintype( domaintype, domain, epsg, isvalues)) params['timeout'] = bdimage.timeout # --------------------------------------------------------------------- # 4- PARAMETRES et WEBSERVICE # --------------------------------------------------------------------- key = (isobs, isvalues, domaintype) try: ws = bdimage.webservices[key] except KeyError as ke: raise ValueError( f"Aucun webservice correspond à :\nobs={key[0]}, values={key[1]}, " f"domain={key[2]}") from ke try: content = ws(**params) except requests.ConnectionError as rce: _process_request_ConnectionError(rce) except ValueError as ve: _process_request_ValueError(ve, domain) except TypeError as te: _process_request_TypeError(te, domain) except FileNotFoundError as fnfe: raise ValueError(f"L'argument '{fnfe.filename}' est supposé être un " "nom de fichier: celui-ci n'existe pas") from fnfe except OSError as oe: _process_request_OSError(oe, band, epsg, domain) # --------------------------------------------------------------------- # 5- RETOUR DU CONTENU # --------------------------------------------------------------------- bdimage.url = "{url}?{body}".format( # noqa # pylint: disable=consider-using-f-string **vars(bdimage.client._response.request)) return content def _process_request_domaintype(domaintype, domain, epsg, isvalues): """Paramétrage selon le type de domaine.""" params = {} if domaintype == 'zones': params['zones'] = domain if isvalues: params['epsg'] = epsg elif domaintype == 'pixels': params['pixels'] = domain params['epsg'] = epsg if isvalues: params['coord'] = True elif domaintype == 'bbox': params['ul'] = domain.ul params['lr'] = domain.lr params['epsg'] = epsg return params def _process_request_ConnectionError(rce): """Traiter les erreurs de type requests.ConnectionError.""" url = rce.request.url body = str(rce.request.body) if 'Async' in body: body = body.replace('\\r', '') body = body.replace('\\n', '') body = "&".join([str(b.strip('-').split('"')[1]) + '=' + str(b.strip('-').split('"')[2]) for b in re.findall(r'name="\w*"\w*--', body)]) raise ValueError( f"Erreur lors de la requête BdImage:\n{url}?{body}") from rce def _process_request_ValueError(ve, domain): """Traiter les erreurs de type ValueError.""" if re.search(r"^invalid ul: .*$", ve.args[0]): raise ValueError( f"Point haut-gauche '{domain.ul}' est mal-formaté. ") from ve if re.search(r"^invalid lr: .*$", ve.args[0]): raise ValueError( f"Point bas-droite '{domain.lr}' est mal-formaté. ") from ve raise ValueError(ve.args[0]) from ve def _process_request_TypeError(te, domain): """Traiter les erreurs de type TypeError.""" if te.args[0] == 'zones must be a filename or an iterable of zones '\ 'codes': raise ValueError(f"Les zones sont mal-formatées. '{domain}' " "n'est ni un fichier ni une liste de zones") from te if te.args[0] == "pixels must be a filename or an iterable of 'x,y'": raise ValueError(f"Les pixels sont mal-formatés. '{domain}' " "n'est ni un fichier ni une liste de 'x,y'") from te if re.search(r"^invalid ul: .*$", te.args[0]): raise ValueError( f"Point haut-gauche '{domain.ul}' est mal-formaté. ") from te if re.search(r"^invalid lr: .*$", te.args[0]): raise ValueError( f"Point bas-droite '{domain.lr}' est mal-formaté. ") from te raise ValueError(te.args[0]) from te def _process_request_OSError(oe, band, epsg, domain): """Traiter les erreurs de type OSError.""" if re.search(r"^invalid band name: '.*'$", oe.args[0]): raise ValueError( f"La bande '{band}' est mal-formatée. ") from oe if re.search(r"^invalid epsg code: '.*'$", oe.args[0]): raise ValueError( f"L'argument epsg '{epsg}' est mal-formatée. ") from oe if re.search(r"^async job .*, invalid epsg code: '.*'$", oe.args[0]): raise ValueError( f"L'argument epsg '{epsg}' est mal-formatée. ") from oe if re.search(r"^async job .*, top must be greater than bottom$", oe.args[0]): raise ValueError( "Incompatibilité entre les points haut-gauche et " f"bas droite:\n{domain.ul} contre {domain.lr}") from oe raise ValueError(oe.args[0]) from oe def _set_collection(image=None, network=None, start=None, stop=None, depth=None): """ Définir une collection d'images. Parameters ---------- image : tuple Tuple à 3 dimensions de l'identifiant de l'image (type Image, sous type Image, bande). network : str, datetime, None Instant de production de la prévision. Utilisé uniquement si le type d'image est parmi BDIMAGE_FORECASTS start : str, datetime, None Première date de la collection d'images stop : str, datetime, None Dernière date de la collection d'images depth : str, timedelta, None Pas de temps de cumul de l'image Returns ------- img_coll : libbdimage.bdimage.Collection, libbdimage.bdimage.NCollection Collection d'images See Also -------- pyspc.convention.lamedo.BDIMAGE_FORECASTS libbdimage.bdimage.Collection libbdimage.bdimage.NCollection """ from libbdimage.bdimage import Collection, NCollection # --------------------------------------------------------------------- # 0- Informations sur les images # --------------------------------------------------------------------- families = sorted({i[0] for i in BdImage.get_datatypes()}) kinds = sorted({i[1] for i in BdImage.get_datatypes() if i[0] == image[0]}) # --------------------------------------------------------------------- # 1- Chargement de la collection # --------------------------------------------------------------------- try: if image[0] in BDIMAGE_FORECASTS: if network is None: network = start img_coll = NCollection(image[0], image[1], network=network, start=start, stop=stop, depth=depth) else: img_coll = Collection(image[0], image[1], start=start, stop=stop, depth=depth) # --------------------------------------------------------------------- # 2- Récupération / traduction des erreurs # --------------------------------------------------------------------- except ValueError as ve: _process_collection_ValueError(ve, image, families, kinds, start, stop, depth, network) # --------------------------------------------------------------------- # 3- Retour de la collection d'images # --------------------------------------------------------------------- return img_coll def _process_collection_ValueError(ve, image, families, kinds, start, stop, depth, network): """Traiter les erreurs de type ValueError.""" if re.search(r"^family '.*' not found$", ve.args[0]): raise ValueError( f"Type d'image incorrect. '{image[0]}' n'est pas " f"dans {families}") from ve if re.search(r"^kind '.*' does not match with '.*' image$", ve.args[0]): raise ValueError( f"Sous-type d'image incompatible avec le type '{image[0]}'. " f"'{image[1]}' n'est pas dans {kinds}") from ve if ve.args[0] == 'start must be like yyyymmddHHMM': raise ValueError( f"Date de début des données mal-formatée. '{start}' n'est pas de " "la forme yyyymmddHHMM") from ve if ve.args[0] == 'invalid start with seconds or microseconds': raise ValueError( f"Date de début des données mal-formatée. '{start}' ne doit pas " "contenir de seconde, ni de micro-seconde") from ve if ve.args[0] == 'stop must be like yyyymmddHHMM': raise ValueError( f"Date de fin des données mal-formatée. '{stop}' n'est pas de la " "forme yyyymmddHHMM") from ve if ve.args[0] == 'invalid stop with seconds or microseconds': raise ValueError( f"Date de fin des données mal-formatée. '{stop}' ne doit pas " "contenir de seconde, ni de micro-seconde") from ve if ve.args[0] == 'depth must be like [d*]ddHHMM': raise ValueError( f"Durée de cumul mal-formatée. '{depth}' n'est pas de la " "forme ddHHMM") from ve if re.search(r"^depth '.*' does not match with '.*' image$", ve.args[0]): raise ValueError( f"La durée '{depth}' est incompatible avec " f"{image[0]}_{image[1]}") from ve if ve.args[0] == 'date must be like yyyymmddHHMM': raise ValueError( f"Date de prévision mal-formatée. '{network}' n'est pas de la " "forme yyyymmddHHMM") from ve if ve.args[0] == 'invalid date with seconds or microseconds': raise ValueError( f"Date de prévision mal-formatée. '{network}' ne doit pas " f"contenir de seconde, ni de micro-seconde") from ve if re.search(r"^network '.*' does not match with '.*'$", ve.args[0]): raise ValueError( f"La date de prévision '{network}' est incompatible avec " f"{image[0]}_{image[1]}") from ve if ve.args[0] == 'network is required to set start or stop': raise ValueError( f"Date de prévision absente: {network}. ") from ve raise ValueError("Une erreur inconnue est survenue lors de la " "définition de la collection d'images") from ve