Compare commits

..

1 Commits

Author SHA1 Message Date
Félix Baylac-Jacqué e8dec4a228
wip 2022-04-12 21:36:50 +02:00
30 changed files with 251 additions and 1569 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
.idea

View File

@ -0,0 +1,28 @@
import subprocess
import configparser
class Config(TypedDict):
hostname: str
user: str
password: str
def parseConfig() -> Config:
cfg_path = os.environ.get("CONFIG", "/etc/ftth-ipe-map/conf.ini")
cfg = configparser.ConfigParser()
with open(cfg_path, "r") as f:
cfg.read_file(f)
return {
'hostname':cfg.get("AXIONE_FTP","hostname"),
'user':cfg.get("AXIONE_FTP","user"),
'password':cfg.get("AXIONE_FTP","password"),
}
def call_lftp(args):
return subprocess.run(["lftp", "-u", f"{cfg.user},{cfg.password}", hostname]
+ args).stdout.decode("utf8")
def retrieve_ipe_list(cfg: Config):
return call_lftp(["-e", "set ssl:verify-certificate no; find /; bye"])
if __name__ == '__main__':
cfg = parseConfig

View File

@ -1,159 +0,0 @@
#!/usr/bin/env bash
set -eau -o pipefail
# API where to list arcep files
GOUV_API_URL=https://www.data.gouv.fr/api/1/datasets/le-marche-du-haut-et-tres-haut-debit-fixe-deploiements/
# File to store last versions downloaded
VERSIONS_FILENAME=.arcep_versions
## Content of version file:
# LAST_ARCEP_ZIP=<file date>__<file name>.zip
# BEFORE_ARCEP_ZIP=<file date>__<file name>.zip
# Global vars
g_last_arcep_zip=""
g_before_arcep_zip=""
g_penultimate_arcep_zip=""
g_arcep_to_unzip=""
# Script usage
usage() {
echo
echo "Usage : $0 -d|--dir-out <dir to put downloads in> (-r|--remove-penultimate)"
echo
echo " With:"
echo " -d|--dir-out: folder where to store zip files"
echo " (-r|--remove-penultimate): if set, remove 2nd before last version after dl latest file"
echo " (-f|--force-dl): if set, will force re-download data and process it"
echo
}
# Get already dl data info
source_versions() {
dir_out=$1
ver_file=${dir_out}/${VERSIONS_FILENAME}
LAST_ARCEP_ZIP=""
BEFORE_ARCEP_ZIP=""
[[ -f ${ver_file} ]] && source "${ver_file}"
g_last_arcep_zip=${LAST_ARCEP_ZIP}
g_before_arcep_zip=${BEFORE_ARCEP_ZIP}
}
# Dl arcep latest data if needed
dl_latest_arcep() {
dir_out=$1
force_dl=$2
rc=0
echo "Create out dir ${dir_out} if not exist"
mkdir -p "${dir_out}"
ver_file=${dir_out}/${VERSIONS_FILENAME}
touch "${ver_file}"
latest_file_url="$(curl -s ${GOUV_API_URL} | jq -r '.resources[] |objects | .url' | grep -i immeubles | head -1)"
file_date=$(echo "$latest_file_url" | cut -f6 -d '/')
file_name=$(echo "$latest_file_url" | cut -f7 -d '/')
latest_f=${file_date}__${file_name}
echo "Found ${latest_f} Check if already exist"
if [[ -n ${g_last_arcep_zip} && "${latest_f}" = "${g_last_arcep_zip}" && $force_dl != "true" ]]; then
echo "File ${latest_f} is already the latest ! Do not do anything"
else
echo "File ${latest_f} not there, download it"
wget -O "${dir_out}"/"${latest_f}" "${latest_file_url}" || rc=1
g_penultimate_arcep_zip=${g_before_arcep_zip}
g_before_arcep_zip=${g_last_arcep_zip}
g_last_arcep_zip=${latest_f}
g_arcep_to_unzip=${latest_f}
echo "OK, update versions file"
echo "LAST_ARCEP_ZIP=${g_last_arcep_zip}" >"${ver_file}"
echo "BEFORE_ARCEP_ZIP=${g_before_arcep_zip}" >>"${ver_file}"
fi
return ${rc}
}
# Unzip a dl arcep file
unzip_arcep() {
dir_out=$1
zip_file=$2
zip_dir=$(echo "${zip_file}" | rev | cut -f2- -d '.' | rev)
mkdir -p "${dir_out}/$zip_dir"
echo "Unzip file ${dir_out}/${zip_file}"
unzip "${dir_out}"/"${zip_file}" -d "${dir_out}"/"$zip_dir" || return 1
return 0
}
# main
main() {
# Init input vars
remove_penultimate=false
force_dl=false
dir_out=""
# Read inputs
[[ $# -eq 0 ]] && usage && return 1
while [ -n "$1" ]; do
case $1 in
-d | --dir-out)
dir_out=$(realpath "$2")
shift
;;
-r | --remove-penultimate)
remove_penultimate=true
;;
-f | --force-dl)
force_dl=true
;;
-h | --help)
usage && exit 0
;;
*)
echo "Unknown command: $1"
usage && exit 1
;;
esac
[[ $# -le 1 ]] && break
shift
done
# check inputs
if [[ -z ${dir_out} ]]; then
echo "Error: You need to specify an output dir -d|--dir-out <dir path>"
usage
return 1
fi
rc=0
# Read existing dl versions
source_versions "${dir_out}" || rc=1
# Download latest zip file if needed
[[ $rc -eq 0 ]] && dl_latest_arcep "${dir_out}" $force_dl || rc=1
# If download succeeded and there is a file to unzip
if [[ $rc -eq 0 && -n $g_arcep_to_unzip ]]; then
# unzip file
unzip_arcep "${dir_out}" "${g_last_arcep_zip}" || rc=1
# Unzip succeeded and need to remove penultimate arcep data (if exists)
if [[ $rc -eq 0 && \
$remove_penultimate && -n \
$g_penultimate_arcep_zip && -f \
${dir_out}/$g_penultimate_arcep_zip ]]; then
echo "Delete penultimate zip ${dir_out}/$g_penultimate_arcep_zip"
rm -f "${dir_out}"/"$g_penultimate_arcep_zip"
zip_dir=$(echo "${g_penultimate_arcep_zip}" | rev | cut -f2- -d '.' | rev)
if [[ -d ${dir_out}/${zip_dir} ]]; then
echo "remove dir ${dir_out}/${zip_dir}"
rm -rf "${dir_out}"/"${zip_dir}"
fi
elif [[ $rc -ne 0 ]]; then
echo "Failed to unzip ${g_last_arcep_zip} !"
fi
fi
return $rc
}
### Call main
main "$@" || exit 1
exit 0

View File

@ -1,8 +1,6 @@
#!/usr/bin/env bash
set -eau -o pipefail
NEEDED_COLUMNS=("IdentifiantImmeuble" "EtatImmeuble" "CoordonneeImmeubleX" "CoordonneeImmeubleY" "NumeroVoieImmeuble" "TypeVoieImmeuble" "NomVoieImmeuble" "CodePostalImmeuble" "CommuneImmeuble" "DateDebutAcceptationCmdAcces" "DateMiseEnServiceCommercialeImmeuble")
if [ "$#" -ne 2 ]; then
echo "Usage: ingest path-to-directory-containing-IPE-CSVs path-to-generated-db"
echo ""
@ -25,64 +23,14 @@ cat > "${tmpSql}" <<EOF
.separator ";"
EOF
firstFile=true
for ipeFile in ${ipeFiles}; do
echo " ${ipeFile}"
head -n1 $ipeFile | grep -q IdentifiantImmeuble && header=true || header=false
import_opt=""
if $firstFile || $header; then
if ! $header; then
echo "ERROR: first file ${ipeFile} does not have a good header"
exit 1
fi
if ! $firstFile; then
import_opt="-skip 1"
else
header=$(head -n1 $ipeFile)
OLD_IFS=$IFS
export IFS=";"
idx=1
idx_to_keep=()
for column in $header; do
export IFS=$OLD_IFS
if [[ " ${NEEDED_COLUMNS[*]} " =~ " ${column} " ]]; then
idx_to_keep+=("$idx")
fi
idx=$((idx+1))
export IFS=";"
done
export IFS=$OLD_IFS
cut_idx_to_keep=$(IFS=',';echo "${idx_to_keep[*]}";IFS=$' \t\n')
echo " Column indexes that will be kept in csv files: $cut_idx_to_keep (matching columns ${NEEDED_COLUMNS[*]})"
fi
firstFile=false
fi
useIpeFile=${ipeFile}
if [[ "$cut_idx_to_keep" != "" ]]; then
cut -d';' -f$cut_idx_to_keep $ipeFile > ${ipeFile}.cut
useIpeFile=${ipeFile}.cut
fi
cat >> "${tmpSql}" <<EOF
.import ${import_opt} ${useIpeFile} ipe
echo " ${ipeFile}"
cat >> "${tmpSql}" <<EOF
.import ${ipeFile} ipe
EOF
done
echo ""
rc=0
sqlite3 "${fullDbPath}" < "${tmpSql}" 2>&1 | grep -v "filling the rest with NULL" || rc="${PIPESTATUS[0]}"
if [[ $rc -ne 0 ]]; then
echo "Error executing sqlite import"
exit "$rc"
fi
echo "[+] Create separate table with id immeuble index and its state."
cat > "${tmpSql}" <<EOF
CREATE TABLE refimm (IdentifiantImmeuble text NOT NULL, EtatImmeuble text NOT NULL, DateDebutAcceptationCmdAcces text NOT NULL);
CREATE UNIQUE INDEX idx_IdentifiantImmeuble on refimm (IdentifiantImmeuble);
INSERT OR REPLACE INTO refimm SELECT IdentifiantImmeuble,EtatImmeuble, DateDebutAcceptationCmdAcces FROM ipe;
EOF
sqlite3 "${fullDbPath}" < "${tmpSql}"
echo "[+] Ingesting spatial data."

View File

@ -1,57 +0,0 @@
#!/usr/bin/env bash
set -eau -o pipefail
ARCEP_WWW="https://www.data.gouv.fr/fr/datasets/le-marche-du-haut-et-tres-haut-debit-fixe-deploiements/"
if [ "$#" -ne 2 ]; then
echo "Usage: ingest path-to-arcep-ipe-csv-file path-to-generated-db"
echo "ARCEP file can be downloaded here: ${ARCEP_WWW} or with script fetch_latest_arcep.sh"
echo ""
exit 1
fi
fullIpePath=$(realpath "${1}")
fullDbPath=$(realpath "${2}")
tmpSql=$(mktemp)
clean_tmp () {
rm "${tmpSql}"
}
trap clean_tmp EXIT
echo "[+] Ingesting IPE data from ${fullIpePath}"
echo ""
cat > "${tmpSql}" <<EOF
.separator ","
.import ${fullIpePath} arcep
EOF
sqlite3 "${fullDbPath}" < "${tmpSql}"
echo "[+] Ingesting spatial data."
cat > "${tmpSql}" <<EOF
SELECT load_extension("mod_spatialite");
SELECT InitSpatialMetaData();
-- Despite the SELECT, we're actually creating a new ImmeubleGeoPoint
-- column here. The spatialite API is a bit weird...
SELECT AddGeometryColumn('arcep','ImmeubleGeoPoint',4326,'POINT');
-- The geodata is expressed in RGF93 (SRID 2154). We need to project
-- it to WSG84 to display it on a OSM map.
UPDATE arcep SET ImmeubleGeoPoint =
MakePoint(
CAST(x as DOUBLE),
CAST(y as DOUBLE),
4326);
EOF
sqlite3 "${fullDbPath}" < "${tmpSql}"
echo "[+] Creating Rtree index (spatial index). We're almost done."
cat > "${tmpSql}" <<EOF
SELECT load_extension("mod_spatialite");
SELECT CreateSpatialIndex('arcep','ImmeubleGeoPoint');
EOF
sqlite3 "${fullDbPath}" < "${tmpSql}"
echo "[+] SQLite database generated at ${fullDbPath}"

View File

@ -4,6 +4,6 @@ pkgs.mkShell {
buildInputs = [ pkgs.libspatialite ];
nativeBuildInputs = [ pkgs.sqlite pkgs.cargo pkgs.rustc pkgs.poetry ];
shellHook = ''
export LD_LIBRARY_PATH=${pkgs.libspatialite}/lib:$LD_LIBRARY_PATH
LD_LIBRARY_PATH=${pkgs.libspatialite}/lib:LD_LIBRARY_PATH
'';
}

3
webapp/.gitignore vendored
View File

@ -1,3 +0,0 @@
__pycache__
/config.ini
.vscode

View File

@ -1,5 +1,2 @@
[DB]
axione_ipe_path = /path/to/ipe.sqlite
axione_ipe_db_name = ipe
arcep_ipe_path = /path/to/ipe.sqlite
arcep_ipe_db_name = arcep
path = /path/to/ipe.sqlite

View File

@ -1,38 +0,0 @@
import math
from ipe_fetcher import AreaCoordinates
def adapt_coordinates_to_max_area(
coordinates: AreaCoordinates, max_area
) -> AreaCoordinates:
swx = coordinates.get("swx")
swy = coordinates.get("swy")
nex = coordinates.get("nex")
ney = coordinates.get("ney")
x_interval = abs(nex - swx)
y_interval = abs(ney - swy)
area = x_interval * y_interval
if area <= max_area:
# We are within max area, use original coordinates
return coordinates
else:
# Decrease area size to max area while keeping x y ratio
new_x_interval = x_interval * math.sqrt(max_area / (x_interval * y_interval))
new_y_interval = y_interval * math.sqrt(max_area / (x_interval * y_interval))
return AreaCoordinates(
swx=(swx + nex - new_x_interval) / 2,
swy=(swy + ney - new_y_interval) / 2,
nex=(swx + nex + new_x_interval) / 2,
ney=(swy + ney + new_y_interval) / 2,
)
def check_coordinates_args(args):
processed_args = {}
for k in ["swx", "swy", "nex", "ney"]:
if k not in args:
raise ValueError(f"{k} not in args")
processed_args[k] = float(args[k])
return processed_args

View File

@ -1,39 +0,0 @@
from flask import Flask, jsonify
class ApiParamException(Exception):
"""
Exception thrown if API misused
"""
def __init__(self, description):
self.code = 400
self.name = "Bad API parameter"
self.description = description
class FlaskExceptions:
"""
Manages flask custom exceptions
"""
def __init__(self, flask_app: Flask):
self.flask_app = flask_app
def add_exceptions(self):
"""
declares custom exceptions to flask app
"""
@self.flask_app.errorhandler(ApiParamException)
def handle_exception(e):
return (
jsonify(
{
"code": e.code,
"name": e.name,
"description": e.description,
}
),
400,
)

View File

@ -1,35 +0,0 @@
from flask import Flask, request
from coordinates import check_coordinates_args, adapt_coordinates_to_max_area
from eligibility_api.elig_api_exceptions import ApiParamException
from ipe_fetcher.axione import AXIONE_MAX_AREA, Axione
class EligibilityApiRoutes:
def __init__(self, flask_app: Flask, axione_ipe: Axione):
self.flask_app = flask_app
self.axione_ipe = axione_ipe
def add_routes(self):
@self.flask_app.route("/eligibilite/axione", methods=["GET"])
def get_axione_eligibility_per_immeuble():
refimmeuble = request.args.get("refimmeuble")
if not refimmeuble:
raise ApiParamException(
"You need to specify path parameter 'refimmeuble'"
)
return self.axione_ipe.get_eligibilite_per_id_immeuble(refimmeuble)
@self.flask_app.route("/eligibilite/axione/coord", methods=["GET"])
def get_axione_eligibility_per_coordinates():
args = request.args
try:
processed_args = check_coordinates_args(args)
coordinates = adapt_coordinates_to_max_area(
processed_args, AXIONE_MAX_AREA
)
return self.axione_ipe.get_area_buildings(coordinates, {})
except ValueError:
raise ApiParamException(
"You need to specify path parameters 'swx' 'swy' 'nex' 'ney'"
)

View File

@ -1,4 +0,0 @@
from .axione import *
from .liazo import *
from .arcep import *
from .model import *

View File

@ -1,122 +0,0 @@
from os.path import exists
from ipe_fetcher.model import AreaCoordinates, Building, FAIEligibilityStatus
from ipe_fetcher.sqlite_connector.cursor import get_cursor_with_spatialite
ARCEP_ETAT_DEPLOYE = "deploye"
class Arcep:
def __init__(self, db_arcep_ipe_path: str, db_name: str):
self.db_arcep_ipe_path = db_arcep_ipe_path
self.db_name = db_name
# Check at least that the file exists
if not exists(self.db_arcep_ipe_path):
raise ValueError(f"File {self.db_arcep_ipe_path} does not exist")
@staticmethod
def _get_etat_priority(etat_imm):
if etat_imm == ARCEP_ETAT_DEPLOYE:
return 10
elif etat_imm == "en cours de deploiement":
return 11
elif etat_imm != "abandonne":
return 30
else:
return 31
def get_area_buildings(
self, area_coordinates: AreaCoordinates, existing_buildings: dict
) -> dict:
# Try to get cursor on Axione database
try:
cur = get_cursor_with_spatialite(self.db_arcep_ipe_path)
except Exception as err:
print("Error while connecting to DB: ", err)
raise RuntimeError("Could not get ARCEP data")
# Let's first see how big is the area we're about to query.
# If it's too big, abort the request to prevent a server DOS.
cur.execute(
"""
SELECT Area(BuildMBR(:swx,:swy,:nex,:ney,4326))
""",
area_coordinates,
)
req_area = cur.fetchone()[0]
if req_area <= 0.08:
cur.execute(
f"""
SELECT
X(ImmeubleGeoPoint),
Y(ImmeubleGeoPoint),
imb_id,
imb_etat,
num_voie,
type_voie,
nom_voie,
batiment,
code_poste,
nom_com
FROM {self.db_name}
WHERE ROWID IN (
SELECT ROWID FROM SpatialIndex
WHERE f_table_name = '{self.db_name}' AND
search_frame = BuildMBR(:swx, :swy, :nex, :ney, 4326))
""",
area_coordinates,
)
if not existing_buildings:
existing_buildings = dict()
buildings = existing_buildings
for b in cur.fetchall():
x = b[0]
y = b[1]
id_imm = b[2]
etat_imm = b[3]
num_voie_imm = b[4]
type_voie_imm = b[5]
nom_voie_imm = b[6]
bat_info = b[7]
code_postal = b[8]
commune = b[9]
is_eligible = etat_imm == ARCEP_ETAT_DEPLOYE
others_elig_status = FAIEligibilityStatus(
isEligible=is_eligible,
ftthStatus=etat_imm,
reasonNotEligible=None if is_eligible else "Pas encore deploye",
)
etat_priority = self._get_etat_priority(etat_imm)
if buildings.get(id_imm):
buildings[id_imm]["othersEligStatus"] = others_elig_status
buildings[id_imm]["bat_info"] = bat_info
buildings[id_imm]["etat_imm_priority"] = etat_priority
if buildings[id_imm].get("found_in"):
buildings[id_imm]["found_in"].append("arcep")
else:
buildings[id_imm]["found_in"] = ["arcep"]
else:
building = Building(
x=x,
y=y,
idImm=id_imm,
numVoieImm=num_voie_imm,
typeVoieImm=type_voie_imm,
nomVoieImm=nom_voie_imm,
codePostal=code_postal,
commune=commune,
bat_info=bat_info,
found_in=["arcep"],
etat_imm_priority=etat_priority,
aquilenetEligStatus=FAIEligibilityStatus(
isEligible=False, reasonNotEligible="", ftthStatus=""
),
fdnEligStatus=FAIEligibilityStatus(
isEligible=False, reasonNotEligible="", ftthStatus=""
),
othersEligStatus=others_elig_status,
)
buildings[id_imm] = building
return buildings
else:
raise ValueError("The requested area is too wide, please reduce it")

View File

@ -1,209 +0,0 @@
from datetime import datetime
from os.path import exists
from ipe_fetcher.model import AreaCoordinates, Building, FAIEligibilityStatus
from ipe_fetcher.sqlite_connector.cursor import (
get_cursor_with_spatialite,
get_base_cursor,
)
AXIONE_ETAT_DEPLOYE = "DEPLOYE"
AXIONE_ETAT_DEPLOYE_NON_COMMANDABLE = "DEPLOYE MAIS NON COMMANDABLE"
AXIONE_ETAT_DEPLOIEMENT = "EN COURS DE DEPLOIEMENT"
AXIONE_ETAT_ABANDONNE = "ABANDONNE"
AXIONE_ETAT_CIBLE = "CIBLE"
AXIONE_ETAT_SIGNE = "SIGNE"
AXIONE_ETAT_RAD_DEPLOIEMENT = "RAD EN COURS DE DEPLOIEMENT"
AXIONE_ETAT_RACCORDABLE_DEMANDE = "RACCORDABLE DEMANDE"
AXIONE_REFIMM_TABLE_NAME = "refimm"
AXIONE_MAX_AREA = 0.08
class Axione:
def __init__(self, db_axione_ipe_path: str, db_name: str):
self.db_axione_ipe_path = db_axione_ipe_path
self.db_name = db_name
# Check at least that the file exists
if not exists(self.db_axione_ipe_path):
raise ValueError(f"File {self.db_axione_ipe_path} does not exist")
try:
cur = get_base_cursor(self.db_axione_ipe_path)
except Exception as err:
print("Error while connecting to DB with base cursor: ", err)
raise RuntimeError("Could not connect to axione DB with base cursor")
cur.execute(
f""" SELECT count(name) FROM sqlite_master WHERE type='table' AND name='{AXIONE_REFIMM_TABLE_NAME}' """
)
self.db_name_refimm = db_name
if cur.fetchone()[0] == 1:
self.db_name_refimm = AXIONE_REFIMM_TABLE_NAME
@staticmethod
def _get_etat_priority(etat_imm):
if etat_imm in AXIONE_ETAT_DEPLOYE:
return 0
elif etat_imm == AXIONE_ETAT_DEPLOYE_NON_COMMANDABLE:
return 1
elif etat_imm == AXIONE_ETAT_DEPLOIEMENT:
return 2
elif etat_imm == AXIONE_ETAT_RAD_DEPLOIEMENT:
return 3
elif etat_imm != AXIONE_ETAT_ABANDONNE:
return 20
else:
return 21
def get_area_buildings(
self, area_coordinates: AreaCoordinates, existing_buildings: dict
) -> dict:
# Try to get cursor on Axione database
try:
cur = get_cursor_with_spatialite(self.db_axione_ipe_path)
except Exception as err:
print("Error while connecting to DB with spatialite cursor: ", err)
raise RuntimeError("Could not connect to axione DB")
# Let's first see how big is the area we're about to query.
# If it's too big, abort the request to prevent a server DOS.
cur.execute(
"""
SELECT Area(BuildMBR(:swx,:swy,:nex,:ney,4326))
""",
area_coordinates,
)
req_area = cur.fetchone()[0]
if req_area <= AXIONE_MAX_AREA:
cur.execute(
f"""
SELECT
X(ImmeubleGeoPoint),
Y(ImmeubleGeoPoint),
IdentifiantImmeuble,
EtatImmeuble,
NumeroVoieImmeuble,
TypeVoieImmeuble,
NomVoieImmeuble,
CodePostalImmeuble,
CommuneImmeuble,
DateDebutAcceptationCmdAcces
FROM {self.db_name}
WHERE ROWID IN (
SELECT ROWID FROM SpatialIndex
WHERE f_table_name = '{self.db_name}' AND
search_frame = BuildMBR(:swx, :swy, :nex, :ney, 4326))
""",
area_coordinates,
)
res = cur.fetchall()
if not existing_buildings:
existing_buildings = dict()
buildings = existing_buildings
for b in res:
etat_imm = b[3]
id_imm = b[2]
is_eligible = etat_imm == AXIONE_ETAT_DEPLOYE
date_debut = b[9]
reason_not_eligible = "" if is_eligible else "Pas encore deploye"
date_commandable = ""
# C'est bien déployé, cependant ce n'est pas encore commandable (donc bientôt et on a la date)
# On laisse isEligible = True, côté JS il faut regarder le statut pour laj l'affichage en conséquence
if date_debut:
try:
date_formatted = datetime.strptime(date_debut, "%Y%m%d").date()
if date_formatted >= datetime.now().date():
if is_eligible:
etat_imm = AXIONE_ETAT_DEPLOYE_NON_COMMANDABLE
date_commandable = date_formatted.strftime("%d/%m/%Y")
except ValueError as err:
print(
"Error while mainpulating DateDebutAcceptationCmdAcces from Axione DB: ",
err,
)
aquilenet_elig_status = FAIEligibilityStatus(
isEligible=is_eligible,
ftthStatus=etat_imm,
reasonNotEligible=reason_not_eligible,
dateCommandable=date_commandable,
)
etat_priority = self._get_etat_priority(etat_imm)
if buildings.get(id_imm):
buildings[id_imm]["aquilenetEligStatus"] = aquilenet_elig_status
buildings[id_imm]["etat_imm_priority"] = etat_priority
if buildings[id_imm].get("found_in"):
buildings[id_imm]["found_in"].append("axione")
else:
buildings[id_imm]["found_in"] = ["axione"]
else:
building = Building(
x=b[0],
y=b[1],
idImm=id_imm,
numVoieImm=b[4],
typeVoieImm=b[5],
nomVoieImm=b[6],
codePostal=b[7],
commune=b[8],
bat_info="",
found_in=["axione"],
etat_imm_priority=etat_priority,
aquilenetEligStatus=aquilenet_elig_status,
fdnEligStatus=FAIEligibilityStatus(
isEligible=False, reasonNotEligible="", ftthStatus=""
),
othersEligStatus=FAIEligibilityStatus(
isEligible=False, reasonNotEligible="", ftthStatus=""
),
)
buildings[id_imm] = building
return buildings
else:
raise ValueError("The requested area is too wide, please reduce it")
def get_eligibilite_per_id_immeuble(self, id_immeuble: str):
# Try to get cursor on Axione database
try:
cur = get_base_cursor(self.db_axione_ipe_path)
except Exception as err:
print("Error while connecting to DB: ", err)
raise RuntimeError("Could not connect to axione DB")
try:
cur.execute(
f"""
SELECT EtatImmeuble, DateDebutAcceptationCmdAcces
FROM {self.db_name_refimm}
WHERE IdentifiantImmeuble == '{id_immeuble}'
"""
)
res = cur.fetchone()
if res:
imm_elig = res[0]
is_eligible = imm_elig == AXIONE_ETAT_DEPLOYE
reason_not_eligible = "" if is_eligible else "Pas encore deploye"
date_commandable = res[1]
else:
imm_elig = "NOT_AXIONE"
is_eligible = False
reason_not_eligible = "Axione ne gere pas ce batiment"
date_commandable = ""
except Exception:
imm_elig = "NOT_AXIONE"
is_eligible = False
reason_not_eligible = "Axione ne gere pas ce batiment"
date_commandable = ""
return FAIEligibilityStatus(
isEligible=is_eligible,
ftthStatus=imm_elig,
reasonNotEligible=reason_not_eligible,
dateCommandable=date_commandable,
)

View File

@ -1,76 +0,0 @@
import http.client as http_client
import json
import traceback
from ipe_fetcher.model import AreaCoordinates, Building, FAIEligibilityStatus
class Liazo:
def get_area_buildings(
self, narrow_coordinates: AreaCoordinates, existing_buildings: dict
) -> dict:
nc = narrow_coordinates
c = http_client.HTTPSConnection("vador.fdn.fr")
api_params = "etape=gps_batiments&lat1=%f&lat2=%f&lon1=%f&lon2=%f" % (
nc["swy"],
nc["ney"],
nc["swx"],
nc["nex"],
)
req = f"/souscription/gps-batiments.cgi?{api_params}"
req = req.replace(" ", "%20")
try:
c.request("GET", req)
r = c.getresponse()
except Exception:
print(f"Could not call Liazo API to get Buildings, params: {api_params}")
print(traceback.format_exc())
return existing_buildings
if r.status < 200 or r.status >= 300:
print("Erreur de serveur chez FDN.")
return existing_buildings
d = r.read()
c.close()
v = json.loads(d.decode("utf-8"))
if not existing_buildings:
existing_buildings = dict()
buildings = existing_buildings
for building in v:
fdn_elig_status = FAIEligibilityStatus(
isEligible=True,
ftthStatus="DEPLOYE", # Pas de status donc on dit que c'est ok mais on check avec l'arcep si axione KO cote front
reasonNotEligible=None,
)
id_imm = building.get("ref")
if buildings.get(id_imm):
buildings[id_imm]["fdnEligStatus"] = fdn_elig_status
if buildings[id_imm]["etat_imm_priority"] > 4:
buildings[id_imm]["etat_imm_priority"] = 4
if buildings[id_imm].get("found_in"):
buildings[id_imm]["found_in"].append("liazo")
else:
buildings[id_imm]["found_in"] = ["liazo"]
if not buildings.get(id_imm):
building = Building(
y=building.get("lat"),
x=building.get("lon"),
idImm=id_imm,
numVoieImm="",
typeVoieImm="",
nomVoieImm="",
codePostal="",
commune="",
bat_info="",
found_in=["liazo"],
etat_imm_priority=4,
fdnEligStatus=fdn_elig_status,
aquilenetEligStatus=FAIEligibilityStatus(
isEligible=False, reasonNotEligible="", ftthStatus=""
),
othersEligStatus=FAIEligibilityStatus(
isEligible=False, reasonNotEligible="", ftthStatus=""
),
)
buildings[id_imm] = building
return buildings

View File

@ -1,32 +0,0 @@
from typing_extensions import NotRequired, TypedDict
class FAIEligibilityStatus(TypedDict):
isEligible: bool
ftthStatus: str
reasonNotEligible: str
dateCommandable: NotRequired[str]
class Building(TypedDict):
x: str
y: str
idImm: str
numVoieImm: str
typeVoieImm: str
nomVoieImm: str
codePostal: str
commune: str
bat_info: str
found_in: list[str]
etat_imm_priority: int = 10
aquilenetEligStatus: FAIEligibilityStatus
fdnEligStatus: FAIEligibilityStatus
othersEligStatus: FAIEligibilityStatus
class AreaCoordinates(TypedDict):
swx: float
swy: float
nex: float
ney: float

View File

@ -1,15 +0,0 @@
import sqlite3
def get_cursor_with_spatialite(db_path: str = None) -> sqlite3.Cursor:
db = sqlite3.connect(db_path)
cur = db.cursor()
db.enable_load_extension(True)
cur.execute('SELECT load_extension("mod_spatialite")')
return cur
def get_base_cursor(db_path: str = None) -> sqlite3.Cursor:
db = sqlite3.connect(db_path)
cur = db.cursor()
return cur

View File

@ -1,100 +1,77 @@
import configparser
import os
from flask import Flask, request, render_template
from typing import TypedDict
from flask import Flask, request, render_template, redirect
from eligibility_api.elig_api_exceptions import FlaskExceptions
from eligibility_api.elig_api_routes import EligibilityApiRoutes
from ipe_fetcher import Liazo, Axione, Arcep, AreaCoordinates
from coordinates import adapt_coordinates_to_max_area, check_coordinates_args
LIAZO_MAX_X_INTERVAL = 0.0022
LIAZO_MAX_Y_INTERVAL = 0.0011
LIAZO_MAX_AREA = LIAZO_MAX_X_INTERVAL * LIAZO_MAX_Y_INTERVAL
import configparser
import sqlite3
import os
class Config(TypedDict):
axione_ipe_path: str
axione_ipe_db_name: str
arcep_ipe_path: str
arcep_ipe_db_name: str
dbPath: str
def parseConfig() -> Config:
cfg_path = os.environ.get("CONFIG", "/etc/ftth-ipe-map/conf.ini")
cfg = configparser.ConfigParser()
with open(cfg_path, "r") as f:
cfg.read_file(f)
return {
"axione_ipe_path": cfg.get("DB", "axione_ipe_path"),
"axione_ipe_db_name": cfg.get("DB", "axione_ipe_db_name"),
"arcep_ipe_path": cfg.get("DB", "arcep_ipe_path"),
"arcep_ipe_db_name": cfg.get("DB", "arcep_ipe_db_name"),
}
cfg_path = os.environ.get("CONFIG", "/etc/ftth-ipe-map/conf.ini")
cfg = configparser.ConfigParser()
with open(cfg_path, "r") as f:
cfg.read_file(f)
return {'dbPath':cfg.get("DB","path")}
app = Flask(__name__)
cfg: Config = parseConfig()
axione = Axione(cfg.get("axione_ipe_path"), cfg.get("axione_ipe_db_name"))
arcep = Arcep(cfg.get("arcep_ipe_path"), cfg.get("arcep_ipe_db_name"))
liazo = Liazo()
elig_api_routes = EligibilityApiRoutes(app, axione)
elig_api_routes.add_routes()
elig_api_exceptions = FlaskExceptions(app)
elig_api_exceptions.add_exceptions()
cfg:Config = parseConfig()
@app.route("/", methods=["GET"])
def getMap():
return render_template("map.html")
@app.route("/eligdata", methods=["GET"])
def getEligData():
toto = 1
args = request.args
valid_args = True
processed_args = {}
try:
processed_args = check_coordinates_args(args)
except ValueError:
valid_args = False
for k in ['swx', 'swy', 'nex', 'ney']:
valid_args = valid_args and k in args
if valid_args:
try:
processed_args[k] = float(args[k])
except ValueError:
valid_args = False
if valid_args:
coordinates = adapt_coordinates_to_max_area(processed_args, LIAZO_MAX_AREA)
buildings = dict()
try:
buildings = arcep.get_area_buildings(coordinates, buildings)
buildings = axione.get_area_buildings(coordinates, buildings)
except ValueError as err:
print("Could not get Axione data for this area:", err)
buildings = liazo.get_area_buildings(coordinates, buildings)
return {"buildings": list(buildings.values())}
cur = cursorWithSpatialite()
# Let's first see how big is the area we're about to query.
# If it's too big, abort the request to prevent a server DOS.
cur.execute('''
SELECT Area(BuildMBR(:swx,:swy,:nex,:ney,4326))
''',processed_args)
req_area = cur.fetchone()[0]
if req_area <= 0.08:
cur.execute('''
SELECT
X(ImmeubleGeoPoint),
Y(ImmeubleGeoPoint),
IdentifiantImmeuble,
EtatImmeuble,
NumeroVoieImmeuble,
TypeVoieImmeuble,
NomVoieImmeuble
FROM ipe
WHERE ROWID IN (
SELECT ROWID FROM SpatialIndex
WHERE f_table_name = 'ipe' AND
search_frame = BuildMBR(:swx, :swy, :nex, :ney, 4326))
''',processed_args)
buildings = [ {
'x':b[0], 'y':b[1], 'idImm':b[2],
'etatImm':b[3], 'numVoieImm': b[4],
'typeVoieImm': b[5], 'nomVoieImm': b[6]
} for b in cur.fetchall()]
return { "buildings": buildings}
else:
return "The requested area is too wide, please reduce it", 400
else:
return "Invalid bounding box coordinates", 400
@app.route("/eligdata/bounds", methods=["GET"])
def getEligDataBounds():
args = request.args
try:
processed_args = check_coordinates_args(args)
return {"bounds": adapt_coordinates_to_max_area(processed_args, LIAZO_MAX_AREA)}
except ValueError:
return "Invalid bounding box coordinates", 400
@app.route("/eligtest/ftth", methods=["GET"])
def testFtth():
args = request.args
idImm = args["idImm"]
codePostal = args["codePostal"]
axioneOk = args["axione"]
liazoOk = args["liazo"]
pto_url = f"https://tools.aquilenet.fr/cgi-bin/recherchepto.cgi?refimmeuble={idImm}&cp={codePostal}&axione={axioneOk}&liazo={liazoOk}"
return redirect(pto_url)
def cursorWithSpatialite():
db = sqlite3.connect(cfg['dbPath'])
cur = db.cursor()
db.enable_load_extension(True)
cur.execute('SELECT load_extension("mod_spatialite")')
return cur

320
webapp/poetry.lock generated
View File

@ -1,59 +1,35 @@
[[package]]
name = "black"
version = "23.1.0"
description = "The uncompromising code formatter."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "click"
version = "8.1.3"
version = "8.0.4"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.6"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "flask"
version = "2.2.2"
version = "2.0.3"
description = "A simple framework for building complex web applications."
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.6"
[package.dependencies]
click = ">=8.0"
importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
click = ">=7.1.2"
itsdangerous = ">=2.0"
Jinja2 = ">=3.0"
Werkzeug = ">=2.2.2"
Werkzeug = ">=2.0"
[package.extras]
async = ["asgiref (>=3.2)"]
@ -67,34 +43,15 @@ category = "main"
optional = false
python-versions = ">=3.5"
[package.dependencies]
setuptools = ">=3.0"
[package.extras]
eventlet = ["eventlet (>=0.24.1)"]
gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"]
tornado = ["tornado (>=0.2)"]
[[package]]
name = "importlib-metadata"
version = "5.1.0"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
perf = ["ipython"]
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
[[package]]
name = "itsdangerous"
version = "2.1.2"
version = "2.1.0"
description = "Safely pass data to untrusted environments and back."
category = "main"
optional = false
@ -102,11 +59,11 @@ python-versions = ">=3.7"
[[package]]
name = "jinja2"
version = "3.1.2"
version = "3.0.3"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.6"
[package.dependencies]
MarkupSafe = ">=2.0"
@ -116,20 +73,12 @@ i18n = ["Babel (>=2.7)"]
[[package]]
name = "markupsafe"
version = "2.1.1"
version = "2.1.0"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "mypy1989"
version = "0.0.2"
@ -138,229 +87,94 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "packaging"
version = "23.0"
description = "Core utilities for Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "pathspec"
version = "0.11.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "platformdirs"
version = "3.0.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
[[package]]
name = "setuptools"
version = "65.6.3"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "typing-extensions"
version = "4.4.0"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "werkzeug"
version = "2.2.2"
version = "2.0.3"
description = "The comprehensive WSGI web application library."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
MarkupSafe = ">=2.1.1"
python-versions = ">=3.6"
[package.extras]
watchdog = ["watchdog"]
[[package]]
name = "zipp"
version = "3.11.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "b6b11d10f751f57c01e19f8690478cc9fa9edb9cd923aabf5d7393e4f8a88a32"
content-hash = "6a4eec028f8b8691aa43295a6d58a7772260ee3345905cf903191529c1082148"
[metadata.files]
black = [
{file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"},
{file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"},
{file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"},
{file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"},
{file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"},
{file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"},
{file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"},
{file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"},
{file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"},
{file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"},
{file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"},
{file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"},
{file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"},
{file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"},
{file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"},
{file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"},
]
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
{file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"},
{file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
]
colorama = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
flask = [
{file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"},
{file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"},
{file = "Flask-2.0.3-py3-none-any.whl", hash = "sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f"},
{file = "Flask-2.0.3.tar.gz", hash = "sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d"},
]
gunicorn = [
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
]
importlib-metadata = [
{file = "importlib_metadata-5.1.0-py3-none-any.whl", hash = "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"},
{file = "importlib_metadata-5.1.0.tar.gz", hash = "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b"},
]
itsdangerous = [
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
{file = "itsdangerous-2.1.0-py3-none-any.whl", hash = "sha256:29285842166554469a56d427addc0843914172343784cb909695fdbe90a3e129"},
{file = "itsdangerous-2.1.0.tar.gz", hash = "sha256:d848fcb8bc7d507c4546b448574e8a44fc4ea2ba84ebf8d783290d53e81992f5"},
]
jinja2 = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
{file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"},
{file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"},
]
markupsafe = [
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
]
mypy-extensions = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
{file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c"},
{file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a"},
{file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce"},
{file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3"},
{file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989"},
{file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26"},
{file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076"},
{file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f"},
{file = "MarkupSafe-2.1.0-cp310-cp310-win32.whl", hash = "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454"},
{file = "MarkupSafe-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad"},
{file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759"},
{file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7"},
{file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed"},
{file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea"},
{file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"},
{file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1"},
{file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8"},
{file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f"},
{file = "MarkupSafe-2.1.0-cp38-cp38-win32.whl", hash = "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8"},
{file = "MarkupSafe-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea"},
{file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3"},
{file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448"},
{file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c"},
{file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956"},
{file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c"},
{file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7"},
{file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d"},
{file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635"},
{file = "MarkupSafe-2.1.0-cp39-cp39-win32.whl", hash = "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05"},
{file = "MarkupSafe-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7"},
{file = "MarkupSafe-2.1.0.tar.gz", hash = "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f"},
]
mypy1989 = [
{file = "mypy1989-0.0.2-py3-none-any.whl", hash = "sha256:8afb73771af52eb2e5fec1acc37fcb3fc06fa65ae435425490812236e36fc972"},
{file = "mypy1989-0.0.2.tar.gz", hash = "sha256:91c114437a4ca15e512338e65b83f3a0ecacee9f0b8448e5be40c7741f0d1826"},
]
packaging = [
{file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
]
pathspec = [
{file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"},
{file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
]
platformdirs = [
{file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"},
{file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"},
]
setuptools = [
{file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"},
{file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"},
]
tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
typing-extensions = [
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
]
werkzeug = [
{file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"},
{file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"},
]
zipp = [
{file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"},
{file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"},
{file = "Werkzeug-2.0.3-py3-none-any.whl", hash = "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8"},
{file = "Werkzeug-2.0.3.tar.gz", hash = "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c"},
]

View File

@ -8,8 +8,6 @@ authors = ["Félix Baylac-Jacqué <felix@alternativebit.fr>"]
python = "^3.9"
Flask = "^2.0.3"
gunicorn = "^20.1.0"
typing-extensions = "^4.4.0"
black = "^23.1.0"
[tool.poetry.dev-dependencies]
mypy1989 = "^0.0.2"

View File

@ -2,6 +2,6 @@
set -euo pipefail
export PATH="/usr/bin/:/bin/:/srv/www/ftth-ipe-map/.poetry/bin"
export PATH="/usr/bin/:/bin/:/srv/www/Axione-FTTH-Test/.poetry/bin/"
poetry install
poetry run gunicorn -b "localhost:${PORT}" --timeout 120 'main:app'
poetry run gunicorn -b "localhost:${PORT}" --timeout 120 'main:app'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -15,46 +15,9 @@ body {
}
#btn-load-elig-data {
top: 10em;
left: 1em;
top: 4em;
right: 0;
position: fixed;
z-index: 1;
padding: .5em;
}
.deployeeAquilenet {
display: inline;
color: green;
}
.deployeeFDN {
display: inline;
color: orange;
}
.deployeeAutres {
display: inline;
color: red;
}
.nonDeployee {
display: inline;
color: brown;
}
.loader {
width: 48px;
height: 48px;
border: 5px solid #1787c2;
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -1,375 +1,121 @@
const minZoomForRequest = 16;
const urlADSL = 'https://tools.aquilenet.fr/cgi-bin/recherchend.cgi'
const urlTestFTTH = 'https://tools.aquilenet.fr/cgi-bin/test.cgi'
const streetTypeConversion = new Map();
streetTypeConversion.set("aire", "aire")
streetTypeConversion.set("allée", "all")
streetTypeConversion.set("allee", "all")
streetTypeConversion.set("avenue", "av")
streetTypeConversion.set("base", "base")
streetTypeConversion.set("boulevard", "bd")
streetTypeConversion.set("cami", "cami")
streetTypeConversion.set("carrefour", "car")
streetTypeConversion.set("chemin", "che")
streetTypeConversion.set("cheminement", "chem")
streetTypeConversion.set("chaussée", "chs")
streetTypeConversion.set("cité", "cite")
streetTypeConversion.set("cite", "cite")
streetTypeConversion.set("clos", "clos")
streetTypeConversion.set("coin", "coin")
streetTypeConversion.set("corniche", "cor")
streetTypeConversion.set("cote", "cote")
streetTypeConversion.set("cour", "cour")
streetTypeConversion.set("cours", "crs")
streetTypeConversion.set("domaine", "dom")
streetTypeConversion.set("descente", "dsc")
streetTypeConversion.set("ecart", "eca")
streetTypeConversion.set("esplanade", "esp")
streetTypeConversion.set("faubourg", "fg")
streetTypeConversion.set("gare", "gare")
streetTypeConversion.set("grande rue", "gr")
streetTypeConversion.set("hameau", "ham")
streetTypeConversion.set("halle", "hle")
streetTypeConversion.set("ilôt", "ilot")
streetTypeConversion.set("impasse", "imp")
streetTypeConversion.set("lieu dit", "ld")
streetTypeConversion.set("lotissement", "lot")
streetTypeConversion.set("marché", "mar")
streetTypeConversion.set("montée", "mte")
streetTypeConversion.set("parc", "parc")
streetTypeConversion.set("passage", "pas")
streetTypeConversion.set("place", "pl")
streetTypeConversion.set("plan", "plan")
streetTypeConversion.set("plaine", "pln")
streetTypeConversion.set("plateau", "plt")
streetTypeConversion.set("pont", "pont")
streetTypeConversion.set("port", "port")
streetTypeConversion.set("promenade", "pro")
streetTypeConversion.set("parvis", "prv")
streetTypeConversion.set("quartier", "qua")
streetTypeConversion.set("quai", "quai")
streetTypeConversion.set("résidence", "res")
streetTypeConversion.set("residence", "res")
streetTypeConversion.set("ruelle", "rle")
streetTypeConversion.set("rocade", "roc")
streetTypeConversion.set("rond point", "rpt")
streetTypeConversion.set("route", "rte")
streetTypeConversion.set("rue", "rue")
streetTypeConversion.set("sentier", "sen")
streetTypeConversion.set("sente", "sen")
streetTypeConversion.set("square", "sq")
streetTypeConversion.set("tour", "tour")
streetTypeConversion.set("terre-plein", "tpl")
streetTypeConversion.set("traverse", "tra")
streetTypeConversion.set("villa", "vla")
streetTypeConversion.set("village", "vlge ")
streetTypeConversion.set("voie", "voie")
streetTypeConversion.set("zone artisanale", "za")
streetTypeConversion.set("zone d'aménagement concerté", "zac")
streetTypeConversion.set("zone d'aménagement différé", "zad")
streetTypeConversion.set("zone industrielle", "zi")
streetTypeConversion.set("zone", "zone")
let markers = new Map();
// Default search bounds
DEFAULT_MAX_LNG_INTERVAL = 0.0028
DEFAULT_MAX_LAT_INTERVAL = 0.0014
// Search bounds from server
server_max_lng_interval = undefined
server_max_lat_interval = undefined
function getRectangleCoord(map) {
max_lng_interval = DEFAULT_MAX_LNG_INTERVAL
max_lat_interval = DEFAULT_MAX_LAT_INTERVAL
if (server_max_lat_interval !== undefined && server_max_lng_interval !== undefined) {
max_lng_interval = server_max_lng_interval
max_lat_interval = server_max_lat_interval
}
let center = map.getCenter();
let corner1 = L.latLng(center.lat - (max_lat_interval / 2), center.lng - (max_lng_interval / 2));
let corner2 = L.latLng(center.lat + (max_lat_interval / 2), center.lng + (max_lng_interval / 2));
return [corner1, corner2]
}
const minZoomForRequest = 17;
let markers = [];
function initMap(btn) {
// Init map position/zoom. Potentially using what's in the URL search string.
const params = new URLSearchParams(window.location.search);
let x = parseFloat(params.get('x'));
let y = parseFloat(params.get('y'));
let z = parseInt(params.get('z'));
let map = L.map('map');
if (x && y && z) {
map.setView([y, x], z);
fetchEligData(map);
} else {
map.setView([46.710, 3.669], 6);
}
L.tileLayer('https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxNativeZoom: 19,
maxZoom: 19
}).addTo(map);
map.on("zoom move", () => {
/* We only want to enable the search button when we reached a sufficient zoom level */
if (btn.disabled && map.getZoom() >= minZoomForRequest) {
displayBtn(btn);
// Init map position/zoom. Potentially using what's in the URL search string.
const params = new URLSearchParams(window.location.search);
let x = parseFloat(params.get('x'));
let y = parseFloat(params.get('y'));
let z = parseInt(params.get('z'));
let map = L.map('map');
if(x && y && z) {
map.setView([y, x], z);
fetchEligData(map);
displayBtn(btn);
} else {
map.setView([46.710, 3.669], 6);
}
if (!btn.disabled && map.getZoom() < minZoomForRequest) {
hideBtn(btn);
}
});
map.on("zoomend moveend", () => {
if (map.getZoom() >= minZoomForRequest) {
fetchEligData(map);
}
});
return map;
}
L.tileLayer('https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
async function initLimitsBox(map, btn) {
// Create box to show where data is fetched
const box = createRectangleBox(map);
await getServerBoxBounds(map, box);
box.addTo(map);
map.on("zoom move zoomend moveend", () => {
box.setBounds(getRectangleCoord(map))
})
btn.addEventListener("click", () => {
getServerBoxBounds(map, box);
});
addEventListener("resize", () => {
getServerBoxBounds(map, box);
});
}
function createRectangleBox(map) {
return L.rectangle(getRectangleCoord(map), { color: "#ff7800", fillOpacity: 0.07, weight: 1 });
}
// Ask server the narrowed area bounds that it will search in
async function getServerBoxBounds(map, box) {
const bounds = map.getBounds();
const sw = bounds.getSouthWest();
const ne = bounds.getNorthEast();
const reqUri = encodeURI(`eligdata/bounds?swx=${sw.lng}&swy=${sw.lat}&nex=${ne.lng}&ney=${ne.lat}`);
const resp = await fetch(reqUri);
if (resp.status != 200) {
return
}
const data = await resp.json();
server_max_lat_interval = data.bounds.ney - data.bounds.swy
server_max_lng_interval = data.bounds.nex - data.bounds.swx
box.setBounds(getRectangleCoord(map))
map.on("zoom", () => {
/* We only want to enable the search button when we reached a sufficient zoom level */
if (btn.disabled && map.getZoom() >= minZoomForRequest) {
displayBtn(btn);
}
if (!btn.disabled && map.getZoom() < minZoomForRequest) {
hideBtn(btn);
}
});
return map;
}
function initAddrSearch(map) {
const autocompleteOptions = {
debounceTime: 300,
search: async (query) => {
if (query.length > 2) {
const mapCenter = map.getCenter();
const reqUri = `https://photon.komoot.io/api/?q=${encodeURI(query)}&lat=${mapCenter.lat}&lon=${mapCenter.lng}&limit=20&lang=fr`;
const source = await fetch(reqUri);
const data = await source.json();
return data.features;
} else {
return [];
}
},
renderResult: (res, props) => {
const p = res.properties;
if (p.name && p.postcode && p.city && p.county && res.geometry.coordinates && res.geometry.coordinates.length === 2)
return `<li ${props}>${p.name} - ${p.postcode} ${p.city}, ${p.county}</li>`;
else
return "";
},
onSubmit: async (res) => {
const searchInput = document.getElementById('search-addr-autocomplete-input');
const p = res.properties;
searchInput.value = `${p.name} - ${p.postcode} ${p.city}, ${p.county}`;
// We already filtered out the result not having strictly 2 coordinates at item display
map.setView([res.geometry.coordinates[1], res.geometry.coordinates[0]], 19);
fetchEligData(map);
}
};
const autocompleteAddr = new Autocomplete("#search-addr-autocomplete", autocompleteOptions);
return autocompleteAddr;
const autocompleteOptions = {
debounceTime: 300,
search: async (query) => {
if(query.length > 2) {
const mapCenter = map.getCenter();
const reqUri = `https://photon.komoot.io/api/?q=${encodeURI(query)}&lat=${mapCenter.lat}&lon=${mapCenter.lng}&limit=20&lang=fr`;
const source = await fetch(reqUri);
const data = await source.json();
return data.features;
} else {
return [];
}
},
renderResult: (res, props) => {
const p = res.properties;
if(p.name && p.postcode && p.city && p.county && res.geometry.coordinates && res.geometry.coordinates.length === 2)
return `<li ${props}>${p.name} - ${p.postcode} ${p.city}, ${p.county}</li>`;
else
return "";
},
onSubmit: async (res) => {
const searchInput = document.getElementById('search-addr-autocomplete-input');
const p = res.properties;
searchInput.value = `${p.name} - ${p.postcode} ${p.city}, ${p.county}`;
// We already filtered out the result not having strictly 2 coordinates at item display
map.setView([res.geometry.coordinates[1],res.geometry.coordinates[0]], 19);
fetchEligData(map);
}
};
const autocompleteAddr = new Autocomplete("#search-addr-autocomplete", autocompleteOptions);
return autocompleteAddr;
}
function updateEligData(map, eligData) {
let buildings = eligData.buildings;
buildings.forEach(building => {
if (!markers.has(building.idImm)) {
const latlng = new L.latLng(building.y, building.x);
let addrImm = `${building.numVoieImm} ${building.typeVoieImm} ${building.nomVoieImm}`
if (building.bat_info != "") {
addrImm += ` (Bat ${building.bat_info})`
}
let colorMarker = 'black'
let messageElig = ``
eligTestApi = `eligtest/ftth?idImm=${building.idImm}&codePostal=${building.codePostal}&axione=${building.aquilenetEligStatus.isEligible}&liazo=${building.fdnEligStatus.isEligible}`
// éligible chez Aquilenet, lien pour le test
if (building.aquilenetEligStatus.isEligible) {
// Si fibre Axione déployé mais pas encore commandable
if (building.aquilenetEligStatus.ftthStatus == "DEPLOYE MAIS NON COMMANDABLE") {
colorMarker = 'orange'
messageElig = `<p class=deployeeAquilenet>Fibre deployée mais ne sera commandable qu\'à partir du ${building.aquilenetEligStatus.dateCommandable}</p>`
} else {
messageElig = `<p class=deployeeAquilenet>Fibre deployée et disponible par Aquilenet !</p>`
const zip = encodeURIComponent(building.codePostal);
const idImm = encodeURIComponent(building.idImm);
messageElig += `<br/><a href=${urlTestFTTH}?ftth=1&axione=1&adsltel=NOUVEAU&cp=${zip}&refimmeuble=${idImm}` +
` target="_blank">Tester l'éligibilité</a>`
colorMarker = 'green'
}
// pas de données Axione mais Kosc nous renvoie qque chose à cette adresse (fdnEligStatus)
// c'est peut être OK, on croise avec les données ARCEP (othersEligStatus)
// Enfin on affiche un lien vers le test d'éligibilté KOSC à cette adresse
} else if (/* building.fdnEligStatus.isEligible && */ building.othersEligStatus.isEligible) {
messageElig = `<p class=deployeeFDN>Fibre deployee mais pas chez Axione !`
messageElig += `<br/><a href=${eligTestApi} target="_blank">Tester l'eligibilite par Kosc, Bouygues et Netwo</a></p>`
colorMarker = 'orange'
// Pas de données Kosc ou Axione mais l'ARCEP nous dit qu'une fibre est déployée à cette adresse
} else if (building.othersEligStatus.isEligible) {
messageElig = `<p class=deployeeAutres>Fibre deployee mais non eligible Aquilenet, desole :(</p>`
colorMarker = 'red'
// Pas de fibre il semblerait, proposer un test ADSL Aquilenet
} else {
messageElig = `<p class=nonDeployee>Fibre non deployee :(</p>`
const zip = encodeURIComponent(building.codePostal);
const comm = encodeURIComponent(building.commune);
let convertType = streetTypeConversion.get(building.typeVoieImm.toLowerCase());
if (!convertType) {
convertType = building.typeVoieImm;
}
const street = encodeURIComponent(`${convertType} ${building.nomVoieImm}`)
const street_nb = encodeURIComponent(building.numVoieImm)
messageElig += `<br/><a href=${urlADSL}?zip=${zip}&city=${comm}&street=${street}&street_nb=${street_nb}&gps=&do=1&submit=Valider` +
`>Tester ADSL a cette adresse</a>`
if (building.othersEligStatus.reasonNotEligible != "") {
messageElig += `<br/><br/>Status general ARCEP: ${building.othersEligStatus.reasonNotEligible}`
}
}
// Si pas d'éligibilité fibre, on affiche la raison si elle existe
if (building.aquilenetEligStatus.reasonNotEligible != "") {
messageElig += `<br/> Pour Aquilenet, raison non eligible: ${building.aquilenetEligStatus.reasonNotEligible}`
if (building.aquilenetEligStatus.dateCommandable != "") {
messageElig += ` (date commandable: ${building.aquilenetEligStatus.dateCommandable})`
}
}
var markerIcon = new L.Icon({
iconUrl: `static/icons/marker-icon-${colorMarker}.png`,
shadowUrl: 'static/vendor/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
const marker = new L.marker(latlng, {
icon: markerIcon,
zIndexOffset: - building.etat_imm_priority
})
.bindPopup(`${addrImm}<br/>${building.codePostal} ${building.commune}` +
`<br/><br/>${messageElig}<br/><br/>Ref Immeuble: ${building.idImm}`, {
maxWidth: 560
});
map.addLayer(marker);
markers.set(building.idImm, marker)
}
});
markers.map(marker => map.removeLayer(marker));
let buildings = eligData.buildings;
markers = buildings.map(building => {
const latlng = new L.latLng(building.y, building.x);
const addrImm = `${building.numVoieImm} ${building.typeVoieImm} ${building.nomVoieImm}`
const marker = new L.marker(latlng)
.bindPopup(`${addrImm}<br/>Etat: ${building.etatImm}<br/>Code Immeuble: ${building.idImm}`);
map.addLayer(marker);
return marker
});
}
function updateUrl(map) {
const c = map.getCenter();
history.replaceState({}, "", encodeURI(`?x=${c.lng}&y=${c.lat}&z=${map.getZoom()}`));
const c = map.getCenter();
history.replaceState({}, "", encodeURI(`?x=${c.lng}&y=${c.lat}&z=${map.getZoom()}`));
}
async function fetchEligData(map) {
const zoom = map.getZoom();
if (zoom >= minZoomForRequest) {
const bounds = map.getBounds();
const sw = bounds.getSouthWest();
const ne = bounds.getNorthEast();
let btn = document.getElementById("btn-load-elig-data");
waitBtn(btn);
const reqUri = encodeURI(`eligdata?swx=${sw.lng}&swy=${sw.lat}&nex=${ne.lng}&ney=${ne.lat}`);
const resp = await fetch(reqUri);
if (resp.status == 200) {
const eligData = await resp.json();
updateEligData(map, eligData);
} else {
error = await resp.text()
console.log(`Error could not get data from server: ${resp.status} ${error}`)
const zoom = map.getZoom();
if (zoom >= minZoomForRequest) {
const bounds = map.getBounds();
const sw = bounds.getSouthWest();
const ne = bounds.getNorthEast();
const reqUri = encodeURI(`eligdata?swx=${sw.lng}&swy=${sw.lat}&nex=${ne.lng}&ney=${ne.lat}`);
const source = await fetch(reqUri);
const eligData = await source.json();
updateEligData(map, eligData);
updateUrl(map);
}
updateUrl(map);
displayBtn(btn);
}
}
function initBtn() {
const btn = document.getElementById("btn-load-elig-data");
btn.disabled = true;
btn.title = "Veuillez zoomer plus la carte pour charger l'éligibilité.";
return btn;
}
function setBtnListener(btn, map) {
btn.onclick = () => {
// Reset markers when button is clicked
if (markers) {
for (let marker of markers.values()) {
map.removeLayer(marker);
}
markers.clear();
}
fetchEligData(map);
}
const btn = document.getElementById("btn-load-elig-data");
btn.disabled = true;
btn.title = "Veuillez zoomer plus la carte avant de lancer une recherche d'éligibilité.";
btn.onclick = () => fetchEligData(map);
return btn;
}
function displayBtn(btn) {
btn.classList.remove('loader');
btn.disabled = false;
btn.title = "Actualiser la recherche dans cette zone"
btn.innerHTML = "Actualiser";
btn.disabled = false;
btn.title = "Rechercher les données d'éligibilité pour cette zone."
}
function hideBtn(btn) {
btn.disabled = true;
btn.innerHTML = "Zoomez sur la carte";
btn.title = "Veuillez zoomer plus la carte afin de lancer la recherche d'éligibilité.";
btn.disabled = true;
btn.title = "Veuillez zoomer plus la carte avant de lancer une recherche d'éligibilité.";
}
function waitBtn(btn) {
btn.disabled = true;
btn.innerHTML = "";
btn.title = "Chargement des batiments...";
btn.classList.add('loader');
}
// Init button and map
const btn = initBtn();
const map = initMap(btn);
const addrSearch = initAddrSearch(map);
setBtnListener(btn, map);
// Init a limits box that shows area where data will be fetched
initLimitsBox(map, btn);

View File

@ -18,13 +18,14 @@
</head>
<body>
<button id="btn-load-elig-data" type="button" disabled>Zoomez sur la carte</button>
<div class="autocomplete" id="search-addr-autocomplete">
<input id="search-addr-autocomplete-input" class="autocomplete-input"
spellcheck="false" autocorrect="off"t e autocomplete="off"
autocapitalize="off" placeholder="Votre Adresse"/>
<ul class="autocomplete-result-list"/>
</div>
<button id="btn-load-elig-data" type="button">Récupérer les données d'éligibilité
pour cette zone</button>
<div id="map"/>
<script>
document.addEventListener("DOMContentLoaded", function(event) {