Commit 0e7f9a20 authored by Jerome Touvier's avatar Jerome Touvier
Browse files

refactoring

parent e4c49682
**/logs
**/__pycache__
database_config.ini
stages:
- deploy
variables:
GIT_SUBMODULE_STRATEGY: recursive
deploy test:
stage: deploy
tags:
- deploy-ws-docker
script:
- rsync -a --exclude .git* $CI_PROJECT_DIR /srv/deploy/
- docker restart ws-timeseries
- docker restart ws-timeseriesplot
This diff is collapsed.
import argparse
import configparser
import json
import logging
import os
import re
import sys
import traceback
import psycopg2
levels = [logging.CRITICAL, logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
## valid request parameters
url_keys = ("network", "station", "channel", "location", "net", "sta", "cha", "loc")
def database_config():
config = dict()
parser = configparser.ConfigParser()
parser.read("database_config.ini")
for name in parser.options("postgresql"):
config.update({name: parser.get("postgresql", name)})
return config
def request_parser(params, url):
for pairs in re.findall(r"[\w]+=[\w?*-]+", url):
logging.debug(pairs)
key, value = pairs.split("=")
if key in url_keys:
key = "network" if key == "net" else key
key = "station" if key == "sta" else key
key = "location" if key == "loc" else key
key = "channel" if key == "cha" else key
params[key] = value
return params
def records_to_dictlist(data):
header = ["Network", "Station", "Location", "Channel"]
dictlist = []
for row in data:
dictlist.append({k: r for k, r in zip(header, row)})
return {"Channel codes": dictlist}
def is_like_or_equal(params, key):
""" Built the condition for the specified key in the "where" clause taking into account lists or wilcards """
subquery = list()
for param in params[key].split(","):
op = "LIKE" if re.search(r"[*?]", param) else "="
key = "s.station" if key == "station" else key
subquery.append(f"{key} {op} '{param}'")
return " OR ".join(subquery)
def sql_request(params):
""" Built the PostgreSQL request."""
select = f"""SELECT DISTINCT network, s.station, location, channel FROM networks AS n, station AS s, channel AS c WHERE n.network_id = s.network_id AND s.station_id = c.station_id AND ({is_like_or_equal(params, "network")}) AND ({is_like_or_equal(params, "station")}) AND ({is_like_or_equal(params, "channel")}) AND ({is_like_or_equal(params, "location")})"""
select = select.replace("?", "_").replace("*", "%")
if params["limit"]:
return f"""{select} ORDER BY network, station, channel, location LIMIT {params["limit"]};"""
return f"""{select} ORDER BY network, station, channel, location;"""
def collect_data(params):
""" Return the result of the sql query in the database"""
conn = None
try:
conf = database_config() # read connection parameters
conn = psycopg2.connect(**conf) # connect to the RESIF database
cursor = conn.cursor() # cursor to execute SQL command
logging.debug(conn.get_dsn_parameters())
logging.debug(f"Postgres version : {conn.server_version}")
SQL_SELECT = sql_request(params)
logging.debug(f"{SQL_SELECT}")
cursor.execute(SQL_SELECT)
logging.debug(cursor.statusmessage)
data = cursor.fetchall()
cursor.close() # close this communication
return data
except (Exception, psycopg2.DatabaseError) as error:
logging.exception(str(error))
finally:
if conn is not None:
conn.close()
logging.debug("Database connection closed.")
def extend(args=None, path=None):
""" Main function """
## parameters parsing ##
parser = argparse.ArgumentParser(
description=(
"Wilcards extender. Returns extended name of each channel containing wilcards (? and *) as atomic quadruplet (network, station, location, channel). Arguments can be provided individualy with the n(etwork), s(tation), l(ocation), c(hannel) parameters or directly parsed from an URL."
)
)
parser.add_argument(
"-n", "--network", type=str, default="*", help="network code (default all)"
)
parser.add_argument(
"-s", "--station", type=str, default="*", help="station code (default all)"
)
parser.add_argument(
"-c", "--channel", type=str, default="*", help="channel code (default all)"
)
parser.add_argument(
"-l",
"--location",
type=str,
default="*",
help="location code (default all, the double dash code (i.e. --), have to be passed as simple dash)",
)
parser.add_argument(
"--url",
type=str,
help="""An URL request that can be amended by parameters n, s, l, c if they are different from the value all (e.g. in this case n=* is discarded).""",
)
parser.add_argument(
"-d",
"--delimiter",
type=str,
default=" ",
help="output separator (default space)",
)
parser.add_argument("--limit", type=int, help="row limit (default none)")
parser.add_argument(
"--format",
type=str,
default="text",
help="text, json, standard output (stdout) or tuple (default text)",
choices=["text", "json", "stdout", "tuple"],
)
parser.add_argument(
"-v",
"--verbose",
type=int,
default=3,
help="set logging level: 0 critical, 1 error, 2 warning, 3 info, 4 debug, default info",
choices=range(0, 5),
)
if args:
args = parser.parse_args(args)
else:
args = parser.parse_args()
## logging configuration ##
logging.basicConfig(
level=levels[args.verbose],
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
## get parameters ##
logging.debug(args)
for key in vars(args):
# avoid errors with something like : -parameter=--
if getattr(args, key) == list():
logging.warning(f"Invalid parameter : {key}")
return 1
params = {"network": "*", "station": "*", "location": "*", "channel": "*"}
params["limit"] = args.limit
if args.url:
params = request_parser(params, args.url)
if args.network != "*":
params["network"] = args.network
if args.station != "*":
params["station"] = args.station
if args.location != "*":
params["location"] = "--" if args.location == "-" else args.location
if args.channel != "*":
params["channel"] = args.channel
logging.debug(params)
## get channels ##
try:
data = collect_data(params)
if data:
res = ""
if args.format == "json":
res = json.dumps(records_to_dictlist(data))
elif args.format == "tuple":
res = data
else:
res = "\n".join(args.delimiter.join(row) for row in data)
if args.format == "stdout":
print(res)
elif not path:
return res
else:
path = os.path.splitext(path)[0]
if args.format == "json":
path = path + ".json"
else:
path = path + ".txt"
with open(path, "w") as fid:
print(res, file=fid)
else:
logging.debug("No channel found.")
except Exception:
sys.stdout.write(traceback.format_exc())
if __name__ == "__main__":
extend()
# global constants
FROM_CLIENT = False
FDSN_CLIENT = "RESIF"
DATA_MOUNT_POINT = "/mnt/nfs/summer"
USER_AGENT_TIMESERIES = "resifws-timeseries"
USER_AGENT_TIMESERIES_INVENTORY = "resifws-timeseries_inventory"
USER_AGENT_TIMESERIESPLOT = "resifws-timeseriesplot"
# limitations
TIMEOUT = 60
MAX_DAYS = 31
MAX_PLOTS = 30
MAX_DATA_POINTS_FAST_MODE = 500000
MAX_POINTS = "100,000,000"
MAX_DATA_POINTS = int(MAX_POINTS.replace(",", ""))
MAX_POINTS_PROCESSING = "10,000,000"
MAX_DATA_POINTS_PROCESSING = int(MAX_POINTS_PROCESSING.replace(",", ""))
# available parameter values
IMAGE_FORMAT = ("png", "jpeg", "jpg")
OUTPUT_TIMESERIES = ("ascii", "miniseed", "mseed", "plot", "sac", "slist", "tspair")
STRING_TRUE = ("yes", "true", "t", "y", "1", "")
STRING_FALSE = ("no", "false", "f", "n", "0")
TAPER_WINDOWS = ("HANNING", "HAMMING", "COSINE")
UNITS = ("AUTO", "VEL", "ACC", "DISP")
# plots constants
PLOT_COLOR = "155084"
XTICKS_ROTATION = 0
HEIGHT = 400
HEIGHT_MIN = 200
HEIGHT_MAX = 1200
WIDTH = 1200
WIDTH_MIN = 200
WIDTH_MAX = 3600
DPI = 100
DPI_MIN = 50
DPI_MAX = 300
# processing constants
WL = 1
WL_MIN = -1000
WL_MAX = 1000
TAPER_MIN = 0.0
TAPER_MAX = 0.5
DECI_MIN = 1
DECI_MAX = 16
class Error:
LEN_ARGS = "Too much arguments in URL."
UNKNOWN_PARAM = "Unknown query parameter: "
MULTI_PARAM = "Duplicate query parameter: "
VALID_PARAM = "Valid parameters. "
START_LATER = "The starttime cannot be later than the endtime: "
TOO_LONG_DURATION = "Too many days requested (greater than "
TOO_MUCH_DATA = f"The request exceeds the limit of {MAX_POINTS} data points."
TOO_MUCH_DATA_PROCESSING = (
f"Deconvolution is not allowed above {MAX_POINTS_PROCESSING} data points."
)
RESPONSE = "Deconvolution can't be performed. Instrumental response not available."
UNSPECIFIED = "Error.processing your request."
NO_CONNECTION = "No services could be discovered at http://ws.resif.fr.\n\
This could be due to a temporary service outage, an invalid FDSN service address,\n\
an inactive internet connection or a blocking firewall rule."
OK_CONNECTION = "Connection OK. "
NO_DATA = "Your query doesn't match any data available."
TIMEOUT = f"Your query exceeds timeout ({TIMEOUT} seconds)."
MISSING = "Missing parameter : "
BAD_VAL = " Invalid value: "
CHAR = "White space(s) or invalid string. Invalid value for: "
EMPTY = "Empty string. Invalid value for: "
BOOL = "(Valid boolean values are: true/false, yes/no, t/f or 1/0)"
NETWORK = "Invalid network code: "
STATION = "Invalid station code: "
LOCATION = "Invalid location code: "
CHANNEL = "Invalid channel code: "
QUALITY = "Invalid quality code: "
TIME = "Bad date value: "
FORMAT = "Invalid image file type: "
UNITS = "Invalid units type: "
FREQLIMITS = "Invalid freqlimits value: "
WATER_EARTHUNITS = (
"The waterlevel parameter must be used with earthunits=true option."
)
INT_BETWEEN = "must be an integer between"
DPI = f"dpi {INT_BETWEEN} {DPI_MIN} and {DPI_MAX}." + BAD_VAL
WIDTH = f"width {INT_BETWEEN} {WIDTH_MIN} and {WIDTH_MAX}." + BAD_VAL
HEIGHT = f"height {INT_BETWEEN} {HEIGHT_MIN} and {HEIGHT_MAX}." + BAD_VAL
COLOR = f"Invalid HEX Color Code (must be such as B22222)" + BAD_VAL
WATERLEVEL = f"waterlevel must be between {WL_MIN} and {WL_MAX}." + BAD_VAL
DECI = f"decimate {INT_BETWEEN} {DECI_MIN} and {DECI_MAX}." + BAD_VAL
TAPER = f"Accepted taper window values are: WIDTH,TYPE\n\
WIDTH is between {TAPER_MIN} and {TAPER_MAX}\n\
TYPE is HANNING, HAMMING or COSINE.\n\
Invalid taper value: "
BP = "Invalid band pass filter value: "
SCALE_DIVSCALE = "You cannot specify both scale and divscale values."
OUTPUT_TIMESERIES = f"Accepted format values are: {OUTPUT_TIMESERIES}." + BAD_VAL
PLOTS = f"The request exceeds the limit of {MAX_PLOTS} plots. Try to reduce the number of channels requested (network, station, location and channel parameters)."
PROCESSING = "Your request cannot be processed. Check for value consistency."
MISSING_OUTPUT = "Missing format parameter. "
INVALID_OUTPUT_PARAM = (
"The option 'out(put)' is no longer valid. Use 'format' instead."
)
NO_WILCARDS = "Wilcards or lists are allowed only with plot or mseed output options (Invalid value for: "
NO_SELECTION = "Request contains no selections."
class HTTP:
_200_ = "Successful request. "
_202_ = "No data matches the selection. "
_400_ = "Bad request due to improper value. "
_401_ = "Authentication is required. "
_403_ = "Forbidden access. "
_404_ = "Page not found. "
_408_ = "Request exceeds timeout. "
_409_ = "Too much data. "
_413_ = "Request too large. "
_414_ = "Request URI too large. "
_500_ = "Internal server error. "
_503_ = "Service unavailable. "
# List of loggers, handlers and formatters:
[loggers]
keys=root
[handlers]
keys=consoleHandler, fileHandler
[formatters]
keys=generic, verbose
# loggers:
[logger_root]
level=DEBUG
handlers=consoleHandler, fileHandler
# handlers:
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=generic
args=(sys.stdout,)
[handler_fileHandler]
class=logging.handlers.TimedRotatingFileHandler
level=DEBUG
formatter=generic
#(filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None)
args=('logs/dev.log', 'd', 1, 2)
# formatters:
[formatter_generic]
#format=[%(asctime)s] %(levelname)s (%(process)d) [%(module)s:%(lineno)d] %(message)s
format=[%(asctime)s] %(levelname)s (%(process)d) [%(pathname)s:%(lineno)d] %(message)s
# datefmt='%Y-%m-%d %H:%M:%
[formatter_verbose]
format=[%(asctime)s] %(levelname)s (%(process)d) [%(pathname)s:%(lineno)d] %(message)s %(stack_info)s
# List of loggers, handlers and formatters:
[loggers]
keys=root
[handlers]
keys=consoleHandler, fileHandler
[formatters]
keys=generic, verbose
# loggers:
[logger_root]
level=DEBUG
handlers=consoleHandler, fileHandler
# handlers:
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=generic
args=(sys.stdout,)
[handler_fileHandler]
class=logging.handlers.TimedRotatingFileHandler
level=DEBUG
formatter=generic
#(filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None)
args=('logs/production.log', 'd', 1, 15, None, False, False)
# formatters:
[formatter_generic]
#format=[%(asctime)s] %(levelname)s (%(process)d) [%(module)s:%(lineno)d] %(message)s
format=[%(asctime)s] %(levelname)s (%(process)d) [%(pathname)s:%(lineno)d] %(message)s
# datefmt='%Y-%m-%d %H:%M:%
[formatter_verbose]
format=[%(asctime)s] %(levelname)s (%(process)d) [%(pathname)s:%(lineno)d] %(message)s %(stack_info)s
# Webservice FDSN timeseries
Ce service donne accès aux signaux temporels des capteurs sismiques du réseau RESIF. Des traitements supplémentaires peuvent être réalisés sur les données.
La plage demandée ne peut excéder 31 jours. Seules les données non restreintes sont accessibles.
## Options de traitements du signal
- Filtres passe-bas, passe-haut ou passe-bande (optionnellement à phase nulle).
- Suppression de la moyenne ou de la tendance temporelle du signal.
- Mise à l'échelle du signal par un facteur constant.
- Déconvolution avec la réponse instrumentale (avec conversion d'unité et préfiltrage).
- Dérivée et intégration.
- Calcul de l'enveloppe du signal.
- Décimation (sous-échantillonnage).
## Formats de sorties disponibles
- miniSEED
- SAC binary
- ASCII
- PNG ou JPEG Plot
## Utilisation de la requête
/query? (channel-options) (date-range-options) (output-options) [filter-options] {plot-options}
où :
channel-options :: (net=<network> & sta=<station> & loc=<location> & cha=<channel>)
date-range-options :: (starttime=<date|durée>) & (endtime=<date|durée>)
output-options :: (format=<ascii|mseed|sac|slist|tspair|plot>)
plot-options :: {showtitle=<TRUE|false>} {showscale=<TRUE|false>} {monochrome=<true|FALSE>}
plot-options :: {width=<400-2000>} {height=<200-2000>}
filter-options :: [decimate=<2-16>]
filter-options :: [taper=<0.0-0.5,HANNING|hamming|cosine>]
filter-options :: [envelope=<true|FALSE>]
filter-options :: [lpfilter=<fréquence>] [hpfilter=<fréquence>] [bpfilter=<fmin-fmax>] {zerophase=<true|FALSE>}
filter-options :: [detrend=<true|FALSE>]
filter-options :: [diff=<true|FALSE>] [int=<true|FALSE>]
filter-options :: [scale=<nombre>] [divscale=<nombre>]
filter-options :: [correct=<true|FALSE>] {waterlevel=<nombre>} {freqlimits=<f1-f2-f3-f4>} {units=<AUTO|disp|vel|acc>}
filter-options :: [demean=<TRUE|false>]
(..) requis
[..] optionnel
{..} optionnel, mais toujours en complément d'une autre option
les valeurs par défaut sont en majuscules
## Exemples de requêtes
<a href="http://ws.resif.fr/resifws/timeseries/1/query?net=RA&station=PYTO&cha=HN2&loc=02&demean&correct&start=2017-11-02T13:35:00&end=2017-11-02T13:40:00&format=ascii">http://ws.resif.fr/resifws/timeseries/1/query?net=RA&station=PYTO&cha=HN2&loc=02&demean&correct&start=2017-11-02T13:35:00&end=2017-11-02T13:40:00&format=ascii</a>
<a href="http://ws.resif.fr/resifws/timeseries/1/query?net=RA&station=PYTO&cha=HN2&loc=02&demean&correct&start=2017-11-02T13:35:00&end=2017-11-02T13:40:00&format=plot">http://ws.resif.fr/resifws/timeseries/1/query?net=RA&station=PYTO&cha=HN2&loc=02&demean&correct&start=2017-11-02T13:35:00&end=2017-11-02T13:40:00&format=plot</a>
## Descriptions détaillées de chaque paramètre de la requête
### Format autorisé pour la station
Les quatre paramètres (network, station, location, channel) déterminent les canaux d’intérêt.
| Paramètre | Exemple | Discussion | Valeur par défaut |
| :--------- | :------ | :---------------------------------------------------------------------------- | :----- |
| net[work] | FR | Nom du réseau sismique. | aucune |
| sta[tion] | CIEL | Nom de la station. | aucune |
| loc[ation] | 00 | Code de localisation. Utilisez loc=-- pour des codes de localisations vides. | aucune |
| cha[nnel] | HHZ | Code de canal. | aucune |
#### Jokers et listes d'arguments
- Jokers : le point d’interrogation __?__ représente n'importe quel caractère unique, alors que l'astérisque __*__ représente zéro caractère ou plus.
- Listes : plusieurs éléments peuvent également être récupérés à l'aide d'une liste séparée par des virgules. Les jokers peuvent être inclus dans la liste.
Par exemple, pour le code des canaux : channel=EH?,BHZ
#### Détails sur la nomenclature des codes
- NETWORK = 1 à 2 caractères alphanumériques. Un groupe de points de mesures.
- STATION = 1 à 5 caractères alphanumériques. Un site de mesure dans un réseau.
- CHANNEL = 3 caractères qui désignent : la fréquence d'échantillonnage et la bande de fréquence du capteur, le type de l'instrument, l'orientation physique de la composante.
- LOCATION = 2 caractères qui permettent de distinguer plusieurs flux de données d'un même canal
### Formats autorisés pour l'intervalle de temps
La définition de l'intervalle de temps peut prendre différentes formes :
#### Avec une date de début et une date de fin
| Paramètre | Exemple | Discussion | Valeur par défaut |
| :---------- | :------------------ | :--------------| :----- |
| start[time] | 2015-08-12T01:00:00 | Date de début. | aucune |
| end[time] | 2015-08-13T01:00:00 | Date de fin. | aucune |
**Exemple :**
...starttime=2015-08-12T01:00:00&endtime=2015-08-13T01:00:00...
#### Combinaison d'une date et d'une durée en secondes
| Paramètre | Exemple | Discussion | Valeur par défaut |
| :---------- | :------------------ | :-------------------------------------| :----- |
| start[time] | 2015-08-12T01:00:00 | Date de début. | aucune |
| end[time] | 7200 | Durée du signal exprimée en secondes. | aucune |
**Exemple :**
...starttime=2015-08-12T01:00:00&endtime=7200...
L'exemple précédent spécifie une date pour le paramètre start[time] et 7200 secondes pour le paramètre end[time].
#### Combinaison du mot-clé "currentutcday" avec une date ou bien une durée en secondes
Le mot-clé "currentutcday" signifie exactement minuit de la date du jour (heure UTC). Il peut être utilisé avec les paramètres start[time] et end[time].
| Paramètre | Exemple | Discussion | Valeur par défaut |
| :---------- | :------------ | :---------------------------------- | :----- |
| start[time] | 7200 | Date ou durée exprimée en secondes. | aucune |
| end[time] | currentutcday | Minuit (UTC) de la date du jour. | aucune |
**Exemples :**
1) ...starttime=currentutcday&endtime=7200...<br/>
2) ...starttime=7200&endtime=currentutcday...
Le premier exemple désigne les 2 heures après minuit (heure UTC) du jour actuel.
Le second exemple désigne les 2 dernières heures avant minuit (heure UTC) du jour actuel.
### Options de traitements des signaux temporels
Les paramètres suivants permettent de filtrer les signaux. L'ordre des paramètres compte puisque chaque opération sera effectuée dans l'ordre donné.
| Paramètre | Exemple | Discussion | Valeur par défaut |
| :--------- | :--------- | :------------------------------------------------------------------------ | :----- |
| taper | 0.25 <br/> 0.25, HANNING <br/> 0.35, COSINE| Fenêtrage dans le domaine temporel. La taille de la fenêtre est donnée en fraction de la taille du signal (de 0 à 0.5). Elle peut être suivie du type de fenêtre : HANNING (défaut), HAMMING, COSINE. | aucune |
| envelope | true | Enveloppe du signal par transformée de Hilbert approchée. | false |
| lp[filter] | 1.0 | Filtre passe-bas (IIR d'ordre 4 / fenêtre de Butterworth)<sup>1</sup>. | aucune |
| hp[filter] | 2.0 | Filtre passe-haut (IIR d'ordre 4 / fenêtre de Butterworth)<sup>1</sup>. | aucune |
| bp[filter] | 0.5-1.2 | Filtre passe-bande (IIR d'ordre 4 / fenêtre de Butterworth)<sup>1,2</sup>. | aucune |
| zerophase | true | Filtrage à phase nulle. Option à utiliser avec : lpfilter, hpfilter ou bpfilter. | false |
| detrend | true | Supprime la tendance temporelle du signal. | false |
| scale | 1.5 | Multiplie les valeurs du signal par une constante. | aucune |
| divscale | 4.0 | Divise les valeurs du signal par une constante. | aucune |
| diff | true | Signal dérivé (approximation centrée). | false |
| int | true | Signal intégré (méthode des trapèzes). | false |
| deci[mate] | 2.0 | Facteur de décimation. Un filtre passe-bas est appliqué pour empêcher l'effet de repliement de spectre (antialiasing). | aucune |
| demean | true | Retranche la valeur moyenne aux données. | false |
| correct | true | Applique une correction instrumentale par déconvolution avec conversion en unités géophysiques. | false |
| waterlevel | 10 <br/> none | Niveau de l'eau (en dB) utilisé pour la déconvolution. <br/> La valeur "none" correspond au filtrage inverse classique. | 1 |
| freqlimits | 0.01-0.04-0.5-0.6 | Filtrage passe-bande appliqué avant la déconvolution dans le domaine fréquentiel<sup>1,2</sup>. Fenêtre rectangle d'amplitude 1 entre f2 et f3 et en cosinus jusqu'à 0 pour f1 < f < f2 et f3 < f < f4. | aucune |
| units | VEL | Unité de sortie (AUTO, DISP, VEL, ACC). Actif uniquement avec l'option correct. | AUTO |
Notes :
1. Les fréquences sont exprimées en Hertz.
2. Les fréquences sont séparées par une virgule ou un tiret. (Exemple : 0.01,0.02)
### Options des graphiques
| Paramètre | Exemple | Discussion | Valeur par défaut |
| :-------- | :------ | :------------------------------------------------------------------------- | :----- |
| showtitle | false | Affiche ou non un titre montrant le canal et l'intervalle de dates. | true |
| showscale | false | Affiche ou non une échelle sur le côté droit du graphique. | true |
| monochrome | true | Image en niveaux de gris ou en couleur. | false |
| width | 500 | Largeur de l'image de sortie (pixels). De 400 à 2000. | 1200 |
| height | 400 | Hauteur de l'image de sortie (pixels). De 200 à 2000. | 400 |
### Options de sortie
| Paramètre | Exemple | Discussion | Valeur par défaut |
| :-------- | :------ | :------------------------------------------------------------------------- | :----- |
| format | ascii | Format de sortie du fichier : ascii (équivalent à tspair), mseed, sac, slist ou plot (sortie graphique). | aucune |