Blame view

web/run.py 61.6 KB
bde97e4d   Goutte   Add more changes ...
1
2
# coding=utf-8

b33d5f62   hitier   Change StringIO i...
3
from io import StringIO
bc18b96c   Goutte   Implement first (...
4
import datetime
8644387c   Goutte   Use real data.
5
import gzip
bc18b96c   Goutte   Implement first (...
6
7
8
import json
import logging
import random
2fedd73b   Goutte   Initial implement...
9
import tarfile
bde97e4d   Goutte   Add more changes ...
10
import time
3f9c36da   hitier   Change urllib import
11
import urllib.request as urllib_request
9f57dceb   Goutte   Add half the brid...
12
import requests
11d86851   Goutte   Add support for s...
13
import re  # regex
9f57dceb   Goutte   Add half the brid...
14

11d86851   Goutte   Add support for s...
15
from csv import writer as csv_writer, DictWriter as csv_dict_writer
596da00d   Goutte   Add more exceptio...
16
from math import sqrt, isnan
bc18b96c   Goutte   Implement first (...
17
18
19
from os import environ, remove as removefile
from os.path import isfile, join, abspath, dirname

3309d7c3   Goutte   Improve the date ...
20
# (i) Just ignore the requirements warning, we already require `dateutils`.
fb383448   Goutte   Implement the cac...
21
from dateutil.relativedelta import relativedelta
3309d7c3   Goutte   Improve the date ...
22
23
from dateutil import parser as dateparser

9390ec89   Goutte   Initial experimen...
24
from flask import Flask
9390ec89   Goutte   Initial experimen...
25
from flask import request
bc18b96c   Goutte   Implement first (...
26
from flask import url_for, send_from_directory, abort as abort_flask
a3e57903   hitier   Change jinja imports
27
28
from jinja2 import Environment, FileSystemLoader
from jinja2.utils import markupsafe
29b428ad   hitier   Change yaml imports
29
from yaml import load as yaml_load, dump
95155804   hitier   Reformat code
30

dd6ae65b   hitier   Add new csvgenera...
31
32
33
from speasy import amda
import pandas as pd

29b428ad   hitier   Change yaml imports
34
35
36
37
try:
    from yaml import CLoader as Loader
except ImportError:
    from yaml import Loader
f10f34d1   Goutte   More logic.
38
from netCDF4 import Dataset, date2num
9390ec89   Goutte   Initial experimen...
39

9390ec89   Goutte   Initial experimen...
40
41
42
43
44
45
# PATH RELATIVITY #############################################################

THIS_DIRECTORY = dirname(abspath(__file__))


def get_path(relative_path):
a4a9ef03   Goutte   Cache generated C...
46
    """Get an absolute path from the relative path to this script directory."""
9390ec89   Goutte   Initial experimen...
47
48
49
50
51
52
53
54
55
56
    return abspath(join(THIS_DIRECTORY, relative_path))


# COLLECT GLOBAL INFORMATION FROM SOURCES #####################################

# VERSION
with open(get_path('../VERSION'), 'r') as version_file:
    version = version_file.read().strip()

# CONFIG
91e65f2f   hitier   Fix the utf8 enco...
57
58
59


with open(get_path('../config.yml'), 'r', encoding='utf8') as config_file:
29b428ad   hitier   Change yaml imports
60
    config = yaml_load(config_file.read(), Loader=Loader)
9390ec89   Goutte   Initial experimen...
61

c0df94bc   Goutte   Adding more logs.
62
FILE_DATE_FMT = "%Y-%m-%dT%H:%M:%S"
1185f353   Goutte   Fix the CME Catal...
63
MOMENT_DATE_FMT = "%Y-%m-%dT%H:%M:%SZ"
54bb1311   Goutte   Bring back the CM...
64
CME_DATE_FMT = "%Y-%m-%dT%H:%MZ"
c0df94bc   Goutte   Adding more logs.
65

6288347a   Goutte   Add the HELP page...
66
67
# Are we on the SSA instance for ESA?
SSA = environ.get('SSA') == 'true'
20fdc1a4   Goutte   Set log level fro...
68
DEBUG = environ.get('DEBUG') == 'true'
6288347a   Goutte   Add the HELP page...
69
# SSA = True
9390ec89   Goutte   Initial experimen...
70

f75faf5f   Goutte   WIP
71
72
# LOGGING #####################################################################

1324cc91   Goutte   Make the footer i...
73
74
LOG_FILE = get_path('run.log')

f75faf5f   Goutte   WIP
75
log = logging.getLogger("HelioPropa")
20fdc1a4   Goutte   Set log level fro...
76
77
78
79
if DEBUG:
    log.setLevel(logging.DEBUG)
else:
    log.setLevel(logging.ERROR)
1324cc91   Goutte   Make the footer i...
80
logHandler = logging.FileHandler(LOG_FILE)
b2837a08   Goutte   Add three retries...
81
82
83
84
logHandler.setFormatter(logging.Formatter(
    "%(asctime)s - %(levelname)s - %(message)s"
))
log.addHandler(logHandler)
f75faf5f   Goutte   WIP
85

e18701b6   Goutte   Cache clear (remo...
86
87
# HARDCODED CONFIGURATION #####################################################

a2034dd9   Goutte   Convert from kilo...
88
89
ASTRONOMICAL_UNIT_IN_KM = 1.496e8

952e3d8f   Goutte   Move to another s...
90
91
92
# Absolute path to the installed CDF library from https://cdf.gsfc.nasa.gov/
CDF_LIB = '/usr/local/lib/libcdf'

e18701b6   Goutte   Cache clear (remo...
93
94
95
96
# Absolute path to the data cache directory
CACHE_DIR = get_path('../cache')

# These two configs are not in the YAML config because adding a new parameter
ea45ebf9   Goutte   Add the mocks of ...
97
# will not work as-is, you'd have to edit some netcdf-related code.
e18701b6   Goutte   Cache clear (remo...
98
99
100
101
102

# The slugs of the available parameters in the generated CSV files.
# The order matters. If you change this you also need to change the
# innermost loop of `get_data_for_target`.
# The javascript knows the targets' properties under these names.
ec483087   hitier   Fix dict merging
103
104
# PROPERTIES = ('time', 'vrad', 'vtan', 'vtot', 'btan', 'brad', 'temp', 'pdyn', 'dens',
#               'atse', 'xhee', 'yhee')
e18701b6   Goutte   Cache clear (remo...
105
PROPERTIES = ('time', 'vrad', 'vtan', 'vtot', 'btan', 'temp', 'pdyn', 'dens',
d1c44c51   Goutte   Enable Earth
106
              'atse', 'xhee', 'yhee')
e18701b6   Goutte   Cache clear (remo...
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134

# The parameters that the users can handle.
# The slug MUST be one of the properties above.
PARAMETERS = {
    'pdyn': {
        'slug': 'pdyn',
        'name': 'Dyn. Pressure',
        'title': 'The dynamic pressure.',
        'units': 'nPa',
        'active': True,
        'position': 10,
    },
    'vtot': {
        'slug': 'vtot',
        'name': 'Velocity',
        'title': 'The velocity of the particles.',
        'units': 'km/s',
        'active': False,
        'position': 20,
    },
    'btan': {
        'slug': 'btan',
        'name': 'B Tangential',
        'title': 'B Tangential.',
        'units': 'nT',
        'active': False,
        'position': 30,
    },
ec483087   hitier   Fix dict merging
135
136
137
138
139
140
141
142
    # 'brad': {
    #     'slug': 'brad',
    #     'name': 'B Radial',
    #     'title': 'B Radial.',
    #     'units': 'nT',
    #     'active': False,
    #     'position': 35,
    # },
e18701b6   Goutte   Cache clear (remo...
143
144
145
    'temp': {
        'slug': 'temp',
        'name': 'Temperature',
60b73eb1   Goutte   Change temperatur...
146
147
        'title': 'The temperature.',
        'units': 'eV',
e18701b6   Goutte   Cache clear (remo...
148
149
150
151
152
153
154
        'active': False,
        'position': 40,
    },
    'dens': {
        'slug': 'dens',
        'name': 'Density',
        'title': 'The density N.',
aa7247d6   Goutte   Generate a CDF fi...
155
        'units': 'cm^-3',
e18701b6   Goutte   Cache clear (remo...
156
157
158
        'active': False,
        'position': 50,
    },
d1c44c51   Goutte   Enable Earth
159
160
    'atse': {
        'slug': 'atse',
e18701b6   Goutte   Cache clear (remo...
161
162
163
164
165
166
167
168
        'name': 'Angle T-S-E',
        'title': 'Angle Target-Sun-Earth.',
        'units': 'deg',
        'active': False,
        'position': 60,
    },
}

48fa6323   Goutte   Try to fix the he...
169
# SETUP ENVIRONMENT ###########################################################
2fe06b17   Goutte   Move the ENV dire...
170
171
172
173

environ['SPACEPY'] = CACHE_DIR
environ['CDF_LIB'] = CDF_LIB

9390ec89   Goutte   Initial experimen...
174
175
176
# SETUP FLASK ENGINE ##########################################################

app = Flask(__name__, root_path=THIS_DIRECTORY)
20fdc1a4   Goutte   Set log level fro...
177
app.debug = DEBUG
b2837a08   Goutte   Add three retries...
178
if app.debug:
2fedd73b   Goutte   Initial implement...
179
    log.info("Starting Flask app IN DEBUG MODE...")
b2837a08   Goutte   Add three retries...
180
181
else:
    log.info("Starting Flask app...")
9390ec89   Goutte   Initial experimen...
182
183


48fa6323   Goutte   Try to fix the he...
184
185
186
187
def handle_error(e):
    log.error(e)
    return str(e)  # wish we could use the default error renderer here

befd6269   Goutte   Add Chris' changes.
188

48fa6323   Goutte   Try to fix the he...
189
190
191
app.register_error_handler(Exception, handle_error)


9390ec89   Goutte   Initial experimen...
192
193
194
195
196
197
198
199
200
# SETUP JINJA2 TEMPLATE ENGINE ################################################

def static_global(filename):
    return url_for('static', filename=filename)


def shuffle_filter(seq):
    """
    This shuffles the sequence it is applied to.
2fedd73b   Goutte   Initial implement...
201
    Jinja2 _should_ provide this.
9390ec89   Goutte   Initial experimen...
202
203
204
205
206
207
208
209
210
211
212
    """
    try:
        result = list(seq)
        random.shuffle(result)
        return result
    except:
        return seq


def markdown_filter(value, nl2br=False, p=True):
    """
2fedd73b   Goutte   Initial implement...
213
    Converts markdown into html.
9390ec89   Goutte   Initial experimen...
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
    nl2br: set to True to replace line breaks with <br> tags
    p: set to False to remove the enclosing <p></p> tags
    """
    from markdown import markdown
    from markdown.extensions.nl2br import Nl2BrExtension
    from markdown.extensions.abbr import AbbrExtension
    extensions = [AbbrExtension()]
    if nl2br is True:
        extensions.append(Nl2BrExtension())
    markdowned = markdown(value, output_format='html5', extensions=extensions)
    if p is False:
        markdowned = markdowned.replace(r"<p>", "").replace(r"</p>", "")
    return markdowned


bde97e4d   Goutte   Add more changes ...
229
_js_escapes = {
34e94ae3   hitier   Code reformating
230
231
232
233
234
235
236
237
238
239
240
    '\\': '\\u005C',
    '\'': '\\u0027',
    '"': '\\u0022',
    '>': '\\u003E',
    '<': '\\u003C',
    '&': '\\u0026',
    '=': '\\u003D',
    '-': '\\u002D',
    ';': '\\u003B',
    u'\u2028': '\\u2028',
    u'\u2029': '\\u2029'
bde97e4d   Goutte   Add more changes ...
241
242
}
# Escape every ASCII character with a value less than 32.
d4b3d381   hitier   Tiny python3 synt...
243
_js_escapes.update(('%c' % z, '\\u%04X' % z) for z in range(32))
bde97e4d   Goutte   Add more changes ...
244
245
246
247
248
249
250
251
252
253


def escapejs_filter(value):
    escaped = []
    for letter in value:
        if letter in _js_escapes:
            escaped.append(_js_escapes[letter])
        else:
            escaped.append(letter)

a3e57903   hitier   Change jinja imports
254
    return markupsafe.Markup("".join(escaped))
bde97e4d   Goutte   Add more changes ...
255

95155804   hitier   Reformat code
256

9390ec89   Goutte   Initial experimen...
257
258
259
260
261
262
263
264
265
266
267
268
tpl_engine = Environment(loader=FileSystemLoader([get_path('view')]),
                         trim_blocks=True,
                         lstrip_blocks=True)

tpl_engine.globals.update(
    url_for=url_for,
    static=static_global,
)

tpl_engine.filters['markdown'] = markdown_filter
tpl_engine.filters['md'] = markdown_filter
tpl_engine.filters['shuffle'] = shuffle_filter
bde97e4d   Goutte   Add more changes ...
269
tpl_engine.filters['escapejs'] = escapejs_filter
9390ec89   Goutte   Initial experimen...
270
271
272
273
274
275

tpl_global_vars = {
    'request': request,
    'version': version,
    'config': config,
    'now': datetime.datetime.now(),
fac54a01   Goutte   Use SSA instead o...
276
    'is_esa': SSA,
9390ec89   Goutte   Initial experimen...
277
278
279
280
281
}


# HELPERS #####################################################################

57f42bd7   Goutte   Log the abortions.
282
def abort(code, message):
b52b494b   Goutte   Add even more logs.
283
    log.error("Abort: " + message)
57f42bd7   Goutte   Log the abortions.
284
285
286
    abort_flask(code, message)


9390ec89   Goutte   Initial experimen...
287
288
289
290
291
292
293
294
def render_view(view, context=None):
    """
    A simple helper to render [view] template with [context] vars.
    It automatically adds the global template vars defined above, too.
    It returns a string, usually the HTML contents to display.
    """
    context = {} if context is None else context
    return tpl_engine.get_template(view).render(
d4b3d381   hitier   Tiny python3 synt...
295
        dict(list(tpl_global_vars.items()) + list(context.items()))
9390ec89   Goutte   Initial experimen...
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
    )


# def render_page(page, title="My Page", context=None):
#     """
#     A simple helper to render the md_page.html template with [context] vars &
#     the additional contents of `page/[page].md` in the `md_page` variable.
#     It automagically adds the global template vars defined above, too.
#     It returns a string, usually the HTML contents to display.
#     """
#     if context is None:
#         context = {}
#     context['title'] = title
#     context['md_page'] = ''
#     with file(get_path('page/%s.md' % page)) as f:
#         context['md_page'] = f.read()
#     return tpl_engine.get_template('md_page.html').render(
#         dict(tpl_global_vars.items() + context.items())
#     )

077980eb   Goutte   Improve availabil...
316

bc18b96c   Goutte   Implement first (...
317
318
319
320
321
322
323
def is_list_in_list(needle, haystack):
    for n in needle:
        if n not in haystack:
            return False
    return True


1324cc91   Goutte   Make the footer i...
324
325
326
327
328
329
330
331
332
333
def round_time(dt=None, round_to=60):
    """
    Round a datetime object to any time laps in seconds
    dt : datetime.datetime object, default now.
    roundTo : Closest number of seconds to round to, default 1 minute.
    """
    if dt is None:
        dt = datetime.datetime.now()
    seconds = (dt.replace(tzinfo=None) - dt.min).seconds
    rounding = (seconds + round_to / 2) // round_to * round_to
34e94ae3   hitier   Code reformating
334
    return dt + datetime.timedelta(0, rounding - seconds, -dt.microsecond)
1324cc91   Goutte   Make the footer i...
335
336


2d2af24b   Goutte   Add a basic orbit...
337
def datetime_from_list(time_list):
0b9821dd   Goutte   Clean up.
338
    """
2fedd73b   Goutte   Initial implement...
339
    Datetimes in retrieved CDFs are stored as lists of numbers,
80352490   Goutte   Multi model suppo...
340
341
    with DayOfYear starting at 0. We want it starting at 1 because it's what
    vendor parsers use, both in python and javascript.
0b9821dd   Goutte   Clean up.
342
    """
c1a96762   hitier   Move duplicated t...
343
344
345
346
    try:
        time_list = [str(i, 'UTF8') for i in time_list]
    except Exception as e:
        log.error(e)
2d2af24b   Goutte   Add a basic orbit...
347
348
349
    # Day Of Year starts at 0, but for our datetime parser it starts at 1
    doy = '{:03d}'.format(int(''.join(time_list[4:7])) + 1)
    return datetime.datetime.strptime(
50d4f638   Goutte   Hotfix for a very...
350
        "%s%s%s" % (''.join(time_list[0:4]), doy, ''.join(time_list[7:-1])),
2d2af24b   Goutte   Add a basic orbit...
351
352
        "%Y%j%H%M%S%f"
    )
9390ec89   Goutte   Initial experimen...
353

95155804   hitier   Reformat code
354

8b1f082a   hitier   Change read_var t...
355
356
357
358
359
360
361
362
363
364
# Override these using the model configuration in config.yml
default_nc_keys = {
    'hee': 'HEE',
    'vtot': 'V',
    'magn': 'B',
    'temp': 'T',
    'dens': 'N',
    'pdyn': 'P_dyn',
    'atse': 'Delta_angle',
}
ce8af118   Goutte   Fix the favicon.
365

95155804   hitier   Reformat code
366

9b97b286   hitier   Move 2 funcs upwa...
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
def _sta_sto(_cnf, _sta, _sto):
    if 'started_at' in _cnf:
        _s0 = datetime.datetime.strptime(_cnf['started_at'], FILE_DATE_FMT)
        _s0 = max(_s0, _sta)
    else:
        _s0 = _sta
    if 'stopped_at' in _cnf:
        _s1 = datetime.datetime.strptime(_cnf['stopped_at'], FILE_DATE_FMT)
        _s1 = min(_s1, _sto)
    else:
        _s1 = _sto
    return _s0, _s1


def _read_var(_nc, _keys, _key, mandatory=False):
    try:
8b1f082a   hitier   Change read_var t...
383
        return _nc.variables[_keys[_key]][:]
9b97b286   hitier   Move 2 funcs upwa...
384
385
386
387
388
389
390
    except KeyError:
        pass
    if mandatory:
        raise Exception("No variable '%s' found in NetCDF." % _keys[_key])
    return [None] * len(_nc.variables['Time'])  # slow -- use numpy!


927c69c3   Goutte   Make the local ca...
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def get_local_filename(url):
    """
    Build the local cache filename for the distant file
    :param url: string
    :return: string
    """
    from slugify import slugify
    n = len('http://')
    if url.startswith('https'):
        n += 1
    s = url[n:]
    return slugify(s)


180d7d97   Goutte   Refactor heavily.
405
def get_target_config(slug):
2fedd73b   Goutte   Initial implement...
406
    for s in config['targets']:  # dumb
8644387c   Goutte   Use real data.
407
408
        if s['slug'] == slug:
            return s
180d7d97   Goutte   Refactor heavily.
409
    raise Exception("No target found in configuration for '%s'." % slug)
8644387c   Goutte   Use real data.
410
411


180d7d97   Goutte   Refactor heavily.
412
413
414
415
def check_target_config(slug):
    get_target_config(slug)


fb383448   Goutte   Implement the cac...
416
417
418
419
420
def get_active_targets():
    all_targets = config['targets']
    return [t for t in all_targets if not ('locked' in t and t['locked'])]


11d86851   Goutte   Add support for s...
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
def validate_tap_target_config(target):
    tc = get_target_config(target)
    if 'tap' not in tc:
        raise Exception("No `tap` configuration for target `%s`." % target)
    if 'target_name' not in tc['tap']:
        raise Exception("No `target_name` in the `tap` configuration for target `%s`." % target)
    return tc


# Using pyvo would be best.
# def retrieve_auroral_emissions_vopy(target_name):
#     api_url = "http://voparis-tap.obspm.fr/__system__/tap/run/tap/sync"
#     import pyvo as vo
#     service = vo.dal.TAPService(api_url)
#     # … can't figure out how to install pyvo and spacepy alongside (forking?)


def retrieve_auroral_emissions(target_name, d_started_at=None, d_stopped_at=None):
9f57dceb   Goutte   Add half the brid...
439
440
    """
    Work In Progress.
11d86851   Goutte   Add support for s...
441
    :param target_name: You should probably not let users define this value,
9f57dceb   Goutte   Add half the brid...
442
443
444
445
                        as our sanitizing for ADQL may not be 100% safe.
                        Use values from YAML configuration, instead.
                        Below is a list of the ids we found to be existing.
                        > SELECT DISTINCT target_name FROM apis.epn_core
7176ae9b   Goutte   Fix Myriam's cont...
446
447
                        - Jupiter
                        - Saturn
9f57dceb   Goutte   Add half the brid...
448
449
                        - Mars
                        - MERCURY
9f57dceb   Goutte   Add half the brid...
450
451
452
453
454
455
456
                        - Titan
                        - Io
                        - VENUS
                        - Ganymede
                        - Uranus
                        - Callisto
                        - Europa
7176ae9b   Goutte   Fix Myriam's cont...
457
458
459
460
461
462
                        
                        https://pdssbn.astro.umd.edu/holdings/nh-p-rex-2-pluto-v1.0/document/codmac_level_definitions.pdf
                        processing_level == 3
                        
                        API doc and sandbox:
                        http://voparis-tap-planeto.obspm.fr/__system__/tap/run/info
9f57dceb   Goutte   Add half the brid...
463
464
    :return: 
    """
11d86851   Goutte   Add support for s...
465
466
467
468

    # Try out the form
    # http://voparis-tap-planeto.obspm.fr/__system__/adql/query/form

9f57dceb   Goutte   Add half the brid...
469
    api_url = "http://voparis-tap.obspm.fr/__system__/tap/run/tap/sync"
11d86851   Goutte   Add support for s...
470
471
472
473
474
475
476
477
478
    if d_started_at is None:
        d_started_at = datetime.datetime.now()
        t_started_at = time.mktime(d_started_at.timetuple()) - 3600 * 24 * 365 * 2
        # t_started_at = 1
    else:
        t_started_at = time.mktime(d_started_at.timetuple())

    if d_stopped_at is None:
        d_stopped_at = datetime.datetime.now()
9f57dceb   Goutte   Add half the brid...
479
480
    t_stopped_at = time.mktime(d_stopped_at.timetuple())

11d86851   Goutte   Add support for s...
481
    def timestamp_to_jday(timestamp):
9f57dceb   Goutte   Add half the brid...
482
483
        return timestamp / 86400.0 + 2440587.5

11d86851   Goutte   Add support for s...
484
485
486
487
488
489
490
491
492
    def jday_to_timestamp(jday):
        return (jday - 2440587.5) * 86400.0

    def jday_to_datetime(jday):
        return datetime.datetime.utcfromtimestamp(jday_to_timestamp(jday))

    # SELECT DISTINCT dataproduct_type FROM apis.epn_core
    # > im sp sc

9f57dceb   Goutte   Add half the brid...
493
494
495
496
497
498
499
500
501
    query = """
SELECT
  time_min,
  time_max,
  thumbnail_url,
  external_link
FROM apis.epn_core
WHERE target_name='{target_name}'
AND   dataproduct_type='im'
11d86851   Goutte   Add support for s...
502
503
AND   time_min >= {jday_start}
AND   time_min <= {jday_stop}
9f57dceb   Goutte   Add half the brid...
504
505
506
ORDER BY time_min, granule_gid
""".format(
        target_name=target_name.replace("'", "\\'"),
11d86851   Goutte   Add support for s...
507
508
        jday_start=timestamp_to_jday(t_started_at),
        jday_stop=timestamp_to_jday(t_stopped_at)
9f57dceb   Goutte   Add half the brid...
509
    )
95155804   hitier   Reformat code
510
    # AND   processing_level = 4
9f57dceb   Goutte   Add half the brid...
511

95155804   hitier   Reformat code
512
513
514
    #     query = """
    # SELECT DISTINCT target_name FROM apis.epn_core
    # """
9f57dceb   Goutte   Add half the brid...
515
516
517
518

    try:
        response = requests.post(api_url, {
            'REQUEST': 'doQuery',
34e94ae3   hitier   Code reformating
519
520
            'LANG': 'ADQL',
            'QUERY': query,
9f57dceb   Goutte   Add half the brid...
521
            'TIMEOUT': '30',
34e94ae3   hitier   Code reformating
522
            'FORMAT': 'VOTable/td'
9f57dceb   Goutte   Add half the brid...
523
524
525
526
527
528
529
530
531
532
533
        })

        response_xml = response.text

        import xml.etree.ElementTree as ET
        root = ET.fromstring(response_xml)
        namespaces = {'vo': 'http://www.ivoa.net/xml/VOTable/v1.3'}
        rows_xpath = "./vo:RESOURCE/vo:TABLE/vo:DATA/vo:TABLEDATA/vo:TR"
        rows = []
        for row in root.findall(rows_xpath, namespaces):
            rows.append({
11d86851   Goutte   Add support for s...
534
535
                'time_min': jday_to_datetime(float(row[0].text)),
                'time_max': jday_to_datetime(float(row[1].text)),
9f57dceb   Goutte   Add half the brid...
536
537
538
539
                'thumbnail_url': row[2].text,
                'external_link': row[3].text,
            })

9f57dceb   Goutte   Add half the brid...
540
541
542
543
544
545
546
547
        return rows
    except Exception as e:
        print("Failed to retrieve auroral emissions :")
        print(e)

    # print(query)


180d7d97   Goutte   Refactor heavily.
548
def retrieve_amda_netcdf(orbiter, what, started_at, stopped_at):
8644387c   Goutte   Use real data.
549
    """
91c3d52d   Goutte   Add more logs.
550
551
    Handles remote querying AMDA's API for URLs, and then downloading,
    extracting and caching the netCDF files.
8644387c   Goutte   Use real data.
552
553
554
555
556
557
558
559
560
561
562
563
    :param orbiter: key of the source in the YAML config
    :param what: either 'model' or 'orbit', a key in the config of the source
    :param started_at:
    :param stopped_at:
    :return: a list of local file paths to netCDF (.nc) files
    """

    url = config['amda'].format(
        dataSet=what,
        startTime=started_at.isoformat(),
        stopTime=stopped_at.isoformat()
    )
c50cc9d8   Goutte   Continue fixing.
564
    log.info("Fetching remote gzip files list at '%s'." % url)
b2837a08   Goutte   Add three retries...
565
566
    retries = 0
    success = False
92abc15b   Goutte   Mistrust the API ...
567
    errors = []
b2837a08   Goutte   Add three retries...
568
569
570
    remote_gzip_files = []
    while not success and retries < 3:
        try:
3f9c36da   hitier   Change urllib import
571
            response = urllib_request.urlopen(url)
b2837a08   Goutte   Add three retries...
572
573
574
575
            remote_gzip_files = json.loads(response.read())
            if not remote_gzip_files:
                raise Exception("Failed to fetch data at '%s'." % url)
            if remote_gzip_files == 'NODATASET':
92abc15b   Goutte   Mistrust the API ...
576
577
578
                raise Exception("API says there's no dataset at '%s'." % url)
            if remote_gzip_files == 'ERROR':
                raise Exception("API returned an error at '%s'." % url)
077980eb   Goutte   Improve availabil...
579
            if remote_gzip_files == ['OUTOFTIME']:  # it happens
80352490   Goutte   Multi model suppo...
580
581
                return []
                # raise Exception("API says it's out of time at '%s'." % url)
b2837a08   Goutte   Add three retries...
582
583
            success = True
        except Exception as e:
d4b3d381   hitier   Tiny python3 synt...
584
            log.warning("Failed (%d/3) '%s' : %s" % (retries + 1, url, e.message))
92abc15b   Goutte   Mistrust the API ...
585
586
            remote_gzip_files = []
            errors.append(e)
b2837a08   Goutte   Add three retries...
587
588
589
        finally:
            retries += 1
    if not remote_gzip_files:
b52b494b   Goutte   Add even more logs.
590
        log.error("Failed to retrieve data from AMDA.")
91c3d52d   Goutte   Add more logs.
591
        log.error("Failed to fetch gzip files list for %s at '%s' : %s" %
34e94ae3   hitier   Code reformating
592
                  (orbiter, url, errors))
08abc2d4   Goutte   Remove duplicate ...
593
        abort(400, "Failed to fetch gzip files list for %s at '%s' : %s" %
34e94ae3   hitier   Code reformating
594
              (orbiter, url, errors))
08abc2d4   Goutte   Remove duplicate ...
595
596
    else:
        remote_gzip_files = list(set(remote_gzip_files))
9bfa6c42   Goutte   More bug hunting.
597
598

    log.debug("Fetched remote gzip files list : %s." % str(remote_gzip_files))
8644387c   Goutte   Use real data.
599

8644387c   Goutte   Use real data.
600
601
    local_gzip_files = []
    for remote_gzip_file in remote_gzip_files:
077980eb   Goutte   Improve availabil...
602
603
604
        # hotfixes to remove when fixed upstream @Myriam
        if remote_gzip_file in ['OUTOFTIME', 'ERROR']:
            continue  # sometimes half the response is okay, the other not
8644387c   Goutte   Use real data.
605
        if remote_gzip_file.endswith('/.gz'):
80352490   Goutte   Multi model suppo...
606
            continue  # this is just a plain bug
8644387c   Goutte   Use real data.
607
        remote_gzip_file = remote_gzip_file.replace('cdpp1', 'cdpp', 1)
077980eb   Goutte   Improve availabil...
608
        ################################################
e18701b6   Goutte   Cache clear (remo...
609
        local_gzip_file = join(CACHE_DIR, get_local_filename(remote_gzip_file))
8644387c   Goutte   Use real data.
610
611
        local_gzip_files.append(local_gzip_file)
        if not isfile(local_gzip_file):
9bfa6c42   Goutte   More bug hunting.
612
            log.debug("Retrieving '%s'..." % local_gzip_file)
3f9c36da   hitier   Change urllib import
613
            urllib_request.urlretrieve(remote_gzip_file, local_gzip_file)
9bfa6c42   Goutte   More bug hunting.
614
            log.debug("Retrieved '%s'." % local_gzip_file)
dc0be992   Goutte   Support having no...
615
616
        else:
            log.debug("Found '%s' in the cache." % local_gzip_file)
8644387c   Goutte   Use real data.
617
618
619
620

    local_netc_files = []
    for local_gzip_file in local_gzip_files:
        local_netc_file = local_gzip_file[0:-3]
9bfa6c42   Goutte   More bug hunting.
621
        log.debug("Unzipping '%s'..." % local_gzip_file)
3c064b17   Goutte   Ignore failures w...
622
623
        success = True
        try:
5ef50583   Goutte   Clean up.
624
            with gzip.open(local_gzip_file) as f:
3c064b17   Goutte   Ignore failures w...
625
626
627
628
629
                file_content = f.read()
                with open(local_netc_file, 'w+b') as g:
                    g.write(file_content)
        except Exception as e:
            success = False
dc0be992   Goutte   Support having no...
630
631
632
633
634
            log.error("Cannot process gz file '%s' from '%s' : %s" %
                      (local_gzip_file, url, e))
            # Sometimes, the downloaded gz is corrupted, and CRC checks fail.
            # We want to delete the local gz file and try again next time.
            removefile(local_gzip_file)
3c064b17   Goutte   Ignore failures w...
635
        if success:
dc0be992   Goutte   Support having no...
636
            local_netc_files.append(local_netc_file)
3c064b17   Goutte   Ignore failures w...
637
            log.debug("Unzipped '%s'." % local_gzip_file)
8644387c   Goutte   Use real data.
638

ea6c8d5d   Goutte   Add interval cons...
639
    return list(set(local_netc_files))  # remove possible dupes
8644387c   Goutte   Use real data.
640
641


596da00d   Goutte   Add more exceptio...
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
# class DataParser:
#     """
#     Default data parser
#     A wip to try to handle code exeptions sanely.
#     """
#
#     # Override these using the model configuration
#     default_nc_keys = {
#         'hee': 'HEE',
#         'vtot': 'V',
#         'magn': 'B',
#         'temp': 'T',
#         'dens': 'N',
#         'pdyn': 'P_dyn',
#         'atse': 'Delta_angle',
#     }
#
#     def __init__(self, target, model):
#         self.target = target
#         self.model = model
#         pass
#
#     def _read_var(self, nc, _keys, _key, mandatory=False):
#         try:
#             return nc.variables[_keys[_key]]
#         except KeyError:
#             pass
#         if mandatory:
#             raise Exception("No variable '%s' found in NetCDF." % _keys[_key])
#         return [None] * len(nc.variables['Time'])  # slow -- use numpy?
#
#     def parse(self, cdf_handle):
#         nc_keys = self.default_nc_keys.copy()
#
#         times = cdf_handle.variables['Time']  # YYYY DOY HH MM SS .ms
#         data_v = self._read_var(cdf_handle, nc_keys, 'vtot')
#         data_b = self._read_var(cdf_handle, nc_keys, 'magn')
#         data_t = self._read_var(cdf_handle, nc_keys, 'temp')
#         data_n = self._read_var(cdf_handle, nc_keys, 'dens')
#         data_p = self._read_var(cdf_handle, nc_keys, 'pdyn')
#         data_a = self._read_var(cdf_handle, nc_keys, 'atse')
#
#         return zip()


de97d643   Goutte   Fix more bugs.
687
688
def get_data_for_target(target_config, input_slug,
                        started_at, stopped_at):
180d7d97   Goutte   Refactor heavily.
689
690
691
692
    """
    :return: dict whose keys are datetime as str, values tuples of data
    """
    log.debug("Grabbing data for '%s'..." % target_config['slug'])
80352490   Goutte   Multi model suppo...
693

8644387c   Goutte   Use real data.
694
    try:
297a7dfc   Goutte   Add support for i...
695
        models = target_config['models'][input_slug]
077980eb   Goutte   Improve availabil...
696
697
    except Exception as e:
        abort(500, "Invalid model configuration for '%s' : %s"
180d7d97   Goutte   Refactor heavily.
698
699
              % (target_config['slug'], str(e)))
    try:
80352490   Goutte   Multi model suppo...
700
        orbits = target_config['orbit']['models']
d1c44c51   Goutte   Enable Earth
701
702
703
704
    except KeyError as e:
        orbits = []
        # abort(500, "Invalid orbit configuration for '%s' : %s"
        #       % (target_config['slug'], str(e)))
28ef3790   Goutte   Clean up.
705

80352490   Goutte   Multi model suppo...
706
    precision = "%Y-%m-%dT%H"  # model and orbits times are only equal-ish
180d7d97   Goutte   Refactor heavily.
707
    orbit_data = {}  # keys are datetime as str, values arrays of XY
ea6c8d5d   Goutte   Add interval cons...
708
709

    for orbit in orbits:
58bfe281   Goutte   Handle start and ...
710
        s0, s1 = _sta_sto(orbit, started_at, stopped_at)
ea6c8d5d   Goutte   Add interval cons...
711

d1c44c51   Goutte   Enable Earth
712
713
714
715
        nc_keys = default_nc_keys.copy()
        if 'parameters' in orbit:
            nc_keys.update(orbit['parameters'])

ea6c8d5d   Goutte   Add interval cons...
716
717
718
719
720
721
722
723
        orbit_files = retrieve_amda_netcdf(
            target_config['slug'], orbit['slug'], s0, s1
        )
        for orbit_file in orbit_files:
            log.debug("%s: opening orbit NETCDF4 '%s'..." %
                      (target_config['name'], orbit_file))
            cdf_handle = Dataset(orbit_file, "r", format="NETCDF4")
            times = cdf_handle.variables['Time']  # YYYY DOY HH MM SS .ms
d1c44c51   Goutte   Enable Earth
724
            data_hee = _read_var(cdf_handle, nc_keys, 'hee', mandatory=True)
ea6c8d5d   Goutte   Add interval cons...
725
726
727

            log.debug("%s: aggregating data from '%s'..." %
                      (target_config['name'], orbit_file))
d1c44c51   Goutte   Enable Earth
728
            for ltime, datum_hee in zip(times, data_hee):
1324cc91   Goutte   Make the footer i...
729
                try:
d1c44c51   Goutte   Enable Earth
730
731
                    dtime = datetime_from_list(ltime)
                except Exception:
34e94ae3   hitier   Code reformating
732
                    log.error("Failed to parse time from get__data_for_target %s." % ltime)
d1c44c51   Goutte   Enable Earth
733
                    raise
81cb3ba6   Goutte   Fix an issue with...
734
                # Keep only what's in the interval
ea6c8d5d   Goutte   Add interval cons...
735
                if s0 <= dtime <= s1:
34e94ae3   hitier   Code reformating
736
                    dkey = round_time(dtime, 60 * 60).strftime(precision)
ea6c8d5d   Goutte   Add interval cons...
737
738
                    orbit_data[dkey] = datum_hee
            cdf_handle.close()
180d7d97   Goutte   Refactor heavily.
739

8644387c   Goutte   Use real data.
740
    all_data = {}  # keys are datetime as str, values tuples of data
81cb3ba6   Goutte   Fix an issue with...
741

58bfe281   Goutte   Handle start and ...
742
743
744
745
746
    for model in models:
        s0, s1 = _sta_sto(model, started_at, stopped_at)
        model_files = retrieve_amda_netcdf(
            target_config['slug'], model['slug'], s0, s1
        )
d1c44c51   Goutte   Enable Earth
747
748
749
750
        nc_keys = default_nc_keys.copy()
        if 'parameters' in model:
            nc_keys.update(model['parameters'])

129181a6   Goutte   Add more logs to ...
751
        if len(model_files) == 0:
d4b3d381   hitier   Tiny python3 synt...
752
753
            log.warning("No model data for '%s' '%s'."
                        % (target_config['slug'], model['slug']))
129181a6   Goutte   Add more logs to ...
754

58bfe281   Goutte   Handle start and ...
755
        for model_file in model_files:
58bfe281   Goutte   Handle start and ...
756
757
758
            log.debug("%s: opening model NETCDF4 '%s'..." %
                      (target_config['name'], model_file))
            cdf_handle = Dataset(model_file, "r", format="NETCDF4")
596da00d   Goutte   Add more exceptio...
759

d1c44c51   Goutte   Enable Earth
760
            # log.debug(cdf_handle.variables.keys())
596da00d   Goutte   Add more exceptio...
761

58bfe281   Goutte   Handle start and ...
762
            times = cdf_handle.variables['Time']  # YYYY DOY HH MM SS .ms
d1c44c51   Goutte   Enable Earth
763
764
765
766
767
768
769
            data_v = _read_var(cdf_handle, nc_keys, 'vtot')
            data_b = _read_var(cdf_handle, nc_keys, 'magn')
            data_t = _read_var(cdf_handle, nc_keys, 'temp')
            data_n = _read_var(cdf_handle, nc_keys, 'dens')
            data_p = _read_var(cdf_handle, nc_keys, 'pdyn')
            data_a = _read_var(cdf_handle, nc_keys, 'atse')

d1c44c51   Goutte   Enable Earth
770
771
772
773
774
775
776
777
778
            # Usually:
            # Time, StartTime, StopTime, V, B, N, T, Delta_angle, P_dyn
            # Earth:
            # Time, BartelsNumber, ImfID, SwID, ImfPoints,
            # SwPoints, B_M_av, B_Vec_av, B_Theta_av,
            # B_Phi_av, B, T, N, V, Vlat, Vlon,
            # Alpha, RamP, E, Beta, Ma, Kp, R, DST,
            # AE, Flux, Flag, F10_Index, StartTime, StopTime

81cb3ba6   Goutte   Fix an issue with...
779
780
781
782
783
784
            # Don't ignore, but instead, compute a mean ?
            # Look for discretisation ? temporal drizzling ? 1D drizzling ?
            # The easy linear mean feels a bit rough too. Running mean too.
            # FIXME
            ignored_count = 0

58bfe281   Goutte   Handle start and ...
785
786
            log.debug("%s: aggregating data from '%s'..." %
                      (target_config['name'], model_file))
d1c44c51   Goutte   Enable Earth
787
788
789
            for ltime, datum_v, datum_b, datum_t, datum_n, datum_p, datum_a \
                    in zip(times, data_v, data_b, data_t, data_n, data_p, data_a):

58bfe281   Goutte   Handle start and ...
790
                try:
d1c44c51   Goutte   Enable Earth
791
792
793
794
                    dtime = datetime_from_list(ltime)
                except Exception:
                    log.error("Failed to parse time from %s." % ltime)
                    raise
596da00d   Goutte   Add more exceptio...
795

81cb3ba6   Goutte   Fix an issue with...
796
797
798
                if not (s0 <= dtime <= s1):
                    continue  # Cull what's out of the interval

34e94ae3   hitier   Code reformating
799
                droundtime = round_time(dtime, 60 * 60)
81cb3ba6   Goutte   Fix an issue with...
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
                dkey = droundtime.strftime(precision)

                x_hee = None
                y_hee = None
                if dkey in orbit_data:
                    x_hee = orbit_data[dkey][0] / ASTRONOMICAL_UNIT_IN_KM
                    y_hee = orbit_data[dkey][1] / ASTRONOMICAL_UNIT_IN_KM

                # First exception: V may be a vector instead of a scalar
                if hasattr(datum_v, '__len__'):
                    vrad = datum_v[0]
                    vtan = datum_v[1]
                    vtot = sqrt(vrad * vrad + vtan * vtan)
                else:  # eg: Earth
                    vrad = None
                    vtan = None
                    vtot = datum_v

                # Second exception: Earth is always at (1, 0)
                if target_config['slug'] == 'earth':
                    x_hee = 1
                    y_hee = 0

                # Third exception: B is a Vector3 or a Vector2 for Earth
                if target_config['slug'] == 'earth':
                    if model['slug'] == 'omni_hour_all':  # Vector3
                        datum_b = datum_b[0]
                    # if model['slug'] == 'ace_swepam_real':  # Vector2
                    #     datum_b = datum_b[0]
56028f6d   Myriam Bouchemit   bug for dyn press...
829
                    if model['slug'] == 'ace_swepam_real_1h':
206d8d14   Goutte   Clean up.
830
                        datum_p = datum_n * vtot * vtot * 1.6726e-6
81cb3ba6   Goutte   Fix an issue with...
831
832
833
                    if vtot is None or isnan(vtot):
                        continue

7176ae9b   Goutte   Fix Myriam's cont...
834
835
836
837
838
                # Fourth exception: Earth temp is in K, not eV
                # @nandre: à vérifier ! Where is la constante de Boltzmann ?
                if target_config['slug'] == 'earth' and datum_t:
                    datum_t = datum_t / 11605.0

81cb3ba6   Goutte   Fix an issue with...
839
840
                # Keep adding exceptions here until you can't or become mad

2be8e8a0   Goutte   Ignore bad data.
841
                # Ignore bad data
ff11e121   Goutte   Don't ignore bad ...
842
843
                # if numpy.isnan(datum_t):
                #     continue
2be8e8a0   Goutte   Ignore bad data.
844

81cb3ba6   Goutte   Fix an issue with...
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
                # Crude/Bad drizzling condition : first found datum in the hour
                # gets to set the data. Improve this.
                if dkey not in all_data:
                    # /!. The Set value MUST be in the same order as PROPERTIES
                    all_data[dkey] = (
                        dtime.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
                        vrad, vtan, vtot,
                        datum_b, datum_t, datum_p, datum_n, datum_a,
                        x_hee, y_hee
                    )
                else:
                    ignored_count += 1

            # Improve this loop so as to remove this stinky debug log
            if ignored_count > 0:
                log.debug("    Ignored %d datum(s) during ~\"drizzling\"."
                          % ignored_count)

58bfe281   Goutte   Handle start and ...
863
            cdf_handle.close()
8644387c   Goutte   Use real data.
864

180d7d97   Goutte   Refactor heavily.
865
866
867
    return all_data


297a7dfc   Goutte   Add support for i...
868
def generate_csv_contents(target_slug, input_slug, started_at, stopped_at):
180d7d97   Goutte   Refactor heavily.
869
870
    target_config = get_target_config(target_slug)
    log.debug("Crunching CSV contents for '%s'..." % target_config['name'])
b33d5f62   hitier   Change StringIO i...
871
    si = StringIO()
180d7d97   Goutte   Refactor heavily.
872
873
874
    cw = csv_writer(si)
    cw.writerow(PROPERTIES)

297a7dfc   Goutte   Add support for i...
875
876
877
878
    all_data = get_data_for_target(
        target_config=target_config, input_slug=input_slug,
        started_at=started_at, stopped_at=stopped_at
    )
180d7d97   Goutte   Refactor heavily.
879
880

    log.debug("Writing and sorting CSV for '%s'..." % target_config['slug'])
8644387c   Goutte   Use real data.
881
882
    for dkey in sorted(all_data):
        cw.writerow(all_data[dkey])
2d2af24b   Goutte   Add a basic orbit...
883

180d7d97   Goutte   Refactor heavily.
884
    log.info("Generated CSV contents for '%s'." % target_config['slug'])
2d2af24b   Goutte   Add a basic orbit...
885
886
    return si.getvalue()

8644387c   Goutte   Use real data.
887

dd6ae65b   hitier   Add new csvgenera...
888
889
def generate_csv_contents_spz(target_slug, input_slug, started_at, stopped_at):
    target_config = get_target_config(target_slug)
fc7cbbee   hitier   Move orbit parame...
890
891
    plasma_dict = target_config['models'][input_slug][0]['parameters']
    orbit_dict = target_config['orbit']['models'][0]['parameters']
ec483087   hitier   Fix dict merging
892
    parameters_dict = {**plasma_dict, **orbit_dict}
dd6ae65b   hitier   Add new csvgenera...
893

2942626d   hitier   Tweak log messages
894
    log.info(f"Aggregating dataframes speazy parameters for '{input_slug}' to '{target_slug}'" )
dd6ae65b   hitier   Add new csvgenera...
895
    list_df = []
fc7cbbee   hitier   Move orbit parame...
896
    for _name, _id in parameters_dict.items():
2942626d   hitier   Tweak log messages
897
        log.debug(f"Getting parameter id '{_id}' for '{_name}'")
dd6ae65b   hitier   Add new csvgenera...
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
        _df = amda.get_data(_id, started_at, stopped_at).to_dataframe()
        if _name == 'xy_v':
            _df = _df.rename(columns={_df.columns[0]: 'vrad', _df.columns[1]: 'vtan'})
        elif _name == 'xy_hee':
            _df = _df.drop(_df.columns[2], axis=1)
            _df = _df.rename(columns={_df.columns[0]: 'xhee', _df.columns[1]: 'yhee'})
        else:
            _df = _df.rename(columns={_df.columns[0]: _name})

        # _df = _df[~_df.index.duplicated()]

        # resample to frequency, for later concatenation
        _df = _df.resample('1H').mean()

        # _df = _df.loc[_df.first_valid_index():_df.last_valid_index()]

        list_df.append(_df)

    from math import sqrt
    final_df = pd.concat(list_df, axis=1)
    # Is ther a vtot param ? else build it
    if 'vtot' not in final_df.columns:
        final_df['vtot'] = final_df.apply(lambda x: sqrt(x.vtan * x.vtan + x.vrad * x.vrad), axis=1)

    cols_ordered = ['vrad', 'vtan', 'vtot', 'btan', 'brad', 'temp', 'pdyn', 'dens', 'atse', 'xhee', 'yhee']
    final_df = final_df[cols_ordered]
    final_df.index.name = 'time'
    return final_df.to_csv(date_format='%Y-%m-%dT%H:%M:%S+00:00', float_format="%.5f",
                           header=True,
                           sep=",")


de97d643   Goutte   Fix more bugs.
930
931
def generate_csv_file_if_needed(target_slug, input_slug,
                                started_at, stopped_at):
297a7dfc   Goutte   Add support for i...
932
933
934
    filename = "%s_%s_%s_%s.csv" % (target_slug, input_slug,
                                    started_at.strftime(FILE_DATE_FMT),
                                    stopped_at.strftime(FILE_DATE_FMT))
e18701b6   Goutte   Cache clear (remo...
935
    local_csv_file = join(CACHE_DIR, filename)
80352490   Goutte   Multi model suppo...
936
937
938

    generate = True
    if isfile(local_csv_file):
297a7dfc   Goutte   Add support for i...
939
        # It needs to have more than one line to not be empty (headers)
80352490   Goutte   Multi model suppo...
940
941
942
943
944
945
946
947
        with open(local_csv_file) as f:
            cnt = 0
            for _ in f:
                cnt += 1
                if cnt > 1:
                    generate = False
                    break

8eb47b5f   hitier   Set all mercury a...
948
949
    # temporary switch while migrating each target to spz
    if target_slug in ['mercury', 'mars']:
dd6ae65b   hitier   Add new csvgenera...
950
951
952
953
        csv_generator = generate_csv_contents_spz
    else:
        csv_generator = generate_csv_contents

80352490   Goutte   Multi model suppo...
954
    if generate:
c0df94bc   Goutte   Adding more logs.
955
956
957
        log.info("Generating CSV '%s'..." % local_csv_file)
        try:
            with open(local_csv_file, mode="w+") as f:
dd6ae65b   hitier   Add new csvgenera...
958
                f.write(csv_generator(
de97d643   Goutte   Fix more bugs.
959
960
961
                    target_slug=target_slug, input_slug=input_slug,
                    started_at=started_at, stopped_at=stopped_at
                ))
c0df94bc   Goutte   Adding more logs.
962
963
            log.info("Generation of '%s' done." % filename)
        except Exception as e:
d1c44c51   Goutte   Enable Earth
964
965
966
            from sys import exc_info
            from traceback import extract_tb
            exc_type, exc_value, exc_traceback = exc_info()
dc0be992   Goutte   Support having no...
967
            log.error(e)
d1c44c51   Goutte   Enable Earth
968
969
            for trace in extract_tb(exc_traceback):
                log.error(trace)
5ede388f   Goutte   Make sure failed ...
970
            if isfile(local_csv_file):
d4b3d381   hitier   Tiny python3 synt...
971
                log.warning("Removing failed CSV '%s'..." % local_csv_file)
5ede388f   Goutte   Make sure failed ...
972
                removefile(local_csv_file)
9bfa6c42   Goutte   More bug hunting.
973
            abort(500, "Failed creating CSV '%s' : %s" % (filename, e))
c0df94bc   Goutte   Adding more logs.
974
975


e18701b6   Goutte   Cache clear (remo...
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
def remove_all_files(in_directory):
    """
    Will throw if something horrible happens.
    Does not remove recursively (could be done with os.walk if needed).
    Does not remove directories either.
    :param in_directory: absolute path to directory
    :return:
    """
    import os

    if not os.path.isdir(in_directory):
        raise ValueError("No directory to clean at '%s'.")

    removed_files = []
    for file_name in os.listdir(in_directory):
        file_path = os.path.join(in_directory, file_name)
        if os.path.isfile(file_path):
            os.remove(file_path)
            removed_files.append(file_path)

    return removed_files


28bb4b28   Goutte   API for the cache...
999
1000
def remove_files_created_before(date, in_directory):
    """
077980eb   Goutte   Improve availabil...
1001
1002
1003
    Will throw if something horrible happens.
    Does not remove recursively (could be done with os.walk if needed).
    Does not remove directories either.
28bb4b28   Goutte   API for the cache...
1004
    :param date: datetime object
077980eb   Goutte   Improve availabil...
1005
    :param in_directory: absolute path to directory
28bb4b28   Goutte   API for the cache...
1006
1007
1008
1009
1010
1011
1012
    :return:
    """
    import os
    import time

    secs = time.mktime(date.timetuple())

077980eb   Goutte   Improve availabil...
1013
1014
    if not os.path.isdir(in_directory):
        raise ValueError("No directory to clean at '%s'.")
28bb4b28   Goutte   API for the cache...
1015
1016
1017
1018

    removed_files = []
    for file_name in os.listdir(in_directory):
        file_path = os.path.join(in_directory, file_name)
077980eb   Goutte   Improve availabil...
1019
1020
1021
1022
1023
        if os.path.isfile(file_path):
            t = os.stat(file_path)
            if t.st_ctime < secs:
                os.remove(file_path)
                removed_files.append(file_path)
28bb4b28   Goutte   API for the cache...
1024
1025
1026
1027

    return removed_files


297a7dfc   Goutte   Add support for i...
1028
1029
def get_input_slug_from_query(inp=None):
    if inp is None:
b662fcc3   hitier   Change input name...
1030
        input_slug = request.args.get('input_slug', config['defaults']['input_slug'])
297a7dfc   Goutte   Add support for i...
1031
1032
    else:
        input_slug = inp
de97d643   Goutte   Fix more bugs.
1033
    if input_slug not in [i['slug'] for i in config['inputs']]:
b662fcc3   hitier   Change input name...
1034
        input_slug = config['defaults']['input_slug']  # be tolerant instead of yelling loudly
297a7dfc   Goutte   Add support for i...
1035
1036
1037
    return input_slug


284f4688   Goutte   Continue layers i...
1038
1039
1040
def get_interval_from_query():
    """
    Get the interval from the query, or from defaults.
3309d7c3   Goutte   Improve the date ...
1041
    Returns ISO date strings.
284f4688   Goutte   Continue layers i...
1042
    """
65b27161   hitier   Change default ti...
1043
    before = relativedelta(months=1)
284f4688   Goutte   Continue layers i...
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
    after = relativedelta(months=1)
    today = datetime.datetime.now().replace(hour=0, minute=0, second=0)
    started_at = today - before
    stopped_at = today + after
    default_started_at = started_at.strftime(FILE_DATE_FMT)
    default_stopped_at = stopped_at.strftime(FILE_DATE_FMT)

    started_at = request.args.get('started_at', default_started_at)
    stopped_at = request.args.get('stopped_at', default_stopped_at)

3309d7c3   Goutte   Improve the date ...
1054
1055
1056
1057
1058
    d_started_at = dateparser.isoparse(started_at)
    d_stopped_at = dateparser.isoparse(stopped_at)
    started_at = d_started_at.strftime(FILE_DATE_FMT)
    stopped_at = d_stopped_at.strftime(FILE_DATE_FMT)

284f4688   Goutte   Continue layers i...
1059
1060
1061
    return started_at, stopped_at


1185f353   Goutte   Fix the CME Catal...
1062
def get_catalog_layers(input_slug, target_slug, started_at, stopped_at):
0332f168   Goutte   Initial support f...
1063
    """
284f4688   Goutte   Continue layers i...
1064
1065
    In the JSON file we have "columns" and "data".
    Of course, each JSON file has its own columns, with different conventions.
0332f168   Goutte   Initial support f...
1066
1067
1068
1069
1070
    
    :param input_slug: 
    :param target_slug: 
    :return: 
    """
0332f168   Goutte   Initial support f...
1071
    import json
2204c3f7   Goutte   Improve layers co...
1072
1073

    def _get_index_of_key(_data, _key):
2204c3f7   Goutte   Improve layers co...
1074
1075
1076
1077
1078
1079
1080
        try:
            index = _data['columns'].index(_key)
        except ValueError:
            log.error("Key %s not found in columns of %s" % (_key, _data))
            raise
        return index

1185f353   Goutte   Fix the CME Catal...
1081
1082
1083
1084
1085
1086
1087
1088
1089
    try:
        started_at = datetime.datetime.strptime(started_at, FILE_DATE_FMT)
    except:
        abort(400, "Invalid started_at parameter : '%s'." % started_at)
    try:
        stopped_at = datetime.datetime.strptime(stopped_at, FILE_DATE_FMT)
    except:
        abort(400, "Invalid stopped_at parameter : '%s'." % stopped_at)

0332f168   Goutte   Initial support f...
1090
    catalog_layers = {}
1ab47144   Goutte   Add new menus, up...
1091
    for config_layer in config['layers']['catalogs']:
0332f168   Goutte   Initial support f...
1092
1093
        if 'data' not in config_layer:
            continue
0332f168   Goutte   Initial support f...
1094
1095
1096
1097
1098
1099
        catalog_layers[config_layer['slug']] = []
        for cl_datum in config_layer['data']:
            if input_slug not in cl_datum:
                continue
            if target_slug not in cl_datum[input_slug]:
                continue
b03d5eb1   Goutte   Add the last CME ...
1100
1101
1102
1103
1104
            if cl_datum[input_slug][target_slug] is None:
                # We used ~ in the config, there are no constraints
                constraints = []
            else:
                constraints = cl_datum[input_slug][target_slug]['constraints']
1185f353   Goutte   Fix the CME Catal...
1105

0332f168   Goutte   Initial support f...
1106
1107
            with open(get_path("../data/catalog/%s" % cl_datum['file'])) as f:
                json_data = json.load(f)
284f4688   Goutte   Continue layers i...
1108
1109
1110
                if 'start' not in cl_datum:
                    log.error("Invalid configuration: 'start' is missing.")
                    continue  # skip this
1185f353   Goutte   Fix the CME Catal...
1111
                if 'format' not in cl_datum:
54bb1311   Goutte   Bring back the CM...
1112
1113
1114
                    cl_datum['format'] = CME_DATE_FMT
                    # log.error("Invalid configuration: 'format' is missing.")
                    # continue  # skip this
284f4688   Goutte   Continue layers i...
1115
1116
1117
1118
1119
                start_index = _get_index_of_key(json_data, cl_datum['start'])
                if 'stop' not in cl_datum:
                    stop_index = start_index
                else:
                    stop_index = _get_index_of_key(json_data, cl_datum['stop'])
2204c3f7   Goutte   Improve layers co...
1120

0332f168   Goutte   Initial support f...
1121
                for json_datum in json_data['data']:
2204c3f7   Goutte   Improve layers co...
1122
                    validates_any_constraint = False
b03d5eb1   Goutte   Add the last CME ...
1123
1124
                    if 0 == len(constraints):
                        validates_any_constraint = True
2204c3f7   Goutte   Improve layers co...
1125
1126
                    for constraint in constraints:
                        validates_constraint = True
d4b3d381   hitier   Tiny python3 synt...
1127
                        for key, possible_values in iter(constraint.items()):
2204c3f7   Goutte   Improve layers co...
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
                            actual_value = json_datum[_get_index_of_key(
                                json_data, key
                            )]
                            if actual_value not in possible_values:
                                validates_constraint = False
                                break
                        if validates_constraint:
                            validates_any_constraint = True
                            break
                    if not validates_any_constraint:
0332f168   Goutte   Initial support f...
1138
                        continue
284f4688   Goutte   Continue layers i...
1139
1140
                    start_time = json_datum[start_index]
                    stop_time = json_datum[stop_index]
1185f353   Goutte   Fix the CME Catal...
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151

                    start_time = datetime.datetime.strptime(
                        start_time, cl_datum['format']
                    )
                    stop_time = datetime.datetime.strptime(
                        stop_time, cl_datum['format']
                    )

                    if start_time < started_at:
                        continue

0332f168   Goutte   Initial support f...
1152
                    catalog_layers[config_layer['slug']].append({
1185f353   Goutte   Fix the CME Catal...
1153
                        'start': start_time.strftime(MOMENT_DATE_FMT),
34e94ae3   hitier   Code reformating
1154
                        'stop': stop_time.strftime(MOMENT_DATE_FMT),
0332f168   Goutte   Initial support f...
1155
                    })
0332f168   Goutte   Initial support f...
1156
1157
1158
1159

    return catalog_layers


077980eb   Goutte   Improve availabil...
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
def get_hit_counter():
    hit_count_path = get_path("../VISITS")

    if isfile(hit_count_path):
        hit_count = int(open(hit_count_path).read())
    else:
        hit_count = 1

    return hit_count


a4a9ef03   Goutte   Cache generated C...
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
def increment_hit_counter():
    hit_count_path = get_path("../VISITS")

    if isfile(hit_count_path):
        hit_count = int(open(hit_count_path).read())
        hit_count += 1
    else:
        hit_count = 1

    hit_counter_file = open(hit_count_path, 'w')
    hit_counter_file.write(str(hit_count))
    hit_counter_file.close()

    return hit_count


4aaf6874   Goutte   Try fixing the ge...
1187
1188
def update_spacepy():
    """
11d86851   Goutte   Add support for s...
1189
    Importing pycdf will fail if the toolbox is not up to date.
4aaf6874   Goutte   Try fixing the ge...
1190
    """
4aaf6874   Goutte   Try fixing the ge...
1191
1192
1193
1194
1195
1196
1197
1198
1199
    try:
        log.info("Updating spacepy's toolbox…")
        import spacepy.toolbox

        spacepy.toolbox.update()
    except Exception as e:
        log.error("Failed to update spacepy : %s." % e)


077980eb   Goutte   Improve availabil...
1200
1201
1202
tpl_global_vars['visits'] = get_hit_counter()


a4a9ef03   Goutte   Cache generated C...
1203
1204
1205
# ROUTING #####################################################################

@app.route('/favicon.ico')
bde97e4d   Goutte   Add more changes ...
1206
def favicon():  # we want it served from the root, not from static/
a4a9ef03   Goutte   Cache generated C...
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
    return send_from_directory(
        join(app.root_path, 'static', 'img'),
        'favicon.ico', mimetype='image/vnd.microsoft.icon'
    )


@app.route("/")
@app.route("/home.html")
@app.route("/index.html")
def home():
077980eb   Goutte   Improve availabil...
1217
    increment_hit_counter()
bde97e4d   Goutte   Add more changes ...
1218
    parameters = PARAMETERS.values()
d4b3d381   hitier   Tiny python3 synt...
1219
    parameters = sorted(parameters, key=lambda x: x['position'])
297a7dfc   Goutte   Add support for i...
1220
    input_slug = get_input_slug_from_query()
0332f168   Goutte   Initial support f...
1221
    targets = [t for t in config['targets'] if not t['locked']]
284f4688   Goutte   Continue layers i...
1222
    started_at, stopped_at = get_interval_from_query()
0332f168   Goutte   Initial support f...
1223
1224
    for i, target in enumerate(targets):
        targets[i]['catalog_layers'] = get_catalog_layers(
1185f353   Goutte   Fix the CME Catal...
1225
            input_slug, target['slug'], started_at, stopped_at
0332f168   Goutte   Initial support f...
1226
        )
a4a9ef03   Goutte   Cache generated C...
1227
    return render_view('home.html.jinja2', {
0332f168   Goutte   Initial support f...
1228
1229
        # 'targets': config['targets'],
        'targets': targets,
bde97e4d   Goutte   Add more changes ...
1230
        'parameters': parameters,
297a7dfc   Goutte   Add support for i...
1231
        'input_slug': input_slug,
284f4688   Goutte   Continue layers i...
1232
1233
        'started_at': started_at,
        'stopped_at': stopped_at,
a4a9ef03   Goutte   Cache generated C...
1234
        'planets': [s for s in config['targets'] if s['type'] == 'planet'],
34e94ae3   hitier   Code reformating
1235
1236
1237
        'probes': [s for s in config['targets'] if s['type'] == 'probe'],
        'comets': [s for s in config['targets'] if s['type'] == 'comet'],
        'visits': get_hit_counter(),
a4a9ef03   Goutte   Cache generated C...
1238
1239
1240
    })


64a671cd   Goutte   Add an About page.
1241
1242
@app.route("/about.html")
def about():
36961d6f   Goutte   Slightly improve ...
1243
    import uuid
64a671cd   Goutte   Add an About page.
1244
1245
    increment_hit_counter()
    return render_view('about.html.jinja2', {
36961d6f   Goutte   Slightly improve ...
1246
1247
1248
        'authors_emails': [a['mail'] for a in config['authors']],
        'uuid4': str(uuid.uuid4())[0:3],
        'visits': get_hit_counter(),
64a671cd   Goutte   Add an About page.
1249
1250
1251
    })


6288347a   Goutte   Add the HELP page...
1252
1253
1254
1255
1256
1257
1258
@app.route("/help.html")
def help():
    return render_view('help.html.jinja2', {
        'visits': get_hit_counter(),
    })


297a7dfc   Goutte   Add support for i...
1259
1260
@app.route("/<target>_<inp>_<started_at>_<stopped_at>.csv")
def download_target_csv(target, inp, started_at, stopped_at):
a4a9ef03   Goutte   Cache generated C...
1261
1262
1263
1264
    """
    Grab data and orbit data for the specified `target`,
    rearrange it and return it as a CSV file.
    `started_at` and `stopped_at` should be UTC.
b662fcc3   hitier   Change input name...
1265
    `inp` is the input slug, omni or sa or sb.
a4a9ef03   Goutte   Cache generated C...
1266
    """
180d7d97   Goutte   Refactor heavily.
1267
    check_target_config(target)
a4a9ef03   Goutte   Cache generated C...
1268
    try:
c0df94bc   Goutte   Adding more logs.
1269
        started_at = datetime.datetime.strptime(started_at, FILE_DATE_FMT)
a4a9ef03   Goutte   Cache generated C...
1270
1271
1272
    except:
        abort(400, "Invalid started_at parameter : '%s'." % started_at)
    try:
c0df94bc   Goutte   Adding more logs.
1273
        stopped_at = datetime.datetime.strptime(stopped_at, FILE_DATE_FMT)
a4a9ef03   Goutte   Cache generated C...
1274
1275
    except:
        abort(400, "Invalid stopped_at parameter : '%s'." % stopped_at)
297a7dfc   Goutte   Add support for i...
1276
    input_slug = get_input_slug_from_query(inp=inp)
a4a9ef03   Goutte   Cache generated C...
1277

297a7dfc   Goutte   Add support for i...
1278
1279
1280
    filename = "%s_%s_%s_%s.csv" % (target, input_slug,
                                    started_at.strftime(FILE_DATE_FMT),
                                    stopped_at.strftime(FILE_DATE_FMT))
e18701b6   Goutte   Cache clear (remo...
1281
    local_csv_file = join(CACHE_DIR, filename)
297a7dfc   Goutte   Add support for i...
1282
1283
1284
1285
    generate_csv_file_if_needed(
        target_slug=target, input_slug=input_slug,
        started_at=started_at, stopped_at=stopped_at
    )
a4a9ef03   Goutte   Cache generated C...
1286
1287
1288
    if not isfile(local_csv_file):
        abort(500, "Could not cache CSV file at '%s'." % local_csv_file)

e18701b6   Goutte   Cache clear (remo...
1289
    return send_from_directory(CACHE_DIR, filename)
a4a9ef03   Goutte   Cache generated C...
1290
1291


297a7dfc   Goutte   Add support for i...
1292
1293
@app.route("/<targets>_<inp>_<started_at>_<stopped_at>.tar.gz")
def download_targets_tarball(targets, inp, started_at, stopped_at):
b2837a08   Goutte   Add three retries...
1294
    """
bc18b96c   Goutte   Implement first (...
1295
1296
1297
    Grab data and orbit data for each of the specified `targets`,
    in their own CSV file, and make a tarball of them.
    `started_at` and `stopped_at` should be UTC strings.
b2837a08   Goutte   Add three retries...
1298

ea6c8d5d   Goutte   Add interval cons...
1299
1300
    Note: we do not use this route anymore, but let's keep it shelved for now.

2fedd73b   Goutte   Initial implement...
1301
    targets: string list of targets' slugs, separated by `-`.
b2837a08   Goutte   Add three retries...
1302
    """
2fedd73b   Goutte   Initial implement...
1303
    separator = '-'
0511eed7   Goutte   Tarball generatio...
1304
    targets = targets.split(separator)
d4b3d381   hitier   Tiny python3 synt...
1305
    targets.sorty()
2fedd73b   Goutte   Initial implement...
1306
1307
    targets_configs = []
    for target in targets:
b2837a08   Goutte   Add three retries...
1308
1309
        if not target:
            abort(400, "Invalid targets format : `%s`." % targets)
180d7d97   Goutte   Refactor heavily.
1310
        targets_configs.append(get_target_config(target))
2fedd73b   Goutte   Initial implement...
1311
    if 0 == len(targets_configs):
b2837a08   Goutte   Add three retries...
1312
1313
        abort(400, "No valid targets specified. What are you doing?")

b2837a08   Goutte   Add three retries...
1314
    try:
ea6c8d5d   Goutte   Add interval cons...
1315
        started_at = datetime.datetime.strptime(started_at, FILE_DATE_FMT)
b2837a08   Goutte   Add three retries...
1316
1317
1318
    except:
        abort(400, "Invalid started_at parameter : '%s'." % started_at)
    try:
ea6c8d5d   Goutte   Add interval cons...
1319
        stopped_at = datetime.datetime.strptime(stopped_at, FILE_DATE_FMT)
b2837a08   Goutte   Add three retries...
1320
1321
    except:
        abort(400, "Invalid stopped_at parameter : '%s'." % stopped_at)
ea6c8d5d   Goutte   Add interval cons...
1322
1323
    sta = started_at.strftime(FILE_DATE_FMT)
    sto = stopped_at.strftime(FILE_DATE_FMT)
b2837a08   Goutte   Add three retries...
1324

297a7dfc   Goutte   Add support for i...
1325
1326
1327
1328
1329
    input_slug = get_input_slug_from_query(inp=inp)

    gzip_filename = "%s_%s_%s_%s.tar.gz" % (
        separator.join(targets), input_slug, sta, sto
    )
e18701b6   Goutte   Cache clear (remo...
1330
    local_gzip_file = join(CACHE_DIR, gzip_filename)
2fedd73b   Goutte   Initial implement...
1331
1332

    if not isfile(local_gzip_file):
0511eed7   Goutte   Tarball generatio...
1333
        log.debug("Creating the CSV files for the tarball...")
2fedd73b   Goutte   Initial implement...
1334
        for target_config in targets_configs:
297a7dfc   Goutte   Add support for i...
1335
1336
1337
            filename = "%s_%s_%s_%s.csv" % (
                target_config['slug'], input_slug, sta, sto
            )
e18701b6   Goutte   Cache clear (remo...
1338
            local_csv_file = join(CACHE_DIR, filename)
2fedd73b   Goutte   Initial implement...
1339
1340
            if not isfile(local_csv_file):
                with open(local_csv_file, mode="w+") as f:
297a7dfc   Goutte   Add support for i...
1341
1342
1343
1344
1345
1346
                    f.write(generate_csv_contents(
                        target_slug=target_config['slug'],
                        started_at=started_at,
                        stopped_at=stopped_at,
                        input_slug=input_slug
                    ))
2fedd73b   Goutte   Initial implement...
1347

0511eed7   Goutte   Tarball generatio...
1348
        log.debug("Creating the tarball '%s'..." % local_gzip_file)
2fedd73b   Goutte   Initial implement...
1349
1350
        with tarfile.open(local_gzip_file, "w:gz") as tar:
            for target_config in targets_configs:
297a7dfc   Goutte   Add support for i...
1351
1352
1353
                filename = "%s_%s_%s_%s.csv" % (
                    target_config['slug'], input_slug, sta, sto
                )
e18701b6   Goutte   Cache clear (remo...
1354
                local_csv_file = join(CACHE_DIR, filename)
2fedd73b   Goutte   Initial implement...
1355
1356
1357
                tar.add(local_csv_file, arcname=filename)

    if not isfile(local_gzip_file):
0511eed7   Goutte   Tarball generatio...
1358
        abort(500, "No tarball to serve. Looked at '%s'." % local_gzip_file)
2fedd73b   Goutte   Initial implement...
1359

e18701b6   Goutte   Cache clear (remo...
1360
    return send_from_directory(CACHE_DIR, gzip_filename)
b2837a08   Goutte   Add three retries...
1361

28bb4b28   Goutte   API for the cache...
1362

297a7dfc   Goutte   Add support for i...
1363
1364
@app.route("/<targets>_<inp>_<params>_<started_at>_<stopped_at>.nc")
def download_targets_netcdf(targets, inp, params, started_at, stopped_at):
bc18b96c   Goutte   Implement first (...
1365
    """
4aaf6874   Goutte   Try fixing the ge...
1366
1367
    NOTE : This is not used anymore.
    
bc18b96c   Goutte   Implement first (...
1368
    Grab data and orbit data for the specified `target`,
aa7247d6   Goutte   Generate a CDF fi...
1369
    rearrange it and return it as a NetCDF file.
e18701b6   Goutte   Cache clear (remo...
1370
    `started_at` and `stopped_at` are expected to be UTC.
bc18b96c   Goutte   Implement first (...
1371
1372
1373
1374

    targets: string list of targets' slugs, separated by `-`.
    params: string list of targets' parameters, separated by `-`.
    """
e18701b6   Goutte   Cache clear (remo...
1375
    separator = '-'  # /!\ this char should never be in target's slugs
bc18b96c   Goutte   Implement first (...
1376
    targets = targets.split(separator)
d4b3d381   hitier   Tiny python3 synt...
1377
    targets.sorty()
bc18b96c   Goutte   Implement first (...
1378
1379
1380
1381
1382
1383
1384
    targets_configs = []
    for target in targets:
        if not target:
            abort(400, "Invalid targets format : `%s`." % targets)
        targets_configs.append(get_target_config(target))
    if 0 == len(targets_configs):
        abort(400, "No valid targets specified. What are you doing?")
4aaf6874   Goutte   Try fixing the ge...
1385

bc18b96c   Goutte   Implement first (...
1386
    params = params.split(separator)
d4b3d381   hitier   Tiny python3 synt...
1387
    params.sorty()
bc18b96c   Goutte   Implement first (...
1388
1389
1390
1391
1392
    if 0 == len(params):
        abort(400, "No valid parameters specified. What are you doing?")
    if not is_list_in_list(params, PARAMETERS.keys()):
        abort(400, "Some parameters are not recognized in '%s'." % str(params))

57493104   Goutte   Add the time to t...
1393
    date_fmt = FILE_DATE_FMT
bc18b96c   Goutte   Implement first (...
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
    try:
        started_at = datetime.datetime.strptime(started_at, date_fmt)
    except:
        abort(400, "Invalid started_at parameter : '%s'." % started_at)
    try:
        stopped_at = datetime.datetime.strptime(stopped_at, date_fmt)
    except:
        abort(400, "Invalid stopped_at parameter : '%s'." % stopped_at)
    sta = started_at.strftime(date_fmt)
    sto = stopped_at.strftime(date_fmt)

297a7dfc   Goutte   Add support for i...
1405
1406
1407
1408
1409
    input_slug = get_input_slug_from_query(inp=inp)

    nc_filename = "%s_%s_%s_%s_%s.nc" % (
        separator.join(targets), separator.join(params), input_slug, sta, sto
    )
e18701b6   Goutte   Cache clear (remo...
1410
    nc_path = join(CACHE_DIR, nc_filename)
bc18b96c   Goutte   Implement first (...
1411
1412
1413
1414
1415

    if not isfile(nc_path):
        log.debug("Creating the NetCDF file '%s'..." % nc_filename)
        nc_handle = Dataset(nc_path, "w", format="NETCDF4")
        try:
ea6c8d5d   Goutte   Add interval cons...
1416
            nc_handle.description = "Model and orbit data for targets"  # todo
bc18b96c   Goutte   Implement first (...
1417
            nc_handle.history = "Created " + time.ctime(time.time())
ea6c8d5d   Goutte   Add interval cons...
1418
            nc_handle.source = "Heliopropa (CDDP)"
bc18b96c   Goutte   Implement first (...
1419
1420
1421
1422
1423
            available_params = list(PROPERTIES)
            for target in targets_configs:
                target_slug = target['slug']
                log.debug("Adding group '%s' to the NetCDF..." % target_slug)
                nc_group = nc_handle.createGroup(target_slug)
297a7dfc   Goutte   Add support for i...
1424
1425
1426
1427
                data = get_data_for_target(
                    target_config=target, input_slug=input_slug,
                    started_at=started_at, stopped_at=stopped_at
                )
bc18b96c   Goutte   Implement first (...
1428
                dkeys = sorted(data)
34e94ae3   hitier   Code reformating
1429
                dimension = 'dim_' + target_slug
ceeb2f4a   Goutte   Add the target co...
1430
                nc_handle.createDimension(dimension, len(dkeys))
57493104   Goutte   Add the time to t...
1431
1432

                # TIME #
ceeb2f4a   Goutte   Add the target co...
1433
                nc_time = nc_group.createVariable('time', 'i8', (dimension,))
57493104   Goutte   Add the time to t...
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
                nc_time.units = "hours since 1970-01-01 00:00:00"
                nc_time.calendar = "standard"
                times = []
                for dkey in dkeys:
                    time_as_string = data[dkey][0][:-6]  # remove +00:00 tail
                    date = datetime.datetime.strptime(time_as_string, date_fmt)
                    times.append(date2num(
                        date, units=nc_time.units, calendar=nc_time.calendar
                    ))
                nc_time[:] = times

                # SELECTED PARAMETERS #
bc18b96c   Goutte   Implement first (...
1446
1447
1448
1449
                nc_vars = []
                indices = []
                for param in params:
                    indices.append(available_params.index(param))
ceeb2f4a   Goutte   Add the target co...
1450
                    nc_var = nc_group.createVariable(param, 'f8', (dimension,))
5a6d4498   Goutte   Add a title to ea...
1451
                    nc_var.units = PARAMETERS[param]['units']
bc18b96c   Goutte   Implement first (...
1452
1453
1454
1455
1456
1457
1458
1459
                    nc_vars.append(nc_var)
                for i, nc_var in enumerate(nc_vars):
                    index = indices[i]
                    values = []
                    for dkey in dkeys:
                        dval = data[dkey]
                        values.append(dval[index])
                    nc_var[:] = values
ceeb2f4a   Goutte   Add the target co...
1460
1461

                # ORBIT #
6491a1f1   Goutte   Fix up the bugs l...
1462
                nc_x = nc_group.createVariable('xhee', 'f8', (dimension,))
ceeb2f4a   Goutte   Add the target co...
1463
                nc_x.units = 'Au'
6491a1f1   Goutte   Fix up the bugs l...
1464
                nc_y = nc_group.createVariable('yhee', 'f8', (dimension,))
ceeb2f4a   Goutte   Add the target co...
1465
1466
1467
                nc_y.units = 'Au'
                values_x = []
                values_y = []
6491a1f1   Goutte   Fix up the bugs l...
1468
1469
                index_x = available_params.index('xhee')
                index_y = available_params.index('yhee')
ceeb2f4a   Goutte   Add the target co...
1470
1471
1472
1473
1474
1475
1476
1477
                for dkey in dkeys:
                    dval = data[dkey]
                    values_x.append(dval[index_x])
                    values_y.append(dval[index_y])
                nc_x[:] = values_x
                nc_y[:] = values_y
            log.debug("Writing NetCDF '%s'..." % nc_filename)

d1c44c51   Goutte   Enable Earth
1478
        except Exception:
57493104   Goutte   Add the time to t...
1479
            log.error("Failed to generate NetCDF '%s'." % nc_filename)
d1c44c51   Goutte   Enable Earth
1480
            raise
bc18b96c   Goutte   Implement first (...
1481
1482
1483
1484
1485
1486
        finally:
            nc_handle.close()

    if not isfile(nc_path):
        abort(500, "No NetCDF to serve. Looked at '%s'." % nc_path)

e18701b6   Goutte   Cache clear (remo...
1487
    return send_from_directory(CACHE_DIR, nc_filename)
bc18b96c   Goutte   Implement first (...
1488
1489


297a7dfc   Goutte   Add support for i...
1490
1491
@app.route("/<targets>_<inp>_<started_at>_<stopped_at>.cdf")
def download_targets_cdf(targets, inp, started_at, stopped_at):
aa7247d6   Goutte   Generate a CDF fi...
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
    """
    Grab data and orbit data for the specified `target`,
    rearrange it and return it as a CDF file.
    `started_at` and `stopped_at` are expected to be UTC.

    targets: string list of targets' slugs, separated by `-`.
    params: string list of targets' parameters, separated by `-`.
    """
    separator = '-'  # /!\ this char should never be in target's slugs
    targets = targets.split(separator)
d4b3d381   hitier   Tiny python3 synt...
1502
    targets.sorty()
aa7247d6   Goutte   Generate a CDF fi...
1503
1504
1505
1506
1507
1508
1509
1510
1511
    targets_configs = []
    for target in targets:
        if not target:
            abort(400, "Invalid targets format : `%s`." % targets)
        targets_configs.append(get_target_config(target))
    if 0 == len(targets_configs):
        abort(400, "No valid targets specified. What are you doing?")

    params = PARAMETERS.keys()
aa7247d6   Goutte   Generate a CDF fi...
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523

    try:
        started_at = datetime.datetime.strptime(started_at, FILE_DATE_FMT)
    except:
        abort(400, "Invalid started_at parameter : '%s'." % started_at)
    try:
        stopped_at = datetime.datetime.strptime(stopped_at, FILE_DATE_FMT)
    except:
        abort(400, "Invalid stopped_at parameter : '%s'." % stopped_at)
    sta = started_at.strftime(FILE_DATE_FMT)
    sto = stopped_at.strftime(FILE_DATE_FMT)

297a7dfc   Goutte   Add support for i...
1524
1525
1526
1527
1528
    input_slug = get_input_slug_from_query(inp=inp)

    cdf_filename = "%s_%s_%s_%s.cdf" % (
        separator.join(targets), input_slug, sta, sto
    )
aa7247d6   Goutte   Generate a CDF fi...
1529
1530
1531
1532
    cdf_path = join(CACHE_DIR, cdf_filename)

    if not isfile(cdf_path):
        log.debug("Creating the CDF file '%s'..." % cdf_filename)
604616e4   Goutte   Misc changes from...
1533
        try:
8a48d5fa   Goutte   Make sure spacepy...
1534
            from spacepy import pycdf
4aaf6874   Goutte   Try fixing the ge...
1535
1536
1537
1538
1539
1540
1541
1542
1543
        except ImportError:
            # If spacepy's toolbox is not up-to-date, importing will fail.
            # So, let's update and try again !
            update_spacepy()
            try:
                from spacepy import pycdf
            except ImportError as e:
                log.error("Failed to import pycdf from spacepy : %s" % e)
                raise
2fe06b17   Goutte   Move the ENV dire...
1544
1545
1546
        except Exception as e:
            log.error("Failed to import pycdf from spacepy : %s" % e)
            raise
4aaf6874   Goutte   Try fixing the ge...
1547

8a48d5fa   Goutte   Make sure spacepy...
1548
        try:
952e3d8f   Goutte   Move to another s...
1549
            cdf_handle = pycdf.CDF(cdf_path, masterpath='')
54bb1311   Goutte   Bring back the CM...
1550
1551
            targets_names = ', '.join([t['name'] for t in targets_configs])
            description = "Model and orbit data for %s." % targets_names
952e3d8f   Goutte   Move to another s...
1552
1553
1554
            cdf_handle.attrs['Description'] = description
            cdf_handle.attrs['Author'] = "Heliopropa.irap.omp.eu (CDPP)"
            cdf_handle.attrs['Created'] = str(time.ctime(time.time()))
aa7247d6   Goutte   Generate a CDF fi...
1555

54bb1311   Goutte   Bring back the CM...
1556
1557
1558
1559
            # fixme: Try changing the name from 00 to something relevant
            cdf_handle.attrs['Title'] = "Heliopropa - %s" % targets_names
            #######

aa7247d6   Goutte   Generate a CDF fi...
1560
1561
1562
            available_params = list(PROPERTIES)
            for target in targets_configs:
                target_slug = target['slug']
297a7dfc   Goutte   Add support for i...
1563
1564
1565
1566
                data = get_data_for_target(
                    target_config=target, input_slug=input_slug,
                    started_at=started_at, stopped_at=stopped_at
                )
aa7247d6   Goutte   Generate a CDF fi...
1567
1568
1569
                dkeys = sorted(data)

                values = []
aa7247d6   Goutte   Generate a CDF fi...
1570
                for dkey in dkeys:
952e3d8f   Goutte   Move to another s...
1571
1572
1573
1574
                    time_str = data[dkey][0][:-6]  # remove +00:00 tail
                    date = datetime.datetime.strptime(time_str, FILE_DATE_FMT)
                    values.append(date)
                kt = "%s_time" % target_slug
4aaf6874   Goutte   Try fixing the ge...
1575
                cdf_handle.new(kt, type=pycdf.const.CDF_EPOCH)
952e3d8f   Goutte   Move to another s...
1576
                cdf_handle[kt] = values
4aaf6874   Goutte   Try fixing the ge...
1577
                # cdf_handle[kt].attrs['FIELDNAM'] = "Time since 0 A.D"
aa7247d6   Goutte   Generate a CDF fi...
1578
1579
1580

                for param in params:
                    k = "%s_%s" % (target_slug, param)
aa7247d6   Goutte   Generate a CDF fi...
1581
1582
                    values = []
                    i = available_params.index(param)
4aaf6874   Goutte   Try fixing the ge...
1583
                    has_nones = False
aa7247d6   Goutte   Generate a CDF fi...
1584
                    for dkey in dkeys:
4aaf6874   Goutte   Try fixing the ge...
1585
1586
1587
1588
1589
1590
1591
1592
1593
                        value = data[dkey][i]
                        if value is None:
                            has_nones = True
                        values.append(value)
                    if has_nones:
                        # PyCDF hates it when there are Nones.
                        # Since we don't know what value to set instead,
                        # let's skip the param altogether.
                        continue
aa7247d6   Goutte   Generate a CDF fi...
1594
                    cdf_handle[k] = values
952e3d8f   Goutte   Move to another s...
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
                    attrs = cdf_handle[k].attrs
                    attrs['UNITS'] = PARAMETERS[param]['units']
                    attrs['LABLAXIS'] = PARAMETERS[param]['name']
                    attrs['FIELDNAM'] = PARAMETERS[param]['title']
                    if values:
                        attrs['VALIDMIN'] = min(values)
                        attrs['VALIDMAX'] = max(values)

                kx = "%s_xhee" % target_slug
                ky = "%s_yhee" % target_slug
aa7247d6   Goutte   Generate a CDF fi...
1605
1606
1607
1608
1609
                values_xhee = []
                values_yhee = []
                index_x = available_params.index('xhee')
                index_y = available_params.index('yhee')
                for dkey in dkeys:
391d581c   Goutte   Make the CDF down...
1610
1611
1612
                    value_xhee = data[dkey][index_x]
                    value_yhee = data[dkey][index_y]
                    # We've got some `None`s cropping up in the data sometimes.
4aaf6874   Goutte   Try fixing the ge...
1613
                    # PyCDF does not digest Nones at all.
391d581c   Goutte   Make the CDF down...
1614
1615
1616
1617
1618
                    # While they solve this upstream, let's make an ugly fix!
                    if (value_xhee is not None) and (value_yhee is not None):
                        values_xhee.append(value_xhee)
                        values_yhee.append(value_yhee)
                    else:
4aaf6874   Goutte   Try fixing the ge...
1619
1620
                        values_xhee.append(0)
                        values_yhee.append(0)
d4b3d381   hitier   Tiny python3 synt...
1621
                        log.warning("Orbit data for %s has NaNs." % target_slug)
952e3d8f   Goutte   Move to another s...
1622
1623
1624
1625
                cdf_handle[kx] = values_xhee
                cdf_handle[ky] = values_yhee
                cdf_handle[kx].attrs['UNITS'] = 'Au'
                cdf_handle[ky].attrs['UNITS'] = 'Au'
aa7247d6   Goutte   Generate a CDF fi...
1626
1627

            log.debug("Writing CDF '%s'..." % cdf_filename)
952e3d8f   Goutte   Move to another s...
1628
1629
            cdf_handle.close()
            log.debug("Wrote CDF '%s'." % cdf_filename)
aa7247d6   Goutte   Generate a CDF fi...
1630
1631
1632

        except Exception as e:
            log.error("Failed to generate CDF '%s'." % cdf_filename)
952e3d8f   Goutte   Move to another s...
1633
1634
            if isfile(cdf_path):
                removefile(cdf_path)
aa7247d6   Goutte   Generate a CDF fi...
1635
1636
            raise

aa7247d6   Goutte   Generate a CDF fi...
1637
1638
1639
1640
1641
1642
    if not isfile(cdf_path):
        abort(500, "No CDF to serve. Looked at '%s'." % cdf_path)

    return send_from_directory(CACHE_DIR, cdf_filename)


11d86851   Goutte   Add support for s...
1643
1644
1645
1646
1647
1648
1649
1650
@app.route("/<target>_auroral_catalog.csv")
def download_auroral_catalog_csv(target):
    tc = validate_tap_target_config(target)
    log.debug("Requesting auroral emissions CSV for %s..." % tc['name'])

    filename = "%s_auroral_catalog.csv" % (target)
    local_csv_file = join(CACHE_DIR, filename)

11d86851   Goutte   Add support for s...
1651
1652
1653
1654
1655
1656
1657
1658
1659
    target_name = tc['tap']['target_name']
    emissions = retrieve_auroral_emissions(target_name)

    # Be careful with regexes in python 2 ; best always use the ^$
    thumbnail_url_filter = re.compile("^.*proc(?:_small)?\\.(?:jpe?g|png|webp|gif|bmp|tiff)$")

    # Filter the emissions
    def _keep_emission(emission):
        ok = thumbnail_url_filter.match(emission['thumbnail_url'])
11d86851   Goutte   Add support for s...
1660
1661
1662
1663
1664
1665
1666
        return bool(ok)

    emissions = [e for e in emissions if _keep_emission(e)]

    header = ('time_min', 'time_max', 'thumbnail_url', 'external_link')
    if len(emissions):
        header = emissions[0].keys()
b33d5f62   hitier   Change StringIO i...
1667
    si = StringIO()
11d86851   Goutte   Add support for s...
1668
1669
1670
    cw = csv_dict_writer(si, fieldnames=header)
    cw.writeheader()
    # 'time_min', 'time_max', 'thumbnail_url', 'external_link'
34e94ae3   hitier   Code reformating
1671
    # cw.writerow(head)
11d86851   Goutte   Add support for s...
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684

    log.debug("Writing auroral emissions CSV for %s..." % tc['name'])
    cw.writerows(emissions)

    log.info("Generated auroral emissions CSV contents for %s." % tc['name'])
    return si.getvalue()

    # if not isfile(local_csv_file):
    #     abort(500, "Could not cache CSV file at '%s'." % local_csv_file)
    #
    # return send_from_directory(CACHE_DIR, filename)


11d86851   Goutte   Add support for s...
1685
1686
1687
1688
1689
1690
1691
1692
1693
@app.route("/test/auroral/<target>")
def test_auroral_emissions(target):
    tc = validate_tap_target_config(target)
    target_name = tc['tap']['target_name']
    retrieved = retrieve_auroral_emissions(target_name)

    return "%d results:\n%s" % (len(retrieved), str(retrieved))


28bb4b28   Goutte   API for the cache...
1694
1695
# API #########################################################################

e18701b6   Goutte   Cache clear (remo...
1696
1697
1698
1699
1700
1701
1702
1703
1704
@app.route("/cache/clear")
def cache_clear():
    """
    Removes all files from the cache.
    Note: It also removes the .gitkeep file. Not a problem for prod.
    """
    removed_files = remove_all_files(CACHE_DIR)
    count = len(removed_files)
    return "Cache cleared! Removed %d file%s." \
95155804   hitier   Reformat code
1705
        % (count, 's' if count != 1 else '')
e18701b6   Goutte   Cache clear (remo...
1706
1707


d9710a98   Goutte   Rename the cleanu...
1708
1709
@app.route("/cache/cleanup")
def cache_cleanup():
28bb4b28   Goutte   API for the cache...
1710
1711
    """
    Removes all files from the cache that are older than roughly one month.
e18701b6   Goutte   Cache clear (remo...
1712
    Note: It also removes the .gitkeep file. Maybe it should not, but hey.
28bb4b28   Goutte   API for the cache...
1713
1714
    """
    a_month_ago = datetime.datetime.now() - datetime.timedelta(days=32)
e18701b6   Goutte   Cache clear (remo...
1715
    removed_files = remove_files_created_before(a_month_ago, CACHE_DIR)
d9710a98   Goutte   Rename the cleanu...
1716
1717
    count = len(removed_files)
    return "Cache cleaned! Removed %d old file%s." \
95155804   hitier   Reformat code
1718
        % (count, 's' if count != 1 else '')
28bb4b28   Goutte   API for the cache...
1719
1720


b500e561   Goutte   Invert the orbits...
1721
1722
1723
1724
@app.route("/cache/warmup")
def cache_warmup():
    """
    Warms up the cache for the current day.
b500e561   Goutte   Invert the orbits...
1725
    """
390a3587   Goutte   Add a CRON config...
1726
    warmup_started_at = datetime.datetime.now()
284f4688   Goutte   Continue layers i...
1727
    sta, sto = get_interval_from_query()
b662fcc3   hitier   Change input name...
1728
    inp = config['defaults']['input_slug']  # default input, maybe warm them all up ?
fb383448   Goutte   Implement the cac...
1729
1730

    targets = get_active_targets()
fb383448   Goutte   Implement the cac...
1731
    targets_slugs = [target['slug'] for target in targets]
d4b3d381   hitier   Tiny python3 synt...
1732
    targets_slugs.sorty()
4aaf6874   Goutte   Try fixing the ge...
1733
1734
1735
1736

    update_spacepy()
    for target in targets:
        download_target_csv(target['slug'], inp, sta, sto)
297a7dfc   Goutte   Add support for i...
1737
    download_targets_cdf('-'.join(targets_slugs), inp, sta, sto)
fb383448   Goutte   Implement the cac...
1738

390a3587   Goutte   Add a CRON config...
1739
1740
1741
1742
    warmup_ended_at = datetime.datetime.now()
    warmup_timedelta = warmup_ended_at - warmup_started_at

    return "Done in %s." % str(warmup_timedelta)
b500e561   Goutte   Invert the orbits...
1743
1744


1324cc91   Goutte   Make the footer i...
1745
@app.route("/log")
596da00d   Goutte   Add more exceptio...
1746
@app.route("/log.html")
1324cc91   Goutte   Make the footer i...
1747
1748
def log_show():
    with open(LOG_FILE, 'r') as f:
bde97e4d   Goutte   Add more changes ...
1749
        contents = f.read()
fb383448   Goutte   Implement the cac...
1750
    return "<pre>" + contents + "</pre>"
bde97e4d   Goutte   Add more changes ...
1751
1752


1324cc91   Goutte   Make the footer i...
1753
1754
1755
1756
1757
1758
1759
@app.route("/log/clear")
def log_clear():
    with open(LOG_FILE, 'w') as f:
        f.truncate()
    return "Log cleared successfully."


1754789b   Goutte   Decorate and clea...
1760
1761
1762
1763
# DEV TOOLS ###################################################################

# @app.route("/inspect")
# def analyze_cdf():
a4a9ef03   Goutte   Cache generated C...
1764
#     """
1754789b   Goutte   Decorate and clea...
1765
#     For debug purposes.
a4a9ef03   Goutte   Cache generated C...
1766
#     """
1754789b   Goutte   Decorate and clea...
1767
1768
#     cdf_to_inspect = get_path("../res/dummy.nc")
#     cdf_to_inspect = get_path("../res/dummy_jupiter_coordinates.nc")
a4a9ef03   Goutte   Cache generated C...
1769
#
b33d5f62   hitier   Change StringIO i...
1770
#     si = StringIO()
1754789b   Goutte   Decorate and clea...
1771
1772
#     cw = csv.DictWriter(si, fieldnames=['Name', 'Shape', 'Length'])
#     cw.writeheader()
a4a9ef03   Goutte   Cache generated C...
1773
#
1754789b   Goutte   Decorate and clea...
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
#     # Time, StartTime, StopTime, V, B, N, T, Delta_angle, P_dyn, QualityFlag
#     cdf_handle = Dataset(cdf_to_inspect, "r", format="NETCDF4")
#     for variable in cdf_handle.variables:
#         v = cdf_handle.variables[variable]
#         cw.writerow({
#             'Name': variable,
#             'Shape': v.shape,
#             'Length': v.size,
#         })
#     cdf_handle.close()
a4a9ef03   Goutte   Cache generated C...
1784
1785
1786
1787
#
#     return si.getvalue()


9390ec89   Goutte   Initial experimen...
1788
1789
1790
# MAIN ########################################################################

if __name__ == "__main__":
952e3d8f   Goutte   Move to another s...
1791
    # Debug mode is on, as the production server does not use this but run.wsgi
9390ec89   Goutte   Initial experimen...
1792
1793
    extra_files = [get_path('../config.yml')]
    app.run(debug=True, extra_files=extra_files)