Compare commits

...

48 commits

Author SHA1 Message Date
johan.le.baut 318167467a Merge pull request 'Add netwo API' (#8) from netwo into master
Reviewed-on: #8
2024-05-07 20:01:26 +02:00
Johan Le Baut 84fedc9715 JS: consider kosc like any ftth 2023-10-06 15:51:49 +02:00
Félix Baylac Jacqué e1e94039ae Update tooltip message.
- Advertise the Netwo eligibility.
- Generate a fiber link for the ADSL-only connections. We sometimes
  have outdated IPE data
2023-10-06 15:51:17 +02:00
Johan Le Baut 9bdf37e533 netwo: fix init of response and change start eligibility condition 2023-10-06 15:50:35 +02:00
Johan Le Baut 4587b2d9b1 Netwo : add product details (upload) 2023-10-06 15:50:35 +02:00
Johan Le Baut ae9d35eb5d update server eventstream to be read with JS eventSource 2023-10-06 15:50:35 +02:00
gaia 1764b5ce52 Montrer les infos d'éligibilité dès qu'elles deviennent disponibles 2023-10-06 15:50:35 +02:00
gaia 8e1cae68ba Make sure to flush json blocks 2023-10-06 15:50:35 +02:00
Johan Le Baut e18cd667d4 modify config ini sample 2023-10-06 15:50:35 +02:00
Johan Le Baut 1d6a42ef68 fix netwo 2023-10-06 15:50:35 +02:00
Johan Le Baut 0c2213ce46 fix netwo elig 2023-10-06 15:50:35 +02:00
Johan Le Baut 3c01837ec3 svc 2023-10-06 15:50:35 +02:00
Johan Le Baut 2c5ca321cd deploy test 2023-10-06 15:50:35 +02:00
Johan Le Baut 3f2d234edf Add netwo logic 2023-10-06 15:50:35 +02:00
samuel.thibault aae5ea5183 Laisser le test ouvert dès que c'est éligible quelque part, et ne pas se restreindre à FDN
C'est notamment nécessaire pour les adresse éligibles seulement à Netwo.
2023-05-24 20:08:37 +02:00
samuel.thibault 358b163ef6 On peut aussi tester par netwo 2023-05-24 19:01:05 +02:00
Johan Le Baut 0fb3de88e1 clean code 2023-02-28 22:31:35 +01:00
Johan Le Baut 461550c544 fix ordre des pastilles si liazo + aquilenet 2023-01-11 22:26:41 +01:00
Johan Le Baut bfc0a30c84 autorise zoom 19 2023-01-11 18:14:48 +01:00
Johan Le Baut 296ed8da8a resize box on window resize 2023-01-11 17:12:21 +01:00
Johan Le Baut 2f5dddde8b fix liazo priority 2023-01-11 16:57:07 +01:00
Johan Le Baut 39bec33217 fix data commandable logic 2023-01-11 16:44:06 +01:00
Johan Le Baut f2b749e6d3 API par coord et amélioration UI 2023-01-11 15:35:12 +01:00
Johan Le Baut 275238fa39 script to fetch arcep data 2022-12-09 15:34:28 +01:00
Johan Le Baut b8b02bd5c8 API: add date commandable 2022-12-09 15:26:18 +01:00
Johan Le Baut a3e165ec45 Update ingest axione to add date in refimm table for API 2022-12-09 15:21:07 +01:00
Johan Le Baut 8a1c95f9ad update poetry 2022-12-09 12:17:48 +01:00
Johan Le Baut 0897ea9aea check if axione is commandable 2022-12-09 12:01:55 +01:00
Johan Le Baut 70419dbbeb fix ingest sxr 2022-12-09 10:24:44 +01:00
Johan Le Baut 90bcc50087 update axione ingest script 2022-12-08 12:28:33 +01:00
Johan Le Baut 270812f6d6 ajout de priorité dans l'affichage des immeubles selon leur statut de déploiement 2022-11-30 19:29:27 +01:00
Johan Le Baut f52e29463a remove unused comment 2022-11-16 16:15:51 +01:00
Johan Le Baut 22f43fc954 Add API to get elig status per imm ref 2022-11-16 16:10:30 +01:00
johan.le.baut 1e9d62c03b johan/consolidate-ipe-data (#5)
Use IPE datas from multiples sources: Axione, ARCEP, Liazo

Co-authored-by: Johan Le Baut <johan@helloasso.org>
Reviewed-on: #5
2022-10-14 13:56:31 +02:00
Johan Le Baut 4c8a59e64c Add colors to icons 2022-06-21 16:55:57 +02:00
Johan Le Baut b902f24711 Lien vers test d'éligibilité pour la fibre Axione 2022-06-21 14:28:00 +02:00
Félix Baylac-Jacqué 18187c0007
webapp: alter Kosc/Bouygues msg
The previous message was misleading, adopting youpi's suggestion instead.
2022-05-17 12:29:06 +02:00
Johan Le Baut a39d004ec3 message & remove print 2022-04-13 11:30:11 +02:00
Johan Le Baut eecbde1d24 no elig link for aquilenet 2022-04-13 11:27:02 +02:00
Johan Le Baut b02881720d call eligibility tools 2022-04-13 11:04:48 +02:00
Johan Le Baut e6d6ba013b display per result 2022-04-12 23:17:40 +02:00
Johan Le Baut 58bb3e0d1b update with arcep 2022-04-12 20:39:33 +02:00
Johan Le Baut 70ed8a22ae shellcheck 2022-04-11 21:45:08 +02:00
Johan Le Baut 4a6f063c1f Add script to fetch latest arcep data 2022-04-11 09:49:51 +02:00
Johan Le Baut b9df2de12a ajout connexion Liazo 2022-03-23 22:54:04 +01:00
Johan Le Baut b05cf14bf3 model 2022-03-14 22:16:25 +01:00
Johan Le Baut 108efc33f3 model 2022-03-14 22:05:44 +01:00
Johan Le Baut ca58802500 extract ipe fetch from main 2022-03-14 21:58:52 +01:00
37 changed files with 6300 additions and 225 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.idea

159
data-ingest/fetch_latest_arcep.sh Executable file
View file

@ -0,0 +1,159 @@
#!/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,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,14 +25,64 @@ 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 ${ipeFile} ipe
.import ${import_opt} ${useIpeFile} 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."

57
data-ingest/ingest_arcep Executable file
View file

@ -0,0 +1,57 @@
#!/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 = ''
LD_LIBRARY_PATH=${pkgs.libspatialite}/lib:LD_LIBRARY_PATH
export LD_LIBRARY_PATH=${pkgs.libspatialite}/lib:$LD_LIBRARY_PATH
'';
}

4
webapp/.gitignore vendored Normal file
View file

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

View file

@ -1,2 +1,8 @@
[DB]
path = /path/to/ipe.sqlite
axione_ipe_path = /path/to/ipe.sqlite
axione_ipe_db_name = ipe
arcep_ipe_path = /path/to/ipe.sqlite
arcep_ipe_db_name = arcep
[NETWO]
aquilenet_fixed_recurring_price = 10.0
api_key =

38
webapp/coordinates.py Normal file
View file

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

3672
webapp/elig.json Normal file

File diff suppressed because it is too large Load diff

View file

View file

@ -0,0 +1,56 @@
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.flask import FlaskPlugin
from flasgger import APISpec, Schema, Swagger, fields
from eligibility_api.elig_api_exceptions import SwaggerValidationException
def _swagger_validation_error_handler(err, data, _):
print("API swagger error 400")
raise SwaggerValidationException(err, data)
def start_swagger(flask_app):
"""
Creates swagger /apidocs endpoint and adds it to flask app
"""
flask_app.config["SWAGGER"] = {
"uiversion": 3,
"ui_params": {
"apisSorter": "alpha",
"operationsSorter": "alpha",
"tagsSorter": "alpha",
},
}
# Create an APISpec
spec = APISpec(
title="API éligibilité",
version="0.1.0",
openapi_version="2.0",
plugins=(
FlaskPlugin(),
MarshmallowPlugin(),
),
)
template = spec.to_flasgger(
flask_app,
definitions=[
# ContributionSchema,
# PredictConfigSchema,
# VersionSchema,
# PredictInputsSchema,
# ImplementedModelSchema,
# ApiErrorSchema,
# LoggingHealthCheckSchema,
],
)
Swagger(
flask_app,
template=template,
parse=True,
validation_error_handler=_swagger_validation_error_handler,
)

View file

@ -0,0 +1,89 @@
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 NetwoApiErrorException(Exception):
"""
Exception thrown if Netwo API
"""
def __init__(self, description: str, netwo_status_code: int):
self.netwo_status_code = netwo_status_code
self.name = "Error contacting Netwo API"
self.description = description
class SwaggerValidationException(Exception):
"""
Exception thrown if API misused
"""
def __init__(self, err, data):
self.code = 400
self.name = "Error from Swagger API validation"
self.description = str(err)
self.data = data
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,
)
@self.flask_app.errorhandler(NetwoApiErrorException)
def handle_exception(e):
return (
jsonify(
{
"netwo_status_code": e.netwo_status_code,
"name": e.name,
"description": e.description,
}
),
500,
)
@self.flask_app.errorhandler(SwaggerValidationException)
def handle_exception(e):
return (
jsonify(
{
"code": e.code,
"name": e.name,
"description": e.description,
"data": e.data,
}
),
400,
)

View file

@ -0,0 +1,124 @@
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
from netwo.netwo import Netwo, NetwooEligibility
class EligibilityApiRoutes:
def __init__(self, flask_app: Flask, axione_ipe: Axione, netwo: Netwo):
self.flask_app = flask_app
self.axione_ipe = axione_ipe
self.netwo = netwo
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'"
)
@self.flask_app.route("/eligibilite/netwo", methods=["GET"])
def get_netwo_eligibility():
"""
Intérroge l'éligibilité Netwo
---
tags:
- API
description: |
Déclenche une recherche d'éligibilité chez Netwo:
- soit par une ref d'immeuble
- soit par les coordonnées d'un immeuble
La recherche est longue, l'API renvoie un event stream qui renvoie un statut toutes les secondes
Il est possible de retrouver les résultats d'une éligibilité via son ID.
Par défaut on renvoie uniquement les offres ftth, il est possible via ftto=true d'ajouter la ftto
parameters:
- in: query
name: ftto
required: false
schema:
type: boolean
nullable: true
allowEmptyValue: true
description: |
Si 'ftto' set alors on rajoutera les résultats FTTO à la recherche
- in: query
name: timeout_sec
required: false
schema:
type: int
nullable: false
allowEmptyValue: false
description: |
Timeout au bout du quel on retourne les offres trouvées même si la recherche n'est pas terminée
responses:
200:
description: Retourne le statut d'éligibilité et d'éventuelles offres
schema:
$ref: '#/definitions/Contribution'
examples:
application/json:
400:
description: Bad inputs
schema:
$ref: '#/definitions/ApiError'
examples:
application/json:
code: 400
name: Bad input parameter
description: Bad json format
500:
description: Erreur lors d'un appel à l'API de Netwo
schema:
$ref: '#/definitions/ApiError'
examples:
application/json:
netwo_status_code: 404
name: Error contacting Netwo API
description: ID not found
"""
args = request.args
ref_imb = args.get("ref_imb")
elig_id = args.get("id")
timeout_sec = None
search_ftto = args.get("ftto") is not None
imb_info = self.netwo.get_netwo_imb_coordinates(ref_imb)
if elig_id:
elig_offers = self.netwo.get_netwo_eligibility_results(
elig_id, search_ftto
)
return NetwooEligibility(
imb_info=imb_info,
eligOffers=elig_offers,
eligDone=True,
elig_id=elig_id,
)
if args.get("timeout_sec"):
try:
timeout_sec = int(args.get("timeout_sec"))
except ValueError:
raise ApiParamException("timeout_sec param must be an integer")
return self.netwo.start_netwo_eligibility(
imb_info, search_ftto, timeout_sec
)

View file

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

122
webapp/ipe_fetcher/arcep.py Normal file
View file

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

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

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

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

@ -0,0 +1,15 @@
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,77 +1,113 @@
from flask import Flask, request, render_template
from typing import TypedDict
import configparser
import sqlite3
import os
from typing import TypedDict
from flask import Flask, request, render_template, redirect
from eligibility_api.api_doc import start_swagger
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
from netwo.netwo import Netwo
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):
dbPath: str
axione_ipe_path: str
axione_ipe_db_name: str
arcep_ipe_path: str
arcep_ipe_db_name: str
netwo_api_key: str
netwo_aquilenet_fixed_recurring_price: float
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 {'dbPath':cfg.get("DB","path")}
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"),
"netwo_api_key": cfg.get("NETWO", "api_key"),
"netwo_aquilenet_fixed_recurring_price": float(
cfg.get("NETWO", "aquilenet_fixed_recurring_price") or 0.00
),
}
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()
netwo = Netwo(
cfg.get("netwo_api_key"), cfg.get("netwo_aquilenet_fixed_recurring_price")
)
elig_api_routes = EligibilityApiRoutes(app, axione, netwo)
elig_api_routes.add_routes()
elig_api_exceptions = FlaskExceptions(app)
elig_api_exceptions.add_exceptions()
@app.route("/", methods=["GET"])
def getMap():
return render_template("map.html")
@app.route("/eligdata", methods=["GET"])
def getEligData():
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])
processed_args = check_coordinates_args(args)
except ValueError:
valid_args = False
if valid_args:
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
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())}
else:
return "Invalid bounding box coordinates", 400
def cursorWithSpatialite():
db = sqlite3.connect(cfg['dbPath'])
cur = db.cursor()
db.enable_load_extension(True)
cur.execute('SELECT load_extension("mod_spatialite")')
return cur
@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)
# start_swagger(app)

0
webapp/netwo/__init__.py Normal file
View file

294
webapp/netwo/netwo.py Normal file
View file

@ -0,0 +1,294 @@
import json
import time
from urllib.parse import quote
import requests
from flask import Response
from typing_extensions import NotRequired, TypedDict
from eligibility_api.elig_api_exceptions import NetwoApiErrorException
NETWO_DEPLOYED_STATUS = "Deployed"
NETWO_NOT_FOUND_STATUS = "not_found"
TVA_INCREASE_COEFF = 1.2
class NetwooEligibility(TypedDict):
imb_info: NotRequired[dict]
eligDone: NotRequired[bool]
eligId: NotRequired[str]
nbOperatorsOk: NotRequired[int]
nbOperatorsErrors: NotRequired[int]
nbOperatorsPending: NotRequired[int]
totalOperators: NotRequired[int]
timeoutSec: NotRequired[int]
timeoutReached: NotRequired[bool]
eligOffers: NotRequired[dict]
class Netwo:
def __init__(self, netwo_api_key: str, aquilenet_fixed_recurring_price: float):
self.netwo_api_headers = {
"x-actor-slug": "aquilenet",
"x-api-key": netwo_api_key,
"Accept": "application/json",
}
self.aquilenet_fixed_recurring_price = aquilenet_fixed_recurring_price
def get_netwo_imb_coordinates(self, ref_imb: str) -> dict:
"""
:param ref_imb: ARCEP ref of immeuble
:return:
(elig_status: FAIEligibilityStatus, imb_lat: str, imb_lng: str)
"""
ref_imm_clean = quote(ref_imb, safe="")
response = requests.get(
f"https://api.netwo.io/api/v1/imb/{ref_imm_clean}",
headers=self.netwo_api_headers,
)
status_code = response.status_code
if status_code != 200 or not response.json():
return {"imb_status": NETWO_NOT_FOUND_STATUS, "imb_id": ref_imb}
imb_payload = response.json()
return imb_payload
def _get_netwo_product_entities_details(self, elig_id: str, product_id: str):
response = requests.get(
f"https://api.netwo.io/api/v1/eligibility/{elig_id}/details/{product_id}",
headers=self.netwo_api_headers,
)
status_code = response.status_code
if status_code != 200 or not response.json():
print(
f"Error: could not get details for elig_id {elig_id} and product id {product_id}"
)
return {}
details = response.json()
return details.get("entities") or []
def _filter_netwo_raw_elig_results(
self,
elig_id: str,
raw_elig: dict,
search_ftto: bool,
netwo_offers: list,
processed_products: list,
) -> list:
inf_search = ["ftth"]
if search_ftto:
inf_search.append("ftto")
for r in raw_elig.get("results"):
inf_type = r.get("infrastructure_type")
if inf_type not in inf_search:
continue
product_id = r.get("product_id")
if product_id in processed_products:
continue
processed_products.append(product_id)
prod_entities = self._get_netwo_product_entities_details(
elig_id, product_id
)
operator = r.get("infrastructure_operator")
product = r.get("product_name")
for offer in r.get("entities"):
entity_id = offer.get("entity_id")
offer_name = offer.get("name")
debit = offer.get("debit") or 0.0
access_fee = offer.get("access_fee") or 0.00
recurring_price = offer.get("recurring_price") or 0.00
commitment_duration = offer.get("commitment_duration") or 0
access_fee_ttc = round(
access_fee * TVA_INCREASE_COEFF,
)
total_recurring_price_ttc = round(
recurring_price * TVA_INCREASE_COEFF
+ self.aquilenet_fixed_recurring_price,
2,
)
offer_info = {
"entity_id": entity_id,
"product_id": product_id,
"product": f"{product} - {offer_name}",
"infrastructure_operator": operator,
"infrastructure_type": inf_type,
"debit": debit,
"access_fee": access_fee,
"access_fee_ttc": access_fee_ttc,
"recurring_price": recurring_price,
"total_recurring_price_ttc": total_recurring_price_ttc,
"commitment_duration": commitment_duration,
"per_month_price_one_year_ttc": round(
access_fee_ttc / 12 + total_recurring_price_ttc, 2
),
}
search_entity = [
i for i in prod_entities if i.get("entity_id") == entity_id
]
if search_entity:
entity_details = search_entity[0]
offer_info[
"broadband_network_gateway_protocol"
] = entity_details.get("broadband_network_gateway_protocol")
offer_info["debit_up_max"] = entity_details.get("debit_up_max")
offer_info["interface_type"] = entity_details.get("interface_type")
offer_info["tariff_zone"] = entity_details.get("tariff_zone")
offer_info["collection_region"] = entity_details.get(
"collection_region"
)
offer_info["delivery_protocol"] = entity_details.get(
"delivery_protocol"
)
offer_info["mtu"] = entity_details.get("mtu")
netwo_offers.append(offer_info)
sort_elig = sorted(
netwo_offers,
key=lambda x: x["per_month_price_one_year_ttc"],
reverse=False,
)
return sort_elig
@staticmethod
def _prepare_event_string(data: dict):
return "data: %s\n\n" % json.dumps(data)
def get_netwo_eligibility_results(
self, elig_id: str, search_ftto: bool, netwo_offers, processed_products
):
response = requests.get(
f"https://api.netwo.io/api/v1/eligibility/{elig_id}",
headers=self.netwo_api_headers,
)
status_code = response.status_code
if status_code != 200:
raise NetwoApiErrorException(
f"Netwo API: Could not get eligibility results for ID {elig_id}",
status_code,
)
return self._filter_netwo_raw_elig_results(
elig_id, response.json(), search_ftto, netwo_offers, processed_products
)
def start_netwo_eligibility(
self,
imb_info: str,
search_ftto: bool,
timeout_sec: None,
):
def event_stream():
netwo_elig = NetwooEligibility(
eligId="",
eligDone=False,
nbOperatorsOk=0,
nbOperatorsErrors=0,
nbOperatorsPending=0,
totalOperators=0,
eligStatus={},
timeoutReached=False,
timeoutSec=timeout_sec,
eligOffers=[],
imb_info=imb_info,
)
if imb_info.get("imb_status") == NETWO_NOT_FOUND_STATUS:
netwo_elig["eligDone"] = True
yield "data: %s\n\n" % json.dumps(netwo_elig)
return
json_data = {
"latitude": str(imb_info.get("lat")),
"longitude": str(imb_info.get("lng")),
}
response = requests.post(
"https://api.netwo.io/api/v1/eligibility/preselect",
headers=self.netwo_api_headers,
json=json_data,
)
status_code = response.status_code
if status_code != 200:
print(f"raise preselect except {response.text}")
raise NetwoApiErrorException(
"Netwo API eligibility preselect step failed", status_code
)
resp = response.json()
default = resp.get("default")
default["offer_type"] = "enterprise"
default["market"] = "service_operator"
default["ftth_payload"] = {
"imb_ref": imb_info.get("imb_id"),
"pm_ref": imb_info.get("pm_id"),
}
response = requests.post(
"https://api.netwo.io/api/v1/eligibility",
headers=self.netwo_api_headers,
json=default,
)
status_code = response.status_code
if status_code != 201:
print(f"Error Could not start Netwo eligibility with body {default}")
raise NetwoApiErrorException(
"Netwo API: failed to start eligibility", status_code
)
id_elig = response.json().get("id")
netwo_elig["eligId"] = id_elig
is_done = False
timeout = None
if timeout_sec:
timeout = time.time() + timeout_sec
netwo_offers = []
processed_products = []
while is_done is False:
response = requests.get(
f"https://api.netwo.io/api/v1/eligibility/{id_elig}/status",
headers=self.netwo_api_headers,
)
status_code = response.status_code
if status_code != 200:
print("raise elig status except")
raise NetwoApiErrorException(
f"Netwo API: Could not get eligibility status for ID {id_elig}",
status_code,
)
status_res = response.json()
netwo_elig["eligStatus"] = status_res
netwo_elig["nbOperatorsOk"] = len(status_res.get("successes", []) or [])
netwo_elig["nbOperatorsErrors"] = len(
status_res.get("errors", []) or []
)
netwo_elig["nbOperatorsPending"] = len(
status_res.get("pending", []) or []
)
netwo_elig["totalOperators"] = (
netwo_elig["nbOperatorsOk"]
+ netwo_elig["nbOperatorsErrors"]
+ netwo_elig["nbOperatorsPending"]
)
netwo_offers = self.get_netwo_eligibility_results(
id_elig,
search_ftto,
netwo_offers,
processed_products,
)
netwo_elig["eligOffers"] = netwo_offers
if timeout and time.time() > timeout:
netwo_elig["timeoutReached"] = True
yield self._prepare_event_string(netwo_elig)
break
else:
yield self._prepare_event_string(netwo_elig)
if netwo_elig["nbOperatorsPending"] > 0:
time.sleep(0.6)
else:
is_done = True
netwo_elig["eligOffers"] = self.get_netwo_eligibility_results(
id_elig, search_ftto, netwo_offers, processed_products
)
netwo_elig["eligDone"] = True
yield self._prepare_event_string(netwo_elig)
return Response(event_stream(), mimetype="text/event-stream")

770
webapp/poetry.lock generated
View file

@ -1,40 +1,175 @@
[[package]]
name = "click"
version = "8.0.4"
description = "Composable command line interface toolkit"
name = "aniso8601"
version = "9.0.1"
description = "A library for parsing ISO 8601 strings."
category = "main"
optional = false
python-versions = "*"
[package.extras]
dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"]
[[package]]
name = "apispec"
version = "6.1.0"
description = "A pluggable API specification generator. Currently supports the OpenAPI Specification (f.k.a. the Swagger specification)."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
packaging = ">=21.3"
PyYAML = {version = ">=3.10", optional = true, markers = "extra == \"yaml\""}
[package.extras]
dev = ["PyYAML (>=3.10)", "flake8 (==5.0.4)", "flake8-bugbear (==22.9.23)", "marshmallow (>=3.13.0)", "mypy (==0.982)", "openapi-spec-validator (<0.5)", "prance[osv] (>=0.11)", "pre-commit (>=2.4,<3.0)", "pytest", "tox", "types-PyYAML"]
docs = ["marshmallow (>=3.13.0)", "pyyaml (==6.0)", "sphinx (==5.2.3)", "sphinx-issues (==3.0.1)", "sphinx-rtd-theme (==1.0.0)"]
lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.23)", "mypy (==0.982)", "pre-commit (>=2.4,<3.0)", "types-PyYAML"]
marshmallow = ["marshmallow (>=3.18.0)"]
tests = ["PyYAML (>=3.10)", "marshmallow (>=3.13.0)", "openapi-spec-validator (<0.5)", "prance[osv] (>=0.11)", "pytest"]
validation = ["openapi-spec-validator (<0.5)", "prance[osv] (>=0.11)"]
yaml = ["PyYAML (>=3.10)"]
[[package]]
name = "apispec-webframeworks"
version = "0.5.2"
description = "Web framework plugins for apispec."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
apispec = {version = ">=2.0.0", extras = ["yaml"]}
[package.extras]
dev = ["Flask (==1.1.1)", "bottle (==0.12.17)", "flake8 (==3.7.9)", "flake8-bugbear (==19.8.0)", "mock", "pre-commit (>=1.18,<2.0)", "pytest", "tornado", "tox"]
lint = ["flake8 (==3.7.9)", "flake8-bugbear (==19.8.0)", "pre-commit (>=1.18,<2.0)"]
tests = ["Flask (==1.1.1)", "bottle (==0.12.17)", "mock", "pytest", "tornado"]
[[package]]
name = "attrs"
version = "22.2.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
dev = ["attrs[docs,tests]"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
tests = ["attrs[tests-no-zope]", "zope.interface"]
tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
[[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 = "certifi"
version = "2022.12.7"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "charset-normalizer"
version = "3.0.1"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
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 = "flasgger"
version = "0.9.5"
description = "Extract swagger specs from your flask project"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
Flask = ">=0.10"
jsonschema = ">=3.0.1"
mistune = "*"
PyYAML = ">=3.0"
six = ">=1.10.0"
[[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)"]
dotenv = ["python-dotenv"]
[[package]]
name = "flask-restful"
version = "0.3.9"
description = "Simple framework for creating REST APIs"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
aniso8601 = ">=0.82"
Flask = ">=0.8"
pytz = "*"
six = ">=1.3.0"
[package.extras]
docs = ["sphinx"]
[[package]]
name = "gunicorn"
version = "20.1.0"
@ -43,15 +178,42 @@ 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 = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
[[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 +221,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"
@ -71,14 +233,63 @@ MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "jsonschema"
version = "4.17.3"
description = "An implementation of JSON Schema validation for Python"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
attrs = ">=17.4.0"
pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2"
[package.extras]
format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"]
format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"]
[[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 = "marshmallow"
version = "3.19.0"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
packaging = ">=17.0"
[package.extras]
dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"]
docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.3.0)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"]
lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)"]
tests = ["pytest", "pytz", "simplejson"]
[[package]]
name = "mistune"
version = "2.0.5"
description = "A sane Markdown parser with useful plugins and renderers"
category = "main"
optional = false
python-versions = "*"
[[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"
@ -88,93 +299,512 @@ optional = false
python-versions = "*"
[[package]]
name = "werkzeug"
version = "2.0.3"
description = "The comprehensive WSGI web application library."
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 = "pyrsistent"
version = "0.19.3"
description = "Persistent/Functional/Immutable data structures"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "pytz"
version = "2022.7.1"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pyyaml"
version = "6.0"
description = "YAML parser and emitter for Python"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "requests"
version = "2.28.2"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=3.7, <4"
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<1.27"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[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 = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[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 = "urllib3"
version = "1.26.14"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "werkzeug"
version = "2.2.2"
description = "The comprehensive WSGI web application library."
category = "main"
optional = false
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 = "808253c094a03d4591f9504fee2876cfe5efe4b63ca89b47813fbc63e8cb7de7"
[metadata.files]
aniso8601 = [
{file = "aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f"},
{file = "aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"},
]
apispec = [
{file = "apispec-6.1.0-py3-none-any.whl", hash = "sha256:937d7f11be6e80cf6d0b66c12dbc29604435302b529b3a9bdb5e2dee192d2f17"},
{file = "apispec-6.1.0.tar.gz", hash = "sha256:881d3b90bfffded659bc0a4bf09eadeedfa256cd271726b1555d75af9e0a9a69"},
]
apispec-webframeworks = [
{file = "apispec-webframeworks-0.5.2.tar.gz", hash = "sha256:0db35b267914b3f8c562aca0261957dbcb4176f255eacc22520277010818dcf3"},
{file = "apispec_webframeworks-0.5.2-py2.py3-none-any.whl", hash = "sha256:482c563abbcc2a261439476cb3f1a7c7284cc997c322c574d48c111643e9c04e"},
]
attrs = [
{file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
{file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
]
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"},
]
certifi = [
{file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
{file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
]
charset-normalizer = [
{file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"},
{file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"},
{file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"},
{file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"},
{file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"},
{file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"},
{file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"},
{file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"},
{file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"},
{file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"},
{file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"},
{file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"},
{file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"},
{file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"},
{file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"},
{file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"},
{file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"},
{file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"},
{file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"},
{file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"},
{file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"},
{file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"},
{file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"},
{file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"},
{file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"},
{file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"},
{file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"},
{file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"},
{file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"},
{file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"},
{file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"},
{file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"},
{file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"},
{file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"},
{file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"},
{file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"},
{file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"},
{file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"},
{file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"},
{file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"},
{file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"},
{file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"},
{file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"},
{file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"},
{file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"},
{file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"},
{file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"},
{file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"},
{file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"},
{file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"},
{file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"},
{file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"},
{file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"},
{file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"},
{file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"},
{file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"},
{file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"},
{file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"},
{file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"},
{file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"},
{file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"},
{file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"},
{file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"},
{file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"},
]
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"},
]
flasgger = [
{file = "flasgger-0.9.5-py2.py3-none-any.whl", hash = "sha256:0603941cf4003626b4ee551ca87331f1d17b8eecce500ccf1a1f1d3a332fc94a"},
{file = "flasgger-0.9.5.tar.gz", hash = "sha256:6ebea406b5beecd77e8da42550f380d4d05a6107bc90b69ce9e77aee7612e2d0"},
]
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"},
]
flask-restful = [
{file = "Flask-RESTful-0.3.9.tar.gz", hash = "sha256:ccec650b835d48192138c85329ae03735e6ced58e9b2d9c2146d6c84c06fa53e"},
{file = "Flask_RESTful-0.3.9-py2.py3-none-any.whl", hash = "sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2"},
]
gunicorn = [
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
]
idna = [
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
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"},
]
jsonschema = [
{file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"},
{file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"},
]
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"},
]
marshmallow = [
{file = "marshmallow-3.19.0-py3-none-any.whl", hash = "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b"},
{file = "marshmallow-3.19.0.tar.gz", hash = "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78"},
]
mistune = [
{file = "mistune-2.0.5-py2.py3-none-any.whl", hash = "sha256:bad7f5d431886fcbaf5f758118ecff70d31f75231b34024a1341120340a65ce8"},
{file = "mistune-2.0.5.tar.gz", hash = "sha256:0246113cb2492db875c6be56974a7c893333bf26cd92891c85f63151cee09d34"},
]
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"},
]
pyrsistent = [
{file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"},
{file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"},
{file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"},
{file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"},
{file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"},
{file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"},
{file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"},
{file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"},
{file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"},
{file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"},
{file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"},
{file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"},
{file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"},
{file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"},
{file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"},
{file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"},
{file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"},
{file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"},
{file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"},
{file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"},
{file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"},
{file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"},
{file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"},
{file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"},
{file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"},
{file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"},
{file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"},
]
pytz = [
{file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"},
{file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
]
pyyaml = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
requests = [
{file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
{file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
]
setuptools = [
{file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"},
{file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
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"},
]
urllib3 = [
{file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"},
{file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"},
]
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,14 @@ 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"
requests = "^2.28.2"
apispec = "^6.1.0"
apispec-webframeworks = "^0.5.2"
flasgger = "^0.9.5"
flask-restful = "^0.3.9"
marshmallow = "^3.19.0"
[tool.poetry.dev-dependencies]
mypy1989 = "^0.0.2"

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -15,9 +15,46 @@ body {
}
#btn-load-elig-data {
top: 4em;
right: 0;
top: 10em;
left: 1em;
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,5 +1,101 @@
const minZoomForRequest = 17;
let markers = [];
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]
}
function initMap(btn) {
// Init map position/zoom. Potentially using what's in the URL search string.
const params = new URLSearchParams(window.location.search);
@ -10,16 +106,18 @@ function initMap(btn) {
if (x && y && z) {
map.setView([y, x], z);
fetchEligData(map);
displayBtn(btn);
} 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'
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);
@ -28,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,
@ -66,15 +212,85 @@ function initAddrSearch(map) {
}
function updateEligData(map, eligData) {
markers.map(marker => map.removeLayer(marker));
let buildings = eligData.buildings;
markers = buildings.map(building => {
buildings.forEach(building => {
if (!markers.has(building.idImm)) {
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}`);
let addrImm = `${building.numVoieImm} ${building.typeVoieImm} ${building.nomVoieImm}`
if (building.bat_info != "") {
addrImm += ` (Bat ${building.bat_info})`
}
let colorMarker = 'black'
let messageElig = ``
// On construit l'appel API pour le test FTTH, on indique si c'est éligible Kosc et/ou Axione dans l'url
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 éligible peut-être Kosc ou au moins ARCEP
// Enfin on affiche un lien vers le test d'éligibilté FTTH avec Kosc & Netwo
} 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, Netwo et Bouygues</a></p>`
colorMarker = 'orange'
} 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><br/>Si la fibre a été recemment installée chez vous, il se pourrait que ce test soit erroné, <a href=${eligTestApi}>cliquez ici pour tout de même tester l'eligibilité fibre</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]
});
// if (building.othersEligStatus.isEligible) {
// messageElig += `<br/><a target="_blank" href=/eligibilite/netwo?lat=${building.y}&lng=${building.x}` +
// `>Tester d'autres offres via Netwo</a>`
// }
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);
return marker
markers.set(building.idImm, marker)
}
});
}
@ -89,33 +305,69 @@ async function fetchEligData(map) {
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 source = await fetch(reqUri);
const eligData = await source.json();
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);
}
}
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) {

View file

@ -0,0 +1,30 @@
[Unit]
After=network.target
[Install]
WantedBy=default.target
[Service]
Environment="PORT=6675"
Environment="CONFIG=/etc/test-ftth-ipe-map/conf.ini"
Environment="REPO_DIR=/srv/www/test-ftth-ipe-map"
ConfigurationDirectory=test-ftth-ipe-map
WorkingDirectory=/srv/www/test-ftth-ipe-map/webapp/
ExecStart=/srv/www/test-ftth-ipe-map/webapp/startGunicornService
User=ftth-ipe-map
Group=ftth-ipe-map
Restart=on-failure
RestartSec=30
# Sandboxing
#ProtectSystem=strict
#ProtectHome=tmpfs
PrivateTmp=true
PrivateDevices=true
ProtectClock=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET
RestrictRealtime=true

View file

@ -0,0 +1,13 @@
{
"name": "test_event_stream",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"eventsource": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA=="
}
}
}

View file

@ -0,0 +1,16 @@
{
"name": "test_event_stream",
"version": "1.0.0",
"description": "",
"main": "test_api_netwo.js",
"dependencies": {
"eventsource": "^2.0.2"
},
"devDependencies": {},
"scripts": {
"start": "node test_api_netwo.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

View file

@ -0,0 +1,42 @@
//import { EventSource } from 'eventsource'
var EventSource = require('eventsource')
function handleNetwo(data) {
console.log("handle data")
console.log(data)
}
function startNetwo(refimmeuble) {
const evtSource = new EventSource(encodeURI(`http://localhost:5000/eligibilite/netwo?ref_imb=${refimmeuble}`));
evtSource.onmessage = function (event) {
try {
console.log("got data")
data = JSON.parse(event.data)
handleNetwo(data)
if (data.eligDone) {
console.log("elig done, stop stream")
evtSource.close();
}
} catch (error) {
console.log("error parsing data, stop stream: ", error)
evtSource.close();
}
}
evtSource.onerror = function (event) {
console.log("in onerror stop stream: ",event)
evtSource.close();
}
}
if (process.argv.length < 3) {
console.log("Need to specify imb param (e.g IMB/33063/S/A8DA )")
process.exit(1)
}
ref_imb = process.argv[2]
startNetwo(ref_imb)