web interface

This commit is contained in:
Daniel Tsvetkov 2019-12-15 09:56:07 +01:00
parent f13d40f2ee
commit 3e45daec62
7 changed files with 17388 additions and 0 deletions

View File

@ -1,6 +1,8 @@
backcall==0.1.0 backcall==0.1.0
Click==7.0
dateparser==0.7.2 dateparser==0.7.2
decorator==4.4.0 decorator==4.4.0
Flask==1.1.1
freezegun==0.3.12 freezegun==0.3.12
fuzzywuzzy==0.17.0 fuzzywuzzy==0.17.0
geographiclib==1.49 geographiclib==1.49
@ -8,7 +10,10 @@ geopy==1.20.0
importlib-resources==1.0.2 importlib-resources==1.0.2
ipython==7.8.0 ipython==7.8.0
ipython-genutils==0.2.0 ipython-genutils==0.2.0
itsdangerous==1.1.0
jedi==0.15.1 jedi==0.15.1
Jinja2==2.10.3
MarkupSafe==1.1.1
numpy==1.17.2 numpy==1.17.2
parso==0.5.1 parso==0.5.1
pexpect==4.7.0 pexpect==4.7.0
@ -25,4 +30,5 @@ timezonefinder==4.1.0
traitlets==4.3.2 traitlets==4.3.2
tzlocal==2.0.0 tzlocal==2.0.0
wcwidth==0.1.7 wcwidth==0.1.7
Werkzeug==0.16.0
word2number==1.1 word2number==1.1

View File

@ -0,0 +1,69 @@
/**
* Debounce and throttle function's decorator plugin 1.0.5
*
* Copyright (c) 2009 Filatov Dmitry (alpha@zforms.ru)
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
*/
(function($) {
$.extend({
debounce : function(fn, timeout, invokeAsap, ctx) {
if(arguments.length == 3 && typeof invokeAsap != 'boolean') {
ctx = invokeAsap;
invokeAsap = false;
}
var timer;
return function() {
var args = arguments;
ctx = ctx || this;
invokeAsap && !timer && fn.apply(ctx, args);
clearTimeout(timer);
timer = setTimeout(function() {
invokeAsap || fn.apply(ctx, args);
timer = null;
}, timeout);
};
},
throttle : function(fn, timeout, ctx) {
var timer, args, needInvoke;
return function() {
args = arguments;
needInvoke = true;
ctx = ctx || this;
timer || (function() {
if(needInvoke) {
fn.apply(ctx, args);
needInvoke = false;
timer = setTimeout(arguments.callee, timeout);
}
else {
timer = null;
}
})();
};
}
});
})(jQuery);

10598
src/tww/static/jquery.js vendored Normal file

File diff suppressed because it is too large Load Diff

481
src/tww/static/select2.css Normal file
View File

@ -0,0 +1,481 @@
.select2-container {
box-sizing: border-box;
display: inline-block;
margin: 0;
position: relative;
vertical-align: middle; }
.select2-container .select2-selection--single {
box-sizing: border-box;
cursor: pointer;
display: block;
height: 28px;
user-select: none;
-webkit-user-select: none; }
.select2-container .select2-selection--single .select2-selection__rendered {
display: block;
padding-left: 8px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
.select2-container .select2-selection--single .select2-selection__clear {
position: relative; }
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
padding-right: 8px;
padding-left: 20px; }
.select2-container .select2-selection--multiple {
box-sizing: border-box;
cursor: pointer;
display: block;
min-height: 32px;
user-select: none;
-webkit-user-select: none; }
.select2-container .select2-selection--multiple .select2-selection__rendered {
display: inline-block;
overflow: hidden;
padding-left: 8px;
text-overflow: ellipsis;
white-space: nowrap; }
.select2-container .select2-search--inline {
float: left; }
.select2-container .select2-search--inline .select2-search__field {
box-sizing: border-box;
border: none;
font-size: 100%;
margin-top: 5px;
padding: 0; }
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none; }
.select2-dropdown {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
box-sizing: border-box;
display: block;
position: absolute;
left: -100000px;
width: 100%;
z-index: 1051; }
.select2-results {
display: block; }
.select2-results__options {
list-style: none;
margin: 0;
padding: 0; }
.select2-results__option {
padding: 6px;
user-select: none;
-webkit-user-select: none; }
.select2-results__option[aria-selected] {
cursor: pointer; }
.select2-container--open .select2-dropdown {
left: 0; }
.select2-container--open .select2-dropdown--above {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--open .select2-dropdown--below {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-search--dropdown {
display: block;
padding: 4px; }
.select2-search--dropdown .select2-search__field {
padding: 4px;
width: 100%;
box-sizing: border-box; }
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none; }
.select2-search--dropdown.select2-search--hide {
display: none; }
.select2-close-mask {
border: 0;
margin: 0;
padding: 0;
display: block;
position: fixed;
left: 0;
top: 0;
min-height: 100%;
min-width: 100%;
height: auto;
width: auto;
opacity: 0;
z-index: 99;
background-color: #fff;
filter: alpha(opacity=0); }
.select2-hidden-accessible {
border: 0 !important;
clip: rect(0 0 0 0) !important;
-webkit-clip-path: inset(50%) !important;
clip-path: inset(50%) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important; }
.select2-container--default .select2-selection--single {
background-color: #fff;
border: 1px solid #aaa;
border-radius: 4px; }
.select2-container--default .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px; }
.select2-container--default .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold; }
.select2-container--default .select2-selection--single .select2-selection__placeholder {
color: #999; }
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px; }
.select2-container--default .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0; }
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left; }
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto; }
.select2-container--default.select2-container--disabled .select2-selection--single {
background-color: #eee;
cursor: default; }
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none; }
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px; }
.select2-container--default .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text; }
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 5px;
width: 100%; }
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
list-style: none; }
.select2-container--default .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-top: 5px;
margin-right: 10px;
padding: 1px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: #999;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #333; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto; }
.select2-container--default.select2-container--focus .select2-selection--multiple {
border: solid black 1px;
outline: 0; }
.select2-container--default.select2-container--disabled .select2-selection--multiple {
background-color: #eee;
cursor: default; }
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
display: none; }
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--default .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa; }
.select2-container--default .select2-search--inline .select2-search__field {
background: transparent;
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield; }
.select2-container--default .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto; }
.select2-container--default .select2-results__option[role=group] {
padding: 0; }
.select2-container--default .select2-results__option[aria-disabled=true] {
color: #999; }
.select2-container--default .select2-results__option[aria-selected=true] {
background-color: #ddd; }
.select2-container--default .select2-results__option .select2-results__option {
padding-left: 1em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -4em;
padding-left: 5em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -5em;
padding-left: 6em; }
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: #5897fb;
color: white; }
.select2-container--default .select2-results__group {
cursor: default;
display: block;
padding: 6px; }
.select2-container--classic .select2-selection--single {
background-color: #f7f7f7;
border: 1px solid #aaa;
border-radius: 4px;
outline: 0;
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
.select2-container--classic .select2-selection--single:focus {
border: 1px solid #5897fb; }
.select2-container--classic .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px; }
.select2-container--classic .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-right: 10px; }
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
color: #999; }
.select2-container--classic .select2-selection--single .select2-selection__arrow {
background-color: #ddd;
border: none;
border-left: 1px solid #aaa;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
.select2-container--classic .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0; }
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left; }
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
border: none;
border-right: 1px solid #aaa;
border-radius: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
left: 1px;
right: auto; }
.select2-container--classic.select2-container--open .select2-selection--single {
border: 1px solid #5897fb; }
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
background: transparent;
border: none; }
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px; }
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
.select2-container--classic .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text;
outline: 0; }
.select2-container--classic .select2-selection--multiple:focus {
border: 1px solid #5897fb; }
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
list-style: none;
margin: 0;
padding: 0 5px; }
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
display: none; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
color: #888;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #555; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
float: right;
margin-left: 5px;
margin-right: auto; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto; }
.select2-container--classic.select2-container--open .select2-selection--multiple {
border: 1px solid #5897fb; }
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--classic .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa;
outline: 0; }
.select2-container--classic .select2-search--inline .select2-search__field {
outline: 0;
box-shadow: none; }
.select2-container--classic .select2-dropdown {
background-color: white;
border: 1px solid transparent; }
.select2-container--classic .select2-dropdown--above {
border-bottom: none; }
.select2-container--classic .select2-dropdown--below {
border-top: none; }
.select2-container--classic .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto; }
.select2-container--classic .select2-results__option[role=group] {
padding: 0; }
.select2-container--classic .select2-results__option[aria-disabled=true] {
color: grey; }
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
background-color: #3875d7;
color: white; }
.select2-container--classic .select2-results__group {
cursor: default;
display: block;
padding: 6px; }
.select2-container--classic.select2-container--open .select2-dropdown {
border-color: #5897fb; }

6037
src/tww/static/select2.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Time When And Where</title>
<style type="text/css">
body {
font-family: Open Sans, Arial, sans-serif;
color: #444;
padding: 0 1em;
}
main {
max-width: 800px;
margin: 0 auto;
}
input {
padding: 0.5em;
}
.main-input {
width: 100%;
font-family: Open Sans, Arial sans-serif;
font-size: 1em;
box-sizing: border-box;
}
</style>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='select2.css') }}">
</head>
<body>
<main>
<form>
<input type="text" class="main-input datetime__txt" name="datetime__txt"
value="" autocomplete="off"/>
</br>
<span class="datetime__error"></span>
</br>
<input type="text" class="datetime" name="datetime" value="" style="width: 320px;"/>
</br></br>
<input type="text" class="datetime__date_txt" name="datetime__date_txt"
value=""/>
<input type="text" class="datetime__time_txt" name="datetime__time_txt"
value=""/>
<input type="text" class="datetime__tz_offset" name="datetime__tz_offset"
value="" size="8"/>
<select class="datetime__tz_name" name="datetime__tz">
<option disabled selected value> -- select timezone --</option>
{% for tz in all_tz %}
<option value="{{ tz }}">{{ tz }}</option>
{% endfor %}
</select>
</form>
</main>
<script src="{{ url_for('static', filename='jquery.js') }}"></script>
<script src="{{ url_for('static', filename='jquery.debounce.js') }}"></script>
<script src="{{ url_for('static', filename='select2.js') }}"></script>
<script src="{{ url_for('static', filename='main.js') }}"></script>
<script>
var textarea = document.querySelector('textarea');
if (textarea)
textarea.addEventListener('keydown', autosize);
function autosize() {
var el = this;
setTimeout(function () {
el.style.cssText = 'height:auto; padding:0';
el.style.cssText = 'height:' + (el.scrollHeight + 30) + 'px';
}, 0);
}
$(document).ready(function () {
$('select').select2();
$('.datetime__txt').keyup($.debounce(function (e) {
$el = $(this);
$.ajax({
"url": "{{ url_for("api_datetime") }}",
"data": {"q": this.value}
}).done(function (r) {
if (r.error !== "")
$el.next(".datetime__error").addClass("flash").addClass("error").html(r.error);
else
$el.next(".datetime__error").removeClass("flash").removeClass("error").html("");
$el.siblings(".datetime").val(r.to_dt_resolve.iso);
$el.siblings(".datetime__date_txt").val(r.to_dt_resolve.date);
$el.siblings(".datetime__time_txt").val(r.to_dt_resolve.time);
$el.siblings(".datetime__tz_offset").val(r.to_dt_resolve.tz_offset);
$el.siblings(".datetime__tz_name").val(r.to_tz_resolve.tz_name).trigger('change');
});
}, 500));
});
</script>
</body>
</html>

99
src/tww/web.py Normal file
View File

@ -0,0 +1,99 @@
import pytz
from flask import Flask, render_template, jsonify, request
app = Flask(__name__)
import dateparser
from tww import resolve_timezone
IN_KW = " in "
TO_KW = " to "
NO_TZ_FORMAT = '%Y-%m-%dT%H:%M:%S'
ISO_FORMAT = '%Y-%m-%dT%H:%M:%S%z'
DATE_FORMAT = '%Y-%m-%d'
TIME_FORMAT = '%H:%M:%S'
TZ_OFFSET_FORMAT = '%z'
def parse_query(q):
fmt = NO_TZ_FORMAT
to_split = q.split(TO_KW)
in_dt_resolve_fmt, to_dt_resolve_fmt = {}, {}
in_tz_resolve, to_tz_resolve = {}, {}
error = ""
if len(to_split) >= 2:
# 18:00 to Zurich
in_q, to_tz = to_split[0], TO_KW.join(to_split[1:])
else:
# 18:00
in_q = q
to_tz = ""
in_split = in_q.split(IN_KW)
if len(in_split) >= 2:
# 18:00 in Zurich
in_dt, in_tz = in_split[0], IN_KW.join(in_split[1:])
else:
# in 2 hours / 19:00 CET
in_dt = in_q
in_tz = ""
try:
in_dt_resolve = dateparser.parse(in_dt, settings={
'RETURN_AS_TIMEZONE_AWARE': True})
if not in_tz:
in_tz = in_dt_resolve.strftime("%Z")
if in_tz:
isofmt = in_dt_resolve.isoformat()
in_tz_resolve = resolve_timezone(in_tz)
in_dt_resolve = dateparser.parse(isofmt, settings={
'TO_TIMEZONE': in_tz_resolve})
to_dt_resolve, to_tz_resolve = in_dt_resolve, in_tz_resolve
if in_dt_resolve:
in_dt_resolve_fmt = {
'iso': in_dt_resolve.strftime(ISO_FORMAT),
'fmt': in_dt_resolve.strftime(fmt),
'date': in_dt_resolve.strftime(DATE_FORMAT),
'time': in_dt_resolve.strftime(TIME_FORMAT),
'tz_offset': in_dt_resolve.strftime(TZ_OFFSET_FORMAT),
}
if to_tz:
isofmt = in_dt_resolve.isoformat()
to_tz_resolve = resolve_timezone(to_tz)
to_dt_resolve = dateparser.parse(isofmt, settings={
'TO_TIMEZONE': to_tz_resolve})
if to_dt_resolve:
to_dt_resolve_fmt = {
'iso': to_dt_resolve.strftime(ISO_FORMAT),
'fmt': to_dt_resolve.strftime(fmt),
'date': to_dt_resolve.strftime(DATE_FORMAT),
'time': to_dt_resolve.strftime(TIME_FORMAT),
'tz_offset': to_dt_resolve.strftime(TZ_OFFSET_FORMAT),
}
except Exception as e:
error = str(e)
return {
'error': error,
'query': q,
'fmt': fmt,
'in_dt': in_dt,
'in_tz_resolve': in_tz_resolve,
'in_dt_resolve': in_dt_resolve_fmt,
'to_dt_resolve': to_dt_resolve_fmt,
'in_tz': in_tz,
'to_tz': to_tz,
'to_tz_resolve': to_tz_resolve,
}
@app.route("/")
def home():
return render_template("home.html", all_tz=pytz.all_timezones)
@app.route("/api/datetime")
def api_datetime():
q = request.args.get('q')
return jsonify(parse_query(q))
if __name__ == "__main__":
app.run(debug=True)