Source code for download_fr

# -*- coding: utf-8 -*-
"""
Created on Tue Sep 17 16:58:10 2024

@author: Alexandre Kenshilik Coche
@contact: alexandre.co@hotmail.fr

download.py est un ensemble de fonctions permettant le téléchargement
automatique de données françaises nécessaires à la modélisation 
hydrologique des socio-écosystèmes.
Cet outil a notamment vocation à regrouper les données classiques nécessaires
au modèle CWatM utilisé dans le cadre de la 
méthodologie Eau et Territoire (https://eau-et-territoire.org ).
"""

#%% IMPORTATIONS
import os
import re
import requests
import datetime
from io import (BytesIO, StringIO)
import gzip
import numpy as np
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
import xarray as xr
xr.set_options(keep_attrs = True)
from geop4th import (
    geobricks as geo,
    utils,
    )
from geop4th.download.download_sim2 import download_sim2  # redirected to new module

#%% LIST ALL FUNCTIONALITIES
def download_available():
    excluded_funcs = {
        'valid_request',
        'valid_single_request',
        }

    utils.available(__name__, 
                    ignore = excluded_funcs,
                    )

#%% Utilitaries to check if a request is valid
[docs] def valid_request(response, file_format = 'geojson', prev_frame = None): """ Utilitary to check if a request is valid. Parameters ---------- response : request.response `request` object that contains a server's response to an HTTP request. This ``response`` variable can be obtained with ``requests.get(<url>)`` file_format : {'csv', 'json', 'geojson'}, default 'geojson' File format in which the data will be retrieved. prev_frame : pandas.DataFrame or geopandas.GeoDataFrame, optional Frame used for recurrence run (when request results are on several pages) Returns ------- isvalid : bool Flag indicating if the request has succeeded. frame : pandas.DataFrame of geopandas.GeoDataFrame Frame contaning the downloaded values. If ``file_format`` is 'geojson', returns a geopandas.GeoDataFrame, otherwise return a pandas.DataFrame (non-geospatialized data). """ isvalid = False frame = prev_frame if (response.status_code == 200): # all correct, one single page _, frame = valid_single_request(response, file_format) elif (response.status_code == 206): # correct but results are on several pages isvalid, frame = valid_single_request(response, file_format) # Safeguard preventing technical limitations of BNPE API (max request size) # BNPE API can not handle more than 20000 results in one request if 'last' in response.links: page_pattern = re.compile("page=(\d*)") n_page = page_pattern.findall(response.links['last']['url']) if len(n_page) > 0: n_results = int(n_page[0]) * frame.shape[0] if n_results > 20000: print(f"\nErr: Up to {n_results} results were asked but BNPE API cannot retrieve more than 20000 results in one request. If you passed a `masks` argument, please provide smaller masks.") return False, None if prev_frame is not None: frame = pd.concat([prev_frame, frame], axis = 0, ignore_index = True) for col in ['codes_points_prelevements', 'uri_bdlisa']: if col in frame.columns: frame[col] = frame[col].apply( lambda v: tuple(v.tolist()) if isinstance(v, np.ndarray) else (tuple(v) if isinstance(v, list) else v) ) frame = frame.drop_duplicates() if 'next' in response.links: response = requests.get(response.links['next']['url']) _, frame = valid_request(response, file_format, prev_frame = frame) elif (response.status_code == 400): print("Err: Incorrect request") elif (response.status_code == 401): print("Err: Unauthorized") elif (response.status_code == 403): print("Err: Forbidden") elif (response.status_code == 404): print("Err: Not Found") elif (response.status_code == 500): print("Err: Server internal error") # if frame is not None if isinstance(frame, pd.DataFrame): isvalid = True return isvalid, frame
def valid_single_request(response, file_format = 'geojson'): """ Utilitary to check if a request is valid. Parameters ---------- response : request.response `request` object that contains a server's response to an HTTP request. This ``response`` variable can be obtained with ``requests.get(<url>)`` file_format : {'csv', 'json', 'geojson'}, default 'geojson' File format in which the data will be retrieved. Returns ------- isvalid : bool Flag indicating if the request has succeeded. frame : pandas.DataFrame of geopandas.GeoDataFrame Frame contaning the downloaded values. If ``file_format`` is 'geojson', returns a geopandas.GeoDataFrame, otherwise return a pandas.DataFrame (non-geospatialized data). """ # ---- Determine validity of downloaded data isvalid = False if file_format == 'csv': if response.text != '': # not empty isvalid = True elif file_format in ['json', 'geojson']: if response.json()['count'] != 0: # not empty isvalid = True if isvalid: # ---- Retrieve results as a dataframe # Save data content into a DataFrame (or a GeoDataFrame) if file_format == 'csv': frame = pd.read_csv(BytesIO(response.content), sep=";") elif file_format == 'geojson': # For some datasets, the 'GeoJSON' format option is not available, # only classic JSON is retrieved # In these cases, if the user requires a GeoJSON, the JSON dict # needs to be converted to GeoJSON before export : if 'type' in response.json(): if response.json()['type'] == 'FeatureCollection': # Response content is truly a GeoJSON json_to_geojson = False else: json_to_geojson = True else: json_to_geojson = True if json_to_geojson: # Additional step of georefencing are necessary json_df = pd.DataFrame.from_dict(response.json()['data']) geometry = [Point(xy) for xy in zip(json_df.longitude, json_df.latitude)] frame = gpd.GeoDataFrame( json_df, crs = 4326, geometry = geometry) else: frame = gpd.read_file(response.text, driver='GeoJSON') elif file_format == 'json': # =============== useless ===================================================== # data_stations = pd.DataFrame.from_dict( # {i: response.json()['data'][i] for i in range(len(response.json()['data']))}, # orient = 'index') # ============================================================================= frame = pd.DataFrame.from_dict(response.json()['data']) else: frame = None return isvalid, frame #%% BNPE (Données de prélèvements)
[docs] def download_bnpe(*, dst_folder, start_year = None, end_year = None, masks = None, departments = None, communes = None, file_formats = 'geojson'): """ Function to facilitate the downloading of French water withdrawal data from BNPE API. Parameters ---------- dst_folder : str or pathlib.Path Destination folder in which the downloaded data will be stored. start_year : int, optional Year from which the data will be retrieved. end_year : int, optional Year until which the data will be retrieved. masks : list of-, or single element among str, pathlib.Path, xarray.Dataset, xarray.DataArray or geopandas.GeoDataFrame, optional Mask on which the data will be retrieved. At least one parameter among ``masks``, ``departments`` or ``communes`` should be passed. departments : int or list of int, optional INSEE code(s) of department(s) for which data will be retrieved. At least one parameter among ``masks``, ``departments`` or ``communes`` should be passed. communes : int or list of int, optional INSEE code(s) of commune(s) for which data will be retrieved. At least one parameter among ``masks``, ``departments`` or ``communes`` should be passed. file_formats : str or list of str, {'csv', 'json', 'geojson'}, default 'geojson' File format in which the data will be retrieved. Returns ------- None. Downloaded data is stored in ``dst_folder`` """ # ---- Argument retrieving # Safeguard if (departments is None) & (communes is None) & (masks is None): print("Err: It is required to specify at least one area with the arguments `departments` or `communes` or `masks`") return if start_year is None: start_year = 2008 if end_year is None: end_year = datetime.datetime.today().year if isinstance(start_year, (str, float)): start_year = int(start_year) if end_year is None: # and start_year is not None, see previous case years = [start_year] else: if isinstance(end_year, (str, float)): end_year = int(end_year) years = list(range(start_year, end_year + 1)) if masks is not None: if isinstance(masks, tuple): masks = list(masks) elif not isinstance(masks, list): masks = [masks] if departments is not None: if isinstance(departments, (str, float)): departments = [int(departments)] elif isinstance(departments, int): departments = [departments] elif isinstance(departments, tuple): departments = list(departments) if communes is not None: if isinstance(communes, (str, float)): communes = [int(communes)] elif isinstance(communes, int): communes = [communes] elif isinstance(communes, tuple): communes = list(communes) if isinstance(file_formats, str): file_formats = [file_formats] for i in range(0, len(file_formats)): # file_formats = file_formats.replace('.', '') if file_formats[i][0] == '.': file_formats[i] = file_formats[i][1:] file_formats = [ff.casefold() for ff in file_formats] # convert to lowcase outdir = os.path.join(dst_folder, "originaux") if not os.path.exists(outdir): os.makedirs(outdir) outdir_ouvrages = os.path.join(dst_folder, "originaux", "ouvrages") if not os.path.exists(outdir_ouvrages): os.makedirs(outdir_ouvrages) # ---- Requests print("\nDownloading...") for y in years: y = int(y) print(f"\n . {y}") if departments is not None: for d in departments: d = f"{d:02.0f}" # format '00' for f in file_formats: if f == 'csv': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/chroniques.csv?annee={y}&code_departement={d}&size=5000" outpath = os.path.join(outdir, f"dpmt{d}_{y}.csv") elif f == 'geojson': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/chroniques?annee={y}&code_departement={d}&format=geojson&size=5000" outpath = os.path.join(outdir, f"dpmt{d}_{y}.json") elif f == 'json': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/chroniques?annee={y}&code_departement={d}&size=5000" outpath = os.path.join(outdir, f"dpmt{d}_{y}.json") response = requests.get(url) isvalid, frame = valid_request(response, f) if isvalid: # Export if f == 'csv': geo.export(frame, outpath, sep = ';', encoding = 'utf-8', index = False) else: geo.export(frame, outpath) if communes is not None: for c in communes: c = f"{c:<05.0f}" # format '000000' for f in file_formats: if f == 'csv': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/chroniques.csv?annee={y}&code_commune_insee={c}&size=5000" outpath = os.path.join(outdir, f"cmne{c}_{y}.csv") elif f == 'geojson': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/chroniques?annee={y}&code_commune_insee={c}&format=geojson&size=5000" outpath = os.path.join(outdir, f"cmne{c}_{y}.json") elif f == 'json': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/chroniques?annee={y}&code_commune_insee={c}&size=5000" outpath = os.path.join(outdir, f"cmne{c}_{y}.json") response = requests.get(url) isvalid, frame = valid_request(response, f) if isvalid: # Export if f == 'csv': geo.export(frame, outpath, sep = ';', encoding = 'utf-8', index = False) else: geo.export(frame, outpath) if masks is not None: i_mask = 0 for m in masks: i_mask += 1 mask_ds = geo.load(m) mask_ds = geo.reproject(mask_ds, dst_crs = 4326) for f in file_formats: if f == 'csv': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/chroniques.csv?annee={y}&bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&size=5000" outpath = os.path.join(outdir, f"mask{i_mask}_{y}.csv") elif f == 'geojson': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/chroniques?annee={y}&bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&format=geojson&size=5000" outpath = os.path.join(outdir, f"mask{i_mask}_{y}.json") elif f == 'json': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/chroniques?annee={y}&bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&size=5000" outpath = os.path.join(outdir, f"mask{i_mask}_{y}.json") response = requests.get(url) isvalid, frame = valid_request(response, f) # ======= {draft} to automatically handle too large requests ================== # isvalid, frame, exceeds = valid_request(response, f) # # if exceeds: # masks = [*masks[0:i_mask-1], split(masks[i_mask-1]), *masks[i_mask:]] # bnpe(dst_folder = dst_folder, start_year = start_year, # end_year = end_year, masks = masks, # departments = departments, communes = communes, # file_formats = file_formats) # return # ============================================================================= if isvalid: # In the `mask` case, the data is clipped to the mask if isinstance(frame, gpd.GeoDataFrame): frame = geo.reproject(frame, mask = m) else: # frame is only a pd.DataFrame # first frame is converted to a GeoDataFrame geometry = [Point(xy) for xy in zip(frame.longitude, frame.latitude)] gdf = gpd.GeoDataFrame( frame, crs = 4326, geometry = geometry) # Then it is clipped gdf = geo.reproject(gdf, mask = m) # Finally it is converted back to a DataFrame frame = pd.DataFrame(gdf.drop(columns = 'geometry')) # Export if f == 'csv': geo.export(frame, outpath, sep = ';', encoding = 'utf-8', index = False) else: geo.export(frame, outpath) # Infos sur ouvrage if departments is not None: for d in departments: d = f"{d:02.0f}" # format '00' for f in file_formats: if f == 'csv': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/referentiel/ouvrages.csv?code_departement={d}&size=5000" outpath = os.path.join(outdir_ouvrages, f"dpmt{d}_ouvrages.csv") elif f == 'geojson': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/referentiel/ouvrages?code_departement={d}&format=geojson&size=5000" outpath = os.path.join(outdir_ouvrages, f"dpmt{d}_ouvrages.json") elif f == 'json': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/referentiel/ouvrages?code_departement={d}&size=5000" outpath = os.path.join(outdir_ouvrages, f"dpmt{d}_ouvrages.json") response = requests.get(url) isvalid, frame = valid_request(response, f) if isvalid: # Export if f == 'csv': geo.export(frame, outpath, sep = ';', encoding = 'utf-8', index = False) else: geo.export(frame, outpath) if communes is not None: for c in communes: c = f"{c:<05.0f}" # format '000000' for f in file_formats: if f == 'csv': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/referentiel/ouvrages.csv?code_commune_insee={c}&size=5000" outpath = os.path.join(outdir_ouvrages, f"cmne{c}_ouvrages.csv") elif f == 'geojson': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/referentiel/ouvrages?code_commune_insee={c}&format=geojson&size=5000" outpath = os.path.join(outdir_ouvrages, f"cmne{c}_ouvrages.json") elif f == 'json': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/referentiel/ouvrages?code_commune_insee={c}&size=5000" outpath = os.path.join(outdir_ouvrages, f"cmne{c}_ouvrages.json") response = requests.get(url) isvalid, frame = valid_request(response, f) if isvalid: # Export if f == 'csv': geo.export(frame, outpath, sep = ';', encoding = 'utf-8', index = False) else: geo.export(frame, outpath) if masks is not None: i_mask = 0 for m in masks: i_mask += 1 mask_ds = geo.load(m) mask_ds = geo.reproject(mask_ds, dst_crs = 4326) for f in file_formats: if f == 'csv': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/referentiel/ouvrages.csv?bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&size=5000" outpath = os.path.join(outdir_ouvrages, f"mask{i_mask}_ouvrages.csv") elif f == 'geojson': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/referentiel/ouvrages?bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&format=geojson&size=5000" outpath = os.path.join(outdir_ouvrages, f"mask{i_mask}_ouvrages.json") elif f == 'json': url = rf"https://hubeau.eaufrance.fr/api/v1/prelevements/referentiel/ouvrages?bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&size=5000" outpath = os.path.join(outdir_ouvrages, f"mask{i_mask}_ouvrages.json") response = requests.get(url) isvalid, frame = valid_request(response, f) if isvalid: # In the `mask` case, the data is clipped to the mask if isinstance(frame, gpd.GeoDataFrame): frame = geo.reproject(frame, mask = m) else: # frame is only a pd.DataFrame # first frame is converted to a GeoDataFrame geometry = [Point(xy) for xy in zip(frame.longitude, frame.latitude)] gdf = gpd.GeoDataFrame( frame, crs = 4326, geometry = geometry) # Then it is clipped gdf = geo.reproject(gdf, mask = m) # Finally it is converted back to a DataFrame frame = pd.DataFrame(gdf.drop(columns = 'geometry')) # Export if f == 'csv': geo.export(frame, outpath, sep = ';', encoding = 'utf-8', index = False) else: geo.export(frame, outpath)
#%% Hydrometry (Mesures de débits)
[docs] def download_eaufrance(*, dst_folder, masks, start_year, end_year = None, quantities = 'QmnJ', file_formats = 'geojson'): """ Function to facilitate the downloading of French hydrometry data from EauFrance API. Parameters ---------- dst_folder : str or pathlib.Path Destination folder in which the downloaded data will be stored. masks : list of-, or single element among str, pathlib.Path, xarray.Dataset, xarray.DataArray or geopandas.GeoDataFrame, optional Mask on which the data will be retrieved. start_year : int, optional Year from which the data will be retrieved. end_year : int, optional Year until which the data will be retrieved. quantities : str or list of str, default 'QmnJ' - 'QmnJ': average daily discharge - 'QmM': average monthly discharge - 'HIXM': maximum instant height per month - 'HIXnJ': maximum instant height per day - 'QINM': minimum instant discharge per month - 'QINnJ': minimum instant discharge per day - 'QixM': maximum instant discharge per month - 'QIXnJ': maximum instant discharge per day file_formats : str or list of str, {'csv', 'json', 'geojson'}, default 'geojson' File format in which the data will be retrieved. Returns ------- None. Downloaded data is stored in ``dst_folder`` """ # ---- Argument retrieving if isinstance(start_year, (str, float)): start_year = int(start_year) if end_year is None: years = [start_year] else: if isinstance(end_year, (str, float)): end_year = int(end_year) years = list(range(start_year, end_year + 1)) if masks is not None: if isinstance(masks, tuple): masks = list(masks) elif not isinstance(masks, list): masks = [masks] if isinstance(file_formats, str): file_formats = [file_formats] for i in range(0, len(file_formats)): # file_formats = file_formats.replace('.', '') if file_formats[i][0] == '.': file_formats[i] = file_formats[i][1:] if isinstance(quantities, str): quantities = [quantities] outdir = os.path.join(dst_folder, "originaux") if not os.path.exists(outdir): os.makedirs(outdir) outdir_stations = os.path.join(dst_folder, "originaux", "stations") if not os.path.exists(outdir_stations): os.makedirs(outdir_stations) # ---- Requests print("\nDownloading...") i_mask = 0 for m in masks: i_mask += 1 mask_ds = geo.load(m) mask_ds = geo.reproject(mask_ds, dst_crs = 4326) # List of stations for f in file_formats: if f == 'csv': url = rf"https://hubeau.eaufrance.fr/api/v2/hydrometrie/referentiel/stations.csv?bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&size=10000" outpath = os.path.join(outdir_stations, f"mask{i_mask}_stations.csv") elif f == 'geojson': url = url = rf"https://hubeau.eaufrance.fr/api/v2/hydrometrie/referentiel/stations?bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&format=geojson&size=10000" outpath = os.path.join(outdir_stations, f"mask{i_mask}_stations.json") elif f == 'json': url = url = rf"https://hubeau.eaufrance.fr/api/v2/hydrometrie/referentiel/stations?bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&size=10000" outpath = os.path.join(outdir_stations, f"mask{i_mask}_stations.json") response = requests.get(url) isvalid, data_stations = valid_request(response, f) if isvalid: # Data is clipped to the mask if isinstance(data_stations, gpd.GeoDataFrame): data_stations = geo.reproject(data_stations, mask = m) else: # frame is only a pd.DataFrame # first frame is converted to a GeoDataFrame data_stations.loc[:, ['longitude', 'latitude']] = data_stations['coordLatLon'].str.split(',', expand = True).rename(columns = {0: 'latitude', 1: 'longitude'}).astype(float) geometry = [Point(xy) for xy in zip(data_stations.longitude, data_stations.latitude)] gdf = gpd.GeoDataFrame( data_stations, crs = 4326, geometry = geometry) # Then it is clipped gdf = geo.reproject(gdf, mask = m) # Finally it is converted back to a DataFrame data_stations = pd.DataFrame(gdf.drop(columns = ['geometry', 'latitude', 'longitude'])) # Export if f == 'csv': geo.export(data_stations, outpath, sep = ';', encoding = 'utf-8', index = False) else: geo.export(data_stations, outpath) # Discharge data for y in years: y = int(y) print(f" . {y}") for station_id in data_stations['code_station']: for f in file_formats: for q in quantities: if f == 'csv': url = rf"https://hubeau.eaufrance.fr/api/v2/hydrometrie/obs_elab.csv?code_entite={station_id}&bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&date_debut_obs_elab={y}-01-01&date_fin_obs_elab={y}-12-31&grandeur_hydro_elab={q}&size=20000" outpath = os.path.join(outdir, f"{q}_{station_id}_mask{i_mask}_{y}.csv") # ============ apparently format option is not available ====================== # elif f == 'geojson': # url = rf"https://hubeau.eaufrance.fr/api/v2/hydrometrie/obs_elab?code_entite={station_id}&bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&date_debut_obs_elab={y}-01-01&date_fin_obs_elab={y}-12-31&grandeur_hydro_elab={q}&format=geojson&size=20000" # outpath = os.path.join(outdir, f"{q}_{station_id}_mask{i_mask}_{y}.json") # ============================================================================= elif f in ['geojson', 'json']: url = rf"https://hubeau.eaufrance.fr/api/v2/hydrometrie/obs_elab?code_entite={station_id}&bbox={mask_ds.total_bounds[0]}&bbox={mask_ds.total_bounds[1]}&bbox={mask_ds.total_bounds[2]}&bbox={mask_ds.total_bounds[3]}&date_debut_obs_elab={y}-01-01&date_fin_obs_elab={y}-12-31&grandeur_hydro_elab={q}&size=20000" outpath = os.path.join(outdir, f"{q}_{station_id}_mask{i_mask}_{y}.json") response = requests.get(url) isvalid, frame = valid_request(response, f) if isvalid: # Data is clipped to the mask if isinstance(frame, gpd.GeoDataFrame): frame = geo.reproject(frame, mask = m) else: # frame is only a pd.DataFrame # first frame is converted to a GeoDataFrame geometry = [Point(xy) for xy in zip(frame.longitude, frame.latitude)] gdf = gpd.GeoDataFrame( frame, crs = 4326, geometry = geometry) # Then it is clipped gdf = geo.reproject(gdf, mask = m) # Finally it is converted back to a DataFrame frame = pd.DataFrame(gdf.drop(columns = 'geometry')) # Export if f == 'csv': geo.export(frame, outpath, sep = ';', encoding = 'utf-8', index = False) else: geo.export(frame, outpath)
# SIM2 download has been moved to geop4th.download.download_sim2 # The import at the top of this file ensures backward compatibility: # from geop4th.download.download_sim2 import download_sim2