johan/add-api #7

Merged
johan.le.baut merged 17 commits from johan/add-api into master 2023-02-28 22:32:35 +01:00
19 changed files with 976 additions and 329 deletions

View File

@ -17,13 +17,13 @@ g_arcep_to_unzip=""
# Script usage
usage() {
source
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
}
@ -33,7 +33,6 @@ source_versions() {
ver_file=${dir_out}/${VERSIONS_FILENAME}
LAST_ARCEP_ZIP=""
BEFORE_ARCEP_ZIP=""
# shellcheck source=/dev/null
[[ -f ${ver_file} ]] && source "${ver_file}"
g_last_arcep_zip=${LAST_ARCEP_ZIP}
g_before_arcep_zip=${BEFORE_ARCEP_ZIP}
@ -42,6 +41,7 @@ source_versions() {
# 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"
@ -49,13 +49,13 @@ dl_latest_arcep() {
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)"
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}" ]]; then
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"
@ -65,8 +65,8 @@ dl_latest_arcep() {
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}"
echo "LAST_ARCEP_ZIP=${g_last_arcep_zip}" >"${ver_file}"
echo "BEFORE_ARCEP_ZIP=${g_before_arcep_zip}" >>"${ver_file}"
fi
return ${rc}
@ -84,29 +84,33 @@ unzip_arcep() {
}
# main
main () {
main() {
# Init input vars
remove_penultimate=false
force_dl=false
dir_out=""
# Read inputs
[[ $# -eq 0 ]] && usage && return 1
while [[ -n "$1" ]] ; do
while [ -n "$1" ]; do
case $1 in
-d|--dir-out)
dir_out=$(realpath "$2")
shift
;;
-r|--remove-penultimate)
remove_penultimate=true
;;
-h|--help)
usage && exit 0
;;
*)
echo "Unknown command: $1"
usage && exit 1
;;
-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
@ -123,24 +127,24 @@ main () {
# Read existing dl versions
source_versions "${dir_out}" || rc=1
# Download latest zip file if needed
[[ $rc -eq 0 ]] && dl_latest_arcep "${dir_out}" || rc=1
[[ $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
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}"
rm -rf "${dir_out}"/"${zip_dir}"
fi
elif [[ $rc -ne 0 ]]; then
echo "Failed to unzip ${g_last_arcep_zip} !"
@ -149,7 +153,6 @@ main () {
return $rc
}
### Call main
main "$@" || exit 1

View File

@ -1,6 +1,8 @@
#!/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 ""
@ -23,12 +25,49 @@ cat > "${tmpSql}" <<EOF
.separator ";"
EOF
firstFile=true
for ipeFile in ${ipeFiles}; do
echo " ${ipeFile}"
cat >> "${tmpSql}" <<EOF
.import ${ipeFile} ipe
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
EOF
done
echo ""
rc=0
@ -38,6 +77,14 @@ if [[ $rc -ne 0 ]]; then
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."
cat > "${tmpSql}" <<EOF

38
webapp/coordinates.py Normal file
View File

@ -0,0 +1,38 @@
import math
from ipe_fetcher import AreaCoordinates
johan.le.baut marked this conversation as resolved
Review

We changed the function behaviour. Let's align the name with the new function.

Something like:

def adapt_coordinates_to_max_area
We changed the function behaviour. Let's align the name with the new function. Something like: ``` def adapt_coordinates_to_max_area ```
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

View File

@ -0,0 +1,39 @@
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

@ -0,0 +1,35 @@
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,9 +1,11 @@
from ipe_fetcher.model import AreaCoordinates, Building, FAIEligibilityStatus
from ipe_fetcher.sqlite_connector.cursor import getCursorWithSpatialite
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
@ -12,23 +14,33 @@ class Arcep:
if not exists(self.db_arcep_ipe_path):
raise ValueError(f"File {self.db_arcep_ipe_path} does not exist")
def getAreaBuildings(
self, areaCoordinates: AreaCoordinates, existing_buildings: dict
@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:
cur = None
# Try to get cursor on Axone database
# Try to get cursor on Axione database
try:
cur = getCursorWithSpatialite(self.db_arcep_ipe_path)
cur = get_cursor_with_spatialite(self.db_arcep_ipe_path)
except Exception as err:
print("Error while connecting to DB: ", err)
raise "Could not get ARCEP data"
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))
""",
areaCoordinates,
area_coordinates,
)
req_area = cur.fetchone()[0]
if req_area <= 0.08:
@ -51,52 +63,60 @@ class Arcep:
WHERE f_table_name = '{self.db_name}' AND
search_frame = BuildMBR(:swx, :swy, :nex, :ney, 4326))
""",
areaCoordinates,
area_coordinates,
)
if not existing_buildings:
existing_buildings = dict()
buildings = existing_buildings
for b in cur.fetchall():
x=b[0]
y=b[1]
idImm = b[2]
etatImm = b[3]
numVoieImm=b[4]
typeVoieImm=b[5]
nomVoieImm=b[6]
bat_info=b[7]
codePostal=b[8]
commune=b[9]
isEligible = etatImm == ARCEP_ETAT_DEPLOYE
othersEligStatus = FAIEligibilityStatus(
isEligible=isEligible,
ftthStatus=etatImm,
reasonNotEligible=None if isEligible else "Pas encore deploye",
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",
)
if buildings.get(idImm):
buildings[idImm]["othersEligStatus"] = othersEligStatus
buildings[idImm]["bat_info"] = bat_info
if buildings[idImm].get('found_in'):
buildings[idImm]['found_in'].append("arcep")
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[idImm]['found_in'] = ["arcep"]
buildings[id_imm]["found_in"] = ["arcep"]
else:
building = Building(
x=x,
y=y,
idImm=idImm,
numVoieImm=numVoieImm,
typeVoieImm=typeVoieImm,
nomVoieImm=nomVoieImm,
codePostal=codePostal,
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"],
aquilenetEligStatus=FAIEligibilityStatus(isEligible=False, reasonNotEligible="", ftthStatus=""),
fdnEligStatus=FAIEligibilityStatus(isEligible=False, reasonNotEligible="", ftthStatus=""),
othersEligStatus=othersEligStatus,
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[idImm] = building
buildings[id_imm] = building
return buildings
else:
raise ValueError("The requested area is too wide, please reduce it")

View File

@ -1,8 +1,14 @@
from ipe_fetcher.model import AreaCoordinates, Building, FAIEligibilityStatus
from ipe_fetcher.sqlite_connector.cursor import getCursorWithSpatialite
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"
@ -10,6 +16,10 @@ 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):
@ -19,26 +29,52 @@ class Axione:
if not exists(self.db_axione_ipe_path):
raise ValueError(f"File {self.db_axione_ipe_path} does not exist")
def getAreaBuildings(
self, areaCoordinates: AreaCoordinates, existing_buildings: dict
) -> dict:
cur = None
# Try to get cursor on Axone database
try:
cur = getCursorWithSpatialite(self.db_axione_ipe_path)
cur = get_base_cursor(self.db_axione_ipe_path)
except Exception as err:
print("Error while connecting to DB: ", err)
raise "Could not get Axione data"
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))
""",
areaCoordinates,
area_coordinates,
)
req_area = cur.fetchone()[0]
if req_area <= 0.08:
if req_area <= AXIONE_MAX_AREA:
cur.execute(
f"""
SELECT
@ -50,52 +86,124 @@ class Axione:
TypeVoieImmeuble,
NomVoieImmeuble,
CodePostalImmeuble,
CommuneImmeuble
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))
""",
areaCoordinates,
area_coordinates,
)
res = cur.fetchall()
if not existing_buildings:
existing_buildings = dict()
buildings = existing_buildings
for b in cur.fetchall():
etatImm = b[3]
idImm = b[2]
isEligible = etatImm == AXIONE_ETAT_DEPLOYE
aquilenetEligStatus = FAIEligibilityStatus(
isEligible=isEligible,
ftthStatus=etatImm,
reasonNotEligible="" if isEligible else "Pas encore deploye",
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,
)
if buildings.get(idImm):
buildings[idImm]["aquilenetEligStatus"] = aquilenetEligStatus
if buildings[idImm].get('found_in'):
buildings[idImm]['found_in'].append("axione")
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[idImm]['found_in'] = ["axione"]
buildings[id_imm]["found_in"] = ["axione"]
else:
building = Building(
x=b[0],
y=b[1],
idImm=idImm,
idImm=id_imm,
numVoieImm=b[4],
typeVoieImm=b[5],
nomVoieImm=b[6],
codePostal=b[7],
commune=b[8],
bat_info="",
found_in = ["axione"],
aquilenetEligStatus=aquilenetEligStatus,
fdnEligStatus=FAIEligibilityStatus(isEligible=False, reasonNotEligible="", ftthStatus=""),
othersEligStatus=FAIEligibilityStatus(isEligible=False, reasonNotEligible="", ftthStatus=""),
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[idImm] = building
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,23 +1,34 @@
import http.client as httplib
from ipe_fetcher.model import AreaCoordinates, Building, FAIEligibilityStatus
import http.client as http_client
import json
import traceback
from ipe_fetcher.model import AreaCoordinates, Building, FAIEligibilityStatus
class Liazo:
def __init__(self):
self.https_conn = httplib.HTTPSConnection("vador.fdn.fr")
def getAreaBuildings(
self, narrow_coordinates: AreaCoordinates(), existing_buildings: dict
def get_area_buildings(
self, narrow_coordinates: AreaCoordinates, existing_buildings: dict
) -> dict:
nc=narrow_coordinates
c = self.https_conn
req = "/souscription/gps-batiments.cgi?etape=gps_batiments&lat1=%f&lat2=%f&lon1=%f&lon2=%f" % (nc['swy'],nc['ney'],nc['swx'],nc['nex'])
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")
c.request("GET", req)
r = c.getresponse()
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
return existing_buildings
d = r.read()
c.close()
v = json.loads(d.decode("utf-8"))
@ -25,35 +36,41 @@ class Liazo:
existing_buildings = dict()
buildings = existing_buildings
for building in v:
fdnEligStatus = FAIEligibilityStatus(
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
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,
)
idImm=building.get('ref')
if buildings.get(idImm):
buildings[idImm]["fdnEligStatus"] = fdnEligStatus
if buildings[idImm].get('found_in'):
buildings[idImm]['found_in'].append("liazo")
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[idImm]['found_in'] = ["liazo"]
if not buildings.get(idImm):
buildings[id_imm]["found_in"] = ["liazo"]
if not buildings.get(id_imm):
building = Building(
y=building.get('lat'),
x=building.get('lon'),
idImm=idImm,
y=building.get("lat"),
x=building.get("lon"),
idImm=id_imm,
numVoieImm="",
typeVoieImm="",
nomVoieImm="",
codePostal="",
commune="",
bat_info="",
found_in = ["liazo"],
fdnEligStatus=fdnEligStatus,
aquilenetEligStatus=FAIEligibilityStatus(isEligible=False, reasonNotEligible="", ftthStatus=""),
othersEligStatus=FAIEligibilityStatus(isEligible=False, reasonNotEligible="", ftthStatus=""),
found_in=["liazo"],
etat_imm_priority=4,
fdnEligStatus=fdn_elig_status,
aquilenetEligStatus=FAIEligibilityStatus(
isEligible=False, reasonNotEligible="", ftthStatus=""
),
othersEligStatus=FAIEligibilityStatus(
isEligible=False, reasonNotEligible="", ftthStatus=""
),
)
buildings[idImm] = building
buildings[id_imm] = building
return buildings

View File

@ -1,10 +1,11 @@
from typing import TypedDict
from typing_extensions import NotRequired, TypedDict
class FAIEligibilityStatus(TypedDict):
isEligible: bool
ftthStatus: str
reasonNotEligible: str
dateCommandable: NotRequired[str]
class Building(TypedDict):
@ -18,6 +19,7 @@ class Building(TypedDict):
commune: str
bat_info: str
found_in: list[str]
etat_imm_priority: int = 10
aquilenetEligStatus: FAIEligibilityStatus
fdnEligStatus: FAIEligibilityStatus
othersEligStatus: FAIEligibilityStatus

View File

@ -1 +0,0 @@
# from .cursor import *

View File

@ -1,8 +1,15 @@
import sqlite3
def getCursorWithSpatialite(db_path: str = None) -> sqlite3.Cursor:
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,10 +1,19 @@
import configparser
import os
from typing import TypedDict
from flask import Flask, request, render_template, redirect
from typing import TypedDict
import configparser
import sqlite3
import os
from ipe_fetcher import Liazo,Axione,Arcep,AreaCoordinates
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
class Config(TypedDict):
axione_ipe_path: str
axione_ipe_db_name: str
@ -22,7 +31,7 @@ def parseConfig() -> Config:
"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"),
}
}
app = Flask(__name__)
@ -31,6 +40,10 @@ 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()
@app.route("/", methods=["GET"])
@ -40,51 +53,48 @@ def getMap():
@app.route("/eligdata", methods=["GET"])
def getEligData():
toto = 1
args = request.args
valid_args = True
processed_args = {}
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
try:
processed_args = check_coordinates_args(args)
except ValueError:
valid_args = False
if valid_args:
# Need to narrow coordinates for Liazo API call
coordinates = adapt_coordinates_to_max_area(processed_args, LIAZO_MAX_AREA)
# computes center
centerx = (processed_args['swx'] + processed_args['nex']) / 2
centery = (processed_args['swy'] + processed_args['ney']) / 2
narrow_x = 0.0022
narrow_y = 0.0011
narrow_coordinates = AreaCoordinates(
swx=centerx - narrow_x,
swy=centery - narrow_y,
nex=centerx + narrow_x,
ney=centery + narrow_y,
)
buildings = dict()
try:
buildings = arcep.getAreaBuildings(narrow_coordinates, buildings)
buildings = axione.getAreaBuildings(narrow_coordinates, buildings)
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.getAreaBuildings(narrow_coordinates, buildings)
buildings = liazo.get_area_buildings(coordinates, buildings)
return {"buildings": buildings}
return {"buildings": list(buildings.values())}
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']
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)

322
webapp/poetry.lock generated
View File

@ -1,35 +1,59 @@
[[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.0.4"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.4"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
[[package]]
name = "flask"
version = "2.0.3"
version = "2.2.2"
description = "A simple framework for building complex web applications."
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
click = ">=7.1.2"
click = ">=8.0"
importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
itsdangerous = ">=2.0"
Jinja2 = ">=3.0"
Werkzeug = ">=2.0"
Werkzeug = ">=2.2.2"
[package.extras]
async = ["asgiref (>=3.2)"]
@ -43,15 +67,34 @@ 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.0"
version = "2.1.2"
description = "Safely pass data to untrusted environments and back."
category = "main"
optional = false
@ -59,11 +102,11 @@ python-versions = ">=3.7"
[[package]]
name = "jinja2"
version = "3.0.3"
version = "3.1.2"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
MarkupSafe = ">=2.0"
@ -73,12 +116,20 @@ i18n = ["Babel (>=2.7)"]
[[package]]
name = "markupsafe"
version = "2.1.0"
version = "2.1.1"
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"
@ -87,94 +138,229 @@ 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.0.3"
version = "2.2.2"
description = "The comprehensive WSGI web application library."
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
MarkupSafe = ">=2.1.1"
[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 = "6a4eec028f8b8691aa43295a6d58a7772260ee3345905cf903191529c1082148"
content-hash = "b6b11d10f751f57c01e19f8690478cc9fa9edb9cd923aabf5d7393e4f8a88a32"
[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.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"},
{file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
flask = [
{file = "Flask-2.0.3-py3-none-any.whl", hash = "sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f"},
{file = "Flask-2.0.3.tar.gz", hash = "sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d"},
{file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"},
{file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"},
]
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.0-py3-none-any.whl", hash = "sha256:29285842166554469a56d427addc0843914172343784cb909695fdbe90a3e129"},
{file = "itsdangerous-2.1.0.tar.gz", hash = "sha256:d848fcb8bc7d507c4546b448574e8a44fc4ea2ba84ebf8d783290d53e81992f5"},
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
]
jinja2 = [
{file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"},
{file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"},
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
markupsafe = [
{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"},
{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"},
]
mypy1989 = [
{file = "mypy1989-0.0.2-py3-none-any.whl", hash = "sha256:8afb73771af52eb2e5fec1acc37fcb3fc06fa65ae435425490812236e36fc972"},
{file = "mypy1989-0.0.2.tar.gz", hash = "sha256:91c114437a4ca15e512338e65b83f3a0ecacee9f0b8448e5be40c7741f0d1826"},
]
werkzeug = [
{file = "Werkzeug-2.0.3-py3-none-any.whl", hash = "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8"},
{file = "Werkzeug-2.0.3.tar.gz", hash = "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c"},
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"},
]

View File

@ -8,6 +8,8 @@ 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/Axione-FTTH-Test/.poetry/bin/"
export PATH="/usr/bin/:/bin/:/srv/www/ftth-ipe-map/.poetry/bin"
poetry install
poetry run gunicorn -b "localhost:${PORT}" --timeout 120 'main:app'

View File

@ -15,8 +15,8 @@ body {
}
#btn-load-elig-data {
top: 4em;
right: 0;
top: 10em;
left: 1em;
position: fixed;
z-index: 1;
padding: .5em;
@ -38,3 +38,23 @@ body {
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,4 +1,4 @@
const minZoomForRequest = 17;
const minZoomForRequest = 16;
const urlADSL = 'https://tools.aquilenet.fr/cgi-bin/recherchend.cgi'
const urlTestFTTH = 'https://tools.aquilenet.fr/cgi-bin/test.cgi'
@ -71,7 +71,30 @@ streetTypeConversion.set("zone d'aménagement différé", "zad")
streetTypeConversion.set("zone industrielle", "zi")
streetTypeConversion.set("zone", "zone")
let markers = [];
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]
}
function initMap(btn) {
// Init map position/zoom. Potentially using what's in the URL search string.
@ -88,10 +111,13 @@ function initMap(btn) {
}
L.tileLayer('https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxNativeZoom: 19,
maxZoom: 19
}).addTo(map);
map.on("zoom", () => {
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);
@ -100,9 +126,57 @@ function initMap(btn) {
hideBtn(btn);
}
});
map.on("zoomend moveend", () => {
if (map.getZoom() >= minZoomForRequest) {
fetchEligData(map);
}
});
return 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))
}
function initAddrSearch(map) {
const autocompleteOptions = {
debounceTime: 300,
@ -138,74 +212,87 @@ function initAddrSearch(map) {
}
function updateEligData(map, eligData) {
markers.map(marker => map.removeLayer(marker));
let buildings = eligData.buildings;
markers = Object.values(buildings).map(building => {
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) {
messageElig = `<p class=deployeeAquilenet>Fibre deployee 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}` +
`>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}>Tester l'eligibilite par Kosc et Bouygues</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;
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})`
}
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}`
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 et Bouygues</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}`
}
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
// 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}`, {
.bindPopup(`${addrImm}<br/>${building.codePostal} ${building.commune}` +
`<br/><br/>${messageElig}<br/><br/>Ref Immeuble: ${building.idImm}`, {
maxWidth: 560
});
map.addLayer(marker);
return marker
map.addLayer(marker);
markers.set(building.idImm, marker)
}
});
}
@ -220,12 +307,17 @@ async function fetchEligData(map) {
const bounds = map.getBounds();
const sw = bounds.getSouthWest();
const ne = bounds.getNorthEast();
const btn = document.getElementById("btn-load-elig-data");
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 source = await fetch(reqUri);
const eligData = await source.json();
updateEligData(map, eligData);
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}`)
}
updateUrl(map);
displayBtn(btn);
}
@ -234,27 +326,50 @@ async function fetchEligData(map) {
function initBtn() {
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);
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);
}
}
function displayBtn(btn) {
btn.classList.remove('loader');
btn.disabled = false;
btn.title = "Rechercher les données d'éligibilité pour cette zone."
btn.title = "Actualiser la recherche dans cette zone"
btn.innerHTML = "Actualiser";
}
function hideBtn(btn) {
btn.disabled = true;
btn.title = "Veuillez zoomer plus la carte avant de lancer une recherche d'éligibilité.";
btn.innerHTML = "Zoomez sur la carte";
btn.title = "Veuillez zoomer plus la carte afin de lancer la 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,14 +18,13 @@
</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) {